Files
test-repo/src/language-html/parse/parse.js
T

249 lines
6.2 KiB
JavaScript

import {
parseHtml as angularHtmlParserParse,
ParseLocation,
ParseSourceFile,
ParseSourceSpan,
} from "angular-html-parser";
import createError from "../../common/parser-create-error.js";
import { parseFrontMatter } from "../../main/front-matter/index.js";
import replaceNonLineBreaksWithSpace from "../../utilities/replace-non-line-breaks-with-space.js";
import {
normalizeParseOptions,
toAngularHtmlParserParseOptions,
} from "./parse-options.js";
import { postprocess } from "./postprocess.js";
/**
@import {HtmlParseOptions, Ast, ParseTreeResult} from "angular-html-parser";
@import {RawParseOptions, ParseOptions} from "./parse-options.js";
@import {FrontMatter} from "../../main/front-matter/parse.js"
@typedef {{filepath?: string}} Options
@typedef {FrontMatter & {kind: "frontMatter", sourceSpan: ParseSourceSpan}} HtmlFrontMatter
*/
/**
@param {string} input
@param {ParseOptions} parseOptions
*/
function parseHtml(input, parseOptions) {
const { rootNodes, errors } = angularHtmlParserParse(
input,
toAngularHtmlParserParseOptions(parseOptions),
);
if (errors.length > 0) {
throwParseError(errors[0]);
}
return { parseOptions, rootNodes };
}
/**
@param {string} input
@param {ParseOptions} parseOptions
*/
function parseVue(input, parseOptions) {
const angularHtmlParserParseOptions =
toAngularHtmlParserParseOptions(parseOptions);
let { rootNodes, errors } = angularHtmlParserParse(
input,
angularHtmlParserParseOptions,
);
const isHtml = rootNodes.some(
(node) =>
(node.kind === "docType" && node.value === "html") ||
(node.kind === "element" && node.name.toLowerCase() === "html"),
);
// If not Vue.js SFC, treat as HTML
if (isHtml) {
return parseHtml(input, HTML_PARSE_OPTIONS);
}
/** @type {ParseTreeResult | undefined} */
let secondParseResult;
const getHtmlParseResult = () =>
(secondParseResult ??= angularHtmlParserParse(input, {
...angularHtmlParserParseOptions,
getTagContentType: undefined,
}));
const getElementWithSameLocation = (node) => {
const { offset } = node.startSourceSpan.start;
return (
getHtmlParseResult().rootNodes.find(
(searching) =>
searching.kind === "element" &&
searching.startSourceSpan.start.offset === offset,
) ?? node
);
};
for (const [index, node] of rootNodes.entries()) {
if (node.kind !== "element") {
continue;
}
if (node.isVoid) {
errors = getHtmlParseResult().errors;
rootNodes[index] = getElementWithSameLocation(node);
} else if (shouldParseVueRootNodeAsHtml(node)) {
const { endSourceSpan, startSourceSpan } = node;
const error = getHtmlParseResult().errors.find(
(error) =>
error.span.start.offset > startSourceSpan.start.offset &&
error.span.start.offset < endSourceSpan.end.offset,
);
if (error) {
throwParseError(error);
}
rootNodes[index] = getElementWithSameLocation(node);
}
}
if (errors.length > 0) {
throwParseError(errors[0]);
}
return { parseOptions, rootNodes };
}
/**
@param {Ast.Node} node
@returns {boolean}
*/
function shouldParseVueRootNodeAsHtml(node) {
if (node.kind !== "element" || node.name !== "template") {
return false;
}
const language = node.attrs.find((attr) => attr.name === "lang")?.value;
return !language || language === "html";
}
function throwParseError(error) {
const {
msg,
span: { start, end },
} = error;
throw createError(msg, {
loc: {
start: { line: start.line + 1, column: start.col + 1 },
end: { line: end.line + 1, column: end.col + 1 },
},
cause: error,
});
}
function parseSubHtml(
parser,
text,
subContent,
startSpan,
parseOptions,
options,
) {
const { offset } = startSpan;
const textToParse =
replaceNonLineBreaksWithSpace(text.slice(0, offset)) + subContent;
const subAst = parse(
textToParse,
parser,
{ ...parseOptions, shouldParseFrontMatter: false },
options,
);
// @ts-expect-error
subAst.sourceSpan = new ParseSourceSpan(
startSpan,
// @ts-expect-error
subAst.children.at(-1).sourceSpan.end,
);
// @ts-expect-error
const firstText = subAst.children[0];
if (firstText.length === offset) {
/* c8 ignore next */ // @ts-expect-error
subAst.children.shift();
} else {
firstText.sourceSpan = new ParseSourceSpan(
firstText.sourceSpan.start.moveBy(offset),
firstText.sourceSpan.end,
);
firstText.value = firstText.value.slice(offset);
}
return subAst;
}
/**
@param {string} text
@param {typeof parseHtml | typeof parseVue} parser
@param {ParseOptions} parseOptions
@param {Options} options
*/
function parse(text, parser, parseOptions, options = {}) {
const { frontMatter, content: textToParse } =
parseOptions.shouldParseFrontMatter
? parseFrontMatter(text)
: { content: text };
const file = new ParseSourceFile(text, options.filepath);
const start = new ParseLocation(file, 0, 0, 0);
const end = start.moveBy(text.length);
const { parseOptions: actualParseOptions, rootNodes } = parser(
textToParse,
parseOptions,
);
const rawAst = {
kind: "root",
sourceSpan: new ParseSourceSpan(start, end),
children: rootNodes,
};
/** @type {HtmlFrontMatter} */
let htmlFrontMatter;
if (frontMatter) {
const [start, end] = [frontMatter.start, frontMatter.end].map(
(location) =>
new ParseLocation(
file,
location.index,
location.line - 1,
location.column,
),
);
htmlFrontMatter = {
...frontMatter,
kind: "frontMatter",
sourceSpan: new ParseSourceSpan(start, end),
};
}
const ast = postprocess(
rawAst,
htmlFrontMatter,
actualParseOptions,
(subContent, startSpan) =>
parseSubHtml(
parser,
text,
subContent,
startSpan,
actualParseOptions,
options,
),
);
return ast;
}
/** @type {ParseOptions} */
const HTML_PARSE_OPTIONS = normalizeParseOptions({
name: "html",
normalizeTagName: true,
normalizeAttributeName: true,
allowHtmComponentClosingTags: true,
});
export { HTML_PARSE_OPTIONS, parse, parseHtml, parseVue };