diff --git a/src/renderers/shared/reconciler/ReactChildReconciler.js b/src/renderers/shared/reconciler/ReactChildReconciler.js index 634b3444c715b..5b12703f522f1 100644 --- a/src/renderers/shared/reconciler/ReactChildReconciler.js +++ b/src/renderers/shared/reconciler/ReactChildReconciler.js @@ -14,6 +14,7 @@ var ReactReconciler = require('ReactReconciler'); var instantiateReactComponent = require('instantiateReactComponent'); +var KeyEscapeUtils = require('KeyEscapeUtils'); var shouldUpdateReactComponent = require('shouldUpdateReactComponent'); var traverseAllChildren = require('traverseAllChildren'); var warning = require('warning'); @@ -27,7 +28,7 @@ function instantiateChild(childInstances, child, name) { 'flattenChildren(...): Encountered two children with the same key, ' + '`%s`. Child keys must be unique; when two children share a key, only ' + 'the first child will be used.', - name + KeyEscapeUtils.unescape(name) ); } if (child != null && keyUnique) { diff --git a/src/shared/utils/KeyEscapeUtils.js b/src/shared/utils/KeyEscapeUtils.js new file mode 100644 index 0000000000000..710068f770448 --- /dev/null +++ b/src/shared/utils/KeyEscapeUtils.js @@ -0,0 +1,64 @@ +/** + * Copyright 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule KeyEscapeUtils + */ + +'use strict'; + +/** + * Escape and wrap key so it is safe to use as a reactid + * + * @param {*} key to be escaped. + * @return {string} the escaped key. + */ +function escape(key) { + var escapeRegex = /[=:]/g; + var escaperLookup = { + '=': '=0', + ':': '=2', + }; + var escapedString = ('' + key).replace( + escapeRegex, + function(match) { + return escaperLookup[match]; + } + ); + + return '$' + escapedString; +} + +/** + * Unescape and unwrap key for human-readable display + * + * @param {string} key to unescape. + * @return {string} the unescaped key. + */ +function unescape(key) { + var unescapeRegex = /(=0|=2)/g; + var unescaperLookup = { + '=0': '=', + '=2': ':', + }; + var keySubstring = (key[0] === '.' && key[1] === '$') + ? key.substring(2) : key.substring(1); + + return ('' + keySubstring).replace( + unescapeRegex, + function(match) { + return unescaperLookup[match]; + } + ); +} + +var KeyEscapeUtils = { + escape: escape, + unescape: unescape, +}; + +module.exports = KeyEscapeUtils; diff --git a/src/shared/utils/__tests__/KeyEscapeUtils-test.js b/src/shared/utils/__tests__/KeyEscapeUtils-test.js new file mode 100644 index 0000000000000..eed9f46785b6c --- /dev/null +++ b/src/shared/utils/__tests__/KeyEscapeUtils-test.js @@ -0,0 +1,38 @@ +/** + * Copyright 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @emails react-core + */ + +'use strict'; + +var KeyEscapeUtils; + +describe('KeyEscapeUtils', () => { + beforeEach(() => { + jest.resetModuleRegistry(); + + KeyEscapeUtils = require('KeyEscapeUtils'); + }); + + describe('escape', () => { + it('should properly escape and wrap user defined keys', () => { + expect(KeyEscapeUtils.escape('1')).toBe('$1'); + expect(KeyEscapeUtils.escape('1=::=2')).toBe('$1=0=2=2=02'); + }); + }); + + describe('unescape', () => { + it('should properly unescape and unwrap user defined keys', () => { + expect(KeyEscapeUtils.unescape('.1')).toBe('1'); + expect(KeyEscapeUtils.unescape('$1')).toBe('1'); + expect(KeyEscapeUtils.unescape('.$1')).toBe('1'); + expect(KeyEscapeUtils.unescape('$1=0=2=2=02')).toBe('1=::=2'); + }); + }); +}); diff --git a/src/shared/utils/flattenChildren.js b/src/shared/utils/flattenChildren.js index b25d399fa84e2..771134edf8776 100644 --- a/src/shared/utils/flattenChildren.js +++ b/src/shared/utils/flattenChildren.js @@ -11,6 +11,7 @@ 'use strict'; +var KeyEscapeUtils = require('KeyEscapeUtils'); var traverseAllChildren = require('traverseAllChildren'); var warning = require('warning'); @@ -29,7 +30,7 @@ function flattenSingleChildIntoContext(traverseContext, child, name) { 'flattenChildren(...): Encountered two children with the same key, ' + '`%s`. Child keys must be unique; when two children share a key, only ' + 'the first child will be used.', - name + KeyEscapeUtils.unescape(name) ); } if (keyUnique && child != null) { diff --git a/src/shared/utils/traverseAllChildren.js b/src/shared/utils/traverseAllChildren.js index be0dcc0fd2baf..7d03dc02d1792 100644 --- a/src/shared/utils/traverseAllChildren.js +++ b/src/shared/utils/traverseAllChildren.js @@ -16,6 +16,7 @@ var ReactElement = require('ReactElement'); var getIteratorFn = require('getIteratorFn'); var invariant = require('invariant'); +var KeyEscapeUtils = require('KeyEscapeUtils'); var warning = require('warning'); var SEPARATOR = '.'; @@ -26,19 +27,8 @@ var SUBSEPARATOR = ':'; * pattern. */ -var userProvidedKeyEscaperLookup = { - '=': '=0', - ':': '=2', -}; - -var userProvidedKeyEscapeRegex = /[=:]/g; - var didWarnAboutMaps = false; -function userProvidedKeyEscaper(match) { - return userProvidedKeyEscaperLookup[match]; -} - /** * Generate a key string that identifies a component within a set. * @@ -51,36 +41,12 @@ function getComponentKey(component, index) { // that we don't block potential future ES APIs. if (component && typeof component === 'object' && component.key != null) { // Explicit key - return wrapUserProvidedKey(component.key); + return KeyEscapeUtils.escape(component.key); } // Implicit key determined by the index in the set return index.toString(36); } -/** - * Escape a component key so that it is safe to use in a reactid. - * - * @param {*} text Component key to be escaped. - * @return {string} An escaped string. - */ -function escapeUserProvidedKey(text) { - return ('' + text).replace( - userProvidedKeyEscapeRegex, - userProvidedKeyEscaper - ); -} - -/** - * Wrap a `key` value explicitly provided by the user to distinguish it from - * implicitly-generated keys generated by a component's index in its parent. - * - * @param {string} key Value of a user-provided `key` attribute - * @return {string} - */ -function wrapUserProvidedKey(key) { - return '$' + escapeUserProvidedKey(key); -} - /** * @param {?*} children Children tree container. * @param {!string} nameSoFar Name of the key path so far. @@ -166,7 +132,7 @@ function traverseAllChildrenImpl( child = entry[1]; nextName = ( nextNamePrefix + - wrapUserProvidedKey(entry[0]) + SUBSEPARATOR + + KeyEscapeUtils.escape(entry[0]) + SUBSEPARATOR + getComponentKey(child, 0) ); subtreeCount += traverseAllChildrenImpl(