fix(formatter_core): keep front matter verbatim when body starts with indented delimiter (#27946)

```
---
 --- # <- THIS should be kept, otherwise closes FM!
---

```
This commit is contained in:
leaysgur committed 2026-10-19 01:42:25 +00:00
1 parent c22a9b2742
commit 529b057811
6 files changed
+159 -5

No files matched your search

+29
View File
@@ -275,3 +275,32 @@ Prettier keeps `json` / `json5` blocks as written: their parser rejects a blank
`prettier-plugin-astro` formats it with bundled `sass-formatter`, which we do not bundle (patched out).
Rare in Astro projects (GitHub code search, 2026-10: about 350 files in 72 repos, vs 29k for `lang="scss"`).
Not tested but `oxc-css-parser` already parses it, so we can add Sass support for `oxc_formatter_css` in the future.
## front-matter-indented-delimiter
- Why: semantics
- Pin: `conformance/fixtures/edge-cases/xxx-in-md/front-matter-indented-delimiter.md`
```markdown
<!-- input -->
---
---
title: Home
---
<!-- ours -->
---
---
title: Home
---
<!-- prettier -->
---
---
title: Home
---
```
A front matter block whose body starts with an indented delimiter (` ---`, ` ...`, ` +++`) stays verbatim in CSS and Markdown (`write_front_matter` in `oxc_formatter_core`).
The indented delimiter does not close the block, but the body is trimmed before formatting, so it prints at the start of a line.
On the next parse (Prettier's and ours), the block closes there and the rest of the body moves into the document.
@@ -0,0 +1,6 @@
---
---
title: Home
---
# Heading
@@ -254,12 +254,14 @@
## markdown
### Option 1: 273/282 (96.81%)
### Option 1: 273/283 (96.47%)
```json
{"printWidth":80}
```
- [edge-cases/xxx-in-md/front-matter-indented-delimiter.md](diffs/markdown/edge-cases__xxx-in-md__front-matter-indented-delimiter.md.md)
- apps/oxfmt/DIVERGENCES.md#front-matter-indented-delimiter
- [edge-cases/xxx-in-md/line-ranged-code-block.md](diffs/markdown/edge-cases__xxx-in-md__line-ranged-code-block.md.md)
- apps/oxfmt/DIVERGENCES.md#line-ranged-code-block
- [edge-cases/xxx-in-md/lwc-code-block.md](diffs/markdown/edge-cases__xxx-in-md__lwc-code-block.md.md)
@@ -279,12 +281,14 @@
- [externals/prettier/markdown/thematicBreak/simple.md](diffs/markdown/externals__prettier__markdown__thematicBreak__simple.md.md)
- crates/oxc_formatter_markdown/DIVERGENCES.md#leading-thematic-break
### Option 2: 269/282 (95.39%)
### Option 2: 269/283 (95.05%)
```json
{"printWidth":100,"proseWrap":"always"}
```
- [edge-cases/xxx-in-md/front-matter-indented-delimiter.md](diffs/markdown/edge-cases__xxx-in-md__front-matter-indented-delimiter.md.md)
- apps/oxfmt/DIVERGENCES.md#front-matter-indented-delimiter
- [edge-cases/xxx-in-md/line-ranged-code-block.md](diffs/markdown/edge-cases__xxx-in-md__line-ranged-code-block.md.md)
- apps/oxfmt/DIVERGENCES.md#line-ranged-code-block
- [edge-cases/xxx-in-md/lwc-code-block.md](diffs/markdown/edge-cases__xxx-in-md__lwc-code-block.md.md)
@@ -0,0 +1,97 @@
# edge-cases/xxx-in-md/front-matter-indented-delimiter.md
## Option 1
`````json
{"printWidth":80}
`````
### Diff
`````diff
===================================================================
--- prettier
+++ oxfmt
@@ -1,6 +1,6 @@
---
+ ---
+title: Home
---
-title: Home
----
# Heading
`````
### Actual (oxfmt)
`````md
---
---
title: Home
---
# Heading
`````
### Expected (prettier)
`````md
---
---
title: Home
---
# Heading
`````
## Option 2
`````json
{"printWidth":100,"proseWrap":"always"}
`````
### Diff
`````diff
===================================================================
--- prettier
+++ oxfmt
@@ -1,6 +1,6 @@
---
+ ---
+title: Home
---
-title: Home
----
# Heading
`````
### Actual (oxfmt)
`````md
---
---
title: Home
---
# Heading
`````
### Expected (prettier)
`````md
---
---
title: Home
---
# Heading
`````
+10
View File
@@ -63,6 +63,16 @@ describe("CSS front matter", () => {
expect(result.code).toBe("---\nkey: [unclosed\n---\n\nb {\n}\n");
});
it("keeps the block verbatim when its body starts with an indented delimiter", async () => {
// DIVERGENCES.md#front-matter-indented-delimiter
for (const block of ["---\n ---\na: b\n---", "---\n ---\n---", "---\n ...\na: b\n..."]) {
// oxlint-disable-next-line no-await-in-loop
const result = await format("a.css", `${block}\nb {}\n`);
expect(result.errors).toStrictEqual([]);
expect(result.code).toBe(`${block}\n\nb {\n}\n`);
}
});
it("keeps the block verbatim under embeddedLanguageFormatting: off", async () => {
const result = await format("a.css", "---\ntitle: Home\n---\nb {}\n", {
embeddedLanguageFormatting: "off",
+11 -3
View File
@@ -23,7 +23,8 @@ use crate::{
/// Membership means "gets the frame treatment", not "gets formatted":
/// a member language without a serving formatter keeps its body verbatim (`PreserveOriginal`),
/// but its EMPTY block still normalizes through the frame when a dispatcher is installed.
/// Never fails; any refusal (`PreserveOriginal`, operational error, no dispatcher, non-embeddable language)
/// Never fails; any refusal (`PreserveOriginal`, operational error, no dispatcher, non-embeddable language,
/// a body starting with a delimiter)
/// keeps the block's bytes as-is while the host document still formats.
///
/// The composed shape (Prettier `embed.js` + its css/markdown printers):
@@ -42,14 +43,21 @@ pub fn write_front_matter<'a, C: FormatContext>(
write_frame(fm, None, f);
return;
}
// An indented delimiter on the first line (` ---`) never closes the block,
// but trimmed and formatted it starts a line and closes it early on the next parse.
// Keep the block verbatim, see DIVERGENCES.md#front-matter-indented-delimiter.
//
// The body is a Fragment:
// front matter inside front matter must never acquire envelope semantics
// (generic FM recursion).
if let Some(ir) = dispatch_fragment_ir(f, language, body, None) {
if !body.starts_with(fm.start_delimiter)
&& !body.starts_with(fm.end_delimiter)
&& let Some(ir) = dispatch_fragment_ir(f, language, body, None)
{
write_frame(fm, Some(ir), f);
return;
}
// `PreserveOriginal` or an operational error:
// A body starting with a delimiter, `PreserveOriginal` or an operational error:
// fall through to the verbatim block below.
}
write!(f, text(fm.raw));