diff --git a/src/compiler/parser/html-parser.js b/src/compiler/parser/html-parser.js index f9f78d5ccf8..a2a2c1ce803 100644 --- a/src/compiler/parser/html-parser.js +++ b/src/compiler/parser/html-parser.js @@ -9,7 +9,7 @@ * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js */ -import { makeMap, no } from 'shared/util' +import { makeMap, no, splitLine } from 'shared/util' import { isNonPhrasingTag } from 'web/compiler/util' // Regular Expressions for parsing tags and attributes @@ -53,11 +53,30 @@ function decodeAttr (value, shouldDecodeNewlines) { return value.replace(re, match => decodingMap[match]) } +function resolveLineNumbers (lines, start) { + let l = 0 + let cur = 0 + for (let i = 0, len = lines.length; i < len; ++i) { + const { line, linefeed = '' } = lines[i] + if (cur + line.length < start) { + cur += (line.length + linefeed.length) + l++ + } else { + break + } + } + return { + line: l, + column: start - cur + } +} + export function parseHTML (html, options) { const stack = [] const expectHTML = options.expectHTML const isUnaryTag = options.isUnaryTag || no const canBeLeftOpenTag = options.canBeLeftOpenTag || no + const lines = splitLine(html) let index = 0 let last, lastTag while (html) { @@ -243,7 +262,7 @@ export function parseHTML (html, options) { } if (!unary) { - stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs }) + stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs, start: match.start }) lastTag = tagName } @@ -280,8 +299,9 @@ export function parseHTML (html, options) { (i > pos || !tagName) && options.warn ) { + const { line, column } = resolveLineNumbers(lines, stack[i].start) options.warn( - `tag <${stack[i].tag}> has no matching end tag.` + `tag <${stack[i].tag}> has no matching end tag at line ${line} column ${column}.` ) } if (options.end) { diff --git a/src/shared/util.js b/src/shared/util.js index 353b9829980..ff90ce9cba8 100644 --- a/src/shared/util.js +++ b/src/shared/util.js @@ -304,3 +304,27 @@ export function once (fn: Function): Function { } } } + +/** + * split string with different linefeed. + */ +const linefeedRE = /\r\n|\n|\u2028|\u2029/ +export function splitLine (str: string): Array { + const lines = [] + while (str) { + const line = linefeedRE.exec(str) + if (line) { + const linefeed = line[0] + const idx = line.index + lines.push({ + line: str.substring(0, idx), + linefeed + }) + str = str.substring(idx + linefeed.length) + } else { + lines.push({ length: str }) + break + } + } + return lines +} diff --git a/test/unit/modules/compiler/parser.spec.js b/test/unit/modules/compiler/parser.spec.js index c2bb6c25129..de17d190423 100644 --- a/test/unit/modules/compiler/parser.spec.js +++ b/test/unit/modules/compiler/parser.spec.js @@ -593,4 +593,29 @@ describe('parser', () => { expect(ast.children[1].isComment).toBe(true) // parse comment with ASTText expect(ast.children[1].text).toBe('comment here') }) + + it('should warn with line and column', () => { + parse(` +
+ + 123 +
+ `, baseOptions) + expect(`tag has no matching end tag at line 2 column 8.`).toHaveBeenWarned() + }) + + it('should warn with correct match', () => { + parse(` +
+
+ 123 +
+ `, baseOptions) + expect(`tag
has no matching end tag at line 1 column 6.`).toHaveBeenWarned() + }) + + it('should work with different linefeed', () => { + parse('
\n
\r\n \u2028 123\u2029 \n
\n
  • \n
  • ', baseOptions) + expect(`tag
  • has no matching end tag at line 6 column 2.`).toHaveBeenWarned() + }) })