From 03e6828bfaa31061e57ff8ec55312fda2707b2e5 Mon Sep 17 00:00:00 2001 From: fisker Cheung Date: Thu, 30 Oct 2025 22:16:54 +0800 Subject: [PATCH] Improve front matter support (#18153) Co-authored-by: Alexander Kachkaev --- src/language-css/clean.js | 7 +- src/language-css/embed.js | 12 +- src/language-css/parser-postcss.js | 3 +- src/language-css/pragma.js | 2 +- src/language-css/print/sequence.js | 2 +- src/language-css/printer-postcss.js | 10 +- src/language-css/visitor-keys.js | 1 - src/language-handlebars/clean.js | 3 - src/language-handlebars/embed.js | 9 - src/language-handlebars/parser-glimmer.js | 27 ++- src/language-handlebars/printer-glimmer.js | 11 +- .../visitor-keys.evaluate.js | 8 +- src/language-html/clean.js | 4 - src/language-html/embed.js | 8 - src/language-html/parse/ast.js | 8 + src/language-html/parse/parse.js | 10 +- src/language-html/printer-html.js | 10 +- src/language-html/utils/index.js | 2 +- src/language-html/visitor-keys.js | 1 - src/language-js/printer.js | 4 +- src/language-markdown/clean.js | 7 +- src/language-markdown/embed.js | 8 - src/language-markdown/pragma.js | 2 +- src/language-markdown/printer-markdown.js | 10 +- .../unified-plugins/front-matter.js | 4 +- src/language-markdown/visitor-keys.js | 1 - src/main/comments/attach.js | 5 +- src/main/create-get-visitor-keys-function.js | 22 +- src/{utils => main}/front-matter/clean.js | 0 src/main/front-matter/constants.js | 3 + src/{utils => main}/front-matter/embed.js | 2 +- src/{utils => main}/front-matter/index.js | 2 + src/main/front-matter/is-front-matter.js | 7 + src/{utils => main}/front-matter/parse.js | 6 +- src/main/front-matter/print.js | 5 + src/main/massage-ast.js | 14 +- src/main/multiparser.js | 24 ++- src/main/normalize-format-options.js | 5 +- src/main/parser-and-printer.js | 141 ++++++++++++- src/utils/front-matter/is-front-matter.js | 5 - .../__snapshots__/format.test.js.snap | 195 ++++++++++++++++++ .../misc/errors/front-matter/format.test.js | 45 ++++ .../front-matter/with-plugins/format.test.js | 11 +- tests/unit/__snapshots__/visitor-keys.js.snap | 4 - tests/unit/massage-ast.js | 5 +- 45 files changed, 533 insertions(+), 142 deletions(-) rename src/{utils => main}/front-matter/clean.js (100%) create mode 100644 src/main/front-matter/constants.js rename src/{utils => main}/front-matter/embed.js (94%) rename src/{utils => main}/front-matter/index.js (68%) create mode 100644 src/main/front-matter/is-front-matter.js rename src/{utils => main}/front-matter/parse.js (95%) create mode 100644 src/main/front-matter/print.js delete mode 100644 src/utils/front-matter/is-front-matter.js create mode 100644 tests/format/misc/errors/front-matter/__snapshots__/format.test.js.snap create mode 100644 tests/format/misc/errors/front-matter/format.test.js diff --git a/src/language-css/clean.js b/src/language-css/clean.js index 063497b81..9d11c8a8d 100644 --- a/src/language-css/clean.js +++ b/src/language-css/clean.js @@ -1,7 +1,4 @@ -import { - cleanFrontMatter, - isFrontMatter, -} from "../utils/front-matter/index.js"; +import { isFrontMatter } from "../main/front-matter/index.js"; const ignoredProperties = new Set([ "raw", // front-matter @@ -15,8 +12,6 @@ const ignoredProperties = new Set([ ]); function clean(original, cloned, parent) { - cleanFrontMatter(original, cloned); - if ( original.type === "css-comment" && parent.type === "css-root" && diff --git a/src/language-css/embed.js b/src/language-css/embed.js index a18568980..6dc021b67 100644 --- a/src/language-css/embed.js +++ b/src/language-css/embed.js @@ -1,13 +1,5 @@ -import { - isEmbedFrontMatter, - printEmbedFrontMatter, -} from "../utils/front-matter/index.js"; - -function embed(path) { - if (isEmbedFrontMatter(path)) { - return printEmbedFrontMatter; - } -} +// A empty function to provide visitor keys, and let core format front matter +function embed() {} // `front-matter` only available on `css-root` embed.getVisitorKeys = (node) => diff --git a/src/language-css/parser-postcss.js b/src/language-css/parser-postcss.js index 1e8908313..605de5111 100644 --- a/src/language-css/parser-postcss.js +++ b/src/language-css/parser-postcss.js @@ -2,7 +2,7 @@ import postcssParse from "postcss/lib/parse"; import postcssLess from "postcss-less"; import postcssScssParse from "postcss-scss/lib/scss-parse"; import createError from "../common/parser-create-error.js"; -import { parseFrontMatter } from "../utils/front-matter/index.js"; +import { parseFrontMatter } from "../main/front-matter/index.js"; import { calculateLoc, locEnd, @@ -407,6 +407,7 @@ function parseWithParser(parse, text, options) { if (frontMatter) { result.frontMatter = { ...frontMatter, + type: "front-matter", source: { startOffset: frontMatter.start.index, endOffset: frontMatter.end.index, diff --git a/src/language-css/pragma.js b/src/language-css/pragma.js index b96e34e64..96003fa1b 100644 --- a/src/language-css/pragma.js +++ b/src/language-css/pragma.js @@ -3,7 +3,7 @@ import { hasPragma as jsHasPragma, insertPragma as jsInsertPragma, } from "../language-js/pragma.js"; -import { parseFrontMatter } from "../utils/front-matter/index.js"; +import { parseFrontMatter } from "../main/front-matter/index.js"; const hasPragma = (text) => jsHasPragma(parseFrontMatter(text).content); const hasIgnorePragma = (text) => diff --git a/src/language-css/print/sequence.js b/src/language-css/print/sequence.js index 11b12664c..859057d86 100644 --- a/src/language-css/print/sequence.js +++ b/src/language-css/print/sequence.js @@ -1,5 +1,5 @@ import { hardline, line } from "../../document/builders.js"; -import { isFrontMatter } from "../../utils/front-matter/index.js"; +import { isFrontMatter } from "../../main/front-matter/index.js"; import hasNewline from "../../utils/has-newline.js"; import isNextLineEmpty from "../../utils/is-next-line-empty.js"; import { locEnd, locStart } from "../loc.js"; diff --git a/src/language-css/printer-postcss.js b/src/language-css/printer-postcss.js index 3370db39c..2cdf84cda 100644 --- a/src/language-css/printer-postcss.js +++ b/src/language-css/printer-postcss.js @@ -53,8 +53,6 @@ function genericPrint(path, options, print) { const { node } = path; switch (node.type) { - case "front-matter": - return node.raw; case "css-root": { const nodes = printSequence(path, options, print); let after = node.raws.after.trim(); @@ -566,6 +564,7 @@ function genericPrint(path, options, print) { case "value-unknown": return node.value; + case "front-matter": // Handled in core case "value-comma": // Handled in `value-comma_group` default: /* c8 ignore next */ @@ -574,6 +573,13 @@ function genericPrint(path, options, print) { } const printer = { + features: { + experimental_frontMatterSupport: { + massageAstNode: true, + embed: true, + print: true, + }, + }, print: genericPrint, embed, insertPragma, diff --git a/src/language-css/visitor-keys.js b/src/language-css/visitor-keys.js index 8e1ff4091..5ba1fe98d 100644 --- a/src/language-css/visitor-keys.js +++ b/src/language-css/visitor-keys.js @@ -1,5 +1,4 @@ const visitorKeys = { - "front-matter": [], "css-root": ["frontMatter", "nodes"], "css-comment": [], "css-rule": ["selector", "nodes"], diff --git a/src/language-handlebars/clean.js b/src/language-handlebars/clean.js index b369521a4..10300d0a4 100644 --- a/src/language-handlebars/clean.js +++ b/src/language-handlebars/clean.js @@ -1,9 +1,6 @@ -import { cleanFrontMatter } from "../utils/front-matter/index.js"; import htmlWhitespaceUtils from "../utils/html-whitespace-utils.js"; function clean(original, cloned, parent) { - cleanFrontMatter(original, cloned); - // (Glimmer/HTML) ignore TextNode if (original.type === "TextNode") { const trimmed = original.chars.trim(); diff --git a/src/language-handlebars/embed.js b/src/language-handlebars/embed.js index bfac69e84..3bdc10b81 100644 --- a/src/language-handlebars/embed.js +++ b/src/language-handlebars/embed.js @@ -1,15 +1,6 @@ -import { - isEmbedFrontMatter, - printEmbedFrontMatter, -} from "../utils/front-matter/embed.js"; - function embed(path /* , options*/) { const { node } = path; - if (isEmbedFrontMatter(path)) { - return printEmbedFrontMatter; - } - if (node.type !== "TextNode") { return; } diff --git a/src/language-handlebars/parser-glimmer.js b/src/language-handlebars/parser-glimmer.js index 67f69a7f3..ef1efb9a5 100644 --- a/src/language-handlebars/parser-glimmer.js +++ b/src/language-handlebars/parser-glimmer.js @@ -1,8 +1,18 @@ import { preprocess as parseGlimmer } from "@glimmer/syntax"; import createError from "../common/parser-create-error.js"; -import parseFrontMatter from "../utils/front-matter/parse.js"; +import parseFrontMatter from "../main/front-matter/parse.js"; import { locEnd, locStart } from "./loc.js"; +/** +@import {AST, PreprocessOptions} from "@glimmer/syntax"; +@typedef {AST.SourcePosition & {offset: number}} SourcePosition +@typedef {{start: SourcePosition, end: SourcePosition}} SourceLocation +@typedef {Omit & { + type: "FrontMatter", + loc: SourceLocation, +}} GlimmerFrontMatter +*/ + /* from the following template: `non-escaped mustache \\{{helper}}` * glimmer parser will produce an AST missing a backslash * so here we add it back @@ -52,18 +62,18 @@ const glimmerPrettierParsePlugin = (/* options*/) => ({ }, }); -/** @type {import("@glimmer/syntax").PreprocessOptions} */ +/** @type {PreprocessOptions} */ const glimmerParseOptions = { mode: "codemod", plugins: { ast: [glimmerPrettierParsePlugin] }, }; function parse(text) { - const { frontMatter, content } = parseFrontMatter(text); + const { frontMatter, content: textToParse } = parseFrontMatter(text); let ast; try { - ast = parseGlimmer(content, glimmerParseOptions); + ast = parseGlimmer(textToParse, glimmerParseOptions); } catch (error) { const location = getErrorLocation(error); @@ -78,8 +88,10 @@ function parse(text) { } if (frontMatter) { - const frontMatterNode = { + /** @type {GlimmerFrontMatter} */ + const glimmerFrontMatter = { ...frontMatter, + type: "FrontMatter", loc: { start: { ...frontMatter.start, @@ -91,9 +103,8 @@ function parse(text) { }, }, }; - - // @ts-expect-error - ast.body.unshift(frontMatterNode); + // @ts-expect-error -- not a real "Node" + ast.body.unshift(glimmerFrontMatter); } return ast; diff --git a/src/language-handlebars/printer-glimmer.js b/src/language-handlebars/printer-glimmer.js index 9ed095681..3ffedc7aa 100644 --- a/src/language-handlebars/printer-glimmer.js +++ b/src/language-handlebars/printer-glimmer.js @@ -395,9 +395,6 @@ function print(path, options, print) { case "CommentStatement": return [""]; - case "front-matter": - return node.raw; - case "StringLiteral": return printStringLiteral(path, options); @@ -410,6 +407,7 @@ function print(path, options, print) { case "NullLiteral": return "null"; + case "FrontMatter": // Handled in core case "AtHead": // Handled in `printPathExpression` case "VarHead": // Handled in `printPathExpression` case "ThisHead": // Handled in `printPathExpression` @@ -839,6 +837,13 @@ function printPathExpression(node) { } const printer = { + features: { + experimental_frontMatterSupport: { + massageAstNode: true, + embed: true, + print: true, + }, + }, print, massageAstNode: clean, hasPrettierIgnore, diff --git a/src/language-handlebars/visitor-keys.evaluate.js b/src/language-handlebars/visitor-keys.evaluate.js index 2a83f193b..0c86c8e91 100644 --- a/src/language-handlebars/visitor-keys.evaluate.js +++ b/src/language-handlebars/visitor-keys.evaluate.js @@ -1,7 +1 @@ -import { visitorKeys } from "@glimmer/syntax"; - -// add front-matter to the glimmer visitor keys -export default { - ...visitorKeys, - "front-matter": [], -}; +export { visitorKeys as default } from "@glimmer/syntax"; diff --git a/src/language-html/clean.js b/src/language-html/clean.js index c4000e6c4..177b9cc91 100644 --- a/src/language-html/clean.js +++ b/src/language-html/clean.js @@ -1,5 +1,3 @@ -import { cleanFrontMatter } from "../utils/front-matter/index.js"; - const ignoredProperties = new Set([ "sourceSpan", "startSourceSpan", @@ -28,8 +26,6 @@ function clean(original, cloned, parent) { return null; } - cleanFrontMatter(original, cloned); - // may be formatted by multiparser if (original.kind === "yaml") { delete cloned.value; diff --git a/src/language-html/embed.js b/src/language-html/embed.js index da924c259..5f363c8bb 100644 --- a/src/language-html/embed.js +++ b/src/language-html/embed.js @@ -5,10 +5,6 @@ import { indent, line, } from "../document/builders.js"; -import { - isEmbedFrontMatter, - printEmbedFrontMatter, -} from "../utils/front-matter/index.js"; import htmlWhitespaceUtils from "../utils/html-whitespace-utils.js"; import printAngularControlFlowBlockParameters from "./embed/angular-control-flow-block-parameters.js"; import printAttribute from "./embed/attribute.js"; @@ -40,10 +36,6 @@ const embeddedAngularControlFlowBlocks = new Set([ function embed(path, options) { const { node } = path; - if (isEmbedFrontMatter(path)) { - return printEmbedFrontMatter; - } - switch (node.kind) { case "element": if (isScriptLikeTag(node, options) || node.kind === "interpolation") { diff --git a/src/language-html/parse/ast.js b/src/language-html/parse/ast.js index d390a1c6d..315009c38 100644 --- a/src/language-html/parse/ast.js +++ b/src/language-html/parse/ast.js @@ -1,3 +1,5 @@ +import { isFrontMatter } from "../../main/front-matter/index.js"; + const NODES_KEYS = { attrs: true, children: true, @@ -21,6 +23,12 @@ class Node { ])) { this.setProperty(property, nodeOrProperties[property]); } + + if (isFrontMatter(nodeOrProperties)) { + for (const property of Object.getOwnPropertySymbols(nodeOrProperties)) { + this.setProperty(property, nodeOrProperties[property]); + } + } } setProperty(property, value) { diff --git a/src/language-html/parse/parse.js b/src/language-html/parse/parse.js index 31515c526..e1c6d6b35 100644 --- a/src/language-html/parse/parse.js +++ b/src/language-html/parse/parse.js @@ -5,7 +5,7 @@ import { ParseSourceSpan, } from "angular-html-parser"; import createError from "../../common/parser-create-error.js"; -import { parseFrontMatter } from "../../utils/front-matter/index.js"; +import { parseFrontMatter } from "../../main/front-matter/index.js"; import { normalizeParseOptions, toAngularHtmlParserParseOptions, @@ -15,9 +15,9 @@ import { postprocess } from "./postprocess.js"; /** @import {ParseOptions as AngularHtmlParserParseOptions, Ast, ParseTreeResult} from "angular-html-parser"; @import {RawParseOptions, ParseOptions} from "./parse-options.js"; -@import {FrontMatter} from "../../utils/front-matter/parse.js" +@import {FrontMatter} from "../../main/front-matter/parse.js" @typedef {{filepath?: string}} Options -@typedef {FrontMatter & {kind: FrontMatter["type"], sourceSpan: ParseSourceSpan}} HtmlFrontMatter +@typedef {FrontMatter & {kind: "frontMatter", sourceSpan: ParseSourceSpan}} HtmlFrontMatter */ /** @@ -182,7 +182,7 @@ function parse(text, parser, parseOptions, options = {}) { const { frontMatter, content: textToParse } = parseOptions.shouldParseFrontMatter ? parseFrontMatter(text) - : { frontMatter: null, content: text }; + : { content: text }; const file = new ParseSourceFile(text, options.filepath); const start = new ParseLocation(file, 0, 0, 0); @@ -213,7 +213,7 @@ function parse(text, parser, parseOptions, options = {}) { ); htmlFrontMatter = { ...frontMatter, - kind: frontMatter.type, + kind: "frontMatter", sourceSpan: new ParseSourceSpan(start, end), }; } diff --git a/src/language-html/printer-html.js b/src/language-html/printer-html.js index 57bea4b7e..d3999ef7d 100644 --- a/src/language-html/printer-html.js +++ b/src/language-html/printer-html.js @@ -35,8 +35,6 @@ function genericPrint(path, options, print) { const { node } = path; switch (node.kind) { - case "front-matter": - return replaceEndOfLine(node.raw); case "root": if (options.__onHtmlRoot) { options.__onHtmlRoot(node); @@ -136,6 +134,7 @@ function genericPrint(path, options, print) { quote, ]; } + case "frontMatter": // Handled in core case "cdata": // Transformed into `text` default: /* c8 ignore next */ @@ -144,6 +143,13 @@ function genericPrint(path, options, print) { } const printer = { + features: { + experimental_frontMatterSupport: { + massageAstNode: true, + embed: true, + print: true, + }, + }, preprocess, print: genericPrint, insertPragma, diff --git a/src/language-html/utils/index.js b/src/language-html/utils/index.js index e55aa60ce..8e52e8b6e 100644 --- a/src/language-html/utils/index.js +++ b/src/language-html/utils/index.js @@ -4,7 +4,7 @@ import { hardline, join, line } from "../../document/builders.js"; import { replaceEndOfLine } from "../../document/utils.js"; -import { isFrontMatter } from "../../utils/front-matter/index.js"; +import { isFrontMatter } from "../../main/front-matter/index.js"; import htmlWhitespaceUtils from "../../utils/html-whitespace-utils.js"; import inferParser from "../../utils/infer-parser.js"; import { diff --git a/src/language-html/visitor-keys.js b/src/language-html/visitor-keys.js index eabe9e62e..534bce879 100644 --- a/src/language-html/visitor-keys.js +++ b/src/language-html/visitor-keys.js @@ -1,5 +1,4 @@ const visitorKeys = { - "front-matter": [], root: ["children"], element: ["attrs", "children"], ieConditionalComment: ["children"], diff --git a/src/language-js/printer.js b/src/language-js/printer.js index 489de3a53..c7839136f 100644 --- a/src/language-js/printer.js +++ b/src/language-js/printer.js @@ -1,6 +1,6 @@ -export const experimentalFeatures = { +export const features = { // TODO: Make this default behavior - avoidAstMutation: true, + experimental_avoidAstMutation: true, }; export { default as massageAstNode } from "./clean.js"; export { diff --git a/src/language-markdown/clean.js b/src/language-markdown/clean.js index ba49ab751..1abd60eb3 100644 --- a/src/language-markdown/clean.js +++ b/src/language-markdown/clean.js @@ -1,8 +1,5 @@ import collapseWhiteSpace from "collapse-white-space"; -import { - cleanFrontMatter, - isFrontMatter, -} from "../utils/front-matter/index.js"; +import { isFrontMatter } from "../main/front-matter/index.js"; import { hasPragma } from "./pragma.js"; const ignoredProperties = new Set([ @@ -21,8 +18,6 @@ function clean(original, cloned, parent) { delete cloned.value; } - cleanFrontMatter(original, cloned); - if (original.type === "list") { delete cloned.isAligned; } diff --git a/src/language-markdown/embed.js b/src/language-markdown/embed.js index c017487a4..34aea0390 100644 --- a/src/language-markdown/embed.js +++ b/src/language-markdown/embed.js @@ -1,9 +1,5 @@ import { hardline, markAsRoot } from "../document/builders.js"; import { replaceEndOfLine } from "../document/utils.js"; -import { - isEmbedFrontMatter, - printEmbedFrontMatter, -} from "../utils/front-matter/index.js"; import getMaxContinuousCount from "../utils/get-max-continuous-count.js"; import inferParser from "../utils/infer-parser.js"; import { getFencedCodeBlockValue } from "./utils.js"; @@ -49,10 +45,6 @@ function embed(path, options) { } } - if (isEmbedFrontMatter(path)) { - return printEmbedFrontMatter; - } - switch (node.type) { // MDX case "import": diff --git a/src/language-markdown/pragma.js b/src/language-markdown/pragma.js index e99a18107..10785416d 100644 --- a/src/language-markdown/pragma.js +++ b/src/language-markdown/pragma.js @@ -1,4 +1,4 @@ -import { parseFrontMatter } from "../utils/front-matter/index.js"; +import { parseFrontMatter } from "../main/front-matter/index.js"; import { FORMAT_PRAGMA_TO_INSERT, MARKDOWN_HAS_IGNORE_PRAGMA_REGEXP, diff --git a/src/language-markdown/printer-markdown.js b/src/language-markdown/printer-markdown.js index a096f6d9c..03f98696d 100644 --- a/src/language-markdown/printer-markdown.js +++ b/src/language-markdown/printer-markdown.js @@ -89,8 +89,6 @@ function genericPrint(path, options, print) { } switch (node.type) { - case "front-matter": - return node.raw; case "root": /* c8 ignore next 3 */ if (node.children.length === 0) { @@ -449,6 +447,7 @@ function genericPrint(path, options, print) { // since it's very possible that it's recognized as math accidentally return options.originalText.slice(locStart(node), locEnd(node)); + case "frontMatter": // Handled in core case "tableRow": // handled in "table" case "listItem": // handled in "list" case "text": // handled in other types @@ -790,6 +789,13 @@ function printFootnoteReference(node) { } const printer = { + features: { + experimental_frontMatterSupport: { + massageAstNode: true, + embed: true, + print: true, + }, + }, preprocess, print: genericPrint, embed, diff --git a/src/language-markdown/unified-plugins/front-matter.js b/src/language-markdown/unified-plugins/front-matter.js index 0ce20c175..3bda14e63 100644 --- a/src/language-markdown/unified-plugins/front-matter.js +++ b/src/language-markdown/unified-plugins/front-matter.js @@ -1,4 +1,4 @@ -import { parseFrontMatter } from "../../utils/front-matter/index.js"; +import { parseFrontMatter } from "../../main/front-matter/index.js"; /** @import {Plugin, Settings} from "unified" */ @@ -14,7 +14,7 @@ const frontMatter = function () { const { frontMatter } = parseFrontMatter(value); if (frontMatter) { - return eat(frontMatter.raw)(frontMatter); + return eat(frontMatter.raw)({ ...frontMatter, type: "frontMatter" }); } } tokenizer.onlyAtStart = true; diff --git a/src/language-markdown/visitor-keys.js b/src/language-markdown/visitor-keys.js index f4ddc80da..f906d15b6 100644 --- a/src/language-markdown/visitor-keys.js +++ b/src/language-markdown/visitor-keys.js @@ -1,5 +1,4 @@ const visitorKeys = { - "front-matter": [], root: ["children"], paragraph: ["children"], sentence: ["children"], diff --git a/src/main/comments/attach.js b/src/main/comments/attach.js index 93a2e10d7..ef56c8c28 100644 --- a/src/main/comments/attach.js +++ b/src/main/comments/attach.js @@ -150,10 +150,7 @@ function attachComments(ast, options) { const tiesToBreak = []; const { printer: { - experimentalFeatures: { - // TODO: Make this as default behavior - avoidAstMutation = false, - } = {}, + features: { experimental_avoidAstMutation: avoidAstMutation }, handleComments = {}, }, originalText: text, diff --git a/src/main/create-get-visitor-keys-function.js b/src/main/create-get-visitor-keys-function.js index 9091a9125..f786c5439 100644 --- a/src/main/create-get-visitor-keys-function.js +++ b/src/main/create-get-visitor-keys-function.js @@ -1,3 +1,8 @@ +import { + FRONT_MATTER_VISITOR_KEYS, + isFrontMatter, +} from "./front-matter/index.js"; + const nonTraversableKeys = new Set([ "tokens", "comments", @@ -10,10 +15,23 @@ const nonTraversableKeys = new Set([ const defaultGetVisitorKeys = (node) => Object.keys(node).filter((key) => !nonTraversableKeys.has(key)); -function createGetVisitorKeysFunction(printerGetVisitorKeys) { - return printerGetVisitorKeys +function createGetVisitorKeysFunction( + printerGetVisitorKeys, + supportFrontMatter, +) { + const getVisitorKeys = printerGetVisitorKeys ? (node) => printerGetVisitorKeys(node, nonTraversableKeys) : defaultGetVisitorKeys; + if (!supportFrontMatter) { + return getVisitorKeys; + } + + return new Proxy(getVisitorKeys, { + apply: (target, thisArgument, argumentsList) => + isFrontMatter(argumentsList[0]) + ? FRONT_MATTER_VISITOR_KEYS + : Reflect.apply(target, thisArgument, argumentsList), + }); } export default createGetVisitorKeysFunction; diff --git a/src/utils/front-matter/clean.js b/src/main/front-matter/clean.js similarity index 100% rename from src/utils/front-matter/clean.js rename to src/main/front-matter/clean.js diff --git a/src/main/front-matter/constants.js b/src/main/front-matter/constants.js new file mode 100644 index 000000000..6b83a539e --- /dev/null +++ b/src/main/front-matter/constants.js @@ -0,0 +1,3 @@ +/** @type {unique symbol} */ +export const FRONT_MATTER_MARK = Symbol.for("PRETTIER_IS_FRONT_MATTER"); +export const FRONT_MATTER_VISITOR_KEYS = []; diff --git a/src/utils/front-matter/embed.js b/src/main/front-matter/embed.js similarity index 94% rename from src/utils/front-matter/embed.js rename to src/main/front-matter/embed.js index fe70bd041..2696a6d48 100644 --- a/src/utils/front-matter/embed.js +++ b/src/main/front-matter/embed.js @@ -1,5 +1,5 @@ import { hardline, markAsRoot } from "../../document/builders.js"; -import inferParser from "../infer-parser.js"; +import inferParser from "../../utils/infer-parser.js"; import isFrontMatter from "./is-front-matter.js"; const SUPPORTED_EMBED_LANGUAGES = new Set(["yaml", "toml"]); diff --git a/src/utils/front-matter/index.js b/src/main/front-matter/index.js similarity index 68% rename from src/utils/front-matter/index.js rename to src/main/front-matter/index.js index bb00e5832..783c89ed3 100644 --- a/src/utils/front-matter/index.js +++ b/src/main/front-matter/index.js @@ -1,4 +1,6 @@ export { default as cleanFrontMatter } from "./clean.js"; +export { FRONT_MATTER_VISITOR_KEYS } from "./constants.js"; export { isEmbedFrontMatter, printEmbedFrontMatter } from "./embed.js"; export { default as isFrontMatter } from "./is-front-matter.js"; export { default as parseFrontMatter } from "./parse.js"; +export { default as printFrontMatter } from "./print.js"; diff --git a/src/main/front-matter/is-front-matter.js b/src/main/front-matter/is-front-matter.js new file mode 100644 index 000000000..8584a5503 --- /dev/null +++ b/src/main/front-matter/is-front-matter.js @@ -0,0 +1,7 @@ +import { FRONT_MATTER_MARK } from "./constants.js"; + +function isFrontMatter(node) { + return Boolean(node?.[FRONT_MATTER_MARK]); +} + +export default isFrontMatter; diff --git a/src/utils/front-matter/parse.js b/src/main/front-matter/parse.js similarity index 95% rename from src/utils/front-matter/parse.js rename to src/main/front-matter/parse.js index 8d15781df..187659371 100644 --- a/src/utils/front-matter/parse.js +++ b/src/main/front-matter/parse.js @@ -1,9 +1,10 @@ +import { FRONT_MATTER_MARK } from "./constants.js"; + const DELIMITER_LENGTH = 3; /** @typedef {{index: number, line: number, column: number}} Position @typedef {{ - type: "front-matter", language: string, explicitLanguage: string | null, value: string, @@ -12,6 +13,7 @@ const DELIMITER_LENGTH = 3; raw: string, start: Position, end: Position, + [FRONT_MATTER_MARK]: true, }} FrontMatter */ @@ -71,7 +73,6 @@ function getFrontMatter(text) { let lines; return { - type: "front-matter", language, explicitLanguage: explicitLanguage || null, value: text.slice(firstLineBreakIndex + 1, endDelimiterIndex), @@ -90,6 +91,7 @@ function getFrontMatter(text) { return lines.at(-1).length; }, }, + [FRONT_MATTER_MARK]: true, }; } diff --git a/src/main/front-matter/print.js b/src/main/front-matter/print.js new file mode 100644 index 000000000..21db88abc --- /dev/null +++ b/src/main/front-matter/print.js @@ -0,0 +1,5 @@ +function printFrontMatter({ node }) { + return node.raw; +} + +export default printFrontMatter; diff --git a/src/main/massage-ast.js b/src/main/massage-ast.js index f1d6f8122..6d356c43d 100644 --- a/src/main/massage-ast.js +++ b/src/main/massage-ast.js @@ -1,17 +1,15 @@ import isObject from "../utils/is-object.js"; -import createGetVisitorKeysFunction from "./create-get-visitor-keys-function.js"; function massageAst(ast, options) { - const cleanFunction = options.printer.massageAstNode; + const { printer } = options; + const clean = printer.massageAstNode; - if (!cleanFunction) { + if (!clean) { return ast; } - const getVisitorKeys = - options.getVisitorKeys ?? - createGetVisitorKeysFunction(options.printer.getVisitorKeys); - const { ignoredProperties } = cleanFunction; + const { getVisitorKeys } = printer; + const { ignoredProperties } = clean; return recurse(ast); @@ -38,7 +36,7 @@ function massageAst(ast, options) { } } - const result = cleanFunction(original, cloned, parent); + const result = clean(original, cloned, parent); if (result === null) { return; } diff --git a/src/main/multiparser.js b/src/main/multiparser.js index a408045b4..496fdbcab 100644 --- a/src/main/multiparser.js +++ b/src/main/multiparser.js @@ -1,5 +1,4 @@ import { stripTrailingHardline } from "../document/utils.js"; -import createGetVisitorKeysFunction from "./create-get-visitor-keys-function.js"; import normalizeFormatOptions from "./normalize-format-options.js"; import parse from "./parse.js"; @@ -12,12 +11,14 @@ async function printEmbeddedLanguages( printAstToDoc, embeds, ) { - const { - embeddedLanguageFormatting, - printer: { embed, hasPrettierIgnore = () => false }, - } = options; + if (options.embeddedLanguageFormatting !== "auto") { + return; + } - if (!embed || embeddedLanguageFormatting !== "auto") { + const { printer } = options; + const { embed } = printer; + + if (!embed) { return; } @@ -27,9 +28,8 @@ async function printEmbeddedLanguages( ); } - const getVisitorKeys = embed.getVisitorKeys - ? createGetVisitorKeysFunction(embed.getVisitorKeys) - : options.getVisitorKeys; + const { hasPrettierIgnore } = printer; + const { getVisitorKeys } = embed; const embedCallResults = []; @@ -61,7 +61,11 @@ async function printEmbeddedLanguages( function recurse() { const { node } = path; - if (node === null || typeof node !== "object" || hasPrettierIgnore(path)) { + if ( + node === null || + typeof node !== "object" || + hasPrettierIgnore?.(path) + ) { return; } diff --git a/src/main/normalize-format-options.js b/src/main/normalize-format-options.js index b9ea5e356..e44ac92bc 100644 --- a/src/main/normalize-format-options.js +++ b/src/main/normalize-format-options.js @@ -1,7 +1,6 @@ import { UndefinedParserError } from "../common/errors.js"; import { getSupportInfo } from "../main/support.js"; import inferParser from "../utils/infer-parser.js"; -import createGetVisitorKeysFunction from "./create-get-visitor-keys-function.js"; import normalizeOptions from "./normalize-options.js"; import { getParserPluginByParserName, @@ -70,9 +69,7 @@ async function normalizeFormatOptions(options, opts = {}) { const printer = await initPrinter(printerPlugin, parser.astFormat); rawOptions.printer = printer; - rawOptions.getVisitorKeys = createGetVisitorKeysFunction( - printer.getVisitorKeys, - ); + rawOptions.getVisitorKeys = printer.getVisitorKeys; const pluginDefaults = printerPlugin.defaultOptions ? Object.fromEntries( diff --git a/src/main/parser-and-printer.js b/src/main/parser-and-printer.js index 29b49c1ac..5dbac83f8 100644 --- a/src/main/parser-and-printer.js +++ b/src/main/parser-and-printer.js @@ -1,4 +1,13 @@ +import * as assert from "#universal/assert"; import { ConfigError } from "../common/errors.js"; +import createGetVisitorKeysFunction from "./create-get-visitor-keys-function.js"; +import { + cleanFrontMatter, + isEmbedFrontMatter, + isFrontMatter, + printEmbedFrontMatter, + printFrontMatter, +} from "./front-matter/index.js"; function getParserPluginByParserName(plugins, parserName) { if (!parserName) { @@ -58,11 +67,134 @@ function initParser(plugin, parserName) { : parserOrParserInitFunction; } -function initPrinter(plugin, astFormat) { +async function initPrinter(plugin, astFormat) { const printerOrPrinterInitFunction = plugin.printers[astFormat]; - return typeof printerOrPrinterInitFunction === "function" - ? printerOrPrinterInitFunction() - : printerOrPrinterInitFunction; + const printer = + typeof printerOrPrinterInitFunction === "function" + ? await printerOrPrinterInitFunction() + : printerOrPrinterInitFunction; + return normalizePrinter(printer); +} + +const normalizedPrinters = new WeakMap(); +const PRINTER_NORMALIZED_MARK = Symbol("PRINTER_NORMALIZED_MARK"); +function normalizePrinter(printer) { + if (normalizedPrinters.has(printer)) { + return normalizedPrinters.get(printer); + } + + /* c8 ignore next 6 */ + if (process.env.NODE_ENV !== "production") { + assert.ok( + !printer[PRINTER_NORMALIZED_MARK], + "Unexpected printer normalization", + ); + } + + let { + features, + getVisitorKeys, + embed: originalEmbed, + massageAstNode: originalCleanFunction, + print: originalPrint, + ...printerRestProperties + } = printer; + + features = normalizePrinterFeatures(features); + const frontMatterSupport = features.experimental_frontMatterSupport; + + getVisitorKeys = createGetVisitorKeysFunction( + getVisitorKeys, + /** frontMatterVisitorKeys */ frontMatterSupport.massageAstNode || + frontMatterSupport.embed || + frontMatterSupport.print, + ); + + let massageAstNode = originalCleanFunction; + if (originalCleanFunction && frontMatterSupport.massageAstNode) { + massageAstNode = new Proxy(originalCleanFunction, { + apply(target, thisArgument, argumentsList) { + cleanFrontMatter(...argumentsList); + return Reflect.apply(target, thisArgument, argumentsList); + }, + }); + } + + let embed = originalEmbed; + if (originalEmbed) { + let embedGetVisitorKeys; + embed = new Proxy(originalEmbed, { + get(target, property, receiver) { + if (property === "getVisitorKeys") { + embedGetVisitorKeys ??= originalEmbed.getVisitorKeys + ? createGetVisitorKeysFunction( + originalEmbed.getVisitorKeys, + /** frontMatterVisitorKeys */ frontMatterSupport.massageAstNode || + frontMatterSupport.embed, + ) + : getVisitorKeys; + return embedGetVisitorKeys; + } + + return Reflect.get(target, property, receiver); + }, + apply: (target, thisArgument, argumentsList) => + frontMatterSupport.embed && isEmbedFrontMatter(...argumentsList) + ? printEmbedFrontMatter + : Reflect.apply(target, thisArgument, argumentsList), + }); + } + + let print = originalPrint; + if (frontMatterSupport.print) { + print = new Proxy(originalPrint, { + apply(target, thisArgument, argumentsList) { + const [path] = argumentsList; + if (isFrontMatter(path.node)) { + return printFrontMatter(path); + } + return Reflect.apply(target, thisArgument, argumentsList); + }, + }); + } + + const normalizedPrinter = { + features, + getVisitorKeys, + embed, + massageAstNode, + print, + ...printerRestProperties, + }; + + /* c8 ignore next 3 */ + if (process.env.NODE_ENV !== "production") { + normalizedPrinter[PRINTER_NORMALIZED_MARK] = true; + } + + normalizedPrinters.set(printer, normalizedPrinter); + return normalizedPrinter; +} + +const PRINTER_FRONT_MATTER_SUPPORT_FEATURES = ["clean", "embed", "print"]; +const PRINTER_FRONT_MATTER_SUPPORT_OFF = Object.fromEntries( + PRINTER_FRONT_MATTER_SUPPORT_FEATURES.map((feature) => [feature, false]), +); +function normalizePrinterFrontMatterSupport(frontMatterSupport) { + return { + ...PRINTER_FRONT_MATTER_SUPPORT_OFF, + ...frontMatterSupport, + }; +} + +function normalizePrinterFeatures(features) { + return { + experimental_avoidAstMutation: false, + ...features, + experimental_frontMatterSupport: normalizePrinterFrontMatterSupport( + features?.experimental_frontMatterSupport, + ), + }; } export { @@ -70,5 +202,6 @@ export { getPrinterPluginByAstFormat, initParser, initPrinter, + normalizePrinter, resolveParser, }; diff --git a/src/utils/front-matter/is-front-matter.js b/src/utils/front-matter/is-front-matter.js deleted file mode 100644 index 4ef2b5b32..000000000 --- a/src/utils/front-matter/is-front-matter.js +++ /dev/null @@ -1,5 +0,0 @@ -function isFrontMatter(node) { - return node?.type === "front-matter"; -} - -export default isFrontMatter; diff --git a/tests/format/misc/errors/front-matter/__snapshots__/format.test.js.snap b/tests/format/misc/errors/front-matter/__snapshots__/format.test.js.snap new file mode 100644 index 000000000..142573f75 --- /dev/null +++ b/tests/format/misc/errors/front-matter/__snapshots__/format.test.js.snap @@ -0,0 +1,195 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`snippet: Should not support front matter [__babel_estree] format 1`] = ` +"Unexpected token (3:4) + 1 | --- + 2 | Front Matter content +> 3 | --- + | ^ +Cause: Unexpected token (3:3)" +`; + +exports[`snippet: Should not support front matter [__js_expression] format 1`] = ` +"The input should contain exactly one expression, but the first expression is followed by the unexpected character \`M\`. (2:7) + 1 | --- +> 2 | Front Matter content + | ^ + 3 | --- +Cause: Unexpected parseExpression() input: The input should contain exactly one expression, but the first expression is followed by the unexpected character \`M\`. (2:6)" +`; + +exports[`snippet: Should not support front matter [__ng_action] format 1`] = `"Unexpected token 'Matter'"`; + +exports[`snippet: Should not support front matter [__ng_binding] format 1`] = `"Unexpected token 'Matter'"`; + +exports[`snippet: Should not support front matter [__ng_directive] format 1`] = ` +"Unexpected end of expression: --- +Front Matter content +--- at the end of the expression [--- +Front Matter content +---] in test.html@0:0" +`; + +exports[`snippet: Should not support front matter [__ng_interpolation] format 1`] = `"Unexpected token 'Matter'"`; + +exports[`snippet: Should not support front matter [__ts_expression] format 1`] = ` +"The input should contain exactly one expression, but the first expression is followed by the unexpected character \`M\`. (2:7) + 1 | --- +> 2 | Front Matter content + | ^ + 3 | --- +Cause: Unexpected parseExpression() input: The input should contain exactly one expression, but the first expression is followed by the unexpected character \`M\`. (2:6)" +`; + +exports[`snippet: Should not support front matter [__vue_event_binding] format 1`] = ` +"Unexpected token (3:4) + 1 | --- + 2 | Front Matter content +> 3 | --- + | ^ +Cause: Unexpected token (3:3)" +`; + +exports[`snippet: Should not support front matter [__vue_expression] format 1`] = ` +"The input should contain exactly one expression, but the first expression is followed by the unexpected character \`M\`. (2:7) + 1 | --- +> 2 | Front Matter content + | ^ + 3 | --- +Cause: Unexpected parseExpression() input: The input should contain exactly one expression, but the first expression is followed by the unexpected character \`M\`. (2:6)" +`; + +exports[`snippet: Should not support front matter [__vue_ts_event_binding] format 1`] = ` +"Unexpected token (3:4) + 1 | --- + 2 | Front Matter content +> 3 | --- + | ^ +Cause: Unexpected token (3:3)" +`; + +exports[`snippet: Should not support front matter [__vue_ts_expression] format 1`] = ` +"The input should contain exactly one expression, but the first expression is followed by the unexpected character \`M\`. (2:7) + 1 | --- +> 2 | Front Matter content + | ^ + 3 | --- +Cause: Unexpected parseExpression() input: The input should contain exactly one expression, but the first expression is followed by the unexpected character \`M\`. (2:6)" +`; + +exports[`snippet: Should not support front matter [acorn] format 1`] = ` +"Assigning to rvalue (1:3) +> 1 | --- + | ^ + 2 | Front Matter content + 3 | --- +Cause: Assigning to rvalue (1:2)" +`; + +exports[`snippet: Should not support front matter [babel] format 1`] = ` +"Unexpected token (3:4) + 1 | --- + 2 | Front Matter content +> 3 | --- + | ^ +Cause: Unexpected token (3:3)" +`; + +exports[`snippet: Should not support front matter [babel-flow] format 1`] = ` +"Unexpected token (3:4) + 1 | --- + 2 | Front Matter content +> 3 | --- + | ^ +Cause: Unexpected token (3:3)" +`; + +exports[`snippet: Should not support front matter [babel-ts] format 1`] = ` +"Unexpected token (3:4) + 1 | --- + 2 | Front Matter content +> 3 | --- + | ^ +Cause: Unexpected token (3:3)" +`; + +exports[`snippet: Should not support front matter [espree] format 1`] = ` +"Assigning to rvalue (1:3) +> 1 | --- + | ^ + 2 | Front Matter content + 3 | --- +Cause: Assigning to rvalue" +`; + +exports[`snippet: Should not support front matter [flow] format 1`] = ` +"Invalid left-hand side in assignment (1:3) +> 1 | --- + | ^ +> 2 | Front Matter content + | ^^^^^^ + 3 | ---" +`; + +exports[`snippet: Should not support front matter [graphql] format 1`] = ` +"Syntax Error: Invalid number, expected digit but got: "-". (1:2) +> 1 | --- + | ^ + 2 | Front Matter content + 3 | --- +Cause: Syntax Error: Invalid number, expected digit but got: "-"." +`; + +exports[`snippet: Should not support front matter [json] format 1`] = ` +"Invalid left-hand side in prefix operation. (1:3) +> 1 | --- + | ^ + 2 | Front Matter content + 3 | --- +Cause: Invalid left-hand side in prefix operation. (1:2)" +`; + +exports[`snippet: Should not support front matter [json-stringify] format 1`] = ` +"Invalid left-hand side in prefix operation. (1:3) +> 1 | --- + | ^ + 2 | Front Matter content + 3 | --- +Cause: Invalid left-hand side in prefix operation. (1:2)" +`; + +exports[`snippet: Should not support front matter [json5] format 1`] = ` +"Invalid left-hand side in prefix operation. (1:3) +> 1 | --- + | ^ + 2 | Front Matter content + 3 | --- +Cause: Invalid left-hand side in prefix operation. (1:2)" +`; + +exports[`snippet: Should not support front matter [jsonc] format 1`] = ` +"Invalid left-hand side in prefix operation. (1:3) +> 1 | --- + | ^ + 2 | Front Matter content + 3 | --- +Cause: Invalid left-hand side in prefix operation. (1:2)" +`; + +exports[`snippet: Should not support front matter [meriyah] format 1`] = ` +"Invalid increment/decrement operand (2:7) + 1 | --- +> 2 | Front Matter content + | ^^^^^^ + 3 | --- +Cause: [2:6-2:12]: Invalid increment/decrement operand" +`; + +exports[`snippet: Should not support front matter [typescript] format 1`] = ` +"Expression expected. (1:3) +> 1 | --- + | ^ + 2 | Front Matter content + 3 | --- +Cause: Expression expected." +`; diff --git a/tests/format/misc/errors/front-matter/format.test.js b/tests/format/misc/errors/front-matter/format.test.js new file mode 100644 index 000000000..29b0a5de3 --- /dev/null +++ b/tests/format/misc/errors/front-matter/format.test.js @@ -0,0 +1,45 @@ +import { outdent } from "outdent"; + +runFormatTest( + { + importMeta: import.meta, + snippets: [ + { + name: "Should not support front matter", + code: outdent` + --- + Front Matter content + --- + `, + }, + ], + }, + [ + "acorn", + "espree", + "__ng_action", + "__ng_binding", + "__ng_interpolation", + "__ng_directive", + "babel", + "babel-flow", + "babel-ts", + "__js_expression", + "__ts_expression", + "__vue_expression", + "__vue_ts_expression", + "__vue_event_binding", + "__vue_ts_event_binding", + "__babel_estree", + "json", + "json5", + "jsonc", + "json-stringify", + "flow", + "graphql", + "meriyah", + "typescript", + // Does not support, but it can parse + // "yaml", + ], +); diff --git a/tests/format/misc/front-matter/with-plugins/format.test.js b/tests/format/misc/front-matter/with-plugins/format.test.js index febd76c3b..1dd9d21a9 100644 --- a/tests/format/misc/front-matter/with-plugins/format.test.js +++ b/tests/format/misc/front-matter/with-plugins/format.test.js @@ -17,6 +17,15 @@ runFormatTest( `, ], }, - ["markdown", "mdx", "css", "scss", "less", "html"], + [ + "markdown", + "mdx", + "css", + "scss", + "less", + "html", + // Can't test since it doesn't print new line + // "glimmer", + ], { plugins: [prettierPluginDummyToml] }, ); diff --git a/tests/unit/__snapshots__/visitor-keys.js.snap b/tests/unit/__snapshots__/visitor-keys.js.snap index e0657a99f..c9ae2ffa9 100644 --- a/tests/unit/__snapshots__/visitor-keys.js.snap +++ b/tests/unit/__snapshots__/visitor-keys.js.snap @@ -1251,7 +1251,6 @@ exports[`visitor keys glimmer 1`] = ` ], "TextNode": [], "UndefinedLiteral": [], - "front-matter": [], } `; @@ -1471,7 +1470,6 @@ exports[`visitor keys html 1`] = ` "attrs", "children", ], - "front-matter": [], "ieConditionalComment": [ "children", ], @@ -1511,7 +1509,6 @@ exports[`visitor keys postcss 1`] = ` "nodes", "selector", ], - "front-matter": [], "media-colon": [], "media-feature": [], "media-feature-expression": [ @@ -1603,7 +1600,6 @@ exports[`visitor keys remark 1`] = ` "children", ], "footnoteReference": [], - "front-matter": [], "heading": [ "children", ], diff --git a/tests/unit/massage-ast.js b/tests/unit/massage-ast.js index f41574f67..f3784cac5 100644 --- a/tests/unit/massage-ast.js +++ b/tests/unit/massage-ast.js @@ -1,4 +1,5 @@ import massageAst from "../../src/main/massage-ast.js"; +import { normalizePrinter } from "../../src/main/parser-and-printer.js"; test("massageAst", () => { const nonNodeObject = { foo: "foo" }; @@ -12,12 +13,12 @@ test("massageAst", () => { }; const result = massageAst(ast, { - printer: { + printer: normalizePrinter({ massageAstNode: Object.assign(() => {}, { ignoredProperties: new Set(["ignored"]), }), getVisitorKeys: () => ["nodeObject"], - }, + }), }); expect(result).not.toBe(ast);