Skip to content
Merged
48 changes: 45 additions & 3 deletions crates/biome_markdown_formatter/src/markdown/auxiliary/bullet.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,52 @@
use crate::markdown::auxiliary::list_marker_prefix::FormatMdListMarkerPrefixOptions;
use crate::prelude::*;
use biome_markdown_syntax::MdBullet;
use biome_rowan::AstNode;
use biome_formatter::write;
use biome_markdown_syntax::{
AnyMdBlock, AnyMdLeafBlock, MarkdownSyntaxKind, MdBullet, MdBulletFields,
};
#[derive(Debug, Clone, Default)]
pub(crate) struct FormatMdBullet;
impl FormatNodeRule<MdBullet> for FormatMdBullet {
fn fmt_fields(&self, node: &MdBullet, f: &mut MarkdownFormatter) -> FormatResult<()> {
format_verbatim_node(node.syntax()).fmt(f)
let MdBulletFields { prefix, content } = node.as_fields();

let prefix = prefix?;
let marker = prefix.marker()?;

// `* - - -` is a bullet containing a `-` thematic break. Normalizing `*`
// to `-` produces `- - - -` which CommonMark 4.1 parses as a thematic
// break, not a list item. Same for `+ - - -`. Skip normalization for marker
// but still format content through child formatters.
let target_marker = if marker.kind() == MarkdownSyntaxKind::MINUS
|| first_block_is_dash_thematic_break(&content)
{
None
} else {
Some("-")
};

write!(
f,
[prefix
.format()
.with_options(FormatMdListMarkerPrefixOptions { target_marker })]
Comment thread
ematipico marked this conversation as resolved.
)?;
content.format().fmt(f)?;
Ok(())
}
}

