prettierx
Version:
prettierX - a less opinionated fork of the Prettier code formatter
420 lines (393 loc) • 12.3 kB
JavaScript
;
const {
ParseSourceSpan,
ParseLocation,
ParseSourceFile,
} = require("angular-html-parser/lib/compiler/src/parse_util");
const parseFrontMatter = require("../utils/front-matter/parse");
const getLast = require("../utils/get-last");
const createError = require("../common/parser-create-error");
const { inferParserByLanguage } = 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");
const { locStart, locEnd } = require("./loc");
/**
* @typedef {import('angular-html-parser/lib/compiler/src/ml_parser/ast').Node} AstNode
* @typedef {import('angular-html-parser/lib/compiler/src/ml_parser/ast').Attribute} Attribute
* @typedef {import('angular-html-parser/lib/compiler/src/ml_parser/ast').Element} Element
* @typedef {import('angular-html-parser/lib/compiler/src/ml_parser/parser').ParseTreeResult} ParserTreeResult
* @typedef {Omit<import('angular-html-parser').ParseOptions, 'canSelfClose'> & {
* recognizeSelfClosing?: boolean;
* normalizeTagName?: boolean;
* normalizeAttributeName?: boolean;
* }} ParserOptions
* @typedef {{
* parser: 'html' | 'angular' | 'vue' | 'lwc',
* filepath?: string
* }} Options
*/
/**
* @param {string} input
* @param {ParserOptions} parserOptions
* @param {Options} options
*/
function ngHtmlParser(
input,
{
recognizeSelfClosing,
normalizeTagName,
normalizeAttributeName,
allowHtmComponentClosingTags,
isTagNameCaseSensitive,
getTagContentType,
},
options
) {
const parser = require("angular-html-parser");
const {
RecursiveVisitor,
visitAll,
} = 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,
});
if (options.parser === "vue") {
const isVueHtml = rootNodes.some(
(node) =>
(node.type === "docType" && node.value === "html") ||
(node.type === "element" && node.name.toLowerCase() === "html")
);
if (!isVueHtml) {
const shouldParseAsHTML = (/** @type {AstNode} */ node) => {
/* istanbul ignore next */
if (!node) {
return false;
}
if (node.type !== "element" || node.name !== "template") {
return false;
}
const langAttr = node.attrs.find((attr) => attr.name === "lang");
const langValue = langAttr && langAttr.value;
return (
!langValue || inferParserByLanguage(langValue, options) === "html"
);
};
if (rootNodes.some(shouldParseAsHTML)) {
/** @type {ParserTreeResult | undefined} */
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 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: { start, end },
} = errors[0];
throw createError(msg, {
start: { line: start.line + 1, column: start.col + 1 },
end: { line: end.line + 1, column: end.col + 1 },
});
}
/**
* @param {Attribute | Element} node
*/
const restoreName = (node) => {
const namespace = node.name.startsWith(":")
? node.name.slice(1).split(":")[0]
: null;
const rawName = node.nameSpan.toString();
const hasExplicitNamespace =
namespace !== null && rawName.startsWith(`${namespace}:`);
const name = hasExplicitNamespace
? rawName.slice(namespace.length + 1)
: rawName;
node.name = name;
node.namespace = namespace;
node.hasExplicitNamespace = hasExplicitNamespace;
};
/**
* @param {AstNode} node
*/
const restoreNameAndValue = (node) => {
if (node.type === "element") {
restoreName(node);
for (const attr of node.attrs) {
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.type === "comment") {
node.value = node.sourceSpan
.toString()
.slice("<!--".length, -"-->".length);
} else if (node.type === "text") {
node.value = node.sourceSpan.toString();
}
};
const lowerCaseIfFn = (text, fn) => {
const lowerCasedText = text.toLowerCase();
return fn(lowerCasedText) ? lowerCasedText : text;
};
const normalizeName = (node) => {
if (node.type === "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);
for (const attr of node.attrs) {
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
);
}
};
/**
* @param {AstNode} node
*/
const addTagDefinition = (node) => {
if (node.type === "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) {
restoreNameAndValue(node);
addTagDefinition(node);
normalizeName(node);
fixSourceSpan(node);
}
})(),
rootNodes
);
return rootNodes;
}
/**
* @param {string} text
* @param {Options} options
* @param {ParserOptions} parserOptions
* @param {boolean} shouldParseFrontMatter
*/
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);
// @ts-ignore
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,
getLast(subAst.children).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;
});
}
/**
* @param {ParserOptions} parserOptions
*/
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(),
},
};