From 4ec2648e617fa388b3c1f4d44b5b995323208877 Mon Sep 17 00:00:00 2001 From: fisker Cheung Date: Wed, 24 Sep 2025 04:15:01 +0800 Subject: [PATCH] Add location information to `parseFrontMatter()` (#17948) --- src/language-css/clean.js | 5 +-- src/language-css/embed.js | 3 +- src/language-css/parser-postcss.js | 16 +++---- src/language-html/clean.js | 11 ++--- src/language-html/embed.js | 5 ++- src/language-html/parser-html.js | 23 ++++++---- src/language-markdown/clean.js | 4 +- src/language-markdown/printer-markdown.js | 5 +-- .../unified-plugins/front-matter.js | 6 +-- src/utils/front-matter/clean.js | 11 +++++ src/utils/front-matter/parse.js | 42 +++++++++++++++++-- src/utils/front-matter/print.js | 2 +- 12 files changed, 93 insertions(+), 40 deletions(-) create mode 100644 src/utils/front-matter/clean.js diff --git a/src/language-css/clean.js b/src/language-css/clean.js index 4508ebf13..0b8e71354 100644 --- a/src/language-css/clean.js +++ b/src/language-css/clean.js @@ -1,3 +1,4 @@ +import cleanFrontMatter from "../utils/front-matter/clean.js"; import isFrontMatter from "../utils/front-matter/is-front-matter.js"; const ignoredProperties = new Set([ @@ -12,9 +13,7 @@ const ignoredProperties = new Set([ ]); function clean(original, cloned, parent) { - if (isFrontMatter(original) && original.language === "yaml") { - delete cloned.value; - } + cleanFrontMatter(original, cloned); if ( original.type === "css-comment" && diff --git a/src/language-css/embed.js b/src/language-css/embed.js index 1d0af9d02..e61e72507 100644 --- a/src/language-css/embed.js +++ b/src/language-css/embed.js @@ -1,10 +1,11 @@ import { hardline } from "../document/builders.js"; +import isFrontMatter from "../utils/front-matter/is-front-matter.js"; import printFrontMatter from "../utils/front-matter/print.js"; function embed(path) { const { node } = path; - if (node.type === "front-matter") { + if (isFrontMatter(node) && node.language === "yaml") { return async (textToDoc) => { const doc = await printFrontMatter(node, textToDoc); return doc ? [doc, hardline] : undefined; diff --git a/src/language-css/parser-postcss.js b/src/language-css/parser-postcss.js index 89ac305dd..34d86906e 100644 --- a/src/language-css/parser-postcss.js +++ b/src/language-css/parser-postcss.js @@ -377,14 +377,12 @@ function parseNestedCSS(node, options) { } function parseWithParser(parse, text, options) { - const parsed = parseFrontMatter(text); - const { frontMatter } = parsed; - text = parsed.content; + const { frontMatter, content: textToParse } = parseFrontMatter(text); let result; try { - result = parse(text, { + result = parse(textToParse, { // Prevent file access https://github.com/postcss/postcss/blob/4f4e2932fc97e2c117e1a4b15f0272ed551ed59d/lib/previous-map.js#L18 map: false, }); @@ -407,11 +405,13 @@ function parseWithParser(parse, text, options) { calculateLoc(result, text); if (frontMatter) { - frontMatter.source = { - startOffset: 0, - endOffset: frontMatter.raw.length, + result.frontMatter = { + ...frontMatter, + source: { + startOffset: frontMatter.start.index, + endOffset: frontMatter.end.index, + }, }; - result.frontMatter = frontMatter; } return result; diff --git a/src/language-html/clean.js b/src/language-html/clean.js index a6fd8a974..b8c89c1b0 100644 --- a/src/language-html/clean.js +++ b/src/language-html/clean.js @@ -1,4 +1,4 @@ -import isFrontMatter from "../utils/front-matter/is-front-matter.js"; +import cleanFrontMatter from "../utils/front-matter/clean.js"; const ignoredProperties = new Set([ "sourceSpan", @@ -28,13 +28,10 @@ function clean(original, cloned) { return null; } + cleanFrontMatter(original, cloned); + // may be formatted by multiparser - if ( - isFrontMatter(original) || - original.type === "yaml" || - original.type === "toml" - ) { - delete cloned.raw; + if (original.type === "yaml") { delete cloned.value; } diff --git a/src/language-html/embed.js b/src/language-html/embed.js index 115f5fac7..1fb788d4d 100644 --- a/src/language-html/embed.js +++ b/src/language-html/embed.js @@ -142,7 +142,10 @@ function embed(path, options) { return printAttribute(path, options); case "front-matter": - return (textToDoc) => printFrontMatter(node, textToDoc); + if (node.language === "yaml") { + return (textToDoc) => printFrontMatter(node, textToDoc); + } + break; case "angularControlFlowBlockParameters": if (!embeddedAngularControlFlowBlocks.has(path.parent.name)) { diff --git a/src/language-html/parser-html.js b/src/language-html/parser-html.js index 28be3609b..e70d86492 100644 --- a/src/language-html/parser-html.js +++ b/src/language-html/parser-html.js @@ -21,7 +21,7 @@ import HTML_TAGS from "./utils/html-tags.evaluate.js"; import isUnknownNamespace from "./utils/is-unknown-namespace.js"; /** - * @import AngularHtmlParser, {ParseOptions as AngularHtmlParserParseOptions} from "angular-html-parser" + * @import {ParseOptions as AngularHtmlParserParseOptions} from "angular-html-parser" * @import {Node as AstNode, Attribute, Element} from "angular-html-parser/lib/compiler/src/ml_parser/ast.js" * @import {ParseTreeResult} from "angular-html-parser/lib/compiler/src/ml_parser/parser.js" */ @@ -348,7 +348,7 @@ function parse( options = {}, shouldParseFrontMatter = true, ) { - const { frontMatter, content } = shouldParseFrontMatter + const { frontMatter, content: textToParse } = shouldParseFrontMatter ? parseFrontMatter(text) : { frontMatter: null, content: text }; @@ -358,15 +358,24 @@ function parse( const rawAst = { type: "root", sourceSpan: new ParseSourceSpan(start, end), - children: ngHtmlParser(content, parseOptions, options), + children: ngHtmlParser(textToParse, parseOptions, options), }; if (frontMatter) { - const start = new ParseLocation(file, 0, 0, 0); - const end = start.moveBy(frontMatter.raw.length); - frontMatter.sourceSpan = new ParseSourceSpan(start, end); + const [start, end] = [frontMatter.start, frontMatter.end].map( + (location) => + new ParseLocation( + file, + location.index, + location.line - 1, + location.column, + ), + ); // @ts-expect-error -- not a real AstNode - rawAst.children.unshift(frontMatter); + rawAst.children.unshift({ + ...frontMatter, + sourceSpan: new ParseSourceSpan(start, end), + }); } const ast = new Node(rawAst); diff --git a/src/language-markdown/clean.js b/src/language-markdown/clean.js index 9c53c6821..00882932d 100644 --- a/src/language-markdown/clean.js +++ b/src/language-markdown/clean.js @@ -1,4 +1,5 @@ import collapseWhiteSpace from "collapse-white-space"; +import cleanFrontMatter from "../utils/front-matter/clean.js"; import isFrontMatter from "../utils/front-matter/is-front-matter.js"; import { FORMAT_PRAGMAS } from "../utils/pragma/pragma.evaluate.js"; import { startWithPragma } from "./pragma.js"; @@ -10,7 +11,6 @@ const ignoredProperties = new Set([ function clean(original, cloned, parent) { // for codeblock if ( - original.type === "front-matter" || original.type === "code" || original.type === "yaml" || original.type === "import" || @@ -20,6 +20,8 @@ function clean(original, cloned, parent) { delete cloned.value; } + cleanFrontMatter(original, cloned); + if (original.type === "list") { delete cloned.isAligned; } diff --git a/src/language-markdown/printer-markdown.js b/src/language-markdown/printer-markdown.js index 5c36aa6fc..06db9a182 100644 --- a/src/language-markdown/printer-markdown.js +++ b/src/language-markdown/printer-markdown.js @@ -90,10 +90,7 @@ function genericPrint(path, options, print) { switch (node.type) { case "front-matter": - return options.originalText.slice( - node.position.start.offset, - node.position.end.offset, - ); + return node.raw; case "root": /* c8 ignore next 3 */ if (node.children.length === 0) { diff --git a/src/language-markdown/unified-plugins/front-matter.js b/src/language-markdown/unified-plugins/front-matter.js index 49c4a2aa1..35dde24ec 100644 --- a/src/language-markdown/unified-plugins/front-matter.js +++ b/src/language-markdown/unified-plugins/front-matter.js @@ -11,10 +11,10 @@ const frontMatter = function () { proto.blockTokenizers.frontMatter = tokenizer; function tokenizer(eat, value) { - const parsed = parseFrontMatter(value); + const { frontMatter } = parseFrontMatter(value); - if (parsed.frontMatter) { - return eat(parsed.frontMatter.raw)(parsed.frontMatter); + if (frontMatter) { + return eat(frontMatter.raw)(frontMatter); } } tokenizer.onlyAtStart = true; diff --git a/src/utils/front-matter/clean.js b/src/utils/front-matter/clean.js new file mode 100644 index 000000000..d286901ac --- /dev/null +++ b/src/utils/front-matter/clean.js @@ -0,0 +1,11 @@ +import isFrontMatter from "./is-front-matter.js"; + +function clean(original, cloned) { + if (isFrontMatter(original) && original.language === "yaml") { + delete cloned.end; + delete cloned.raw; + delete cloned.value; + } +} + +export default clean; diff --git a/src/utils/front-matter/parse.js b/src/utils/front-matter/parse.js index 5e426b35c..8d15781df 100644 --- a/src/utils/front-matter/parse.js +++ b/src/utils/front-matter/parse.js @@ -1,5 +1,24 @@ const DELIMITER_LENGTH = 3; +/** +@typedef {{index: number, line: number, column: number}} Position +@typedef {{ + type: "front-matter", + language: string, + explicitLanguage: string | null, + value: string, + startDelimiter: string, + endDelimiter: string, + raw: string, + start: Position, + end: Position, +}} FrontMatter +*/ + +/** +@param {string} text +@returns {FrontMatter | undefined} +*/ function getFrontMatter(text) { const startDelimiter = text.slice(0, DELIMITER_LENGTH); @@ -48,15 +67,29 @@ function getFrontMatter(text) { } const raw = text.slice(0, frontMatterEndIndex); + /** @type {string[]} */ + let lines; return { type: "front-matter", language, - explicitLanguage, + explicitLanguage: explicitLanguage || null, value: text.slice(firstLineBreakIndex + 1, endDelimiterIndex), startDelimiter, endDelimiter: raw.slice(-DELIMITER_LENGTH), raw, + start: { line: 1, column: 0, index: 0 }, + end: { + index: raw.length, + get line() { + lines ??= raw.split("\n"); + return lines.length; + }, + get column() { + lines ??= raw.split("\n"); + return lines.at(-1).length; + }, + }, }; } @@ -67,11 +100,12 @@ function parse(text) { return { content: text }; } - const { raw } = frontMatter; - return { frontMatter, - content: raw.replaceAll(/[^\n]/gu, " ") + text.slice(raw.length), + get content() { + const { raw } = frontMatter; + return raw.replaceAll(/[^\n]/gu, " ") + text.slice(raw.length); + }, }; } diff --git a/src/utils/front-matter/print.js b/src/utils/front-matter/print.js index d01d97d4b..218eb51f8 100644 --- a/src/utils/front-matter/print.js +++ b/src/utils/front-matter/print.js @@ -6,7 +6,7 @@ async function print(node, textToDoc) { const doc = value ? await textToDoc(value, { parser: "yaml" }) : ""; return markAsRoot([ node.startDelimiter, - node.explicitLanguage, + node.explicitLanguage ?? "", hardline, doc, doc ? hardline : "",