403 lines
11 KiB
JavaScript
403 lines
11 KiB
JavaScript
"use strict";
|
|
|
|
const {
|
|
ParseSourceSpan,
|
|
ParseLocation,
|
|
ParseSourceFile,
|
|
} = require("angular-html-parser/lib/compiler/src/parse_util");
|
|
const { parse: parseFrontMatter } = require("../utils/front-matter");
|
|
const createError = require("../common/parser-create-error");
|
|
const { getParserName } = require("../common/util");
|
|
const {
|
|
HTML_ELEMENT_ATTRIBUTES,
|
|
HTML_TAGS,
|
|
isUnknownNamespace,
|
|
} = require("./utils");
|
|
const { hasPragma } = require("./pragma");
|
|
const { Node } = require("./ast");
|
|
const { parseIeConditionalComment } = require("./conditional-comment");
|
|
|
|
function ngHtmlParser(
|
|
input,
|
|
{
|
|
recognizeSelfClosing,
|
|
normalizeTagName,
|
|
normalizeAttributeName,
|
|
allowHtmComponentClosingTags,
|
|
isTagNameCaseSensitive,
|
|
getTagContentType,
|
|
},
|
|
options
|
|
) {
|
|
const parser = require("angular-html-parser");
|
|
const {
|
|
RecursiveVisitor,
|
|
visitAll,
|
|
Attribute,
|
|
CDATA,
|
|
Comment,
|
|
DocType,
|
|
Element,
|
|
Text,
|
|
} = require("angular-html-parser/lib/compiler/src/ml_parser/ast");
|
|
const {
|
|
ParseSourceSpan,
|
|
} = require("angular-html-parser/lib/compiler/src/parse_util");
|
|
const {
|
|
getHtmlTagDefinition,
|
|
} = require("angular-html-parser/lib/compiler/src/ml_parser/html_tags");
|
|
|
|
let { rootNodes, errors } = parser.parse(input, {
|
|
canSelfClose: recognizeSelfClosing,
|
|
allowHtmComponentClosingTags,
|
|
isTagNameCaseSensitive,
|
|
getTagContentType,
|
|
});
|
|
|
|
const isVueHtml =
|
|
options.parser === "vue" &&
|
|
rootNodes.some(
|
|
(node) =>
|
|
(node instanceof DocType && node.value === "html") ||
|
|
(node instanceof Element && node.name.toLowerCase() === "html")
|
|
);
|
|
|
|
if (options.parser === "vue" && !isVueHtml) {
|
|
const shouldParseAsHTML = (node) => {
|
|
/* istanbul ignore next */
|
|
if (!node) {
|
|
return false;
|
|
}
|
|
if (node.name !== "template") {
|
|
return false;
|
|
}
|
|
const langAttr = node.attrs.find((attr) => attr.name === "lang");
|
|
const langValue = langAttr && langAttr.value;
|
|
return langValue == null || getParserName(langValue, options) === "html";
|
|
};
|
|
if (rootNodes.some(shouldParseAsHTML)) {
|
|
let secondParseResult;
|
|
const doSecondParse = () =>
|
|
parser.parse(input, {
|
|
canSelfClose: recognizeSelfClosing,
|
|
allowHtmComponentClosingTags,
|
|
isTagNameCaseSensitive,
|
|
});
|
|
const getSecondParse = () =>
|
|
secondParseResult || (secondParseResult = doSecondParse());
|
|
const getSameLocationNode = (node) =>
|
|
getSecondParse().rootNodes.find(
|
|
({ startSourceSpan }) =>
|
|
startSourceSpan &&
|
|
startSourceSpan.start.offset === node.startSourceSpan.start.offset
|
|
);
|
|
for (let i = 0; i < rootNodes.length; i++) {
|
|
const node = rootNodes[i];
|
|
const { endSourceSpan, startSourceSpan } = node;
|
|
const isUnclosedNode = endSourceSpan === null;
|
|
if (isUnclosedNode) {
|
|
const result = getSecondParse();
|
|
errors = result.errors;
|
|
rootNodes[i] = getSameLocationNode(node) || node;
|
|
} else if (shouldParseAsHTML(node)) {
|
|
const result = getSecondParse();
|
|
const startOffset = startSourceSpan.end.offset;
|
|
const endOffset = endSourceSpan.start.offset;
|
|
for (const error of result.errors) {
|
|
const { offset } = error.span.start;
|
|
/* istanbul ignore next */
|
|
if (startOffset < offset && offset < endOffset) {
|
|
errors = [error];
|
|
break;
|
|
}
|
|
}
|
|
rootNodes[i] = getSameLocationNode(node) || node;
|
|
}
|
|
}
|
|
}
|
|
} else if (isVueHtml) {
|
|
// If not Vue SFC, treat as html
|
|
recognizeSelfClosing = true;
|
|
normalizeTagName = true;
|
|
normalizeAttributeName = true;
|
|
allowHtmComponentClosingTags = true;
|
|
isTagNameCaseSensitive = false;
|
|
const htmlParseResult = parser.parse(input, {
|
|
canSelfClose: recognizeSelfClosing,
|
|
allowHtmComponentClosingTags,
|
|
isTagNameCaseSensitive,
|
|
});
|
|
|
|
rootNodes = htmlParseResult.rootNodes;
|
|
errors = htmlParseResult.errors;
|
|
}
|
|
|
|
if (errors.length !== 0) {
|
|
const { msg, span } = errors[0];
|
|
const { line, col } = span.start;
|
|
throw createError(msg, { start: { line: line + 1, column: col + 1 } });
|
|
}
|
|
|
|
const addType = (node) => {
|
|
if (node instanceof Attribute) {
|
|
node.type = "attribute";
|
|
} else if (node instanceof CDATA) {
|
|
node.type = "cdata";
|
|
} else if (node instanceof Comment) {
|
|
node.type = "comment";
|
|
} else if (node instanceof DocType) {
|
|
node.type = "docType";
|
|
} else if (node instanceof Element) {
|
|
node.type = "element";
|
|
} else if (node instanceof Text) {
|
|
node.type = "text";
|
|
} else {
|
|
/* istanbul ignore next */
|
|
throw new Error(`Unexpected node ${JSON.stringify(node)}`);
|
|
}
|
|
};
|
|
|
|
const restoreName = (node) => {
|
|
const namespace = node.name.startsWith(":")
|
|
? node.name.slice(1).split(":")[0]
|
|
: null;
|
|
const rawName = node.nameSpan.toString();
|
|
const hasExplicitNamespace = rawName.startsWith(`${namespace}:`);
|
|
const name = hasExplicitNamespace
|
|
? rawName.slice(namespace.length + 1)
|
|
: rawName;
|
|
|
|
node.name = name;
|
|
node.namespace = namespace;
|
|
node.hasExplicitNamespace = hasExplicitNamespace;
|
|
};
|
|
|
|
const restoreNameAndValue = (node) => {
|
|
if (node instanceof Element) {
|
|
restoreName(node);
|
|
node.attrs.forEach((attr) => {
|
|
restoreName(attr);
|
|
if (!attr.valueSpan) {
|
|
attr.value = null;
|
|
} else {
|
|
attr.value = attr.valueSpan.toString();
|
|
if (/["']/.test(attr.value[0])) {
|
|
attr.value = attr.value.slice(1, -1);
|
|
}
|
|
}
|
|
});
|
|
} else if (node instanceof Comment) {
|
|
node.value = node.sourceSpan
|
|
.toString()
|
|
.slice("<!--".length, -"-->".length);
|
|
} else if (node instanceof Text) {
|
|
node.value = node.sourceSpan.toString();
|
|
}
|
|
};
|
|
|
|
const lowerCaseIfFn = (text, fn) => {
|
|
const lowerCasedText = text.toLowerCase();
|
|
return fn(lowerCasedText) ? lowerCasedText : text;
|
|
};
|
|
const normalizeName = (node) => {
|
|
if (node instanceof Element) {
|
|
if (
|
|
normalizeTagName &&
|
|
(!node.namespace ||
|
|
node.namespace === node.tagDefinition.implicitNamespacePrefix ||
|
|
isUnknownNamespace(node))
|
|
) {
|
|
node.name = lowerCaseIfFn(
|
|
node.name,
|
|
(lowerCasedName) => lowerCasedName in HTML_TAGS
|
|
);
|
|
}
|
|
|
|
if (normalizeAttributeName) {
|
|
const CURRENT_HTML_ELEMENT_ATTRIBUTES =
|
|
HTML_ELEMENT_ATTRIBUTES[node.name] || Object.create(null);
|
|
node.attrs.forEach((attr) => {
|
|
if (!attr.namespace) {
|
|
attr.name = lowerCaseIfFn(
|
|
attr.name,
|
|
(lowerCasedAttrName) =>
|
|
node.name in HTML_ELEMENT_ATTRIBUTES &&
|
|
(lowerCasedAttrName in HTML_ELEMENT_ATTRIBUTES["*"] ||
|
|
lowerCasedAttrName in CURRENT_HTML_ELEMENT_ATTRIBUTES)
|
|
);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
const fixSourceSpan = (node) => {
|
|
if (node.sourceSpan && node.endSourceSpan) {
|
|
node.sourceSpan = new ParseSourceSpan(
|
|
node.sourceSpan.start,
|
|
node.endSourceSpan.end
|
|
);
|
|
}
|
|
};
|
|
|
|
const addTagDefinition = (node) => {
|
|
if (node instanceof Element) {
|
|
const tagDefinition = getHtmlTagDefinition(
|
|
isTagNameCaseSensitive ? node.name : node.name.toLowerCase()
|
|
);
|
|
if (
|
|
!node.namespace ||
|
|
node.namespace === tagDefinition.implicitNamespacePrefix ||
|
|
isUnknownNamespace(node)
|
|
) {
|
|
node.tagDefinition = tagDefinition;
|
|
} else {
|
|
node.tagDefinition = getHtmlTagDefinition(""); // the default one
|
|
}
|
|
}
|
|
};
|
|
|
|
visitAll(
|
|
new (class extends RecursiveVisitor {
|
|
visit(node) {
|
|
addType(node);
|
|
restoreNameAndValue(node);
|
|
addTagDefinition(node);
|
|
normalizeName(node);
|
|
fixSourceSpan(node);
|
|
}
|
|
})(),
|
|
rootNodes
|
|
);
|
|
|
|
return rootNodes;
|
|
}
|
|
|
|
function _parse(text, options, parserOptions, shouldParseFrontMatter = true) {
|
|
const { frontMatter, content } = shouldParseFrontMatter
|
|
? parseFrontMatter(text)
|
|
: { frontMatter: null, content: text };
|
|
|
|
const file = new ParseSourceFile(text, options.filepath);
|
|
const start = new ParseLocation(file, 0, 0, 0);
|
|
const end = start.moveBy(text.length);
|
|
const rawAst = {
|
|
type: "root",
|
|
sourceSpan: new ParseSourceSpan(start, end),
|
|
children: ngHtmlParser(content, parserOptions, options),
|
|
};
|
|
|
|
if (frontMatter) {
|
|
const start = new ParseLocation(file, 0, 0, 0);
|
|
const end = start.moveBy(frontMatter.raw.length);
|
|
frontMatter.sourceSpan = new ParseSourceSpan(start, end);
|
|
rawAst.children.unshift(frontMatter);
|
|
}
|
|
|
|
const ast = new Node(rawAst);
|
|
|
|
const parseSubHtml = (subContent, startSpan) => {
|
|
const { offset } = startSpan;
|
|
const fakeContent = text.slice(0, offset).replace(/[^\n\r]/g, " ");
|
|
const realContent = subContent;
|
|
const subAst = _parse(
|
|
fakeContent + realContent,
|
|
options,
|
|
parserOptions,
|
|
false
|
|
);
|
|
subAst.sourceSpan = new ParseSourceSpan(
|
|
startSpan,
|
|
subAst.children[subAst.children.length - 1].sourceSpan.end
|
|
);
|
|
const firstText = subAst.children[0];
|
|
if (firstText.length === offset) {
|
|
/* istanbul ignore next */
|
|
subAst.children.shift();
|
|
} else {
|
|
firstText.sourceSpan = new ParseSourceSpan(
|
|
firstText.sourceSpan.start.moveBy(offset),
|
|
firstText.sourceSpan.end
|
|
);
|
|
firstText.value = firstText.value.slice(offset);
|
|
}
|
|
return subAst;
|
|
};
|
|
|
|
return ast.map((node) => {
|
|
if (node.type === "comment") {
|
|
const ieConditionalComment = parseIeConditionalComment(
|
|
node,
|
|
parseSubHtml
|
|
);
|
|
if (ieConditionalComment) {
|
|
return ieConditionalComment;
|
|
}
|
|
}
|
|
|
|
return node;
|
|
});
|
|
}
|
|
|
|
function locStart(node) {
|
|
return node.sourceSpan.start.offset;
|
|
}
|
|
|
|
function locEnd(node) {
|
|
return node.sourceSpan.end.offset;
|
|
}
|
|
|
|
function createParser({
|
|
recognizeSelfClosing = false,
|
|
normalizeTagName = false,
|
|
normalizeAttributeName = false,
|
|
allowHtmComponentClosingTags = false,
|
|
isTagNameCaseSensitive = false,
|
|
getTagContentType,
|
|
} = {}) {
|
|
return {
|
|
parse: (text, parsers, options) =>
|
|
_parse(text, options, {
|
|
recognizeSelfClosing,
|
|
normalizeTagName,
|
|
normalizeAttributeName,
|
|
allowHtmComponentClosingTags,
|
|
isTagNameCaseSensitive,
|
|
getTagContentType,
|
|
}),
|
|
hasPragma,
|
|
astFormat: "html",
|
|
locStart,
|
|
locEnd,
|
|
};
|
|
}
|
|
|
|
module.exports = {
|
|
parsers: {
|
|
html: createParser({
|
|
recognizeSelfClosing: true,
|
|
normalizeTagName: true,
|
|
normalizeAttributeName: true,
|
|
allowHtmComponentClosingTags: true,
|
|
}),
|
|
angular: createParser(),
|
|
vue: createParser({
|
|
recognizeSelfClosing: true,
|
|
isTagNameCaseSensitive: true,
|
|
getTagContentType: (tagName, prefix, hasParent, attrs) => {
|
|
if (
|
|
tagName.toLowerCase() !== "html" &&
|
|
!hasParent &&
|
|
(tagName !== "template" ||
|
|
attrs.some(
|
|
({ name, value }) => name === "lang" && value !== "html"
|
|
))
|
|
) {
|
|
return require("angular-html-parser").TagContentType.RAW_TEXT;
|
|
}
|
|
},
|
|
}),
|
|
lwc: createParser(),
|
|
},
|
|
};
|