Refactor directive parsing for code reuse

This removes the copy-pasta for directive parsing and removes the need for special if-else cases for each directive.
pull/1242/head
Jacob Wright 7 years ago
parent 0ac77019d0
commit 6d4f8d889a

@ -2,6 +2,65 @@ import { parseExpressionAt } from 'acorn';
import repeat from '../../utils/repeat'; import repeat from '../../utils/repeat';
import { Parser } from '../index'; 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) { function readExpression(parser: Parser, start: number, quoteMark: string|null) {
let str = ''; let str = '';
let escaped = false; let escaped = false;
@ -24,7 +83,7 @@ function readExpression(parser: Parser, start: number, quoteMark: string|null) {
} else { } else {
str += char; str += char;
} }
} else if (/\s/.test(char)) { } else if (/[\s\r\n\/>]/.test(char)) {
break; break;
} else { } else {
str += char; str += char;
@ -42,125 +101,70 @@ function readExpression(parser: Parser, start: number, quoteMark: string|null) {
return expression; return expression;
} }
export function readEventHandlerDirective( export function readDirective(
parser: Parser, parser: Parser,
start: number, start: number,
name: string, attrName: string
hasValue: boolean
) { ) {
let expression; const [ directiveName, name ] = attrName.split(':');
if (name === undefined) return; // No colon in the name
if (hasValue) {
const quoteMark = parser.eat(`'`) ? `'` : parser.eat(`"`) ? `"` : null;
const expressionStart = parser.index; const type = lookupByName[directiveName];
if (!type) return; // not a registered directive
expression = readExpression(parser, expressionStart, quoteMark); const directive = DIRECTIVES[type];
let expression = null;
if (expression.type !== 'CallExpression') {
parser.error(`Expected call expression`, expressionStart);
}
}
return {
start,
end: parser.index,
type: 'EventHandler',
name,
expression,
};
}
export function readBindingDirective(
parser: Parser,
start: number,
name: string
) {
let value;
if (parser.eat('=')) { if (parser.eat('=')) {
const quoteMark = parser.eat(`'`) ? `'` : parser.eat(`"`) ? `"` : null; const quoteMark = parser.eat(`'`) ? `'` : parser.eat(`"`) ? `"` : null;
const a = parser.index; const expressionStart = parser.index;
if (parser.eat('{{')) { if (parser.eat('{{')) {
let message = 'bound values should not be wrapped'; let message = 'directive values should not be wrapped';
const b = parser.template.indexOf('}}', a); const expressionEnd = parser.template.indexOf('}}', expressionStart);
if (b !== -1) { if (expressionEnd !== -1) {
const value = parser.template.slice(parser.index, b); const value = parser.template.slice(parser.index, expressionEnd);
message += ` — use '${value}', not '{{${value}}}'`; 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 expression = readExpression(parser, expressionStart, quoteMark);
let b; if (directive.allowedExpressionTypes.indexOf(expression.type) === -1) {
if (quoteMark) { parser.error(`Expected ${directive.allowedExpressionTypes.join(' or ')}`, expressionStart);
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);
} }
}
parser.allowWhitespace(); if (directive.attribute) {
return directive.attribute(start, parser.index, type, name, expression, directiveName);
if (quoteMark) {
parser.eat(quoteMark, true);
}
} else { } else {
// shorthand bind:foo equivalent to bind:foo='foo' return {
value = { start,
type: 'Identifier',
start: start + 5,
end: parser.index, end: parser.index,
type: type,
name, 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 { function getShorthandValue(start: number, name: string) {
start, const end = start + name.length;
end: parser.index,
type: 'Transition', return [
name, {
intro: type === 'in' || type === 'transition', type: 'AttributeShorthand',
outro: type === 'out' || type === 'transition', start,
expression, end,
}; expression: {
type: 'Identifier',
start,
end,
name,
},
},
];
} }

@ -1,11 +1,7 @@
import readExpression from '../read/expression'; import readExpression from '../read/expression';
import readScript from '../read/script'; import readScript from '../read/script';
import readStyle from '../read/style'; import readStyle from '../read/style';
import { import { readDirective } from '../read/directives';
readEventHandlerDirective,
readBindingDirective,
readTransitionDirective,
} from '../read/directives';
import { trimStart, trimEnd } from '../../utils/trim'; import { trimStart, trimEnd } from '../../utils/trim';
import { decodeCharacterReferences } from '../utils/html'; import { decodeCharacterReferences } from '../utils/html';
import isVoidElementName from '../../utils/isVoidElementName'; import isVoidElementName from '../../utils/isVoidElementName';
@ -303,42 +299,10 @@ function readAttribute(parser: Parser, uniqueNames: Set<string>) {
parser.allowWhitespace(); parser.allowWhitespace();
if (/^on:/.test(name)) { const attribute = readDirective(parser, start, name);
return readEventHandlerDirective(parser, start, name.slice(3), parser.eat('=')); if (attribute) return attribute;
}
if (/^bind:/.test(name)) {
return readBindingDirective(parser, start, name.slice(5));
}
if (/^ref:/.test(name)) { let value = parser.eat('=') ? readAttributeValue(parser) : true;
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;
}
return { return {
start, start,
@ -364,24 +328,6 @@ function readAttributeValue(parser: Parser) {
return value; 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) { function readSequence(parser: Parser, done: () => boolean) {
let currentChunk: Node = { let currentChunk: Node = {
start: parser.index, start: parser.index,

@ -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": { "loc": {
"line": 1, "line": 1,
"column": 19 "column": 19

@ -1,5 +1,5 @@
{ {
"message": "Cannot bind to rvalue", "message": "Expected Identifier or MemberExpression",
"pos": 19, "pos": 19,
"loc": { "loc": {
"line": 1, "line": 1,

@ -1,5 +1,5 @@
{ {
"message": "Expected call expression", "message": "Expected CallExpression",
"loc": { "loc": {
"line": 1, "line": 1,
"column": 15 "column": 15

Loading…
Cancel
Save