fix(formatter_markdown): honor range ignore inside directives (#28164)

Fixes #28154

Range ignore is supported only at the top level.

Prettier does not support directives, so the contents of `:::` end up at the top level and the range works there.
We parse `:::` as a container directive, so its contents were not top-level and the range was ignored.

This PR treats a `:::` directive with no blockquote / list item / footnote above it as top-level.

Supporting ranges everywhere is possible, but it has concerns such as indentation drift in verbatim output. So this starts small.
This commit is contained in:
leaysgur committed 2026-10-26 03:52:44 +00:00
1 parent 3e7f0f5b73
commit 23d23d69ae
4 files changed
+102 -8

No files matched your search

@@ -31,8 +31,18 @@ pub enum Parent {
loose: bool,
after_checkbox: bool,
},
/// Blockquote or container directive.
Container,
/// Blockquote, footnote definition or container directive.
/// `top_level`: no prefix / indent above (a directive at the root, or in such a directive).
Container {
top_level: bool,
},
}
impl Parent {
/// No prefix / indent above: the root, or a `top_level` container.
const fn is_top_level(self) -> bool {
matches!(self, Self::Root | Self::Container { top_level: true })
}
}
/// Emits sibling blocks with a blank line between them, except where one line break is the rule
@@ -53,8 +63,9 @@ pub fn write_blocks<'a>(
}
// `<!-- prettier-ignore-start -->` ... `<!-- prettier-ignore-end -->`: the range is source text.
// Only at the document root; nested, the comments are plain HTML.
if parent == Parent::Root
// Only at the document root and in directives with no prefix / indent above them
// (top-level for Prettier, which has no directives); nested elsewhere, the comments are plain HTML.
if parent.is_top_level()
&& let Some(end) = ignore_range_end(blocks, index, f)
{
let source = f.context().source_text();
@@ -221,7 +232,7 @@ pub fn write_block<'a>(
Block::Paragraph(p) => {
let parent = inline::InlineParent {
paragraph: true,
first_in_container: parent == Parent::Container && index == 0,
first_in_container: matches!(parent, Parent::Container { .. }) && index == 0,
first_in_document: front_matter_risk(parent, siblings, index, f),
after_checkbox: matches!(parent, Parent::ListItem { after_checkbox: true, .. }),
..inline::InlineParent::default()
@@ -255,7 +266,8 @@ pub fn write_block<'a>(
write_verbatim(d.opening, f);
if !d.children.is_empty() {
super::write_gap(after_opening, f);
write_blocks(&d.children, Parent::Container, f);
let inner = Parent::Container { top_level: parent.is_top_level() };
write_blocks(&d.children, inner, f);
}
if let Some(closing) = d.closing {
super::write_gap(before_closing, f);
@@ -331,7 +343,7 @@ fn write_footnote_definition<'a>(
) {
write!(f, [token("[^"), text(f.context().slice(def.label)), token("]:")]);
let children = &def.children;
let parent = Parent::Container;
let parent = Parent::Container { top_level: false };
let first = format_with(|f| write_block(&children[0], children, 0, parent, f)).memoized();
let on_label_line = matches!(children.as_slice(), [Block::Paragraph(_)])
&& f.options().prose_wrap != ProseWrap::Always
@@ -15,7 +15,9 @@ pub fn write_blockquote<'a>(quote: &'a Blockquote<'a>, f: &mut MarkdownFormatter
write!(f, ">");
return;
}
let body = format_with(|f| block::write_blocks(&quote.children, block::Parent::Container, f));
let body = format_with(|f| {
block::write_blocks(&quote.children, block::Parent::Container { top_level: false }, f);
});
// The prefixed column is the root that verbatim continuation lines return to
write!(f, ["> ", prefix_align(&"> ", &mark_as_root(&body))]);
}
@@ -11,3 +11,22 @@
<!-- prettier-ignore -->
* also kept
::: details X
<!-- prettier-ignore-start -->
| a | b |
| - | - |
* kept
<!-- prettier-ignore-end -->
:::
> ::: details Y
>
> <!-- prettier-ignore-start -->
> | a | b |
> | - | - |
> <!-- prettier-ignore-end -->
>
> :::
@@ -16,6 +16,25 @@ source: crates/oxc_formatter_markdown/tests/fixtures/mod.rs
<!-- prettier-ignore -->
* also kept
::: details X
<!-- prettier-ignore-start -->
| a | b |
| - | - |
* kept
<!-- prettier-ignore-end -->
:::
> ::: details Y
>
> <!-- prettier-ignore-start -->
> | a | b |
> | - | - |
> <!-- prettier-ignore-end -->
>
> :::
==================== Output ====================
------------------
{ printWidth: 80 }
@@ -34,6 +53,27 @@ source: crates/oxc_formatter_markdown/tests/fixtures/mod.rs
<!-- prettier-ignore -->
* also kept
::: details X
<!-- prettier-ignore-start -->
| a | b |
| - | - |
* kept
<!-- prettier-ignore-end -->
:::
> ::: details Y
>
> <!-- prettier-ignore-start -->
>
> | a | b |
> | --- | --- |
>
> <!-- prettier-ignore-end -->
>
> :::
-------------------
{ printWidth: 100 }
-------------------
@@ -51,4 +91,25 @@ source: crates/oxc_formatter_markdown/tests/fixtures/mod.rs
<!-- prettier-ignore -->
* also kept
::: details X
<!-- prettier-ignore-start -->
| a | b |
| - | - |
* kept
<!-- prettier-ignore-end -->
:::
> ::: details Y
>
> <!-- prettier-ignore-start -->
>
> | a | b |
> | --- | --- |
>
> <!-- prettier-ignore-end -->
>
> :::
===================== End =====================