Skip to content

Commit 89b16de

Browse files
committed
Fix: wrong ranges for HTML entities
1 parent a864dd5 commit 89b16de

19 files changed

+2152
-74
lines changed

lib/decode-html-entities.js

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/**
2+
* @author Toru Nagashima <https://github.com/mysticatea>
3+
* @copyright 2017 Toru Nagashima. All rights reserved.
4+
* See LICENSE file in root directory for full license.
5+
*/
6+
"use strict"
7+
8+
//------------------------------------------------------------------------------
9+
// Requirements
10+
//------------------------------------------------------------------------------
11+
12+
const entities = require("./entities.json")
13+
14+
//------------------------------------------------------------------------------
15+
// Helpers
16+
//------------------------------------------------------------------------------
17+
18+
const ENTITY_PATTERN = /&(#?[\w\d]+);?/g
19+
20+
/**
21+
* Replace all HTML entities.
22+
* @param {string} source The string to replace.
23+
* @param {(number[])[]} gaps The gap array. This is output.
24+
* This is the array of tuples which have 2 elements. The 1st element is the
25+
* offset of the location that gap was changed. The 2nd element is the gap size.
26+
* For example, in `a &amp;&amp; b` case, it's `[ [ 3, 4 ], [ 4, 8 ] ]`.
27+
* @returns {string} The replaced string.
28+
*/
29+
function decodeHtmlEntities(source, gaps) {
30+
let result = ""
31+
let match = null
32+
let lastIndex = 0
33+
let gap = 0
34+
35+
ENTITY_PATTERN.lastIndex = 0
36+
while ((match = ENTITY_PATTERN.exec(source)) != null) {
37+
const whole = match[0]
38+
const s = match[1]
39+
let c = ""
40+
41+
if (s[0] === "#") {
42+
const code = s[1] === "x" ?
43+
parseInt(s.slice(2).toLowerCase(), 16) :
44+
parseInt(s.slice(1), 10)
45+
46+
if (!(isNaN(code) || code < -32768 || code > 65535)) {
47+
c = String.fromCharCode(code)
48+
}
49+
}
50+
c = entities[s] || whole
51+
52+
result += source.slice(lastIndex, match.index)
53+
result += c
54+
lastIndex = match.index + whole.length
55+
if (whole.length !== c.length) {
56+
gap += (whole.length - c.length)
57+
gaps.push([result.length, gap])
58+
}
59+
}
60+
result += source.slice(lastIndex)
61+
62+
return result
63+
}
64+
65+
//------------------------------------------------------------------------------
66+
// Exports
67+
//------------------------------------------------------------------------------
68+
69+
module.exports = decodeHtmlEntities

lib/entities.json

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

lib/script-parser.js

Lines changed: 88 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
// Requirements
1010
//------------------------------------------------------------------------------
1111

12-
const entities = require("entities")
12+
const sortedIndexBy = require("lodash.sortedindexby")
13+
const decodeHtmlEntities = require("./decode-html-entities")
14+
const traverseNodes = require("./traverse-nodes")
1315

1416
//------------------------------------------------------------------------------
1517
// Helpers
@@ -18,6 +20,83 @@ const entities = require("entities")
1820
const NON_LT = /[^\r\n\u2028\u2029]/g
1921
const SPACE = /\s/
2022

23+
/**
24+
* Get the 1st element of the given array.
25+
* @param {any[]} item The array to get.
26+
* @returns {any} The 1st element.
27+
*/
28+
function first(item) {
29+
return item[0]
30+
}
31+
32+
/**
33+
* Fix the range of location of the given node.
34+
* This will expand ranges because those have shrunk by decoding HTML entities.
35+
* @param {ASTNode} node The node to fix range and location.
36+
* @param {TokenGenerator} tokenGenerator The token generator to re-calculate locations.
37+
* @param {(number[])[]} gaps The gap array to re-calculate ranges.
38+
* @param {number} codeStart The start offset of this expression.
39+
* @returns {void}
40+
*/
41+
function fixRangeAndLocByGap(node, tokenGenerator, gaps, codeStart) {
42+
const range = node.range
43+
const loc = node.loc
44+
const start = range[0] - codeStart
45+
const end = range[1] - codeStart
46+
47+
let i = sortedIndexBy(gaps, [start], first) - 1
48+
if (i >= 0) {
49+
range[0] += (i + 1 < gaps.length && gaps[i + 1][0] === start)
50+
? gaps[i + 1][1]
51+
: gaps[i][1]
52+
loc.start = tokenGenerator.getLocPart(range[0])
53+
}
54+
i = sortedIndexBy(gaps, [end], first) - 1
55+
if (i >= 0) {
56+
range[1] += (i + 1 < gaps.length && gaps[i + 1][0] === end)
57+
? gaps[i + 1][1]
58+
: gaps[i][1]
59+
loc.end = tokenGenerator.getLocPart(range[1])
60+
}
61+
}
62+
63+
/**
64+
* Do post-process of parsing an expression.
65+
*
66+
* 1. Set `node.parent`.
67+
* 2. Fix `node.range` and `node.loc` for HTML entities.
68+
*
69+
* @param {ASTNode} ast The AST root node to initialize.
70+
* @param {TokenGenerator} tokenGenerator The token generator to calculate locations.
71+
* @param {(number[])[]} gaps The gaps to re-calculate locations.
72+
* @param {number} codeStart The start offset of the expression.
73+
* @returns {void}
74+
*/
75+
function postprocess(ast, tokenGenerator, gaps, codeStart) {
76+
const gapsExist = gaps.length >= 1
77+
78+
traverseNodes(ast, {
79+
enterNode(node, parent) {
80+
node.parent = parent
81+
if (gapsExist) {
82+
fixRangeAndLocByGap(node, tokenGenerator, gaps, codeStart)
83+
}
84+
},
85+
leaveNode() {
86+
// Do nothing.
87+
},
88+
})
89+
90+
if (gapsExist) {
91+
for (const token of ast.tokens) {
92+
fixRangeAndLocByGap(token, tokenGenerator, gaps, codeStart)
93+
}
94+
for (const comment of ast.comments) {
95+
fixRangeAndLocByGap(comment, tokenGenerator, gaps, codeStart)
96+
}
97+
}
98+
}
99+
21100
/**
22101
* The script parser.
23102
*/
@@ -96,9 +175,10 @@ class ScriptParser {
96175
* Parse the script which is on the given range.
97176
* @param {number} start The start offset to parse.
98177
* @param {number} end The end offset to parse.
178+
* @param {TokenGenerator} tokenGenerator The token generator to fix loc.
99179
* @returns {ASTNode} The created AST node.
100180
*/
101-
parseExpression(start, end) {
181+
parseExpression(start, end, tokenGenerator) {
102182
const codeStart = this.getInlineScriptStart(start)
103183
const codeEnd = this.getInlineScriptEnd(end)
104184
if (codeStart >= codeEnd) {
@@ -109,8 +189,10 @@ class ScriptParser {
109189
}
110190

111191
const prefix = this.text.slice(0, codeStart - 1).replace(NON_LT, " ")
112-
const code = entities.decodeHTML(this.text.slice(codeStart, codeEnd))
113-
const ast = this._parseScript(`${prefix}(${code})`)
192+
const code = this.text.slice(codeStart, codeEnd)
193+
const gaps = []
194+
const decodedCode = decodeHtmlEntities(code, gaps)
195+
const ast = this._parseScript(`${prefix}(${decodedCode})`)
114196

115197
if (ast.body.length === 0) {
116198
throw new Error(
@@ -131,6 +213,8 @@ class ScriptParser {
131213
expression.tokens.shift()
132214
expression.tokens.pop()
133215

216+
postprocess(expression, tokenGenerator, gaps, codeStart)
217+
134218
return expression
135219
}
136220
}

lib/transform-html.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@
1010
//------------------------------------------------------------------------------
1111

1212
const debug = require("debug")("vue-eslint-parser")
13+
const decodeHtmlEntities = require("./decode-html-entities")
1314

1415
//------------------------------------------------------------------------------
1516
// Helpers
1617
//------------------------------------------------------------------------------
1718

18-
const MUSTACHE = /\{\{.+?}}/g
19+
const MUSTACHE = /\{\{[\s\S]+?}}/g
1920
const DIRECTIVE_NAME = /^(?:v-|[:@]).+[^.:@]$/
2021
const QUOTES = /^["']$/
2122
const SPACE = /\s/
@@ -147,7 +148,7 @@ class HTMLTransformer {
147148
type: text.type,
148149
range: text.range,
149150
loc: text.loc,
150-
value: text.value,
151+
value: decodeHtmlEntities(text.value, []),
151152
}
152153
}
153154

@@ -173,7 +174,8 @@ class HTMLTransformer {
173174
try {
174175
const ast = this.scriptParser.parseExpression(
175176
start + quoteSize,
176-
end - quoteSize
177+
end - quoteSize,
178+
this.tokenGenerator
177179
)
178180
if (ast != null) {
179181
const tokens = ast.tokens || []
@@ -298,7 +300,6 @@ class HTMLTransformer {
298300
range: literal.range,
299301
loc: literal.loc,
300302
value,
301-
raw: literal.value,
302303
}
303304
}
304305

@@ -439,7 +440,7 @@ class HTMLTransformer {
439440
type: "VComment",
440441
range: [start, end],
441442
loc: this.getLoc(start, end),
442-
value: this.text.slice(start + 4, end - 3),
443+
value: decodeHtmlEntities(this.text.slice(start + 4, end - 3), []),
443444
}
444445

445446
this.comments.push(comment)

lib/traverse-nodes.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ function getFallbackKeys(node) {
125125
* Traverse the given node.
126126
* `NodeEventGenerator` supports AST selectors!
127127
* @param {ASTNode} node The node to traverse.
128+
* @param {ASTNode|null} parent The parent node.
128129
* @param {NodeEventGenerator} generator The event generator.
129130
* @returns {void}
130131
*/

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@
2424
},
2525
"dependencies": {
2626
"debug": "^2.6.3",
27-
"entities": "^1.1.1",
2827
"espree": ">=3.3.2",
2928
"lodash.sortedindex": "^4.1.0",
29+
"lodash.sortedindexby": "^4.6.0",
3030
"parse5": "^3.0.0"
3131
},
3232
"devDependencies": {

test/fixtures/template-ast/attributes.ast.json

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -485,8 +485,7 @@
485485
"column": 12
486486
}
487487
},
488-
"value": "b",
489-
"raw": "b"
488+
"value": "b"
490489
}
491490
}
492491
],
@@ -649,8 +648,7 @@
649648
"column": 14
650649
}
651650
},
652-
"value": "b",
653-
"raw": "\"b\""
651+
"value": "b"
654652
}
655653
}
656654
],
@@ -813,8 +811,7 @@
813811
"column": 14
814812
}
815813
},
816-
"value": "b",
817-
"raw": "'b'"
814+
"value": "b"
818815
}
819816
}
820817
],

test/fixtures/template-ast/directives.ast.json

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -485,8 +485,7 @@
485485
"column": 14
486486
}
487487
},
488-
"value": "b",
489-
"raw": "b"
488+
"value": "b"
490489
}
491490
}
492491
],
@@ -649,8 +648,7 @@
649648
"column": 16
650649
}
651650
},
652-
"value": "b",
653-
"raw": "\"b\""
651+
"value": "b"
654652
}
655653
}
656654
],
@@ -813,8 +811,7 @@
813811
"column": 16
814812
}
815813
},
816-
"value": "b",
817-
"raw": "'b'"
814+
"value": "b"
818815
}
819816
}
820817
],
@@ -977,8 +974,7 @@
977974
"column": 15
978975
}
979976
},
980-
"value": "",
981-
"raw": "\"\""
977+
"value": ""
982978
}
983979
}
984980
],
@@ -1141,8 +1137,7 @@
11411137
"column": 15
11421138
}
11431139
},
1144-
"value": "",
1145-
"raw": "''"
1140+
"value": ""
11461141
}
11471142
}
11481143
],

0 commit comments

Comments
 (0)