use dprint_plugin_markdown::configuration::{ConfigurationBuilder, TextWrap};
use dprint_plugin_markdown::format_text;
use crate::hygiene::hygiene;
use crate::{FormatError, Style};
pub fn format(source: &str, style: &Style) -> Result<String, FormatError> {
let config = ConfigurationBuilder::new()
.line_width(style.max_line_length.unwrap_or(80) as u32)
.text_wrap(TextWrap::Always)
.build();
let (guard_markdown, guard_md) = fence_sentinels(source);
let guarded = guard_markdown_fences(source, &guard_markdown, &guard_md);
let result = format_text(&guarded, &config, |_, _, _| Ok(None));
match result {
Ok(Some(formatted)) => {
let restored = formatted
.replace(&guard_markdown, "markdown")
.replace(&guard_md, "md");
Ok(hygiene(&restored, style))
}
Ok(None) => Ok(hygiene(source, style)),
Err(err) => Err(FormatError::Parse(err.to_string())),
}
}
fn fence_sentinels(source: &str) -> (String, String) {
let mut nonce = 0u32;
loop {
let markdown = format!("prim-guard-{nonce}-markdown");
let md = format!("prim-guard-{nonce}-md");
if !source.contains(&markdown) && !source.contains(&md) {
return (markdown, md);
}
nonce += 1;
}
}
fn guard_markdown_fences(source: &str, guard_markdown: &str, guard_md: &str) -> String {
let swaps = [("markdown", guard_markdown), ("md", guard_md)];
source
.split_inclusive('\n')
.map(|line| swap_fence_language_line(line, &swaps))
.collect()
}
fn swap_fence_language_line(line: &str, swaps: &[(&str, &str)]) -> String {
let bytes = line.as_bytes();
let mut i = 0;
while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'>') {
i += 1;
}
let fence_char = match bytes.get(i) {
Some(b'`') => b'`',
Some(b'~') => b'~',
_ => return line.to_string(),
};
let fence_start = i;
while i < bytes.len() && bytes[i] == fence_char {
i += 1;
}
if i - fence_start < 3 {
return line.to_string();
}
let lang_start = i;
while i < bytes.len() && !bytes[i].is_ascii_whitespace() {
i += 1;
}
let lang = &line[lang_start..i];
for (from, to) in swaps {
if lang == *from {
return format!("{}{}{}", &line[..lang_start], to, &line[i..]);
}
}
line.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::LineEnding;
fn max_line_width(s: &str) -> usize {
s.lines().map(|l| l.chars().count()).max().unwrap_or(0)
}
#[test]
fn normalizes_atx_heading_spacing() {
let out = format("# Title\n", &Style::default()).unwrap();
assert!(out.contains("# Title"), "{out:?}");
}
#[test]
fn hard_wraps_long_prose_to_width() {
let para = "word ".repeat(40); let out = format(&format!("{para}\n"), &Style::default()).unwrap();
assert!(out.contains('\n'), "wrapped onto multiple lines: {out:?}");
assert!(max_line_width(&out) <= 80, "no line exceeds 80: {out:?}");
}
#[test]
fn never_breaks_inline_code() {
let long = "a ".repeat(50);
let src = format!("{long}`do not break this code span` {long}\n");
let out = format(&src, &Style::default()).unwrap();
assert!(
out.contains("`do not break this code span`"),
"inline code intact: {out:?}"
);
}
#[test]
fn preserves_fenced_code_verbatim() {
let src = "```js\nconst x=1\n```\n";
let out = format(src, &Style::default()).unwrap();
assert!(out.contains("const x=1"), "code not reformatted: {out:?}");
}
#[test]
fn never_splits_a_link_url() {
let long = "word ".repeat(30);
let src = format!("{long}[link](https://example.com/a/very/long/path/here)\n");
let out = format(&src, &Style::default()).unwrap();
assert!(
out.contains("https://example.com/a/very/long/path/here"),
"URL intact: {out:?}"
);
}
#[test]
fn preserves_hard_break() {
let out = format("line one \nline two\n", &Style::default()).unwrap();
let one = out.find("line one").unwrap();
let two = out.find("line two").unwrap();
assert!(out[one..two].contains('\n'), "hard break kept: {out:?}");
}
#[test]
fn wraps_to_editorconfig_width() {
let style = Style {
max_line_length: Some(40),
..Style::default()
};
let para = "word ".repeat(40);
let out = format(&format!("{para}\n"), &style).unwrap();
assert!(max_line_width(&out) <= 40, "no line exceeds 40: {out:?}");
}
#[test]
fn crlf_end_of_line_from_style() {
let style = Style {
end_of_line: LineEnding::CrLf,
..Style::default()
};
let out = format("# Title\n", &style).unwrap();
assert!(out.contains("\r\n"), "{out:?}");
}
#[test]
fn inline_code_spanning_a_newline_does_not_panic() {
let src = "A paragraph with `format_text(input, &Opts) -> Result<String, Error>` inline.\n";
let out = format(src, &Style::default()).unwrap();
assert!(out.contains("format_text(input"), "code text kept: {out:?}");
assert_eq!(format(&out, &Style::default()).unwrap(), out);
}
#[test]
fn preserves_markdown_tagged_fence_verbatim() {
let src = "```markdown\nThis single line is deliberately much longer than eighty columns so that the formatter would want to wrap it.\n```\n";
let out = format(src, &Style::default()).unwrap();
assert_eq!(out, src, "markdown fence content and tag must survive");
}
#[test]
fn preserves_md_tagged_fence_and_restores_the_tag() {
let src = "```md\n# spaced heading stays exactly as written\n```\n";
let out = format(src, &Style::default()).unwrap();
assert!(out.contains("```md\n"), "{out:?}");
assert!(out.contains("# spaced heading"), "{out:?}");
}
#[test]
fn no_sentinel_leaks_into_output() {
let src = "prose\n\n```markdown\ntext\n```\n\n```md\ntext\n```\n";
let out = format(src, &Style::default()).unwrap();
assert!(!out.contains("prim-guard-"), "{out:?}");
}
#[test]
fn other_fence_tags_are_untouched_by_the_guard() {
let src = "```js\nconst x=1\n```\n";
assert_eq!(guard_markdown_fences(src, "GM", "GD"), src);
}
#[test]
fn guard_handles_tilde_and_blockquote_fences() {
assert_eq!(
guard_markdown_fences("~~~markdown\n", "GM", "GD"),
"~~~GM\n"
);
assert_eq!(guard_markdown_fences("> ```md\n", "GM", "GD"), "> ```GD\n");
let guarded = guard_markdown_fences("> ```md\n", "GM", "GD");
assert_eq!(guarded.replace("GD", "md"), "> ```md\n");
}
#[test]
fn fence_sentinels_bump_the_nonce_past_any_collision() {
assert_eq!(
fence_sentinels("nothing here"),
(
"prim-guard-0-markdown".to_string(),
"prim-guard-0-md".to_string()
)
);
let (markdown, _) = fence_sentinels("```prim-guard-0-markdown\nx\n```\n");
assert_ne!(markdown, "prim-guard-0-markdown");
}
#[test]
fn preexisting_sentinel_like_tag_is_preserved() {
let src = "# Heading\n\n```prim-guard-0-markdown\nverbatim content\n```\n";
let out = format(src, &Style::default()).unwrap();
assert!(
out.contains("```prim-guard-0-markdown\n"),
"pre-existing sentinel-like tag must survive: {out:?}"
);
assert!(
out.contains("verbatim content"),
"content verbatim: {out:?}"
);
}
#[test]
fn is_idempotent() {
let src = "# Heading\n\nA paragraph with odd spacing that goes on and on and on past the wrap width here.\n\n- item\n- item\n";
let once = format(src, &Style::default()).unwrap();
let twice = format(&once, &Style::default()).unwrap();
assert_eq!(once, twice);
}
#[test]
fn is_idempotent_with_a_markdown_tagged_fence() {
let src = "# Heading\n\n```markdown\nThis single line is deliberately much longer than eighty columns so that the formatter would want to wrap it.\n```\n";
let once = format(src, &Style::default()).unwrap();
let twice = format(&once, &Style::default()).unwrap();
assert_eq!(once, twice);
}
}