From fd695f4c7a5d659edb36d457e3100b744ca46e2d Mon Sep 17 00:00:00 2001
From: leaysgur <6259812+leaysgur@users.noreply.github.com>
Date: Thu, 24 Sep 2026 07:35:13 +0000
Subject: [PATCH] fix(formatter_markdown): fix more mismatches found in
ecosystem-ci repos (#27003)
- Stop CJK whitespace rules at inline node edges
- Keep a kept line break before a lone `|` row under `preserve`
- Escape a fake setext underline only when it ends its line
- Ignore backslash-escaped backticks when choosing a code span fence
---
crates/oxc_formatter_markdown/DIVERGENCES.md | 55 +++++++++++++++++
.../oxc_formatter_markdown/src/print/block.rs | 2 +-
.../src/print/inline.rs | 31 ++++++----
.../oxc_formatter_markdown/src/print/text.rs | 20 +++++--
.../markdown/fake-setext-underline.md | 5 ++
.../markdown/fake-setext-underline.md.snap | 30 ++++++++++
.../fixtures/markdown/lazy-html-block.md | 3 +
.../fixtures/markdown/lazy-html-block.md.snap | 26 ++++++++
.../tests/fixtures/markdown/prose-wrap/cjk.md | 9 +++
.../fixtures/markdown/prose-wrap/cjk.md.snap | 60 +++++++++++++++++++
.../code-span-fence-at-line-start.md | 6 +-
.../code-span-fence-at-line-start.md.snap | 30 +++++++---
.../prose-wrap/container-directive.md | 5 ++
.../prose-wrap/container-directive.md.snap | 41 +++++++++++++
.../definition-shaped-first-line.md | 4 ++
.../definition-shaped-first-line.md.snap | 59 ++++++++++++++++++
.../prose-wrap/wrapped-block-starts.md | 6 ++
.../prose-wrap/wrapped-block-starts.md.snap | 34 +++++++++++
18 files changed, 399 insertions(+), 27 deletions(-)
create mode 100644 crates/oxc_formatter_markdown/tests/fixtures/markdown/fake-setext-underline.md
create mode 100644 crates/oxc_formatter_markdown/tests/fixtures/markdown/fake-setext-underline.md.snap
create mode 100644 crates/oxc_formatter_markdown/tests/fixtures/markdown/lazy-html-block.md
create mode 100644 crates/oxc_formatter_markdown/tests/fixtures/markdown/lazy-html-block.md.snap
create mode 100644 crates/oxc_formatter_markdown/tests/fixtures/markdown/prose-wrap/definition-shaped-first-line.md
create mode 100644 crates/oxc_formatter_markdown/tests/fixtures/markdown/prose-wrap/definition-shaped-first-line.md.snap
diff --git a/crates/oxc_formatter_markdown/DIVERGENCES.md b/crates/oxc_formatter_markdown/DIVERGENCES.md
index 7eca5dedeb..64fd2aa846 100644
--- a/crates/oxc_formatter_markdown/DIVERGENCES.md
+++ b/crates/oxc_formatter_markdown/DIVERGENCES.md
@@ -183,6 +183,34 @@ Prettier drops the alignment when the html starts at another column than the par
An HTML block closed by its container's end carries that line ending as content (micromark's `trailing_newline`); it is not printed.
Prettier prints it as a blank line of the blockquote, and inside a list item that line is not a fixpoint: every pass adds another.
+## lazy-html-block
+
+- Why: semantics
+- Pin: `tests/fixtures/markdown/lazy-html-block.md`
+
+```markdown
+
+> a
+
+> c
+
+
+> a
+>
+>
+> c
+
+
+> a
+>
+> c
+```
+
+A complete type 7 tag on a lazy line opens an HTML block inside the container (micromark's rule, oxc-markdown-parser DIVERGENCES.md),
+though no type 7 block can interrupt a paragraph on a regular line.
+Printed with the container's prefix the line is no longer lazy, so a blank line keeps the block apart;
+Prettier prints it adjacent, and the next parse reads it as part of the paragraph.
+
## list-marker-after-ignored-list
- Why: semantics
@@ -239,6 +267,9 @@ The parser opens one on any name-like fence line (`:::name[label]{attrs}`, `:::
Prettier has no directive construct (#19662 would add micromark's grammar only), so under `always` / `never` the fences are words of the paragraph and wrapping merges them into the text, which breaks the container for every dialect that reads it.
+An opener interrupts a paragraph (as in micromark's directive extension and markdown-it-container),
+so like any block it is printed after a blank line, under `preserve` too; Prettier keeps it on the paragraph's next line.
+
## line-shapes
- Why: semantics
@@ -263,6 +294,28 @@ a stray `:::`, or `[!` as the first line of a blockquote (GitHub / Obsidian aler
Prettier joins and wraps them like any word: the alert above loses its marker line (GitHub needs `[!NOTE]` alone on it),
a wrapped `<<<` line loses its `[title]`, and a component tag wrapped to column 0 opens an HTML block in MDX / VitePress.
+## definition-shaped-first-line
+
+- Why: semantics
+- Pin: `tests/fixtures/markdown/prose-wrap/definition-shaped-first-line.md`
+
+```markdown
+
+[core]: https://example.com/a/very/long/path/that/fills/most/xxxxxxxxxxxxxxxxxxxxxxxx then more
+
+
+[core]: https://example.com/a/very/long/path/that/fills/most/xxxxxxxxxxxxxxxxxxxxxxxx then more
+
+
+[core]:
+https://example.com/a/very/long/path/that/fills/most/xxxxxxxxxxxxxxxxxxxxxxxx
+then more
+```
+
+A paragraph's first line shaped like a definition (`[label]: dest text`) is a paragraph only because of what follows the destination on the line,
+so it keeps its line boundaries and is never re-wrapped, under every `proseWrap`.
+Prettier wraps it like any line: a break right after the destination makes it a definition on the next parse.
+
## wrapped-block-starts
- Why: semantics (prettier/prettier#13634, prettier/prettier#19112, prettier/prettier#19847)
@@ -355,6 +408,8 @@ resumes at the container's content column, the only indentation the parser strip
whatever whitespace follows is the node's content and is printed as is.
Prettier prints code span lines from column 0 and the others from its own alignment (a task item's checkbox counts there, not for the parser),
so the content of `` `x y` `` loses two spaces per pass and an HTML comment gains four.
+With no whitespace past the container's column the same column-0 line is a lazy continuation and reads the same,
+so there the difference is layout only (most real-world cases).
## wiki-link-code-span
diff --git a/crates/oxc_formatter_markdown/src/print/block.rs b/crates/oxc_formatter_markdown/src/print/block.rs
index cdde2b6337..e94bcb7977 100644
--- a/crates/oxc_formatter_markdown/src/print/block.rs
+++ b/crates/oxc_formatter_markdown/src/print/block.rs
@@ -132,7 +132,7 @@ fn double_gap(
// A type 7 HTML block can only follow a paragraph without a blank line as a lazy line of a container
// (a micromark quirk, oxc-markdown-parser DIVERGENCES.md);
// printed with the container's prefix it needs the blank line to stay a block.
- // Prettier prints it adjacent, where it becomes paragraph text.
+ // Prettier prints it adjacent, where it becomes paragraph text (DIVERGENCES.md#lazy-html-block).
let html_after_paragraph =
matches!((previous, node), (Block::Paragraph(_), Block::HtmlBlock(h)) if h.kind != 7);
let liquid = matches!(previous, Block::Liquid(_)) || matches!(node, Block::Liquid(_));
diff --git a/crates/oxc_formatter_markdown/src/print/inline.rs b/crates/oxc_formatter_markdown/src/print/inline.rs
index fbc6fec67b..f964bf3c7e 100644
--- a/crates/oxc_formatter_markdown/src/print/inline.rs
+++ b/crates/oxc_formatter_markdown/src/print/inline.rs
@@ -11,7 +11,7 @@ use oxc_formatter_core::arena_cow_str;
use oxc_markdown_parser::{
Span,
ast::{CodeSpan, Emphasis, HardBreakKind, Inline, LinkKind, Strong},
- unicode,
+ escapes_next, unicode,
};
use crate::{
@@ -124,7 +124,7 @@ fn code_span_literal_runs<'a>(
let mut at = first.span().start;
for &(span, is_code) in opaque {
if span.start > at {
- mask |= run_mask(source.slice_range(at, span.start).as_bytes(), b'`');
+ mask |= run_mask(source.slice_range(at, span.start).as_bytes(), b'`', true);
}
if is_code {
out.push((span.start, mask));
@@ -550,23 +550,33 @@ fn mark_delimiter(open: u32, parts: &mut Parts<'_>, strong: bool) {
}
/// Bit `n - 1` set: a run of exactly `n` `ch`s occurs in `bytes` (runs past 64 count as 64).
-fn run_mask(bytes: &[u8], ch: u8) -> u64 {
+/// `escapes`: `bytes` is markdown text, where a backslash-escaped `ch` is not part of a run.
+fn run_mask(bytes: &[u8], ch: u8, escapes: bool) -> u64 {
let mut present: u64 = 0;
let mut run = 0usize;
- for &b in bytes.iter().chain(std::iter::once(&0)) {
- if b == ch {
+ let mut i = 0;
+ loop {
+ let b = bytes.get(i).copied();
+ if b == Some(ch) {
run += 1;
- } else if run > 0 {
+ i += 1;
+ continue;
+ }
+ if run > 0 {
present |= 1u64 << (run.min(64) - 1);
run = 0;
}
+ match b {
+ None => return present,
+ Some(b'\\') if escapes && escapes_next(bytes, i) => i += 2,
+ Some(_) => i += 1,
+ }
}
- present
}
/// The smallest run length of `ch` absent from `text`.
fn min_absent_run(text: &str, ch: u8) -> usize {
- run_mask(text.as_bytes(), ch).trailing_ones() as usize + 1
+ run_mask(text.as_bytes(), ch, false).trailing_ones() as usize + 1
}
/// Runs of tabs / newlines become one space (wiki link contents under wrapping).
@@ -606,7 +616,8 @@ fn next_word_of<'a>(
let word = first_word(line)?;
// A text's leading run after a soft break is escaped inside emphasis
// (as `push_text` decides: a space before it, and after it the text's next character or the node edge)
- let escaped = matches!(next, Inline::Text(_))
+ let in_sentence = matches!(next, Inline::Text(_));
+ let escaped = in_sentence
&& f.context().delimiter_depth().get() > 0
&& !f.context().literal_markers().get()
&& escape::leading_run_escaped(
@@ -614,7 +625,7 @@ fn next_word_of<'a>(
Some(' '),
edge_char(children, i + 1, delimiter, false, f),
);
- Some(words::NextWord { word, line, escaped })
+ Some(words::NextWord { word, line, escaped, in_sentence })
}
fn first_word(line: &str) -> Option<&str> {
diff --git a/crates/oxc_formatter_markdown/src/print/text.rs b/crates/oxc_formatter_markdown/src/print/text.rs
index 770997d411..6fd92c9fc9 100644
--- a/crates/oxc_formatter_markdown/src/print/text.rs
+++ b/crates/oxc_formatter_markdown/src/print/text.rs
@@ -25,7 +25,7 @@ use super::{
print_delimited_word,
},
is_split_whitespace,
- line_shape::{is_line_shape_start, may_open_block},
+ line_shape::{is_line_shape_start, line_opens_block, may_open_block},
parts::{Parts, Sep},
};
@@ -39,6 +39,9 @@ pub struct NextWord<'a> {
/// The word's leading `*` / `_` run gets escaped (inside emphasis):
/// printed, it starts with `\` and cannot open a block.
pub escaped: bool,
+ /// The word starts a text of the same sentence: the CJK rules see it.
+ /// Past another node's edge they do not (Prettier's sentence ends there), so the whitespace may break and stays a space.
+ pub in_sentence: bool,
}
#[derive(Clone, Copy, Default)]
@@ -116,7 +119,7 @@ pub fn push_text<'a>(
Some(glued) if is_last => glued,
_ => word,
};
- Some(NextWord { word, line: "", escaped })
+ Some(NextWord { word, line: "", escaped, in_sentence: true })
};
let cx = TextContext {
next_word: next,
@@ -152,6 +155,8 @@ pub fn push_text<'a>(
&& is_last
&& !leading_ws
&& cx.after_soft_break
+ // Alone on its line: a node after it (`== `x``) makes the line text
+ && cx.next_word.is_none()
&& is_fake_setext_underline(word)
{
Cow::Owned(format!("\\{word}"))
@@ -208,7 +213,9 @@ pub fn push_whitespace<'a>(
let (prev, next) = if cx.cj_spaces.is_some() {
(
cx.prev_word.and_then(|w| cjk::edges(w).map(|(_, last)| last)),
- cx.next_word.and_then(|w| cjk::edges(w.word).map(|(first, _)| first)),
+ cx.next_word
+ .filter(|w| w.in_sentence)
+ .and_then(|w| cjk::edges(w.word).map(|(first, _)| first)),
)
} else {
(None, None)
@@ -355,15 +362,16 @@ fn looks_like_block_start(next: NextWord<'_>, exact: bool) -> bool {
if !target.as_bytes().first().is_some_and(|&b| may_open_block(b)) {
return false;
}
+ if exact {
+ // A `|` row is inert on its own: the delimiter row after it is what makes a table, and that break is kept apart
+ return line_opens_block(target, true) || is_line_shape_start(target);
+ }
let constructs = Constructs::markdown();
// A dialect line shape at a line start is printed raw from then on: never create one
// (asked about the word alone, a `:::` word counts even where `::: note` would be an opener)
if lexical::line_start(&constructs, target, true).is_some() || is_line_shape_start(target) {
return true;
}
- if exact {
- return false;
- }
let mut buf = [0u8; 64];
let len = next.word.len();
if len + 2 > buf.len() {
diff --git a/crates/oxc_formatter_markdown/tests/fixtures/markdown/fake-setext-underline.md b/crates/oxc_formatter_markdown/tests/fixtures/markdown/fake-setext-underline.md
new file mode 100644
index 0000000000..c3399e31db
--- /dev/null
+++ b/crates/oxc_formatter_markdown/tests/fixtures/markdown/fake-setext-underline.md
@@ -0,0 +1,5 @@
+A lone `==` / `--` line that stayed text by its indentation
+ ==
+is escaped, one with more after it on the line
+ == `code` or -- **strong**
+is text as it stands.
diff --git a/crates/oxc_formatter_markdown/tests/fixtures/markdown/fake-setext-underline.md.snap b/crates/oxc_formatter_markdown/tests/fixtures/markdown/fake-setext-underline.md.snap
new file mode 100644
index 0000000000..4bd65c7e12
--- /dev/null
+++ b/crates/oxc_formatter_markdown/tests/fixtures/markdown/fake-setext-underline.md.snap
@@ -0,0 +1,30 @@
+---
+source: crates/oxc_formatter_markdown/tests/fixtures/mod.rs
+---
+==================== Input ====================
+A lone `==` / `--` line that stayed text by its indentation
+ ==
+is escaped, one with more after it on the line
+ == `code` or -- **strong**
+is text as it stands.
+
+==================== Output ====================
+------------------
+{ printWidth: 80 }
+------------------
+A lone `==` / `--` line that stayed text by its indentation
+\==
+is escaped, one with more after it on the line
+== `code` or -- **strong**
+is text as it stands.
+
+-------------------
+{ printWidth: 100 }
+-------------------
+A lone `==` / `--` line that stayed text by its indentation
+\==
+is escaped, one with more after it on the line
+== `code` or -- **strong**
+is text as it stands.
+
+===================== End =====================
diff --git a/crates/oxc_formatter_markdown/tests/fixtures/markdown/lazy-html-block.md b/crates/oxc_formatter_markdown/tests/fixtures/markdown/lazy-html-block.md
new file mode 100644
index 0000000000..b238c78a85
--- /dev/null
+++ b/crates/oxc_formatter_markdown/tests/fixtures/markdown/lazy-html-block.md
@@ -0,0 +1,3 @@
+> a
+
+> c
diff --git a/crates/oxc_formatter_markdown/tests/fixtures/markdown/lazy-html-block.md.snap b/crates/oxc_formatter_markdown/tests/fixtures/markdown/lazy-html-block.md.snap
new file mode 100644
index 0000000000..2b456dac00
--- /dev/null
+++ b/crates/oxc_formatter_markdown/tests/fixtures/markdown/lazy-html-block.md.snap
@@ -0,0 +1,26 @@
+---
+source: crates/oxc_formatter_markdown/tests/fixtures/mod.rs
+---
+==================== Input ====================
+> a
+
+> c
+
+==================== Output ====================
+------------------
+{ printWidth: 80 }
+------------------
+> a
+>
+>
+> c
+
+-------------------
+{ printWidth: 100 }
+-------------------
+> a
+>
+>
+> c
+
+===================== End =====================
diff --git a/crates/oxc_formatter_markdown/tests/fixtures/markdown/prose-wrap/cjk.md b/crates/oxc_formatter_markdown/tests/fixtures/markdown/prose-wrap/cjk.md
index 9f512c0372..09ecfea5ea 100644
--- a/crates/oxc_formatter_markdown/tests/fixtures/markdown/prose-wrap/cjk.md
+++ b/crates/oxc_formatter_markdown/tests/fixtures/markdown/prose-wrap/cjk.md
@@ -21,3 +21,12 @@ Englishの前でも消えます。
括弧の前 (paren) と後ろ、ASCII 記号の
:: 前後は空白になります。
+
+
+
+使用方法请参考文档中的说明,如果需要了解更多信息,请参考 [应用部署](./deployment.md)。
+
+更多细节,请参考文档:
+www.example.com 和 `code`。
diff --git a/crates/oxc_formatter_markdown/tests/fixtures/markdown/prose-wrap/cjk.md.snap b/crates/oxc_formatter_markdown/tests/fixtures/markdown/prose-wrap/cjk.md.snap
index 5db6059793..46136e2967 100644
--- a/crates/oxc_formatter_markdown/tests/fixtures/markdown/prose-wrap/cjk.md.snap
+++ b/crates/oxc_formatter_markdown/tests/fixtures/markdown/prose-wrap/cjk.md.snap
@@ -26,6 +26,15 @@ Englishの前でも消えます。
括弧の前 (paren) と後ろ、ASCII 記号の
:: 前後は空白になります。
+
+
+使用方法请参考文档中的说明,如果需要了解更多信息,请参考 [应用部署](./deployment.md)。
+
+更多细节,请参考文档:
+www.example.com 和 `code`。
+
==================== Output ====================
---------------------------------------
{ printWidth: 80, proseWrap: "always" }
@@ -45,6 +54,15 @@ Englishの前でも消えます。
括弧の前 (paren) と後ろ、ASCII 記号の :: 前後は空白になります。
+
+
+使用方法请参考文档中的说明,如果需要了解更多信息,请参考
+[应用部署](./deployment.md)。
+
+更多细节,请参考文档: www.example.com 和 `code`。
+
----------------------------------------
{ printWidth: 100, proseWrap: "always" }
----------------------------------------
@@ -63,6 +81,14 @@ Englishの前でも消えます。
括弧の前 (paren) と後ろ、ASCII 記号の :: 前後は空白になります。
+
+
+使用方法请参考文档中的说明,如果需要了解更多信息,请参考 [应用部署](./deployment.md)。
+
+更多细节,请参考文档: www.example.com 和 `code`。
+
--------------------------------------
{ printWidth: 80, proseWrap: "never" }
--------------------------------------
@@ -81,6 +107,14 @@ Englishの前でも消えます。
括弧の前 (paren) と後ろ、ASCII 記号の :: 前後は空白になります。
+
+
+使用方法请参考文档中的说明,如果需要了解更多信息,请参考 [应用部署](./deployment.md)。
+
+更多细节,请参考文档: www.example.com 和 `code`。
+
---------------------------------------
{ printWidth: 100, proseWrap: "never" }
---------------------------------------
@@ -99,6 +133,14 @@ Englishの前でも消えます。
括弧の前 (paren) と後ろ、ASCII 記号の :: 前後は空白になります。
+
+
+使用方法请参考文档中的说明,如果需要了解更多信息,请参考 [应用部署](./deployment.md)。
+
+更多细节,请参考文档: www.example.com 和 `code`。
+
-----------------------------------------
{ printWidth: 80, proseWrap: "preserve" }
-----------------------------------------
@@ -126,6 +168,15 @@ Englishの前でも消えます。
括弧の前 (paren) と後ろ、ASCII 記号の
:: 前後は空白になります。
+
+
+使用方法请参考文档中的说明,如果需要了解更多信息,请参考 [应用部署](./deployment.md)。
+
+更多细节,请参考文档:
+www.example.com 和 `code`。
+
------------------------------------------
{ printWidth: 100, proseWrap: "preserve" }
------------------------------------------
@@ -153,4 +204,13 @@ Englishの前でも消えます。
括弧の前 (paren) と後ろ、ASCII 記号の
:: 前後は空白になります。
+
+
+使用方法请参考文档中的说明,如果需要了解更多信息,请参考 [应用部署](./deployment.md)。
+
+更多细节,请参考文档:
+www.example.com 和 `code`。
+
===================== End =====================
diff --git a/crates/oxc_formatter_markdown/tests/fixtures/markdown/prose-wrap/code-span-fence-at-line-start.md b/crates/oxc_formatter_markdown/tests/fixtures/markdown/prose-wrap/code-span-fence-at-line-start.md
index f1f22fc612..86077b83d7 100644
--- a/crates/oxc_formatter_markdown/tests/fixtures/markdown/prose-wrap/code-span-fence-at-line-start.md
+++ b/crates/oxc_formatter_markdown/tests/fixtures/markdown/prose-wrap/code-span-fence-at-line-start.md
@@ -1,4 +1,6 @@
In order to submit this form to a web application, call ```submit(url, [callback])``` method:
-A stray \` and a lone `` then
-`x` at a line start.
+A stray ` and a lone `` then
+```x``` at a line start.
+
+An escaped \` pairs with nothing, so `a` keeps one backtick.
diff --git a/crates/oxc_formatter_markdown/tests/fixtures/markdown/prose-wrap/code-span-fence-at-line-start.md.snap b/crates/oxc_formatter_markdown/tests/fixtures/markdown/prose-wrap/code-span-fence-at-line-start.md.snap
index 3db25c6ad5..4bd5e12534 100644
--- a/crates/oxc_formatter_markdown/tests/fixtures/markdown/prose-wrap/code-span-fence-at-line-start.md.snap
+++ b/crates/oxc_formatter_markdown/tests/fixtures/markdown/prose-wrap/code-span-fence-at-line-start.md.snap
@@ -4,8 +4,10 @@ source: crates/oxc_formatter_markdown/tests/fixtures/mod.rs
==================== Input ====================
In order to submit this form to a web application, call ```submit(url, [callback])``` method:
-A stray \` and a lone `` then
-`x` at a line start.
+A stray ` and a lone `` then
+```x``` at a line start.
+
+An escaped \` pairs with nothing, so `a` keeps one backtick.
==================== Output ====================
---------------------------------------
@@ -14,43 +16,55 @@ A stray \` and a lone `` then
In order to submit this form to a web application, call
`submit(url, [callback])` method:
-A stray \` and a lone `` then ```x``` at a line start.
+A stray ` and a lone `` then ```x``` at a line start.
+
+An escaped \` pairs with nothing, so `a` keeps one backtick.
----------------------------------------
{ printWidth: 100, proseWrap: "always" }
----------------------------------------
In order to submit this form to a web application, call `submit(url, [callback])` method:
-A stray \` and a lone `` then ```x``` at a line start.
+A stray ` and a lone `` then ```x``` at a line start.
+
+An escaped \` pairs with nothing, so `a` keeps one backtick.
--------------------------------------
{ printWidth: 80, proseWrap: "never" }
--------------------------------------
In order to submit this form to a web application, call `submit(url, [callback])` method:
-A stray \` and a lone `` then ```x``` at a line start.
+A stray ` and a lone `` then ```x``` at a line start.
+
+An escaped \` pairs with nothing, so `a` keeps one backtick.
---------------------------------------
{ printWidth: 100, proseWrap: "never" }
---------------------------------------
In order to submit this form to a web application, call `submit(url, [callback])` method:
-A stray \` and a lone `` then ```x``` at a line start.
+A stray ` and a lone `` then ```x``` at a line start.
+
+An escaped \` pairs with nothing, so `a` keeps one backtick.
-----------------------------------------
{ printWidth: 80, proseWrap: "preserve" }
-----------------------------------------
In order to submit this form to a web application, call `submit(url, [callback])` method:
-A stray \` and a lone `` then
+A stray ` and a lone `` then
```x``` at a line start.
+An escaped \` pairs with nothing, so `a` keeps one backtick.
+
------------------------------------------
{ printWidth: 100, proseWrap: "preserve" }
------------------------------------------
In order to submit this form to a web application, call `submit(url, [callback])` method:
-A stray \` and a lone `` then
+A stray ` and a lone `` then
```x``` at a line start.
+An escaped \` pairs with nothing, so `a` keeps one backtick.
+
===================== End =====================
diff --git a/crates/oxc_formatter_markdown/tests/fixtures/markdown/prose-wrap/container-directive.md b/crates/oxc_formatter_markdown/tests/fixtures/markdown/prose-wrap/container-directive.md
index 920d8a7da2..045ea2a155 100644
--- a/crates/oxc_formatter_markdown/tests/fixtures/markdown/prose-wrap/container-directive.md
+++ b/crates/oxc_formatter_markdown/tests/fixtures/markdown/prose-wrap/container-directive.md
@@ -19,3 +19,8 @@ long enough to wrap when the width is small and prose wrap is always on.
::: {.class}
Pandoc fenced div.
:::
+
+A paragraph right before an opener:
+:::note
+The opener interrupts it, so a blank line separates them.
+:::
diff --git a/crates/oxc_formatter_markdown/tests/fixtures/markdown/prose-wrap/container-directive.md.snap b/crates/oxc_formatter_markdown/tests/fixtures/markdown/prose-wrap/container-directive.md.snap
index 6aa4d67e7c..8719527e93 100644
--- a/crates/oxc_formatter_markdown/tests/fixtures/markdown/prose-wrap/container-directive.md.snap
+++ b/crates/oxc_formatter_markdown/tests/fixtures/markdown/prose-wrap/container-directive.md.snap
@@ -24,6 +24,11 @@ long enough to wrap when the width is small and prose wrap is always on.
Pandoc fenced div.
:::
+A paragraph right before an opener:
+:::note
+The opener interrupts it, so a blank line separates them.
+:::
+
==================== Output ====================
---------------------------------------
{ printWidth: 80, proseWrap: "always" }
@@ -50,6 +55,12 @@ is always on.
Pandoc fenced div.
:::
+A paragraph right before an opener:
+
+:::note
+The opener interrupts it, so a blank line separates them.
+:::
+
----------------------------------------
{ printWidth: 100, proseWrap: "always" }
----------------------------------------
@@ -73,6 +84,12 @@ Some **text** that is long enough to wrap when the width is small and prose wrap
Pandoc fenced div.
:::
+A paragraph right before an opener:
+
+:::note
+The opener interrupts it, so a blank line separates them.
+:::
+
--------------------------------------
{ printWidth: 80, proseWrap: "never" }
--------------------------------------
@@ -96,6 +113,12 @@ Some **text** that is long enough to wrap when the width is small and prose wrap
Pandoc fenced div.
:::
+A paragraph right before an opener:
+
+:::note
+The opener interrupts it, so a blank line separates them.
+:::
+
---------------------------------------
{ printWidth: 100, proseWrap: "never" }
---------------------------------------
@@ -119,6 +142,12 @@ Some **text** that is long enough to wrap when the width is small and prose wrap
Pandoc fenced div.
:::
+A paragraph right before an opener:
+
+:::note
+The opener interrupts it, so a blank line separates them.
+:::
+
-----------------------------------------
{ printWidth: 80, proseWrap: "preserve" }
-----------------------------------------
@@ -144,6 +173,12 @@ long enough to wrap when the width is small and prose wrap is always on.
Pandoc fenced div.
:::
+A paragraph right before an opener:
+
+:::note
+The opener interrupts it, so a blank line separates them.
+:::
+
------------------------------------------
{ printWidth: 100, proseWrap: "preserve" }
------------------------------------------
@@ -169,4 +204,10 @@ long enough to wrap when the width is small and prose wrap is always on.
Pandoc fenced div.
:::
+A paragraph right before an opener:
+
+:::note
+The opener interrupts it, so a blank line separates them.
+:::
+
===================== End =====================
diff --git a/crates/oxc_formatter_markdown/tests/fixtures/markdown/prose-wrap/definition-shaped-first-line.md b/crates/oxc_formatter_markdown/tests/fixtures/markdown/prose-wrap/definition-shaped-first-line.md
new file mode 100644
index 0000000000..e7fd81c2cb
--- /dev/null
+++ b/crates/oxc_formatter_markdown/tests/fixtures/markdown/prose-wrap/definition-shaped-first-line.md
@@ -0,0 +1,4 @@
+[core]: https://example.com/a/very/long/path/that/fills/most/xxxxxxxxxxxxxxxxxxxxxxxx then more
+
+[core]: fix: Error in the integration where an event would not be sent in case of
+non-assignable object properties.
diff --git a/crates/oxc_formatter_markdown/tests/fixtures/markdown/prose-wrap/definition-shaped-first-line.md.snap b/crates/oxc_formatter_markdown/tests/fixtures/markdown/prose-wrap/definition-shaped-first-line.md.snap
new file mode 100644
index 0000000000..5d9007df7b
--- /dev/null
+++ b/crates/oxc_formatter_markdown/tests/fixtures/markdown/prose-wrap/definition-shaped-first-line.md.snap
@@ -0,0 +1,59 @@
+---
+source: crates/oxc_formatter_markdown/tests/fixtures/mod.rs
+---
+==================== Input ====================
+[core]: https://example.com/a/very/long/path/that/fills/most/xxxxxxxxxxxxxxxxxxxxxxxx then more
+
+[core]: fix: Error in the integration where an event would not be sent in case of
+non-assignable object properties.
+
+==================== Output ====================
+---------------------------------------
+{ printWidth: 80, proseWrap: "always" }
+---------------------------------------
+[core]: https://example.com/a/very/long/path/that/fills/most/xxxxxxxxxxxxxxxxxxxxxxxx then more
+
+[core]: fix: Error in the integration where an event would not be sent in case of
+non-assignable object properties.
+
+----------------------------------------
+{ printWidth: 100, proseWrap: "always" }
+----------------------------------------
+[core]: https://example.com/a/very/long/path/that/fills/most/xxxxxxxxxxxxxxxxxxxxxxxx then more
+
+[core]: fix: Error in the integration where an event would not be sent in case of
+non-assignable object properties.
+
+--------------------------------------
+{ printWidth: 80, proseWrap: "never" }
+--------------------------------------
+[core]: https://example.com/a/very/long/path/that/fills/most/xxxxxxxxxxxxxxxxxxxxxxxx then more
+
+[core]: fix: Error in the integration where an event would not be sent in case of
+non-assignable object properties.
+
+---------------------------------------
+{ printWidth: 100, proseWrap: "never" }
+---------------------------------------
+[core]: https://example.com/a/very/long/path/that/fills/most/xxxxxxxxxxxxxxxxxxxxxxxx then more
+
+[core]: fix: Error in the integration where an event would not be sent in case of
+non-assignable object properties.
+
+-----------------------------------------
+{ printWidth: 80, proseWrap: "preserve" }
+-----------------------------------------
+[core]: https://example.com/a/very/long/path/that/fills/most/xxxxxxxxxxxxxxxxxxxxxxxx then more
+
+[core]: fix: Error in the integration where an event would not be sent in case of
+non-assignable object properties.
+
+------------------------------------------
+{ printWidth: 100, proseWrap: "preserve" }
+------------------------------------------
+[core]: https://example.com/a/very/long/path/that/fills/most/xxxxxxxxxxxxxxxxxxxxxxxx then more
+
+[core]: fix: Error in the integration where an event would not be sent in case of
+non-assignable object properties.
+
+===================== End =====================
diff --git a/crates/oxc_formatter_markdown/tests/fixtures/markdown/prose-wrap/wrapped-block-starts.md b/crates/oxc_formatter_markdown/tests/fixtures/markdown/prose-wrap/wrapped-block-starts.md
index 09a2b868af..8c3d938f0e 100644
--- a/crates/oxc_formatter_markdown/tests/fixtures/markdown/prose-wrap/wrapped-block-starts.md
+++ b/crates/oxc_formatter_markdown/tests/fixtures/markdown/prose-wrap/wrapped-block-starts.md
@@ -28,6 +28,12 @@ padding words to push the risky token over the edge padding words to push the >
| x | y |
|---|---|
+
+
+A grammar line
+| alternative | another
+| last
+
text then
- item
diff --git a/crates/oxc_formatter_markdown/tests/fixtures/markdown/prose-wrap/wrapped-block-starts.md.snap b/crates/oxc_formatter_markdown/tests/fixtures/markdown/prose-wrap/wrapped-block-starts.md.snap
index 7c48c3fa4e..126526f854 100644
--- a/crates/oxc_formatter_markdown/tests/fixtures/markdown/prose-wrap/wrapped-block-starts.md.snap
+++ b/crates/oxc_formatter_markdown/tests/fixtures/markdown/prose-wrap/wrapped-block-starts.md.snap
@@ -32,6 +32,12 @@ padding words to push the risky token over the edge padding words to push the >
| x | y |
|---|---|
+
+
+A grammar line
+| alternative | another
+| last
+
text then
- item
@@ -83,6 +89,10 @@ not a quote tail words.
| x | y | |---|---|
+
+
+A grammar line | alternative | another | last
+
text then - item
A math span starting a line after a hard break stays behind four spaces
@@ -123,6 +133,10 @@ words.
| x | y | |---|---|
+
+
+A grammar line | alternative | another | last
+
text then - item
A math span starting a line after a hard break stays behind four spaces
@@ -161,6 +175,10 @@ padding words to push the risky token over the edge padding words to push the >
| x | y | |---|---|
+
+
+A grammar line | alternative | another | last
+
text then - item
A math span starting a line after a hard break stays behind four spaces
@@ -199,6 +217,10 @@ padding words to push the risky token over the edge padding words to push the >
| x | y | |---|---|
+
+
+A grammar line | alternative | another | last
+
text then - item
A math span starting a line after a hard break stays behind four spaces
@@ -237,6 +259,12 @@ padding words to push the risky token over the edge padding words to push the >
| x | y | |---|---|
+
+
+A grammar line
+| alternative | another
+| last
+
text then - item
A math span starting a line after a hard break stays behind four spaces
@@ -275,6 +303,12 @@ padding words to push the risky token over the edge padding words to push the >
| x | y | |---|---|
+
+
+A grammar line
+| alternative | another
+| last
+
text then - item
A math span starting a line after a hard break stays behind four spaces