/// Returns true if the first block in `content` is a thematic break using `-`.
fn first_block_is_dash_thematic_break(content: &biome_markdown_syntax::MdBlockList) -> bool {
let Some(AnyMdBlock::AnyMdLeafBlock(AnyMdLeafBlock::MdThematicBreakBlock(block))) =
content.iter().next()
else {
return false;
};
block
.parts()
.into_iter()
.find_map(|p| p.as_md_thematic_break_char().cloned())
.and_then(|c| c.value().ok())
.is_some_and(|t| t.text_trimmed() == "-")
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
use crate::prelude::*;
use biome_markdown_syntax::MdBulletListItem;
use biome_rowan::AstNode;
use biome_formatter::write;
use biome_markdown_syntax::{MdBulletListItem, MdBulletListItemFields};
#[derive(Debug, Clone, Default)]
pub(crate) struct FormatMdBulletListItem;
impl FormatNodeRule<MdBulletListItem> for FormatMdBulletListItem {
fn fmt_fields(&self, node: &MdBulletListItem, f: &mut MarkdownFormatter) -> FormatResult<()> {
format_verbatim_node(node.syntax()).fmt(f)
let MdBulletListItemFields { md_bullet_list } = node.as_fields();
write!(f, [md_bullet_list.format()])
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use crate::shared::{TextPrintMode, TrimMode};
use crate::verbatim::format_verbatim_node;
use biome_formatter::write;
use biome_markdown_syntax::{MdHeader, MdHeaderFields};
use biome_rowan::AstNode;

#[derive(Debug, Clone, Default)]
pub(crate) struct FormatMdHeader;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,29 @@
use crate::prelude::*;
use biome_markdown_syntax::MdInlineEmphasis;
use biome_rowan::AstNode;
use biome_formatter::write;
use biome_markdown_syntax::{
MdInlineEmphasis, MdInlineEmphasisFields, emphasis_ext::MdEmphasisFence,
};
#[derive(Debug, Clone, Default)]
pub(crate) struct FormatMdInlineEmphasis;
impl FormatNodeRule<MdInlineEmphasis> for FormatMdInlineEmphasis {
fn fmt_fields(&self, node: &MdInlineEmphasis, f: &mut MarkdownFormatter) -> FormatResult<()> {
format_verbatim_node(node.syntax()).fmt(f)
let MdInlineEmphasisFields {
l_fence,
content,
r_fence,
} = node.as_fields();

if node.fence().ok() == Some(MdEmphasisFence::DoubleStar) {
write!(f, [l_fence.format(), content.format(), r_fence.format()])
} else {
write!(
f,
[
format_replaced(&l_fence?, &token(MdEmphasisFence::DoubleStar.as_str())),
content.format(),
format_replaced(&r_fence?, &token(MdEmphasisFence::DoubleStar.as_str())),
]
)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,72 @@
use crate::prelude::*;
use biome_markdown_syntax::MdInlineItalic;
use biome_formatter::write;
use biome_markdown_syntax::{MarkdownSyntaxKind, MdInlineItalic, MdInlineItalicFields};
use biome_rowan::AstNode;
#[derive(Debug, Clone, Default)]
pub(crate) struct FormatMdInlineItalic;
impl FormatNodeRule<MdInlineItalic> for FormatMdInlineItalic {
fn fmt_fields(&self, node: &MdInlineItalic, f: &mut MarkdownFormatter) -> FormatResult<()> {
format_verbatim_node(node.syntax()).fmt(f)
let MdInlineItalicFields {
l_fence,
content,
r_fence,
} = node.as_fields();

let l_fence = l_fence?;
let r_fence = r_fence?;

// Nested italic anywhere in the subtree → keep entire node verbatim.
// Normalizing to `_` could create `__` (bold) adjacency: `_*foo*_` → `__foo__`.
if node
.syntax()
.descendants()
Comment thread
tidefield marked this conversation as resolved.
.skip(1)
.any(|d| d.kind() == MarkdownSyntaxKind::MD_INLINE_ITALIC)
{
// TODO: instead of format_verbatim_node, pass options to child formatters so
// other normalizations (bold, code, etc.) still run inside nested italic content.
// See example-383.md for a case where Prettier handles escapes inside italic.
return format_verbatim_node(node.syntax()).fmt(f);
Comment thread
tidefield marked this conversation as resolved.
}

let prev_is_alphanum = l_fence
.prev_token()
.and_then(|t| t.text_trimmed().chars().last())
.is_some_and(|c| c.is_alphanumeric());
let next_is_alphanum = r_fence
.next_token()
.and_then(|t| t.text_trimmed().chars().next())
.is_some_and(|c| c.is_alphanumeric());

// See https://spec.commonmark.org/0.31.2/#emphasis-and-strong-emphasis
// Prefer `_` but use `*` when adjacent to alphanumeric
// For example, `a_b_c` won't parse `b` as italic, but `a*b*c` will).
let target_kind = if prev_is_alphanum || next_is_alphanum {
MarkdownSyntaxKind::STAR
} else {
MarkdownSyntaxKind::UNDERSCORE
};

write_fence(&l_fence, target_kind, f)?;
write!(f, [content.format()])?;
write_fence(&r_fence, target_kind, f)
}
}

/// Write a fence token, reusing it if it already matches the target kind.
fn write_fence(
fence: &biome_markdown_syntax::MarkdownSyntaxToken,
target_kind: MarkdownSyntaxKind,
f: &mut MarkdownFormatter,
) -> FormatResult<()> {
if fence.kind() == target_kind {
write!(f, [fence.format()])
} else {
let text = if target_kind == MarkdownSyntaxKind::STAR {
"*"
} else {
"_"
};
write!(f, [format_replaced(fence, &token(text))])
}
}
Comment thread
ematipico marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -1,10 +1,48 @@
use crate::prelude::*;
use biome_markdown_syntax::MdListMarkerPrefix;
use biome_rowan::AstNode;
use biome_formatter::{FormatRuleWithOptions, write};
use biome_markdown_syntax::{MdListMarkerPrefix, MdListMarkerPrefixFields};
#[derive(Debug, Clone, Default)]
pub(crate) struct FormatMdListMarkerPrefix;
pub(crate) struct FormatMdListMarkerPrefix {
/// Target marker to replace with (e.g. `"-"`). `None` keeps the original.
target_marker: Option<&'static str>,
}
impl FormatNodeRule<MdListMarkerPrefix> for FormatMdListMarkerPrefix {
fn fmt_fields(&self, node: &MdListMarkerPrefix, f: &mut MarkdownFormatter) -> FormatResult<()> {
format_verbatim_node(node.syntax()).fmt(f)
let MdListMarkerPrefixFields {
pre_marker_indent,
marker,
post_marker_space_token,
content_indent,
} = node.as_fields();

let marker = marker?;

write!(f, [pre_marker_indent.format()])?;
// Note that for `- `, the parser treats the indent as part of the marker, not the content
// This is a parser bug that causes a regression
// in crates/biome_markdown_formatter/tests/specs/prettier/markdown/spec/example-242.md.snap
match self.target_marker {
Some(target) => write!(f, [format_replaced(&marker, &token(target))])?,
None => write!(f, [marker.format()])?,
}

if let Some(space) = post_marker_space_token {
write!(f, [space.format()])?;
}
write!(f, [content_indent.format()])
}
}

pub(crate) struct FormatMdListMarkerPrefixOptions {
/// Target marker to replace with (e.g. `Some("-")`). `None` keeps the original.
pub(crate) target_marker: Option<&'static str>,
}

impl FormatRuleWithOptions<MdListMarkerPrefix> for FormatMdListMarkerPrefix {
type Options = FormatMdListMarkerPrefixOptions;

fn with_options(mut self, options: Self::Options) -> Self {
self.target_marker = options.target_marker;
self
}
}
3 changes: 1 addition & 2 deletions crates/biome_markdown_formatter/tests/quick_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ use biome_markdown_parser::parse_markdown;
#[ignore]
#[test]
fn quick_test() {
let source = r#"[ See `AsyncGeneratorFunction`]: ./index.html
"#;
let source = "";
let parse = parse_markdown(source);

// Print CST
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ info: markdown/blockquote.md
>> second level
>>> third level

> quote with **bold** and *italic*
> quote with **bold** and _italic_

> quote with `inline code`

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
+ item with __bold__ and *italic*

- item with __bold__ and *italic*

* item with `code`

* - - -

* - - -\n __bold__
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
source: crates/biome_formatter_test/src/snapshot_builder.rs
info: markdown/bullet_list.md
---

# Input

```md
+ item with __bold__ and *italic*

- item with __bold__ and *italic*

* item with `code`

* - - -

* - - -\n __bold__

```


# Formatted

```md
- item with **bold** and _italic_

- item with **bold** and _italic_

- item with `code`

* - - -

- - - -\n **bold**

```
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ info: markdown/inline_image.md

![This is a long alt text that describes the image in great detail for accessibility purposes](https://example.com/very/long/path/to/some/deeply/nested/image.png "And a long title too")

![alt with **bold** and *italic*](image.png)
![alt with **bold** and _italic_](image.png)

![alt with `code`](image.png)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
a*b*c

a_b_c
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
source: crates/biome_formatter_test/src/snapshot_builder.rs
info: markdown/inline_italic.md
---

# Input

```md
a*b*c

a_b_c

```


# Formatted

```md
a*b*c

a_b_c

```
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,17 @@ info: markdown/blockquote/ignore-code.md

> - test
> ```md
@@ -36,15 +38,19 @@
@@ -18,7 +20,8 @@
> - This is a long long
> long long long long
> long long paragraph.
-> ```
+>
+ ```

````md
> ```md
@@ -36,15 +39,19 @@
> > long long long long
> > long long paragraph.
> > ```
Expand Down Expand Up @@ -135,7 +145,8 @@ info: markdown/blockquote/ignore-code.md
> - This is a long long
> long long long long
> long long paragraph.
> ```
>
```

````md
> ```md
Expand Down
Loading
Loading