Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::markdown::lists::block_list::FormatMdBlockListOptions;
use crate::prelude::*;
use biome_formatter::write;
use biome_markdown_syntax::{MdDocument, MdDocumentFields};
Expand All @@ -16,6 +17,17 @@ impl FormatNodeRule<MdDocument> for FormatMdDocument {
write!(f, [bom.format()])?;
}

write!(f, [value.format(), format_removed(&eof_token?)])
write!(
f,
[
value
.format()
.with_options(FormatMdBlockListOptions { trim: true }),
format_removed(&eof_token?)
]
)?;

// when trimming, we remove the last newline, so we add it back here
write!(f, [hard_line_break()])
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ impl FormatNodeRule<MdInlineImage> for FormatMdInlineImage {
destination
.format()
.with_options(FormatMdFormatInlineItemListOptions {
print_mode: TextPrintMode::Trim(TrimMode::All)
print_mode: TextPrintMode::Trim(TrimMode::AutoLinkLike)
})
]
)?;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use crate::prelude::*;
use biome_formatter::write;
use biome_markdown_syntax::{MarkdownSyntaxKind, MdInlineItalic, MdInlineItalicFields};
use biome_markdown_syntax::{
MarkdownSyntaxKind, MdInlineItalic, MdInlineItalicFields, MdReferenceImage,
};
use biome_rowan::AstNode;
#[derive(Debug, Clone, Default)]
pub(crate) struct FormatMdInlineItalic;
Expand Down Expand Up @@ -29,6 +31,18 @@ impl FormatNodeRule<MdInlineItalic> for FormatMdInlineItalic {
return format_verbatim_node(node.syntax()).fmt(f);
}

// Inside reference images the alt text doubles as the reference label.
// Normalizing `*` → `_` would change the label and break reference resolution.
// E.g. `![foo *bar*]` with `[foo *bar*]: url` must keep `*`.
if node
.syntax()
.ancestors()
.skip(1)
.any(|a| MdReferenceImage::can_cast(a.kind()))
{
return write!(f, [l_fence.format(), content.format(), r_fence.format()]);
}

