Skip to content

Commit 810310c

Browse files
committed
fix(compiler): decode all HTML entities in the value of prop and attribute (fix #8895)
1 parent 7094fc0 commit 810310c

File tree

6 files changed

+46
-47
lines changed

6 files changed

+46
-47
lines changed

src/compiler/parser/html-decoder.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/* @flow */
2+
3+
import he from 'he'
4+
import { cached } from 'shared/util'
5+
6+
export default cached(he.decode)

src/compiler/parser/html-parser.js

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import { makeMap, no } from 'shared/util'
1313
import { isNonPhrasingTag } from 'web/compiler/util'
1414
import { unicodeRegExp } from 'core/util/lang'
15+
import decodeHTML from './html-decoder'
1516

1617
// Regular Expressions for parsing tags and attributes
1718
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
@@ -30,27 +31,10 @@ const conditionalComment = /^<!\[/
3031
export const isPlainTextElement = makeMap('script,style,textarea', true)
3132
const reCache = {}
3233

33-
const decodingMap = {
34-
'&lt;': '<',
35-
'&gt;': '>',
36-
'&quot;': '"',
37-
'&amp;': '&',
38-
'&#10;': '\n',
39-
'&#9;': '\t',
40-
'&#39;': "'"
41-
}
42-
const encodedAttr = /&(?:lt|gt|quot|amp|#39);/g
43-
const encodedAttrWithNewLines = /&(?:lt|gt|quot|amp|#39|#10|#9);/g
44-
4534
// #5992
4635
const isIgnoreNewlineTag = makeMap('pre,textarea', true)
4736
const shouldIgnoreFirstNewline = (tag, html) => tag && isIgnoreNewlineTag(tag) && html[0] === '\n'
4837

49-
function decodeAttr (value, shouldDecodeNewlines) {
50-
const re = shouldDecodeNewlines ? encodedAttrWithNewLines : encodedAttr
51-
return value.replace(re, match => decodingMap[match])
52-
}
53-
5438
export function parseHTML (html, options) {
5539
const stack = []
5640
const expectHTML = options.expectHTML
@@ -229,12 +213,9 @@ export function parseHTML (html, options) {
229213
for (let i = 0; i < l; i++) {
230214
const args = match.attrs[i]
231215
const value = args[3] || args[4] || args[5] || ''
232-
const shouldDecodeNewlines = tagName === 'a' && args[1] === 'href'
233-
? options.shouldDecodeNewlinesForHref
234-
: options.shouldDecodeNewlines
235216
attrs[i] = {
236217
name: args[1],
237-
value: decodeAttr(value, shouldDecodeNewlines)
218+
value: decodeHTML(value)
238219
}
239220
if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
240221
attrs[i].start = args.start + args[0].match(/^\s*/).length

src/compiler/parser/index.js

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
/* @flow */
22

3-
import he from 'he'
43
import { parseHTML } from './html-parser'
54
import { parseText } from './text-parser'
65
import { parseFilters } from './filter-parser'
76
import { genAssignmentCode } from '../directives/model'
8-
import { extend, cached, no, camelize, hyphenate } from 'shared/util'
7+
import { extend, no, camelize, hyphenate } from 'shared/util'
98
import { isIE, isEdge, isServerRendering } from 'core/util/env'
9+
import decodeHTML from './html-decoder'
1010

1111
import {
1212
addProp,
@@ -42,8 +42,6 @@ const whitespaceRE = /[ \f\t\r\n]+/g
4242

4343
const invalidAttributeRE = /[\s"'<>\/=]/
4444

45-
const decodeHTMLCached = cached(he.decode)
46-
4745
export const emptySlotScopeToken = `_empty_`
4846

4947
// configurable state
@@ -210,8 +208,6 @@ export function parse (
210208
expectHTML: options.expectHTML,
211209
isUnaryTag: options.isUnaryTag,
212210
canBeLeftOpenTag: options.canBeLeftOpenTag,
213-
shouldDecodeNewlines: options.shouldDecodeNewlines,
214-
shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
215211
shouldKeepComment: options.comments,
216212
outputSourceRange: options.outputSourceRange,
217213
start (tag, attrs, unary, start, end) {
@@ -339,7 +335,7 @@ export function parse (
339335
}
340336
const children = currentParent.children
341337
if (inPre || text.trim()) {
342-
text = isTextTag(currentParent) ? text : decodeHTMLCached(text)
338+
text = isTextTag(currentParent) ? text : decodeHTML(text)
343339
} else if (!children.length) {
344340
// remove the whitespace-only node right after an opening tag
345341
text = ''

src/platforms/web/entry-runtime-with-compiler.js

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import { mark, measure } from 'core/util/perf'
77
import Vue from './runtime/index'
88
import { query } from './util/index'
99
import { compileToFunctions } from './compiler/index'
10-
import { shouldDecodeNewlines, shouldDecodeNewlinesForHref } from './util/compat'
1110

1211
const idToTemplate = cached(id => {
1312
const el = query(id)
@@ -64,8 +63,6 @@ Vue.prototype.$mount = function (
6463

6564
const { render, staticRenderFns } = compileToFunctions(template, {
6665
outputSourceRange: process.env.NODE_ENV !== 'production',
67-
shouldDecodeNewlines,
68-
shouldDecodeNewlinesForHref,
6966
delimiters: options.delimiters,
7067
comments: options.comments
7168
}, this)

src/platforms/web/util/compat.js

Lines changed: 0 additions & 16 deletions
This file was deleted.

test/unit/modules/compiler/parser.spec.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { parse } from 'compiler/parser/index'
22
import { extend } from 'shared/util'
33
import { baseOptions } from 'web/compiler/options'
44
import { isIE, isEdge } from 'core/util/env'
5+
import Vue from 'vue'
56

67
describe('parser', () => {
78
it('simple element', () => {
@@ -925,4 +926,38 @@ describe('parser', () => {
925926
expect(`<template v-slot> can only appear at the root level inside the receiving the component`)
926927
.not.toHaveBeenWarned()
927928
})
929+
930+
it(`HTML entities in the value of attribute should be decoded`, () => {
931+
const options = extend({}, baseOptions)
932+
const ast = parse('<input value="white&nbsp;space,single-&#39;-quote,double-&quot;-quote,an-&amp;-ampersand,less-&lt;-than,great-&gt;-than,line-&#10;-break,tab-&#9;-space" />', options)
933+
expect(ast.attrsList[0].value).toBe('white space,single-' + "'" + '-quote,double-' + '"' + '-quote,an-&-ampersand,less-<-than,great->-than,line-\n-break,tab-\t-space')
934+
})
935+
936+
it(`HTML entities in template should be decoded`, () => {
937+
const vm = new Vue({
938+
template: '<test></test>',
939+
components: {
940+
test: {
941+
template: '<input value="&#102;&#111;&#111;">'
942+
}
943+
}
944+
}).$mount()
945+
expect(vm.$el.value).toBe('foo')
946+
})
947+
948+
it(`HTML entities in the value of props should be decoded`, () => {
949+
const vm = new Vue({
950+
template: '<test name="-&nbsp;-"></test>',
951+
components: {
952+
test: {
953+
template: '<div>{{ name }}</div>',
954+
props: {
955+
name: String,
956+
},
957+
}
958+
}
959+
}).$mount()
960+
expect(vm.$el.innerHTML).toBe('-&nbsp;-')
961+
expect(vm.$el.innerText).toBe('- -')
962+
})
928963
})

0 commit comments

Comments
 (0)