Skip to content

Commit 097889f

Browse files
committed
proper fix for attribute decode in PhantomJS
1 parent 3ce450b commit 097889f

File tree

6 files changed

+62
-41
lines changed

6 files changed

+62
-41
lines changed

src/compiler/parser/entity-decoder.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@
22

33
const decoder = document.createElement('div')
44

5-
export function decodeHTML (html: string): string {
5+
export function decodeHTML (html: string, asAttribute?: boolean): string {
6+
if (asAttribute) {
7+
html = html
8+
.replace(/</g, '&lt;')
9+
.replace(/>/g, '&gt;')
10+
}
611
decoder.innerHTML = html
712
return decoder.textContent
813
}

src/compiler/parser/html-parser.js

Lines changed: 27 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,7 @@ import { isNonPhrasingTag, canBeLeftOpenTag } from 'web/util/index'
1515

1616
// Regular Expressions for parsing tags and attributes
1717
const singleAttrIdentifier = /([^\s"'<>\/=]+)/
18-
const singleAttrAssign = /=/
19-
const singleAttrAssigns = [singleAttrAssign]
18+
const singleAttrAssign = /(?:=)/
2019
const singleAttrValues = [
2120
// attr value double quotes
2221
/"([^"]*)"+/.source,
@@ -25,6 +24,12 @@ const singleAttrValues = [
2524
// attr value, no quotes
2625
/([^\s"'=<>`]+)/.source
2726
]
27+
const attribute = new RegExp(
28+
'^\\s*' + singleAttrIdentifier.source +
29+
'(?:\\s*(' + singleAttrAssign.source + ')' +
30+
'\\s*(?:' + singleAttrValues.join('|') + '))?'
31+
)
32+
2833
// could use https://www.w3.org/TR/1999/REC-xml-names-19990114/#NT-QName
2934
// but for Vue templates we can enforce a simple charset
3035
const ncname = '[a-zA-Z_][\\w\\-\\.]*'
@@ -44,24 +49,11 @@ const isSpecialTag = makeMap('script,style', true)
4449

4550
const reCache = {}
4651

47-
function attrForHandler (handler) {
48-
const pattern = singleAttrIdentifier.source +
49-
'(?:\\s*(' + joinSingleAttrAssigns(handler) + ')' +
50-
'\\s*(?:' + singleAttrValues.join('|') + '))?'
51-
return new RegExp('^\\s*' + pattern)
52-
}
53-
54-
function joinSingleAttrAssigns (handler) {
55-
return singleAttrAssigns.map(function (assign) {
56-
return '(?:' + assign.source + ')'
57-
}).join('|')
58-
}
59-
60-
export function parseHTML (html, handler) {
52+
export function parseHTML (html, options) {
6153
const stack = []
62-
const attribute = attrForHandler(handler)
63-
const expectHTML = handler.expectHTML
64-
const isUnaryTag = handler.isUnaryTag || no
54+
const expectHTML = options.expectHTML
55+
const isUnaryTag = options.isUnaryTag || no
56+
const shouldDecodeAttr = options.shouldDecodeAttr
6557
let index = 0
6658
let last, lastTag
6759
while (html) {
@@ -93,9 +85,6 @@ export function parseHTML (html, handler) {
9385
// Doctype:
9486
const doctypeMatch = html.match(doctype)
9587
if (doctypeMatch) {
96-
if (handler.doctype) {
97-
handler.doctype(doctypeMatch[0])
98-
}
9988
advance(doctypeMatch[0].length)
10089
continue
10190
}
@@ -126,8 +115,8 @@ export function parseHTML (html, handler) {
126115
html = ''
127116
}
128117

129-
if (handler.chars) {
130-
handler.chars(text)
118+
if (options.chars) {
119+
options.chars(text)
131120
}
132121
} else {
133122
const stackedTag = lastTag.toLowerCase()
@@ -140,8 +129,8 @@ export function parseHTML (html, handler) {
140129
.replace(/<!--([\s\S]*?)-->/g, '$1')
141130
.replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, '$1')
142131
}
143-
if (handler.chars) {
144-
handler.chars(text)
132+
if (options.chars) {
133+
options.chars(text)
145134
}
146135
return ''
147136
})
@@ -211,9 +200,10 @@ export function parseHTML (html, handler) {
211200
if (args[4] === '') { delete args[4] }
212201
if (args[5] === '') { delete args[5] }
213202
}
203+
const value = args[3] || args[4] || args[5] || ''
214204
attrs[i] = {
215205
name: args[1],
216-
value: decodeHTML(args[3] || args[4] || args[5] || '')
206+
value: shouldDecodeAttr ? decodeHTML(value, true) : value
217207
}
218208
}
219209

@@ -223,8 +213,8 @@ export function parseHTML (html, handler) {
223213
unarySlash = ''
224214
}
225215

226-
if (handler.start) {
227-
handler.start(tagName, attrs, unary, match.start, match.end)
216+
if (options.start) {
217+
options.start(tagName, attrs, unary, match.start, match.end)
228218
}
229219
}
230220

@@ -249,24 +239,24 @@ export function parseHTML (html, handler) {
249239
if (pos >= 0) {
250240
// Close all the open elements, up the stack
251241
for (let i = stack.length - 1; i >= pos; i--) {
252-
if (handler.end) {
253-
handler.end(stack[i].tag, start, end)
242+
if (options.end) {
243+
options.end(stack[i].tag, start, end)
254244
}
255245
}
256246

257247
// Remove the open elements from the stack
258248
stack.length = pos
259249
lastTag = pos && stack[pos - 1].tag
260250
} else if (tagName.toLowerCase() === 'br') {
261-
if (handler.start) {
262-
handler.start(tagName, [], true, start, end)
251+
if (options.start) {
252+
options.start(tagName, [], true, start, end)
263253
}
264254
} else if (tagName.toLowerCase() === 'p') {
265-
if (handler.start) {
266-
handler.start(tagName, [], false, start, end)
255+
if (options.start) {
256+
options.start(tagName, [], false, start, end)
267257
}
268-
if (handler.end) {
269-
handler.end(tagName, start, end)
258+
if (options.end) {
259+
options.end(tagName, start, end)
270260
}
271261
}
272262
}

src/compiler/parser/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export function parse (
5454
parseHTML(template, {
5555
expectHTML: options.expectHTML,
5656
isUnaryTag: options.isUnaryTag,
57+
shouldDecodeAttr: options.shouldDecodeAttr,
5758
start (tag, attrs, unary) {
5859
// check namespace.
5960
// inherit parent ns if there is one

src/entries/web-runtime-with-compiler.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import Vue from './web-runtime'
44
import { warn, cached } from 'core/util/index'
5-
import { query } from 'web/util/index'
5+
import { query, shouldDecodeAttr } from 'web/util/index'
66
import { compileToFunctions } from 'web/compiler/index'
77

88
const idToTemplate = cached(id => {
@@ -20,12 +20,15 @@ Vue.prototype.$mount = function (
2020
// resolve template/el and convert to render function
2121
if (!options.render) {
2222
let template = options.template
23+
let isFromDOM = false
2324
if (template) {
2425
if (typeof template === 'string') {
2526
if (template.charAt(0) === '#') {
27+
isFromDOM = true
2628
template = idToTemplate(template)
2729
}
2830
} else if (template.nodeType) {
31+
isFromDOM = true
2932
template = template.innerHTML
3033
} else {
3134
if (process.env.NODE_ENV !== 'production') {
@@ -34,10 +37,12 @@ Vue.prototype.$mount = function (
3437
return this
3538
}
3639
} else if (el) {
40+
isFromDOM = true
3741
template = getOuterHTML(el)
3842
}
3943
if (template) {
4044
const { render, staticRenderFns } = compileToFunctions(template, {
45+
shouldDecodeAttr: isFromDOM && shouldDecodeAttr,
4146
delimiters: options.delimiters,
4247
warn
4348
}, this)

src/platforms/web/util/index.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ export const isIE = UA && /msie|trident/.test(UA)
1111
export const isIE9 = UA && UA.indexOf('msie 9.0') > 0
1212
export const isAndroid = UA && UA.indexOf('android') > 0
1313

14+
// some browsers, e.g. PhantomJS, encodes attribute values for innerHTML
15+
// this causes problems with the in-browser parser.
16+
const div = document.createElement('div')
17+
div.innerHTML = '<div a=">">'
18+
export const shouldDecodeAttr = div.innerHTML.indexOf('&gt;') > 0
19+
1420
/**
1521
* Query an element selector if it's not an element already.
1622
*/

test/unit/features/directives/html.spec.js

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,23 @@ describe('Directive v-html', () => {
1212
it('should encode html entities', () => {
1313
const vm = new Vue({
1414
template: '<div v-html="a"></div>',
15-
data: { a: '<span></span>' }
15+
data: { a: '<span>&lt;</span>' }
1616
}).$mount()
17-
expect(vm.$el.innerHTML).toBe('<span></span>')
17+
expect(vm.$el.innerHTML).toBe('<span>&lt;</span>')
18+
})
19+
20+
it('should work inline', () => {
21+
const vm = new Vue({
22+
template: `<div v-html="'<span>&lt;</span>'"></div>`
23+
}).$mount()
24+
expect(vm.$el.innerHTML).toBe('<span>&lt;</span>')
25+
})
26+
27+
it('should work inline in DOM', () => {
28+
const el = document.createElement('div')
29+
el.innerHTML = `<div v-html="'<span>&lt;</span>'"></div>`
30+
const vm = new Vue({ el })
31+
expect(vm.$el.children[0].innerHTML).toBe('<span>&lt;</span>')
1832
})
1933

2034
it('should support all value types', done => {

0 commit comments

Comments
 (0)