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 { 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(
export function readDirective(
parser: Parser,
start: number,
name: string,
hasValue: boolean
attrName: string
) {
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 type = lookupByName[directiveName];
if (!type) return; // not a registered directive
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(
parser: Parser,
start: number,
name: string
) {
let value;
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;
expression = readExpression(parser, expressionStart, quoteMark);
if (directive.allowedExpressionTypes.indexOf(expression.type) === -1) {
parser.error(`Expected ${directive.allowedExpressionTypes.join(' or ')}`, expressionStart);
}
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 (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,
end: parser.index,
name,
};
}
return {
start,
end: parser.index,
type: 'Binding',
type: type,
name,
value,
expression,
};
}
}
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);
function getShorthandValue(start: number, name: string) {
const end = start + name.length;
if (expression.type !== 'ObjectExpression') {
parser.error(`Expected object expression`, expressionStart);
}
}
return {
return [
{
type: 'AttributeShorthand',
start,
end: parser.index,
type: 'Transition',
end,
expression: {
type: 'Identifier',
start,
end,
name,
intro: type === 'in' || type === 'transition',
outro: type === 'out' || type === 'transition',
expression,
};
},
},
];
}

@ -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<string>) {
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));
}
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;
const attribute = readDirective(parser, start, name);
if (attribute) return attribute;
// :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,

@ -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

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

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

Loading…
Cancel
Save