426 lines
11 KiB
JavaScript
426 lines
11 KiB
JavaScript
"use strict";
|
|
|
|
const { hasPragma } = require("./pragma");
|
|
|
|
/*!
|
|
* Extracted from vue codebase
|
|
* https://github.com/vuejs/vue/blob/cfd73c2386623341fdbb3ac636c4baf84ea89c2c/src/compiler/parser/html-parser.js
|
|
* HTML Parser By John Resig (ejohn.org)
|
|
* Modified by Juriy "kangax" Zaytsev
|
|
* Original code by Erik Arvidsson, Mozilla Public License
|
|
* http://erik.eae.net/simplehtmlparser/simplehtmlparser.js
|
|
*/
|
|
|
|
/**
|
|
* Make a map and return a function for checking if a key
|
|
* is in that map.
|
|
*/
|
|
function makeMap(str, expectsLowerCase) {
|
|
const map = Object.create(null);
|
|
const list = str.split(",");
|
|
const listLength = list.length;
|
|
for (let i = 0; i < listLength; i++) {
|
|
map[list[i]] = true;
|
|
}
|
|
return expectsLowerCase ? val => map[val.toLowerCase()] : val => map[val];
|
|
}
|
|
|
|
/**
|
|
* Always return false.
|
|
*/
|
|
const no = () => false;
|
|
|
|
// HTML5 tags https://html.spec.whatwg.org/multipage/indices.html#elements-3
|
|
// Phrasing Content https://html.spec.whatwg.org/multipage/dom.html#phrasing-content
|
|
const isNonPhrasingTag = makeMap(
|
|
"address,article,aside,base,blockquote,body,caption,col,colgroup,dd," +
|
|
"details,dialog,div,dl,dt,fieldset,figcaption,figure,footer,form," +
|
|
"h1,h2,h3,h4,h5,h6,head,header,hgroup,hr,html,legend,li,menuitem,meta," +
|
|
"optgroup,option,param,rp,rt,source,style,summary,tbody,td,tfoot,th,thead," +
|
|
"title,tr,track"
|
|
);
|
|
|
|
// Regular Expressions for parsing tags and attributes
|
|
const attribute = /^\s*([^\s"'<>/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
|
|
// 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\\-\\.]*";
|
|
const qnameCapture = `((?:${ncname}\\:)?${ncname})`;
|
|
const startTagOpen = new RegExp(`^<${qnameCapture}`);
|
|
const startTagClose = /^\s*(\/?)>/;
|
|
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`);
|
|
const doctype = /^<!DOCTYPE [^>]+>/i;
|
|
const comment = /^<!--/;
|
|
const conditionalComment = /^<!\[/;
|
|
|
|
let IS_REGEX_CAPTURING_BROKEN = false;
|
|
"x".replace(/x(.)?/g, (m, g) => {
|
|
IS_REGEX_CAPTURING_BROKEN = g === "";
|
|
});
|
|
|
|
// Special Elements (can contain anything)
|
|
const isPlainTextElement = makeMap("script,style,textarea", true);
|
|
const reCache = {};
|
|
|
|
const decodingMap = {
|
|
"<": "<",
|
|
">": ">",
|
|
""": '"',
|
|
"&": "&",
|
|
" ": "\n",
|
|
"	": "\t"
|
|
};
|
|
const encodedAttr = /&(?:lt|gt|quot|amp);/g;
|
|
const encodedAttrWithNewLines = /&(?:lt|gt|quot|amp|#10|#9);/g;
|
|
|
|
// #5992
|
|
const isIgnoreNewlineTag = makeMap("pre,textarea", true);
|
|
const shouldIgnoreFirstNewline = (tag, html) =>
|
|
tag && isIgnoreNewlineTag(tag) && html[0] === "\n";
|
|
|
|
function decodeAttr(value, shouldDecodeNewlines) {
|
|
const re = shouldDecodeNewlines ? encodedAttrWithNewLines : encodedAttr;
|
|
return value.replace(re, match => decodingMap[match]);
|
|
}
|
|
|
|
function parseHTML(html, options) {
|
|
const stack = [];
|
|
const expectHTML = options.expectHTML;
|
|
const isUnaryTag = options.isUnaryTag || no;
|
|
const canBeLeftOpenTag = options.canBeLeftOpenTag || no;
|
|
let index = 0;
|
|
let last;
|
|
let lastTag;
|
|
while (html) {
|
|
last = html;
|
|
// Make sure we're not in a plaintext content element like script/style
|
|
if (!lastTag || !isPlainTextElement(lastTag)) {
|
|
let textEnd = html.indexOf("<");
|
|
if (textEnd === 0) {
|
|
// Comment:
|
|
if (comment.test(html)) {
|
|
const commentEnd = html.indexOf("-->");
|
|
|
|
if (commentEnd >= 0) {
|
|
if (options.shouldKeepComment) {
|
|
options.comment(html.substring(4, commentEnd));
|
|
}
|
|
advance(commentEnd + 3);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// http://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment
|
|
if (conditionalComment.test(html)) {
|
|
const conditionalEnd = html.indexOf("]>");
|
|
|
|
if (conditionalEnd >= 0) {
|
|
advance(conditionalEnd + 2);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Doctype:
|
|
const doctypeMatch = html.match(doctype);
|
|
if (doctypeMatch) {
|
|
advance(doctypeMatch[0].length);
|
|
continue;
|
|
}
|
|
|
|
// End tag:
|
|
const endTagMatch = html.match(endTag);
|
|
if (endTagMatch) {
|
|
const curIndex = index;
|
|
advance(endTagMatch[0].length);
|
|
parseEndTag(endTagMatch[1], curIndex, index);
|
|
continue;
|
|
}
|
|
|
|
// Start tag:
|
|
const startTagMatch = parseStartTag();
|
|
if (startTagMatch) {
|
|
handleStartTag(startTagMatch);
|
|
if (shouldIgnoreFirstNewline(lastTag, html)) {
|
|
advance(1);
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
|
|
let text;
|
|
let rest;
|
|
let next;
|
|
|
|
if (textEnd >= 0) {
|
|
rest = html.slice(textEnd);
|
|
while (
|
|
!endTag.test(rest) &&
|
|
!startTagOpen.test(rest) &&
|
|
!comment.test(rest) &&
|
|
!conditionalComment.test(rest)
|
|
) {
|
|
// < in plain text, be forgiving and treat it as text
|
|
next = rest.indexOf("<", 1);
|
|
if (next < 0) {
|
|
break;
|
|
}
|
|
textEnd += next;
|
|
rest = html.slice(textEnd);
|
|
}
|
|
text = html.substring(0, textEnd);
|
|
advance(textEnd);
|
|
}
|
|
|
|
if (textEnd < 0) {
|
|
text = html;
|
|
html = "";
|
|
}
|
|
|
|
if (options.chars && text) {
|
|
options.chars(text);
|
|
}
|
|
} else {
|
|
let endTagLength = 0;
|
|
const stackedTag = lastTag.toLowerCase();
|
|
const reStackedTag =
|
|
reCache[stackedTag] ||
|
|
(reCache[stackedTag] = new RegExp(
|
|
"([\\s\\S]*?)(</" + stackedTag + "[^>]*>)",
|
|
"i"
|
|
));
|
|
const rest = html.replace(reStackedTag, (all, text, endTag) => {
|
|
endTagLength = endTag.length;
|
|
if (!isPlainTextElement(stackedTag) && stackedTag !== "noscript") {
|
|
text = text
|
|
.replace(/<!--([\s\S]*?)-->/g, "$1")
|
|
.replace(/<!\[CDATA\[([\s\S]*?)]]>/g, "$1");
|
|
}
|
|
if (shouldIgnoreFirstNewline(stackedTag, text)) {
|
|
text = text.slice(1);
|
|
}
|
|
if (options.chars) {
|
|
options.chars(text);
|
|
}
|
|
return "";
|
|
});
|
|
index += html.length - rest.length;
|
|
html = rest;
|
|
parseEndTag(stackedTag, index - endTagLength, index);
|
|
}
|
|
|
|
if (html === last) {
|
|
options.chars && options.chars(html);
|
|
if (
|
|
process.env.NODE_ENV !== "production" &&
|
|
!stack.length &&
|
|
options.warn
|
|
) {
|
|
options.warn(`Mal-formatted tag at end of template: "${html}"`);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Clean up any remaining tags
|
|
parseEndTag();
|
|
|
|
function advance(n) {
|
|
index += n;
|
|
html = html.substring(n);
|
|
}
|
|
|
|
function parseStartTag() {
|
|
const start = html.match(startTagOpen);
|
|
if (start) {
|
|
const match = {
|
|
tagName: start[1],
|
|
attrs: [],
|
|
start: index
|
|
};
|
|
advance(start[0].length);
|
|
let end;
|
|
let attr;
|
|
while (
|
|
!(end = html.match(startTagClose)) &&
|
|
(attr = html.match(attribute))
|
|
) {
|
|
advance(attr[0].length);
|
|
match.attrs.push(attr);
|
|
}
|
|
if (end) {
|
|
match.unarySlash = end[1];
|
|
advance(end[0].length);
|
|
match.end = index;
|
|
return match;
|
|
}
|
|
}
|
|
}
|
|
|
|
function handleStartTag(match) {
|
|
const { tagName, unarySlash } = match;
|
|
|
|
if (expectHTML) {
|
|
if (lastTag === "p" && isNonPhrasingTag(tagName)) {
|
|
parseEndTag(lastTag);
|
|
}
|
|
if (canBeLeftOpenTag(tagName) && lastTag === tagName) {
|
|
parseEndTag(tagName);
|
|
}
|
|
}
|
|
|
|
const unary = isUnaryTag(tagName) || !!unarySlash;
|
|
|
|
const l = match.attrs.length;
|
|
const attrs = new Array(l);
|
|
for (let i = 0; i < l; i++) {
|
|
const args = match.attrs[i];
|
|
// hackish work around FF bug https://bugzilla.mozilla.org/show_bug.cgi?id=369778
|
|
if (IS_REGEX_CAPTURING_BROKEN && args[0].indexOf('""') === -1) {
|
|
if (args[3] === "") {
|
|
delete args[3];
|
|
}
|
|
if (args[4] === "") {
|
|
delete args[4];
|
|
}
|
|
if (args[5] === "") {
|
|
delete args[5];
|
|
}
|
|
}
|
|
const value = args[3] || args[4] || args[5] || "";
|
|
const shouldDecodeNewlines =
|
|
tagName === "a" && args[1] === "href"
|
|
? options.shouldDecodeNewlinesForHref
|
|
: options.shouldDecodeNewlines;
|
|
attrs[i] = {
|
|
name: args[1],
|
|
value: decodeAttr(value, shouldDecodeNewlines)
|
|
};
|
|
}
|
|
|
|
if (!unary) {
|
|
stack.push({
|
|
tag: tagName,
|
|
lowerCasedTag: tagName.toLowerCase(),
|
|
attrs: attrs
|
|
});
|
|
lastTag = tagName;
|
|
}
|
|
|
|
if (options.start) {
|
|
options.start(tagName, attrs, unary, match.start, match.end);
|
|
}
|
|
}
|
|
|
|
function parseEndTag(tagName, start, end) {
|
|
let pos;
|
|
let lowerCasedTagName;
|
|
if (start == null) {
|
|
start = index;
|
|
}
|
|
if (end == null) {
|
|
end = index;
|
|
}
|
|
|
|
if (tagName) {
|
|
lowerCasedTagName = tagName.toLowerCase();
|
|
}
|
|
|
|
// Find the closest opened tag of the same type
|
|
if (tagName) {
|
|
for (pos = stack.length - 1; pos >= 0; pos--) {
|
|
if (stack[pos].lowerCasedTag === lowerCasedTagName) {
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
// If no tag name is provided, clean shop
|
|
pos = 0;
|
|
}
|
|
|
|
if (pos >= 0) {
|
|
// Close all the open elements, up the stack
|
|
for (let i = stack.length - 1; i >= pos; i--) {
|
|
if (
|
|
process.env.NODE_ENV !== "production" &&
|
|
(i > pos || !tagName) &&
|
|
options.warn
|
|
) {
|
|
options.warn(`tag <${stack[i].tag}> has no matching end tag.`);
|
|
}
|
|
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 (lowerCasedTagName === "br") {
|
|
if (options.start) {
|
|
options.start(tagName, [], true, start, end);
|
|
}
|
|
} else if (lowerCasedTagName === "p") {
|
|
if (options.start) {
|
|
options.start(tagName, [], false, start, end);
|
|
}
|
|
if (options.end) {
|
|
options.end(tagName, start, end);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function parse(text /*, parsers, opts*/) {
|
|
const rootObj = {
|
|
tag: "root",
|
|
attrs: [],
|
|
unary: false,
|
|
start: 0,
|
|
contentStart: 0,
|
|
contentEnd: text.length,
|
|
end: text.length,
|
|
children: [],
|
|
comments: []
|
|
};
|
|
const objStack = [rootObj];
|
|
let obj = rootObj;
|
|
parseHTML(text, {
|
|
start: function(tag, attrs, unary, start, end) {
|
|
const newObj = {
|
|
tag,
|
|
attrs,
|
|
unary,
|
|
start,
|
|
children: []
|
|
};
|
|
obj.children.push(newObj);
|
|
if (unary) {
|
|
newObj.end = end;
|
|
} else {
|
|
newObj.contentStart = end;
|
|
objStack.push(newObj);
|
|
obj = newObj;
|
|
}
|
|
},
|
|
end: function(tag, start, end) {
|
|
objStack.pop();
|
|
obj.contentEnd = start;
|
|
obj.end = end;
|
|
obj = objStack[objStack.length - 1];
|
|
}
|
|
});
|
|
return rootObj;
|
|
}
|
|
|
|
module.exports = {
|
|
parsers: {
|
|
vue: {
|
|
parse,
|
|
hasPragma,
|
|
astFormat: "vue",
|
|
locStart: node => node.start,
|
|
locEnd: node => node.end
|
|
}
|
|
}
|
|
};
|