Skip to content

Commit

Permalink
proper fix for attribute decode in PhantomJS
Browse files Browse the repository at this point in the history
  • Loading branch information
yyx990803 committed Jul 26, 2016
1 parent 3ce450b commit 097889f
Show file tree
Hide file tree
Showing 6 changed files with 62 additions and 41 deletions.
7 changes: 6 additions & 1 deletion src/compiler/parser/entity-decoder.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@

const decoder = document.createElement('div')

export function decodeHTML (html: string): string {
export function decodeHTML (html: string, asAttribute?: boolean): string {
if (asAttribute) {
html = html
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
}
decoder.innerHTML = html
return decoder.textContent
}
64 changes: 27 additions & 37 deletions src/compiler/parser/html-parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ import { isNonPhrasingTag, canBeLeftOpenTag } from 'web/util/index'

// Regular Expressions for parsing tags and attributes
const singleAttrIdentifier = /([^\s"'<>\/=]+)/
const singleAttrAssign = /=/
const singleAttrAssigns = [singleAttrAssign]
const singleAttrAssign = /(?:=)/
const singleAttrValues = [
// attr value double quotes
/"([^"]*)"+/.source,
Expand All @@ -25,6 +24,12 @@ const singleAttrValues = [
// attr value, no quotes
/([^\s"'=<>`]+)/.source
]
const attribute = new RegExp(
'^\\s*' + singleAttrIdentifier.source +
'(?:\\s*(' + singleAttrAssign.source + ')' +
'\\s*(?:' + singleAttrValues.join('|') + '))?'
)

// could use https://www.w3.org/TR/1999/REC-xml-names-19990114/#NT-QName
// but for Vue templates we can enforce a simple charset
const ncname = '[a-zA-Z_][\\w\\-\\.]*'
Expand All @@ -44,24 +49,11 @@ const isSpecialTag = makeMap('script,style', true)

const reCache = {}

function attrForHandler (handler) {
const pattern = singleAttrIdentifier.source +
'(?:\\s*(' + joinSingleAttrAssigns(handler) + ')' +
'\\s*(?:' + singleAttrValues.join('|') + '))?'
return new RegExp('^\\s*' + pattern)
}

function joinSingleAttrAssigns (handler) {
return singleAttrAssigns.map(function (assign) {
return '(?:' + assign.source + ')'
}).join('|')
}

export function parseHTML (html, handler) {
export function parseHTML (html, options) {
const stack = []
const attribute = attrForHandler(handler)
const expectHTML = handler.expectHTML
const isUnaryTag = handler.isUnaryTag || no
const expectHTML = options.expectHTML
const isUnaryTag = options.isUnaryTag || no
const shouldDecodeAttr = options.shouldDecodeAttr
let index = 0
let last, lastTag
while (html) {
Expand Down Expand Up @@ -93,9 +85,6 @@ export function parseHTML (html, handler) {
// Doctype:
const doctypeMatch = html.match(doctype)
if (doctypeMatch) {
if (handler.doctype) {
handler.doctype(doctypeMatch[0])
}
advance(doctypeMatch[0].length)
continue
}
Expand Down Expand Up @@ -126,8 +115,8 @@ export function parseHTML (html, handler) {
html = ''
}

if (handler.chars) {
handler.chars(text)
if (options.chars) {
options.chars(text)
}
} else {
const stackedTag = lastTag.toLowerCase()
Expand All @@ -140,8 +129,8 @@ export function parseHTML (html, handler) {
.replace(/<!--([\s\S]*?)-->/g, '$1')
.replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, '$1')
}
if (handler.chars) {
handler.chars(text)
if (options.chars) {
options.chars(text)
}
return ''
})
Expand Down Expand Up @@ -211,9 +200,10 @@ export function parseHTML (html, handler) {
if (args[4] === '') { delete args[4] }
if (args[5] === '') { delete args[5] }
}
const value = args[3] || args[4] || args[5] || ''
attrs[i] = {
name: args[1],
value: decodeHTML(args[3] || args[4] || args[5] || '')
value: shouldDecodeAttr ? decodeHTML(value, true) : value
}
}

Expand All @@ -223,8 +213,8 @@ export function parseHTML (html, handler) {
unarySlash = ''
}

if (handler.start) {
handler.start(tagName, attrs, unary, match.start, match.end)
if (options.start) {
options.start(tagName, attrs, unary, match.start, match.end)
}
}

Expand All @@ -249,24 +239,24 @@ export function parseHTML (html, handler) {
if (pos >= 0) {
// Close all the open elements, up the stack
for (let i = stack.length - 1; i >= pos; i--) {
if (handler.end) {
handler.end(stack[i].tag, start, end)
if (options.end) {
options.end(stack[i].tag, start, end)
}
}

// Remove the open elements from the stack
stack.length = pos
lastTag = pos && stack[pos - 1].tag
} else if (tagName.toLowerCase() === 'br') {
if (handler.start) {
handler.start(tagName, [], true, start, end)
if (options.start) {
options.start(tagName, [], true, start, end)
}
} else if (tagName.toLowerCase() === 'p') {
if (handler.start) {
handler.start(tagName, [], false, start, end)
if (options.start) {
options.start(tagName, [], false, start, end)
}
if (handler.end) {
handler.end(tagName, start, end)
if (options.end) {
options.end(tagName, start, end)
}
}
}
Expand Down
1 change: 1 addition & 0 deletions src/compiler/parser/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export function parse (
parseHTML(template, {
expectHTML: options.expectHTML,
isUnaryTag: options.isUnaryTag,
shouldDecodeAttr: options.shouldDecodeAttr,
start (tag, attrs, unary) {
// check namespace.
// inherit parent ns if there is one
Expand Down
7 changes: 6 additions & 1 deletion src/entries/web-runtime-with-compiler.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import Vue from './web-runtime'
import { warn, cached } from 'core/util/index'
import { query } from 'web/util/index'
import { query, shouldDecodeAttr } from 'web/util/index'
import { compileToFunctions } from 'web/compiler/index'

const idToTemplate = cached(id => {
Expand All @@ -20,12 +20,15 @@ Vue.prototype.$mount = function (
// resolve template/el and convert to render function
if (!options.render) {
let template = options.template
let isFromDOM = false
if (template) {
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
isFromDOM = true
template = idToTemplate(template)
}
} else if (template.nodeType) {
isFromDOM = true
template = template.innerHTML
} else {
if (process.env.NODE_ENV !== 'production') {
Expand All @@ -34,10 +37,12 @@ Vue.prototype.$mount = function (
return this
}
} else if (el) {
isFromDOM = true
template = getOuterHTML(el)
}
if (template) {
const { render, staticRenderFns } = compileToFunctions(template, {
shouldDecodeAttr: isFromDOM && shouldDecodeAttr,
delimiters: options.delimiters,
warn
}, this)
Expand Down
6 changes: 6 additions & 0 deletions src/platforms/web/util/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ export const isIE = UA && /msie|trident/.test(UA)
export const isIE9 = UA && UA.indexOf('msie 9.0') > 0
export const isAndroid = UA && UA.indexOf('android') > 0

// some browsers, e.g. PhantomJS, encodes attribute values for innerHTML
// this causes problems with the in-browser parser.
const div = document.createElement('div')
div.innerHTML = '<div a=">">'
export const shouldDecodeAttr = div.innerHTML.indexOf('&gt;') > 0

/**
* Query an element selector if it's not an element already.
*/
Expand Down
18 changes: 16 additions & 2 deletions test/unit/features/directives/html.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,23 @@ describe('Directive v-html', () => {
it('should encode html entities', () => {
const vm = new Vue({
template: '<div v-html="a"></div>',
data: { a: '<span></span>' }
data: { a: '<span>&lt;</span>' }
}).$mount()
expect(vm.$el.innerHTML).toBe('<span></span>')
expect(vm.$el.innerHTML).toBe('<span>&lt;</span>')
})

it('should work inline', () => {
const vm = new Vue({
template: `<div v-html="'<span>&lt;</span>'"></div>`
}).$mount()
expect(vm.$el.innerHTML).toBe('<span>&lt;</span>')
})

it('should work inline in DOM', () => {
const el = document.createElement('div')
el.innerHTML = `<div v-html="'<span>&lt;</span>'"></div>`
const vm = new Vue({ el })
expect(vm.$el.children[0].innerHTML).toBe('<span>&lt;</span>')
})

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

0 comments on commit 097889f

Please sign in to comment.