prettier/src/language-vue/parser-vue.js

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 = {
"&lt;": "<",
"&gt;": ">",
"&quot;": '"',
"&amp;": "&",
"&#10;": "\n",
"&#9;": "\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
}
}
};