diff --git a/src/parse/index.ts b/src/parse/index.ts index 6711e9929f..03c7cb95e5 100644 --- a/src/parse/index.ts +++ b/src/parse/index.ts @@ -1,9 +1,11 @@ +import { isIdentifierStart, isIdentifierChar } from 'acorn'; import { locate, Location } from 'locate-character'; import fragment from './state/fragment'; import { whitespace } from '../utils/patterns'; import { trimStart, trimEnd } from '../utils/trim'; import getCodeFrame from '../utils/getCodeFrame'; import reservedNames from '../utils/reservedNames'; +import fullCharCodeAt from '../utils/fullCharCodeAt'; import hash from './utils/hash'; import { Node, Parsed } from '../interfaces'; import CompileError from '../utils/CompileError'; @@ -147,7 +149,22 @@ export class Parser { readIdentifier() { const start = this.index; - const identifier = this.read(/[a-zA-Z_$][a-zA-Z0-9_$]*/); + + let i = this.index; + + const code = fullCharCodeAt(this.template, i); + if (!isIdentifierStart(code, true)) return null; + + i += code <= 0xffff ? 1 : 2; + + while (i < this.template.length) { + const code = fullCharCodeAt(this.template, i); + + if (!isIdentifierChar(code, true)) break; + i += code <= 0xffff ? 1 : 2; + } + + const identifier = this.template.slice(this.index, this.index = i); if (reservedNames.has(identifier)) { this.error(`'${identifier}' is a reserved word in JavaScript and cannot be used here`, start); diff --git a/src/utils/fullCharCodeAt.ts b/src/utils/fullCharCodeAt.ts new file mode 100644 index 0000000000..034da9258b --- /dev/null +++ b/src/utils/fullCharCodeAt.ts @@ -0,0 +1,10 @@ +// Adapted from https://github.com/acornjs/acorn/blob/6584815dca7440e00de841d1dad152302fdd7ca5/src/tokenize.js +// Reproduced under MIT License https://github.com/acornjs/acorn/blob/master/LICENSE + +export default function fullCharCodeAt(str: string, i: number): number { + let code = str.charCodeAt(i) + if (code <= 0xd7ff || code >= 0xe000) return code; + + let next = str.charCodeAt(i + 1); + return (code << 10) + next - 0x35fdc00; +} \ No newline at end of file diff --git a/src/utils/isValidIdentifier.ts b/src/utils/isValidIdentifier.ts index 6c86967b0a..20ceeb4bbf 100644 --- a/src/utils/isValidIdentifier.ts +++ b/src/utils/isValidIdentifier.ts @@ -1,10 +1,14 @@ import { isIdentifierStart, isIdentifierChar } from 'acorn'; +import fullCharCodeAt from './fullCharCodeAt'; export default function isValidIdentifier(str: string): boolean { - if (!isIdentifierStart(str.charCodeAt(0), true)) return false; + let i = 0; - for (let i = 0; i < str.length; i += 1) { - if (!isIdentifierChar(str.charCodeAt(i), true)) return false; + while (i < str.length) { + const code = fullCharCodeAt(str, i); + if (!(i === 0 ? isIdentifierStart : isIdentifierChar)(code, true)) return false; + + i += code <= 0xffff ? 1 : 2; } return true; diff --git a/test/parser/samples/unusual-identifier/input.html b/test/parser/samples/unusual-identifier/input.html new file mode 100644 index 0000000000..b7da53a800 --- /dev/null +++ b/test/parser/samples/unusual-identifier/input.html @@ -0,0 +1,3 @@ +{{#each things as 𐊧}} +

𐊧

+{{/each}} \ No newline at end of file diff --git a/test/parser/samples/unusual-identifier/output.json b/test/parser/samples/unusual-identifier/output.json new file mode 100644 index 0000000000..72f576c84d --- /dev/null +++ b/test/parser/samples/unusual-identifier/output.json @@ -0,0 +1,41 @@ +{ + "hash": 2991613308, + "html": { + "start": 0, + "end": 43, + "type": "Fragment", + "children": [ + { + "start": 0, + "end": 43, + "type": "EachBlock", + "expression": { + "type": "Identifier", + "start": 8, + "end": 14, + "name": "things" + }, + "children": [ + { + "start": 24, + "end": 33, + "type": "Element", + "name": "p", + "attributes": [], + "children": [ + { + "start": 27, + "end": 29, + "type": "Text", + "data": "𐊧" + } + ] + } + ], + "context": "𐊧" + } + ] + }, + "css": null, + "js": null +} \ No newline at end of file