let prev_is_alphanum = l_fence
.prev_token()
.and_then(|t| t.text_trimmed().chars().last())
Expand All @@ -39,7 +53,7 @@ impl FormatNodeRule<MdInlineItalic> for FormatMdInlineItalic {
.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
// 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
Expand Down
37 changes: 34 additions & 3 deletions crates/biome_markdown_formatter/src/markdown/auxiliary/newline.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,40 @@
use crate::prelude::*;
use biome_markdown_syntax::MdNewline;
use biome_formatter::{FormatRuleWithOptions, write};
use biome_markdown_syntax::{MdHeader, MdNewline};
use biome_rowan::AstNode;
#[derive(Debug, Clone, Default)]
pub(crate) struct FormatMdNewline;
pub(crate) struct FormatMdNewline {
should_remove: bool,
}
impl FormatNodeRule<MdNewline> for FormatMdNewline {
fn fmt_fields(&self, node: &MdNewline, f: &mut MarkdownFormatter) -> FormatResult<()> {
node.value_token().format().fmt(f)
if self.should_remove {
return write!(f, [format_removed(&node.value_token()?)]);
}

let after_header = node
.syntax()
.prev_sibling()
.is_some_and(|s| MdHeader::can_cast(s.kind()));

if after_header {
let token = node.value_token()?;
write!(f, [format_removed(&token), hard_line_break()])
} else {
node.value_token().format().fmt(f)
}
}
}

pub(crate) struct FormatMdNewlineOptions {
pub(crate) should_remove: bool,
}

impl FormatRuleWithOptions<MdNewline> for FormatMdNewline {
type Options = FormatMdNewlineOptions;

fn with_options(mut self, options: Self::Options) -> Self {
self.should_remove = options.should_remove;
self
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ impl FormatNodeRule<MdQuotePrefix> for FormatMdQuotePrefix {

if let Some(post_marker_space_token) = post_marker_space_token {
write!(f, [post_marker_space_token.format()])?;
} else {
let marker = marker_token?;
let next_has_text = marker
.next_token()
.is_some_and(|t| t.text().starts_with(|c: char| !c.is_whitespace()));
if next_has_text {
write!(f, [space()])?;
}
}

Ok(())
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,32 @@
use crate::prelude::*;
use biome_markdown_syntax::MdReferenceImage;
use biome_rowan::AstNode;
use biome_formatter::write;
use biome_markdown_syntax::{MdReferenceImage, MdReferenceImageFields};
#[derive(Debug, Clone, Default)]
pub(crate) struct FormatMdReferenceImage;
impl FormatNodeRule<MdReferenceImage> for FormatMdReferenceImage {
fn fmt_fields(&self, node: &MdReferenceImage, f: &mut MarkdownFormatter) -> FormatResult<()> {
format_verbatim_node(node.syntax()).fmt(f)
let MdReferenceImageFields {
excl_token,
l_brack_token,
alt,
r_brack_token,
label,
} = node.as_fields();

write!(
f,
[
excl_token.format(),
l_brack_token.format(),
alt.format(),
r_brack_token.format()
]
)?;

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

Ok(())
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::prelude::*;
use biome_markdown_syntax::MdReferenceLinkLabel;
use biome_rowan::AstNode;
use biome_formatter::write;
use biome_markdown_syntax::{MdReferenceLinkLabel, MdReferenceLinkLabelFields};
#[derive(Debug, Clone, Default)]
pub(crate) struct FormatMdReferenceLinkLabel;
impl FormatNodeRule<MdReferenceLinkLabel> for FormatMdReferenceLinkLabel {
Expand All @@ -9,6 +9,19 @@ impl FormatNodeRule<MdReferenceLinkLabel> for FormatMdReferenceLinkLabel {
node: &MdReferenceLinkLabel,
f: &mut MarkdownFormatter,
) -> FormatResult<()> {
format_verbatim_node(node.syntax()).fmt(f)
let MdReferenceLinkLabelFields {
l_brack_token,
label,
r_brack_token,
} = node.as_fields();

write!(
f,
[
l_brack_token.format(),
label.format(),
r_brack_token.format()
]
)
}
}
59 changes: 56 additions & 3 deletions crates/biome_markdown_formatter/src/markdown/lists/block_list.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,63 @@
use crate::markdown::auxiliary::newline::FormatMdNewlineOptions;
use crate::prelude::*;
use biome_markdown_syntax::MdBlockList;
use biome_formatter::FormatRuleWithOptions;
use biome_markdown_syntax::{AnyMdBlock, AnyMdLeafBlock, MdBlockList};

#[derive(Debug, Clone, Default)]
pub(crate) struct FormatMdBlockList;
pub(crate) struct FormatMdBlockList {
/// When true, it removes all leading newlines and trailing newlines
trim: bool,
}
impl FormatRule<MdBlockList> for FormatMdBlockList {
type Context = MarkdownFormatContext;
fn fmt(&self, node: &MdBlockList, f: &mut MarkdownFormatter) -> FormatResult<()> {
f.join().entries(node.iter().formatted()).finish()
let mut joiner = f.join();

if !self.trim {
return f.join().entries(node.iter().formatted()).finish();
}

let mut iter = node.iter();

// Count trailing newlines using next_back
let mut trailing_count = 0;
while let Some(AnyMdBlock::AnyMdLeafBlock(AnyMdLeafBlock::MdNewline(_))) = iter.next_back()
{
trailing_count += 1;
}

// we don't need the iter anymore
drop(iter);

// Single forward pass in document order
let mut still_leading = true;
let content_count = node.len() - trailing_count;
for (index, node) in node.iter().enumerate() {
if let AnyMdBlock::AnyMdLeafBlock(AnyMdLeafBlock::MdNewline(newline)) = node {
let is_leading = still_leading;
let is_trailing = index >= content_count;
joiner.entry(&newline.format().with_options(FormatMdNewlineOptions {
should_remove: is_leading || is_trailing,
}));
} else {
still_leading = false;
joiner.entry(&node.format());
}
}

joiner.finish()
}
}

pub(crate) struct FormatMdBlockListOptions {
pub(crate) trim: bool,
}

impl FormatRuleWithOptions<MdBlockList> for FormatMdBlockList {
type Options = FormatMdBlockListOptions;

fn with_options(mut self, options: Self::Options) -> Self {
self.trim = options.trim;
self
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ pub(crate) struct FormatMdInlineItemList {
impl FormatRule<MdInlineItemList> for FormatMdInlineItemList {
type Context = MarkdownFormatContext;
fn fmt(&self, node: &MdInlineItemList, f: &mut MarkdownFormatter) -> FormatResult<()> {
if self.print_mode.is_normalize_words() {
if self.print_mode.is_auto_link_like() {
return self.fmt_auto_link_like(node, f);
} else if self.print_mode.is_normalize_words() {
return self.fmt_normalize_words(node, f);
} else if self.print_mode.is_all() {
return self.fmt_trim_all(node, f);
Expand Down Expand Up @@ -91,6 +93,40 @@ impl FormatRule<MdInlineItemList> for FormatMdInlineItemList {
}

impl FormatMdInlineItemList {
/// If the first and last [MdTextual] are `<` and `>` respectively,
/// they are removed. Otherwise falls back to [TrimMode::All].
fn fmt_auto_link_like(
&self,
node: &MdInlineItemList,
f: &mut MarkdownFormatter,
) -> FormatResult<()> {
let items: Vec<_> = node.iter().collect();

let starts_with_lt = matches!(items.first(), Some(AnyMdInline::MdTextual(t)) if t.value_token().is_ok_and(|tok| tok.text() == "<"));
let ends_with_gt = matches!(items.last(), Some(AnyMdInline::MdTextual(t)) if t.value_token().is_ok_and(|tok| tok.text() == ">"));

let is_auto_link = starts_with_lt && ends_with_gt && items.len() > 2;

if !is_auto_link {
return self.fmt_trim_all(node, f);
}

let mut joiner = f.join();
for (index, item) in items.iter().enumerate() {
if (index == 0 || index == items.len() - 1)
&& let AnyMdInline::MdTextual(text) = item
{
joiner.entry(&text.format().with_options(FormatMdTextualOptions {
should_remove: true,
..Default::default()
}));
continue;
}
joiner.entry(&item.format());
}
Comment thread
ematipico marked this conversation as resolved.
joiner.finish()
}

/// Strips leading and trailing whitespace/hard-lines around the content.
/// Items between the first and last non-empty nodes are kept as-is;
/// items outside those boundaries are removed.
Expand Down
7 changes: 7 additions & 0 deletions crates/biome_markdown_formatter/src/shared.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ pub(crate) enum TrimMode {
Start,
/// Trim start and end of the list
All,
/// If the first and last [MdTextual] are `<` and `>` respectively, they are trimmed.
/// If no link has been detected, if falls back to [Self::All]
AutoLinkLike,
Comment thread
ematipico marked this conversation as resolved.
/// This mode works similarly to [TrimMode::All], however, text that contains
/// words and have more than trailing/leading spaces are normalized to one
NormalizeWords,
Expand All @@ -56,6 +59,10 @@ impl TextPrintMode {
matches!(self, Self::Trim(TrimMode::NormalizeWords))
}

pub(crate) const fn is_auto_link_like(&self) -> bool {
matches!(self, Self::Trim(TrimMode::AutoLinkLike))
}

pub(crate) const fn is_pristine(&self) -> bool {
matches!(self, Self::Pristine)
}
Expand Down
22 changes: 18 additions & 4 deletions crates/biome_markdown_formatter/tests/quick_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use biome_markdown_parser::parse_markdown;
#[ignore]
#[test]
fn quick_test() {
let source = "";
let source = "foo \nbar without empty line after ";
let parse = parse_markdown(source);

// Print CST
Expand All @@ -13,10 +13,24 @@ fn quick_test() {
eprintln!("{:#?}", parse.tree());

let options = MdFormatOptions::default();
let result =
biome_formatter::format_node(&parse.syntax(), MdFormatLanguage::new(options), false);
let result = biome_formatter::format_node(
&parse.syntax(),
MdFormatLanguage::new(options.clone()),
false,
);

// Print formatted output
let formatted = result.unwrap();
eprintln!("Formatted:\n{}", formatted.print().unwrap().as_code());
let output = formatted.print().unwrap();
eprintln!("Formatted:\n{}", output.as_code());

// Now re-parse the formatted output and show its CST
let reparse = parse_markdown(output.as_code());
eprintln!("\n--- Re-parsed CST ---");
eprintln!("{:#?}", reparse.tree());

let result2 =
biome_formatter::format_node(&reparse.syntax(), MdFormatLanguage::new(options), false);
let output2 = result2.unwrap();
eprintln!("Re-formatted:\n{}", output2.print().unwrap().as_code());
}
Loading
Loading