Skip to content

Commit 68c026d

Browse files
author
Guillaume Chau
committed
Don't use regexes for parsing root html tags
1 parent 5be84d8 commit 68c026d

File tree

2 files changed

+127
-54
lines changed

2 files changed

+127
-54
lines changed

packages/vue-component/package.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Package.describe({
22
name: 'akryum:vue-component',
3-
version: '0.8.17',
3+
version: '0.8.18',
44
summary: 'VueJS single-file components that hot-reloads',
55
git: 'https://github.com/Akryum/meteor-vue-component',
66
documentation: 'README.md'
@@ -12,6 +12,7 @@ Package.registerBuildPlugin({
1212
'ecmascript@0.6.1',
1313
'caching-compiler@1.1.9',
1414
'babel-compiler@6.13.0',
15+
'templating-tools@1.1.2',
1516
],
1617
sources: [
1718
'plugin/regexps.js',

packages/vue-component/plugin/tag-scanner.js

Lines changed: 125 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -25,73 +25,145 @@ class HtmlScan {
2525
tagNames
2626
}) {
2727
this.sourceName = sourceName;
28-
this.originalContents = contents;
29-
this.contents = normalizeCarriageReturns(contents).replace(tagCommentRegex, '');
28+
this.contents = contents;
3029
this.tagNames = tagNames;
3130

31+
this.rest = contents;
32+
this.index = 0;
33+
3234
this.tags = [];
3335

34-
let result;
36+
tagNameRegex = this.tagNames.join("|");
37+
const openTagRegex = new RegExp(`^((<(${tagNameRegex})\\b)|(<!--)|(<!DOCTYPE|{{!)|$)`, "i");
3538

36-
// Unique tags: Template & Script
37-
while(result = expandedTagRegex.exec(this.contents)) {
38-
this.addTagFromResult(result);
39-
}
39+
while (this.rest) {
40+
// skip whitespace first (for better line numbers)
41+
this.advance(this.rest.match(/^\s*/)[0].length);
4042

41-
// Multiple styles
42-
while(result = limitedTagRegex.exec(this.contents)) {
43-
this.addTagFromResult(result);
44-
}
45-
}
43+
const match = openTagRegex.exec(this.rest);
44+
45+
if (! match) {
46+
this.throwCompileError(`Expected one of: <${this.tagNames.join('>, <')}>`);
47+
}
4648

47-
addTagFromResult(result) {
48-
let tagName = result[1];
49-
let attrs = result[2];
50-
let tagContents = result[3];
49+
const matchToken = match[1];
50+
const matchTokenTagName = match[3];
51+
const matchTokenComment = match[4];
52+
const matchTokenUnsupported = match[5];
5153

52-
let tagAttribs = {};
53-
if(attrs) {
54-
let attr;
55-
while(attr = attrsRegex.exec(attrs)) {
56-
let attrValue;
57-
if(attr.length === 5) {
58-
attrValue = attr[4];
59-
if(attrValue === undefined) {
60-
attrValue = true;
61-
}
62-
} else {
63-
attrValue = true;
64-
}
65-
tagAttribs[attr[1]] = attrValue;
54+
const tagStartIndex = this.index;
55+
this.advance(match.index + match[0].length);
56+
57+
if (! matchToken) {
58+
break; // matched $ (end of file)
6659
}
67-
}
6860

69-
const originalContents = this.originalContents;
70-
71-
const tag = {
72-
tagName: tagName,
73-
attribs: tagAttribs,
74-
contents: tagContents,
75-
fileContents: this.contents,
76-
sourceName: this.sourceName,
77-
_tagStartIndex: null,
78-
get tagStartIndex() {
79-
if(this._tagStartIndex === null) {
80-
this._tagStartIndex = originalContents.indexOf(tagContents.substr(0, 10));
61+
if (matchTokenComment === '<!--') {
62+
// top-level HTML comment
63+
const commentEnd = /--\s*>/.exec(this.rest);
64+
if (! commentEnd)
65+
this.throwCompileError("unclosed HTML comment in template file");
66+
this.advance(commentEnd.index + commentEnd[0].length);
67+
continue;
68+
}
69+
70+
if (matchTokenUnsupported) {
71+
switch (matchTokenUnsupported.toLowerCase()) {
72+
case '<!doctype':
73+
this.throwCompileError(
74+
"Can't set DOCTYPE here. (Meteor sets <!DOCTYPE html> for you)");
75+
case '{{!':
76+
this.throwCompileError(
77+
"Can't use '{{! }}' outside a template. Use '<!-- -->'.");
8178
}
82-
return this._tagStartIndex;
83-
},
84-
_tagEndIndex: null,
85-
get tagEndIndex() {
86-
if(this._tagEndIndex === null) {
87-
this._tagEndIndex = originalContents.indexOf('</script>') - 1;
79+
80+
this.throwCompileError();
81+
}
82+
83+
// otherwise, a <tag>
84+
const tagName = matchTokenTagName.toLowerCase();
85+
const tagAttribs = {}; // bare name -> value dict
86+
const tagPartRegex = /^\s*((([a-zA-Z0-9:_-]+)\s*(=\s*(["'])(.*?)\5)?)|(>))/;
87+
88+
// read attributes
89+
let attr;
90+
while ((attr = tagPartRegex.exec(this.rest))) {
91+
const attrToken = attr[1];
92+
const attrKey = attr[3];
93+
let attrValue = attr[6];
94+
this.advance(attr.index + attr[0].length);
95+
96+
if (attrToken === '>') {
97+
break;
8898
}
89-
return this._tagEndIndex;
99+
100+
// XXX we don't HTML unescape the attribute value
101+
// (e.g. to allow "abcd&quot;efg") or protect against
102+
// collisions with methods of tagAttribs (e.g. for
103+
// a property named toString)
104+
attrValue = attrValue && attrValue.match(/^\s*([\s\S]*?)\s*$/)[1]; // trim
105+
tagAttribs[attrKey] = attrValue;
90106
}
91-
};
92107

93-
// save the tag
94-
this.tags.push(tag);
108+
if (! attr) { // didn't end on '>'
109+
this.throwCompileError(`Parse error in tag ${tagName}`);
110+
}
111+
112+
// find </tag>
113+
const end = (new RegExp('</'+tagName+'\\s*>', 'i')).exec(this.rest);
114+
if (! end) {
115+
this.throwCompileError("unclosed <"+tagName+">");
116+
}
117+
118+
const tagContents = this.rest.slice(0, end.index);
119+
const contentsStartIndex = this.index;
120+
121+
// trim the tag contents.
122+
// this is a courtesy and is also relied on by some unit tests.
123+
var m = tagContents.match(/^([ \t\r\n]*)([\s\S]*?)[ \t\r\n]*$/);
124+
const trimmedContentsStartIndex = contentsStartIndex + m[1].length;
125+
const trimmedTagContents = m[2];
126+
127+
const tag = {
128+
tagName: tagName,
129+
attribs: tagAttribs,
130+
contents: trimmedTagContents,
131+
contentsStartIndex: trimmedContentsStartIndex,
132+
tagStartIndex: tagStartIndex,
133+
fileContents: this.contents,
134+
sourceName: this.sourceName
135+
};
136+
137+
// save the tag
138+
this.tags.push(tag);
139+
140+
// advance afterwards, so that line numbers in errors are correct
141+
this.advance(end.index + end[0].length);
142+
}
143+
}
144+
145+
/**
146+
* Advance the parser
147+
* @param {Number} amount The amount of characters to advance
148+
*/
149+
advance(amount) {
150+
this.rest = this.rest.substring(amount);
151+
this.index += amount;
152+
}
153+
154+
throwCompileError(msg, overrideIndex) {
155+
const finalIndex = (typeof overrideIndex === 'number' ? overrideIndex : this.index);
156+
157+
const err = new TemplatingTools.CompileError();
158+
err.message = msg || "bad formatting in template file";
159+
err.file = this.sourceName;
160+
err.line = this.contents.substring(0, finalIndex).split('\n').length;
161+
162+
throw err;
163+
}
164+
165+
throwBodyAttrsError(msg) {
166+
this.parseError(msg);
95167
}
96168

97169
getTags() {

0 commit comments

Comments
 (0)