diff --git a/src/parse/read/directives.ts b/src/parse/read/directives.ts index 99744e0244..a3ff94c68c 100644 --- a/src/parse/read/directives.ts +++ b/src/parse/read/directives.ts @@ -2,6 +2,65 @@ import { parseExpressionAt } from 'acorn'; import repeat from '../../utils/repeat'; import { Parser } from '../index'; +const DIRECTIVES = { + Ref: { + names: [ 'ref' ], + attribute(start, end, type, name) { + return { start, end, type, name }; + } + }, + + EventHandler: { + names: [ 'on' ], + allowedExpressionTypes: [ 'CallExpression' ], + }, + + Binding: { + names: [ '', 'bind' ], + allowedExpressionTypes: [ 'Identifier', 'MemberExpression' ], + attribute(start, end, type, name, expression, directiveName) { + let value; + + // :foo is shorthand for foo='{{foo}}' + if (!directiveName) { + const valueStart = start + 1; + const valueEnd = start + name.length; + type = 'Attribute'; + value = getShorthandValue(start + 1, name); + } else { + value = expression || { + type: 'Identifier', + start: start + 5, + end, + name, + }; + } + + return { start, end, type, name, value }; + }, + }, + + Transition: { + names: [ 'in', 'out', 'transition' ], + allowedExpressionTypes: [ 'ObjectExpression' ], + attribute(start, end, type, name, expression, directiveName) { + return { + start, end, type, name, expression, + intro: directiveName === 'in' || directiveName === 'transition', + outro: directiveName === 'out' || directiveName === 'transition', + } + }, + }, +}; + + +const lookupByName = {}; + +Object.keys(DIRECTIVES).forEach(name => { + const directive = DIRECTIVES[name]; + directive.names.forEach(type => lookupByName[type] = name); +}); + function readExpression(parser: Parser, start: number, quoteMark: string|null) { let str = ''; let escaped = false; @@ -24,7 +83,7 @@ function readExpression(parser: Parser, start: number, quoteMark: string|null) { } else { str += char; } - } else if (/\s/.test(char)) { + } else if (/[\s\r\n\/>]/.test(char)) { break; } else { str += char; @@ -42,125 +101,70 @@ function readExpression(parser: Parser, start: number, quoteMark: string|null) { return expression; } -export function readEventHandlerDirective( - parser: Parser, - start: number, - name: string, - hasValue: boolean -) { - let expression; - - if (hasValue) { - const quoteMark = parser.eat(`'`) ? `'` : parser.eat(`"`) ? `"` : null; - - const expressionStart = parser.index; - - expression = readExpression(parser, expressionStart, quoteMark); - - if (expression.type !== 'CallExpression') { - parser.error(`Expected call expression`, expressionStart); - } - } - - return { - start, - end: parser.index, - type: 'EventHandler', - name, - expression, - }; -} - -export function readBindingDirective( +export function readDirective( parser: Parser, start: number, - name: string + attrName: string ) { - let value; + const [ directiveName, name ] = attrName.split(':'); + if (name === undefined) return; // No colon in the name + + const type = lookupByName[directiveName]; + if (!type) return; // not a registered directive + + const directive = DIRECTIVES[type]; + let expression = null; if (parser.eat('=')) { const quoteMark = parser.eat(`'`) ? `'` : parser.eat(`"`) ? `"` : null; - const a = parser.index; + const expressionStart = parser.index; if (parser.eat('{{')) { - let message = 'bound values should not be wrapped'; - const b = parser.template.indexOf('}}', a); - if (b !== -1) { - const value = parser.template.slice(parser.index, b); + let message = 'directive values should not be wrapped'; + const expressionEnd = parser.template.indexOf('}}', expressionStart); + if (expressionEnd !== -1) { + const value = parser.template.slice(parser.index, expressionEnd); message += ` — use '${value}', not '{{${value}}}'`; } - parser.error(message, a); + parser.error(message, expressionStart); } - // this is a bit of a hack so that we can give Acorn something parseable - let b; - if (quoteMark) { - b = parser.index = parser.template.indexOf(quoteMark, parser.index); - } else { - parser.readUntil(/[\s\r\n\/>]/); - b = parser.index; - } - - const source = repeat(' ', a) + parser.template.slice(a, b); - value = parseExpressionAt(source, a, { ecmaVersion: 9 }); - - if (value.type !== 'Identifier' && value.type !== 'MemberExpression') { - parser.error(`Cannot bind to rvalue`, value.start); + expression = readExpression(parser, expressionStart, quoteMark); + if (directive.allowedExpressionTypes.indexOf(expression.type) === -1) { + parser.error(`Expected ${directive.allowedExpressionTypes.join(' or ')}`, expressionStart); } + } - parser.allowWhitespace(); - - if (quoteMark) { - parser.eat(quoteMark, true); - } + if (directive.attribute) { + return directive.attribute(start, parser.index, type, name, expression, directiveName); } else { - // shorthand – bind:foo equivalent to bind:foo='foo' - value = { - type: 'Identifier', - start: start + 5, + return { + start, end: parser.index, + type: type, name, + expression, }; } - - return { - start, - end: parser.index, - type: 'Binding', - name, - value, - }; } -export function readTransitionDirective( - parser: Parser, - start: number, - name: string, - type: string -) { - let expression = null; - - if (parser.eat('=')) { - const quoteMark = parser.eat(`'`) ? `'` : parser.eat(`"`) ? `"` : null; - - const expressionStart = parser.index; - - expression = readExpression(parser, expressionStart, quoteMark); - - if (expression.type !== 'ObjectExpression') { - parser.error(`Expected object expression`, expressionStart); - } - } - return { - start, - end: parser.index, - type: 'Transition', - name, - intro: type === 'in' || type === 'transition', - outro: type === 'out' || type === 'transition', - expression, - }; +function getShorthandValue(start: number, name: string) { + const end = start + name.length; + + return [ + { + type: 'AttributeShorthand', + start, + end, + expression: { + type: 'Identifier', + start, + end, + name, + }, + }, + ]; } diff --git a/src/parse/state/tag.ts b/src/parse/state/tag.ts index b24fa31ae3..79aa289f20 100644 --- a/src/parse/state/tag.ts +++ b/src/parse/state/tag.ts @@ -1,11 +1,7 @@ import readExpression from '../read/expression'; import readScript from '../read/script'; import readStyle from '../read/style'; -import { - readEventHandlerDirective, - readBindingDirective, - readTransitionDirective, -} from '../read/directives'; +import { readDirective } from '../read/directives'; import { trimStart, trimEnd } from '../../utils/trim'; import { decodeCharacterReferences } from '../utils/html'; import isVoidElementName from '../../utils/isVoidElementName'; @@ -303,42 +299,10 @@ function readAttribute(parser: Parser, uniqueNames: Set) { parser.allowWhitespace(); - if (/^on:/.test(name)) { - return readEventHandlerDirective(parser, start, name.slice(3), parser.eat('=')); - } - - if (/^bind:/.test(name)) { - return readBindingDirective(parser, start, name.slice(5)); - } + const attribute = readDirective(parser, start, name); + if (attribute) return attribute; - if (/^ref:/.test(name)) { - return { - start, - end: parser.index, - type: 'Ref', - name: name.slice(4), - }; - } - - const match = /^(in|out|transition):/.exec(name); - if (match) { - return readTransitionDirective( - parser, - start, - name.slice(match[0].length), - match[1] - ); - } - - let value; - - // :foo is shorthand for foo='{{foo}}' - if (/^:\w+$/.test(name)) { - name = name.slice(1); - value = getShorthandValue(start + 1, name); - } else { - value = parser.eat('=') ? readAttributeValue(parser) : true; - } + let value = parser.eat('=') ? readAttributeValue(parser) : true; return { start, @@ -364,24 +328,6 @@ function readAttributeValue(parser: Parser) { return value; } -function getShorthandValue(start: number, name: string) { - const end = start + name.length; - - return [ - { - type: 'AttributeShorthand', - start, - end, - expression: { - type: 'Identifier', - start, - end, - name, - }, - }, - ]; -} - function readSequence(parser: Parser, done: () => boolean) { let currentChunk: Node = { start: parser.index, diff --git a/test/parser/samples/error-binding-mustaches/error.json b/test/parser/samples/error-binding-mustaches/error.json index 9d67bfba1a..f4a820d6d6 100644 --- a/test/parser/samples/error-binding-mustaches/error.json +++ b/test/parser/samples/error-binding-mustaches/error.json @@ -1,5 +1,5 @@ { - "message": "bound values should not be wrapped — use 'foo', not '{{foo}}'", + "message": "directive values should not be wrapped — use 'foo', not '{{foo}}'", "loc": { "line": 1, "column": 19 diff --git a/test/parser/samples/error-binding-rvalue/error.json b/test/parser/samples/error-binding-rvalue/error.json index 85b4a6f9ac..2f25dbef0d 100644 --- a/test/parser/samples/error-binding-rvalue/error.json +++ b/test/parser/samples/error-binding-rvalue/error.json @@ -1,5 +1,5 @@ { - "message": "Cannot bind to rvalue", + "message": "Expected Identifier or MemberExpression", "pos": 19, "loc": { "line": 1, diff --git a/test/parser/samples/error-event-handler/error.json b/test/parser/samples/error-event-handler/error.json index a56e3c3b6e..45032c2707 100644 --- a/test/parser/samples/error-event-handler/error.json +++ b/test/parser/samples/error-event-handler/error.json @@ -1,5 +1,5 @@ { - "message": "Expected call expression", + "message": "Expected CallExpression", "loc": { "line": 1, "column": 15