diff --git a/src/animation/PropertyBinding.js b/src/animation/PropertyBinding.js index 8eecd5bde9061a11c46eaab09bcf10181bdaf045..2020b9028297b91845f95e719adc6158115df01a 100644 --- a/src/animation/PropertyBinding.js +++ b/src/animation/PropertyBinding.js @@ -8,6 +8,9 @@ * @author tschw */ +// Characters [].:/ are reserved for track binding syntax. +var RESERVED_CHARS_RE = '\\[\\]\\.:\\/'; + function Composite( targetGroup, path, optionalParsedPath ) { var parsedPath = optionalParsedPath || PropertyBinding.parseTrackName( path ); @@ -111,33 +114,41 @@ Object.assign( PropertyBinding, { */ sanitizeNodeName: function ( name ) { - return name.replace( /\s/g, '_' ).replace( /[^\w-]/g, '' ); + var reservedRe = new RegExp( '[' + RESERVED_CHARS_RE + ']', 'g' ); + + return name.replace( /\s/g, '_' ).replace( reservedRe, '' ); }, parseTrackName: function () { + // Attempts to allow node names from any language. ES5's `\w` regexp matches + // only latin characters, and the unicode \p{L} is not yet supported. So + // instead, we exclude reserved characters and match everything else. + var wordChar = '[^' + RESERVED_CHARS_RE + ']'; + var wordCharOrDot = '[^' + RESERVED_CHARS_RE.replace( '\\.', '' ) + ']'; + // Parent directories, delimited by '/' or ':'. Currently unused, but must // be matched to parse the rest of the track name. - var directoryRe = /((?:[\w-]+[\/:])*)/; + var directoryRe = /((?:WC+[\/:])*)/.source.replace( 'WC', wordChar ); // Target node. May contain word characters (a-zA-Z0-9_) and '.' or '-'. - var nodeRe = /([\w-\.]+)?/; + var nodeRe = /(WCOD+)?/.source.replace( 'WCOD', wordCharOrDot ); - // Object on target node, and accessor. Name may contain only word + // Object on target node, and accessor. May not contain reserved // characters. Accessor may contain any character except closing bracket. - var objectRe = /(?:\.([\w-]+)(?:\[(.+)\])?)?/; + var objectRe = /(?:\.(WC+)(?:\[(.+)\])?)?/.source.replace( 'WC', wordChar ); - // Property and accessor. May contain only word characters. Accessor may + // Property and accessor. May not contain reserved characters. Accessor may // contain any non-bracket characters. - var propertyRe = /\.([\w-]+)(?:\[(.+)\])?/; + var propertyRe = /\.(WC+)(?:\[(.+)\])?/.source.replace( 'WC', wordChar ); var trackRe = new RegExp( '' + '^' - + directoryRe.source - + nodeRe.source - + objectRe.source - + propertyRe.source + + directoryRe + + nodeRe + + objectRe + + propertyRe + '$' ); diff --git a/test/unit/src/animation/PropertyBinding.tests.js b/test/unit/src/animation/PropertyBinding.tests.js index da70aaef29da343f488e024784a2508ec12d9f53..434fd0592570e2cee2652d7b858c01c254a18ddc 100644 --- a/test/unit/src/animation/PropertyBinding.tests.js +++ b/test/unit/src/animation/PropertyBinding.tests.js @@ -41,6 +41,12 @@ export default QUnit.module( 'Animation', () => { 'Leaves valid name intact.' ); + assert.equal( + PropertyBinding.sanitizeNodeName( '急須' ), + '急須', + 'Leaves non-latin unicode characters intact.' + ); + assert.equal( PropertyBinding.sanitizeNodeName( 'space separated name 123_ -' ), 'space_separated_name_123__-', @@ -48,9 +54,15 @@ export default QUnit.module( 'Animation', () => { ); assert.equal( - PropertyBinding.sanitizeNodeName( '"invalid" name %123%_' ), - 'invalid_name_123_', - 'Strips invalid characters.' + PropertyBinding.sanitizeNodeName( '"Mátyás" %_* 😇' ), + '"Mátyás"_%_*_😇', + 'Allows various punctuation and symbols.' + ); + + assert.equal( + PropertyBinding.sanitizeNodeName( '/invalid: name ^123.[_]' ), + 'invalid_name_^123_', + 'Strips reserved characters.' ); } ); @@ -236,7 +248,30 @@ export default QUnit.module( 'Animation', () => { propertyName: 'position', propertyIndex: undefined } + ], + + [ + '急須.材料[零]', + { + nodeName: '急須', + objectName: undefined, + objectIndex: undefined, + propertyName: '材料', + propertyIndex: '零' + } + ], + + [ + '📦.🎨[🔴]', + { + nodeName: '📦', + objectName: undefined, + objectIndex: undefined, + propertyName: '🎨', + propertyIndex: '🔴' + } ] + ]; paths.forEach( function ( path ) {