Skip to content

Commit 6f866bb

Browse files
authored
Merge pull request #1242 from jacwright/directive-parsing-cleanup
Refactor directive parsing for code reuse
2 parents ef1e2d0 + 6d4f8d8 commit 6f866bb

File tree

5 files changed

+109
-159
lines changed

5 files changed

+109
-159
lines changed

src/parse/read/directives.ts

+102-98
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,65 @@ import { parseExpressionAt } from 'acorn';
22
import repeat from '../../utils/repeat';
33
import { Parser } from '../index';
44

5+
const DIRECTIVES = {
6+
Ref: {
7+
names: [ 'ref' ],
8+
attribute(start, end, type, name) {
9+
return { start, end, type, name };
10+
}
11+
},
12+
13+
EventHandler: {
14+
names: [ 'on' ],
15+
allowedExpressionTypes: [ 'CallExpression' ],
16+
},
17+
18+
Binding: {
19+
names: [ '', 'bind' ],
20+
allowedExpressionTypes: [ 'Identifier', 'MemberExpression' ],
21+
attribute(start, end, type, name, expression, directiveName) {
22+
let value;
23+
24+
// :foo is shorthand for foo='{{foo}}'
25+
if (!directiveName) {
26+
const valueStart = start + 1;
27+
const valueEnd = start + name.length;
28+
type = 'Attribute';
29+
value = getShorthandValue(start + 1, name);
30+
} else {
31+
value = expression || {
32+
type: 'Identifier',
33+
start: start + 5,
34+
end,
35+
name,
36+
};
37+
}
38+
39+
return { start, end, type, name, value };
40+
},
41+
},
42+
43+
Transition: {
44+
names: [ 'in', 'out', 'transition' ],
45+
allowedExpressionTypes: [ 'ObjectExpression' ],
46+
attribute(start, end, type, name, expression, directiveName) {
47+
return {
48+
start, end, type, name, expression,
49+
intro: directiveName === 'in' || directiveName === 'transition',
50+
outro: directiveName === 'out' || directiveName === 'transition',
51+
}
52+
},
53+
},
54+
};
55+
56+
57+
const lookupByName = {};
58+
59+
Object.keys(DIRECTIVES).forEach(name => {
60+
const directive = DIRECTIVES[name];
61+
directive.names.forEach(type => lookupByName[type] = name);
62+
});
63+
564
function readExpression(parser: Parser, start: number, quoteMark: string|null) {
665
let str = '';
766
let escaped = false;
@@ -24,7 +83,7 @@ function readExpression(parser: Parser, start: number, quoteMark: string|null) {
2483
} else {
2584
str += char;
2685
}
27-
} else if (/\s/.test(char)) {
86+
} else if (/[\s\r\n\/>]/.test(char)) {
2887
break;
2988
} else {
3089
str += char;
@@ -42,125 +101,70 @@ function readExpression(parser: Parser, start: number, quoteMark: string|null) {
42101
return expression;
43102
}
44103

45-
export function readEventHandlerDirective(
46-
parser: Parser,
47-
start: number,
48-
name: string,
49-
hasValue: boolean
50-
) {
51-
let expression;
52-
53-
if (hasValue) {
54-
const quoteMark = parser.eat(`'`) ? `'` : parser.eat(`"`) ? `"` : null;
55-
56-
const expressionStart = parser.index;
57-
58-
expression = readExpression(parser, expressionStart, quoteMark);
59-
60-
if (expression.type !== 'CallExpression') {
61-
parser.error(`Expected call expression`, expressionStart);
62-
}
63-
}
64-
65-
return {
66-
start,
67-
end: parser.index,
68-
type: 'EventHandler',
69-
name,
70-
expression,
71-
};
72-
}
73-
74-
export function readBindingDirective(
104+
export function readDirective(
75105
parser: Parser,
76106
start: number,
77-
name: string
107+
attrName: string
78108
) {
79-
let value;
109+
const [ directiveName, name ] = attrName.split(':');
110+
if (name === undefined) return; // No colon in the name
111+
112+
const type = lookupByName[directiveName];
113+
if (!type) return; // not a registered directive
114+
115+
const directive = DIRECTIVES[type];
116+
let expression = null;
80117

81118
if (parser.eat('=')) {
82119
const quoteMark = parser.eat(`'`) ? `'` : parser.eat(`"`) ? `"` : null;
83120

84-
const a = parser.index;
121+
const expressionStart = parser.index;
85122

86123
if (parser.eat('{{')) {
87-
let message = 'bound values should not be wrapped';
88-
const b = parser.template.indexOf('}}', a);
89-
if (b !== -1) {
90-
const value = parser.template.slice(parser.index, b);
124+
let message = 'directive values should not be wrapped';
125+
const expressionEnd = parser.template.indexOf('}}', expressionStart);
126+
if (expressionEnd !== -1) {
127+
const value = parser.template.slice(parser.index, expressionEnd);
91128
message += ` — use '${value}', not '{{${value}}}'`;
92129
}
93130

94-
parser.error(message, a);
131+
parser.error(message, expressionStart);
95132
}
96133

97-
// this is a bit of a hack so that we can give Acorn something parseable
98-
let b;
99-
if (quoteMark) {
100-
b = parser.index = parser.template.indexOf(quoteMark, parser.index);
101-
} else {
102-
parser.readUntil(/[\s\r\n\/>]/);
103-
b = parser.index;
104-
}
105-
106-
const source = repeat(' ', a) + parser.template.slice(a, b);
107-
value = parseExpressionAt(source, a, { ecmaVersion: 9 });
108-
109-
if (value.type !== 'Identifier' && value.type !== 'MemberExpression') {
110-
parser.error(`Cannot bind to rvalue`, value.start);
134+
expression = readExpression(parser, expressionStart, quoteMark);
135+
if (directive.allowedExpressionTypes.indexOf(expression.type) === -1) {
136+
parser.error(`Expected ${directive.allowedExpressionTypes.join(' or ')}`, expressionStart);
111137
}
138+
}
112139

113-
parser.allowWhitespace();
114-
115-
if (quoteMark) {
116-
parser.eat(quoteMark, true);
117-
}
140+
if (directive.attribute) {
141+
return directive.attribute(start, parser.index, type, name, expression, directiveName);
118142
} else {
119-
// shorthand – bind:foo equivalent to bind:foo='foo'
120-
value = {
121-
type: 'Identifier',
122-
start: start + 5,
143+
return {
144+
start,
123145
end: parser.index,
146+
type: type,
124147
name,
148+
expression,
125149
};
126150
}
127-
128-
return {
129-
start,
130-
end: parser.index,
131-
type: 'Binding',
132-
name,
133-
value,
134-
};
135151
}
136152

137-
export function readTransitionDirective(
138-
parser: Parser,
139-
start: number,
140-
name: string,
141-
type: string
142-
) {
143-
let expression = null;
144-
145-
if (parser.eat('=')) {
146-
const quoteMark = parser.eat(`'`) ? `'` : parser.eat(`"`) ? `"` : null;
147-
148-
const expressionStart = parser.index;
149-
150-
expression = readExpression(parser, expressionStart, quoteMark);
151-
152-
if (expression.type !== 'ObjectExpression') {
153-
parser.error(`Expected object expression`, expressionStart);
154-
}
155-
}
156153

157-
return {
158-
start,
159-
end: parser.index,
160-
type: 'Transition',
161-
name,
162-
intro: type === 'in' || type === 'transition',
163-
outro: type === 'out' || type === 'transition',
164-
expression,
165-
};
154+
function getShorthandValue(start: number, name: string) {
155+
const end = start + name.length;
156+
157+
return [
158+
{
159+
type: 'AttributeShorthand',
160+
start,
161+
end,
162+
expression: {
163+
type: 'Identifier',
164+
start,
165+
end,
166+
name,
167+
},
168+
},
169+
];
166170
}

src/parse/state/tag.ts

+4-58
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
11
import readExpression from '../read/expression';
22
import readScript from '../read/script';
33
import readStyle from '../read/style';
4-
import {
5-
readEventHandlerDirective,
6-
readBindingDirective,
7-
readTransitionDirective,
8-
} from '../read/directives';
4+
import { readDirective } from '../read/directives';
95
import { trimStart, trimEnd } from '../../utils/trim';
106
import { decodeCharacterReferences } from '../utils/html';
117
import isVoidElementName from '../../utils/isVoidElementName';
@@ -303,42 +299,10 @@ function readAttribute(parser: Parser, uniqueNames: Set<string>) {
303299

304300
parser.allowWhitespace();
305301

306-
if (/^on:/.test(name)) {
307-
return readEventHandlerDirective(parser, start, name.slice(3), parser.eat('='));
308-
}
309-
310-
if (/^bind:/.test(name)) {
311-
return readBindingDirective(parser, start, name.slice(5));
312-
}
302+
const attribute = readDirective(parser, start, name);
303+
if (attribute) return attribute;
313304

314-
if (/^ref:/.test(name)) {
315-
return {
316-
start,
317-
end: parser.index,
318-
type: 'Ref',
319-
name: name.slice(4),
320-
};
321-
}
322-
323-
const match = /^(in|out|transition):/.exec(name);
324-
if (match) {
325-
return readTransitionDirective(
326-
parser,
327-
start,
328-
name.slice(match[0].length),
329-
match[1]
330-
);
331-
}
332-
333-
let value;
334-
335-
// :foo is shorthand for foo='{{foo}}'
336-
if (/^:\w+$/.test(name)) {
337-
name = name.slice(1);
338-
value = getShorthandValue(start + 1, name);
339-
} else {
340-
value = parser.eat('=') ? readAttributeValue(parser) : true;
341-
}
305+
let value = parser.eat('=') ? readAttributeValue(parser) : true;
342306

343307
return {
344308
start,
@@ -364,24 +328,6 @@ function readAttributeValue(parser: Parser) {
364328
return value;
365329
}
366330

367-
function getShorthandValue(start: number, name: string) {
368-
const end = start + name.length;
369-
370-
return [
371-
{
372-
type: 'AttributeShorthand',
373-
start,
374-
end,
375-
expression: {
376-
type: 'Identifier',
377-
start,
378-
end,
379-
name,
380-
},
381-
},
382-
];
383-
}
384-
385331
function readSequence(parser: Parser, done: () => boolean) {
386332
let currentChunk: Node = {
387333
start: parser.index,

test/parser/samples/error-binding-mustaches/error.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"message": "bound values should not be wrapped — use 'foo', not '{{foo}}'",
2+
"message": "directive values should not be wrapped — use 'foo', not '{{foo}}'",
33
"loc": {
44
"line": 1,
55
"column": 19

test/parser/samples/error-binding-rvalue/error.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"message": "Cannot bind to rvalue",
2+
"message": "Expected Identifier or MemberExpression",
33
"pos": 19,
44
"loc": {
55
"line": 1,

test/parser/samples/error-event-handler/error.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"message": "Expected call expression",
2+
"message": "Expected CallExpression",
33
"loc": {
44
"line": 1,
55
"column": 15

0 commit comments

Comments
 (0)