#![allow(clippy::unwrap_used)]
use flowmark::Wrap;
use flowmark::config::ListSpacing;
use flowmark::fill_markdown;
use flowmark::fill_text;
fn fmt(input: &str) -> String {
fill_markdown(input, true, 88, false, false, false, false, None, ListSpacing::Preserve)
}
fn fmt_semantic(input: &str) -> String {
fill_markdown(input, true, 88, true, false, false, false, None, ListSpacing::Preserve)
}
fn fmt_width(input: &str, width: usize) -> String {
fill_markdown(input, true, width, false, false, false, false, None, ListSpacing::Preserve)
}
fn _fmt_semantic_width(input: &str, width: usize) -> String {
fill_markdown(input, true, width, true, false, false, false, None, ListSpacing::Preserve)
}
fn fmt_tight(input: &str) -> String {
fill_markdown(input, true, 88, true, false, false, false, None, ListSpacing::Tight)
}
fn fmt_loose(input: &str) -> String {
fill_markdown(input, true, 88, true, false, false, false, None, ListSpacing::Loose)
}
fn fmt_plaintext(input: &str) -> String {
fill_text(input, Wrap::Wrap, 88, "", "", 0, None)
}
#[test]
fn test_d1_plaintext_preserves_code_fences() {
let input =
"Some text.\n\n```javascript\n// This is a code block\nvar x = 5;\n```\n\nMore text.\n";
let result = fmt_plaintext(input);
assert!(
result.contains("```javascript\n// This is a code block\nvar x = 5;\n```"),
"D1: Plaintext mode should preserve code fence structure, got:\n{result}"
);
}
#[test]
fn test_d1_plaintext_preserves_empty_code_block() {
let input = "Before.\n\n```\nThis is\nanother.\n```\n\nAfter.\n";
let result = fmt_plaintext(input);
assert!(
result.contains("```\nThis is\nanother.\n```"),
"D1: Plaintext mode should preserve unfenced code block, got:\n{result}"
);
}
#[test]
fn test_d2_plaintext_treats_markdown_links_as_atomic() {
let input = "The school is [St. John's Beaumont School](https://en.wikipedia.org/wiki/St_John%27s_Beaumont_School) in the area.\n";
let result = fmt_plaintext(input);
assert!(
result.contains("[St. John's Beaumont School](https://en.wikipedia.org/wiki/St_John%27s_Beaumont_School)"),
"D2: Plaintext mode should treat markdown links as atomic (matching Python), got:\n{result}"
);
assert!(
result.lines().count() >= 2,
"D2: Long plaintext with link should wrap to multiple lines, got:\n{result}"
);
}
#[test]
fn test_d3_sup_tag_wrapping_at_width_56() {
let input = "wb\\+ mode (binary read/write), automatically deleted when closed or on process termination.<sup>19</sup> While convenient, POSIX notes potential permission issues and recommends mkstemp followed by fdopen for multithreaded apps to avoid leaking file descriptors.<sup>59</sup>\n";
let python_output = "wb\\+ mode (binary read/write), automatically\ndeleted when closed or on process\ntermination.<sup>19</sup> While convenient, POSIX\nnotes potential permission issues and recommends\nmkstemp followed by fdopen for multithreaded apps\nto avoid leaking file descriptors.<sup>59</sup>\n";
let result = fmt_width(input, 56);
let result_lines: Vec<&str> = result.trim_end().lines().collect();
let python_lines: Vec<&str> = python_output.trim_end().lines().collect();
assert!(
result.contains("<sup>19</sup>") && result.contains("<sup>59</sup>"),
"D3: <sup> tags should be preserved in output, got:\n{result}"
);
assert_eq!(
result_lines.len(),
python_lines.len(),
"D3: Width 56 wrapping should produce same number of lines.\nRust ({} lines):\n{}\nPython ({} lines):\n{}",
result_lines.len(),
result,
python_lines.len(),
python_output,
);
}
#[test]
fn test_d4_tight_nested_lists_match_python() {
let input = "- Level 1a\n - Level 2a\n - Level 3a\n- Level 1b\n - Level 2b\n";
let result = fmt_tight(input);
let python_output = "- Level 1a\n\n - Level 2a\n - Level 3a\n\n- Level 1b\n - Level 2b\n";
assert_eq!(
result, python_output,
"D4: Tight nested lists should match Python behavior.\nGot:\n{result}"
);
}
#[test]
fn test_d4_tight_simple_sublists() {
let input = "- A\n - B\n- C\n - D\n";
let result = fmt_tight(input);
let python_output = "- A\n - B\n\n- C\n - D\n";
assert_eq!(
result, python_output,
"D4: Tight simple sublists should match Python.\nGot:\n{result}"
);
}
#[test]
fn test_d4_tight_ordered_sublists() {
let input = "1. Ordered 1\n 1. Sub 1\n 2. Sub 2\n2. Ordered 2\n";
let result = fmt_tight(input);
let python_output = "1. Ordered 1\n 1. Sub 1\n 2. Sub 2\n\n2. Ordered 2\n";
assert_eq!(
result, python_output,
"D4: Tight ordered sublists should match Python.\nGot:\n{result}"
);
}
#[test]
fn test_d5_loose_footnote_list_items() {
let input = "[^217]: Testing - : Is Ketamine Contraindicated?\n - REBEL EM - more words,\n <https://rebelem.com/test>\n\n[^multiline]: Another footnote.\n";
let result = fmt_loose(input);
assert!(
result.contains("<https://rebelem.com/test>\n\n[^multiline]:"),
"D5: Loose mode should add blank line after footnote list items, got:\n{result}"
);
}
#[test]
fn test_d6_nested_blockquotes_no_extra_blanks() {
let input = "> Level 1\n> > Level 2\n> > > Level 3\n";
let python_output = "> Level 1\n> > Level 2\n> > > Level 3\n";
let result = fmt(input);
assert_eq!(result, python_output, "D6: Nested blockquotes should not have extra blank lines");
}
#[test]
fn test_d6_two_level_blockquote() {
let input = "> Outer\n> > Inner\n";
let result = fmt(input);
assert!(
!result.contains(">\n>"),
"D6: Should not have blank '> ' line between blockquote levels, got:\n{result}"
);
}
#[test]
fn test_d6_nested_blockquote_preserves_blank_separator() {
let input = "> Outer quote.\n>\n> > Inner quote.\n";
let python_output = "> Outer quote.\n> \n> > Inner quote.\n";
let result = fmt(input);
assert_eq!(
result, python_output,
"D6: Blank `>` separator between blockquote levels should be preserved.\nGot:\n{result}"
);
}
#[test]
fn test_d7_footnote_with_list_items() {
let input = "[^3]: Footnote with a list:\n - Item 1\n - Item 2\n - Item 3\n";
let result = fmt_semantic(input);
assert!(
result.contains("- Item 1\n"),
"D7: Footnote list items should be on separate lines, got:\n{result}"
);
assert!(
result.contains("- Item 2\n"),
"D7: Footnote list items should be on separate lines, got:\n{result}"
);
}
#[test]
fn test_d7_footnote_preamble_then_list() {
let input = "[^3]: Footnote with a list:\n - Item 1\n - Item 2\n";
let result = fmt(input);
assert!(
!result.contains("list: - Item"),
"D7: Footnote preamble should not collapse with list items, got:\n{result}"
);
}
#[test]
fn test_d8_footnote_with_blockquote() {
let input = "[^4]: Footnote with blockquote:\n > This is quoted inside footnote.\n";
let result = fmt(input);
assert!(
result.contains("> This is quoted"),
"D8: Footnote blockquote should be on its own line, got:\n{result}"
);
assert!(
!result.contains("blockquote: > This"),
"D8: Footnote blockquote should not be collapsed onto preamble, got:\n{result}"
);
}
#[test]
fn test_d9_empty_input_outputs_newline() {
let result = fmt("");
assert_eq!(result, "\n", "D9: Empty input should produce a trailing newline");
}
#[test]
fn test_d9_whitespace_input_outputs_newline() {
let result = fmt(" \n \n");
assert_eq!(result, "\n", "D9: Whitespace-only input should produce a trailing newline");
}
#[test]
fn test_d9_single_newline_input() {
let result = fmt("\n");
assert_eq!(result, "\n", "D9: Single newline input should produce a trailing newline");
}
#[test]
fn test_d10_html_entities_preserved() {
let input = "& < > "\n";
let result = fmt(input);
assert_eq!(result, "& < > "\n", "D10: HTML entities should be preserved as-is");
}
#[test]
fn test_d10_html_entity_in_paragraph() {
let input = "The value is > 5 and < 10.\n";
let result = fmt(input);
assert!(
result.contains(">") && result.contains("<"),
"D10: HTML entities should be preserved in paragraphs, got:\n{result}"
);
}
#[cfg(feature = "cli")]
fn run_cli(bin: &str, args: &[&str]) -> (String, i32) {
let output = std::process::Command::new(bin)
.args(args)
.output()
.unwrap_or_else(|e| panic!("Failed to run {bin}: {e}"));
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
let code = output.status.code().unwrap_or(-1);
(stderr.trim_end().to_string(), code)
}
#[cfg(feature = "cli")]
fn run_cli_stdin(bin: &str, args: &[&str], stdin: &str) -> (String, i32) {
use std::io::Write;
let mut child = std::process::Command::new(bin)
.args(args)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.unwrap_or_else(|e| panic!("Failed to run {bin}: {e}"));
if let Some(mut stdin_pipe) = child.stdin.take() {
let _ = stdin_pipe.write_all(stdin.as_bytes());
}
let output = child.wait_with_output().unwrap();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
let code = output.status.code().unwrap_or(-1);
(stderr.trim_end().to_string(), code)
}
#[cfg(feature = "cli")]
fn python_flowmark() -> &'static str {
"flowmark"
}
#[cfg(feature = "cli")]
fn rust_flowmark() -> String {
let root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
root.join("target/debug/flowmark").to_string_lossy().to_string()
}
#[test]
#[cfg(feature = "cli")]
fn test_d11_no_args_error_matches_python() {
let (py_err, py_code) = run_cli(python_flowmark(), &[]);
let (rs_err, rs_code) = run_cli(&rust_flowmark(), &[]);
assert_eq!(rs_err, py_err, "D11: No-args error message should match Python");
assert_eq!(rs_code, py_code, "D11: No-args exit code should match Python");
}
#[test]
#[cfg(feature = "cli")]
fn test_d11_auto_no_args_error_matches_python() {
let (py_err, py_code) = run_cli(python_flowmark(), &["--auto"]);
let (rs_err, rs_code) = run_cli(&rust_flowmark(), &["--auto"]);
assert_eq!(rs_err, py_err, "D11: --auto no-args error should match Python");
assert_eq!(rs_code, py_code, "D11: --auto no-args exit code should match Python");
}
#[test]
#[cfg(feature = "cli")]
fn test_d11_inplace_stdin_error_matches_python() {
let (py_err, py_code) = run_cli_stdin(python_flowmark(), &["--inplace", "-"], "hello\n");
let (rs_err, rs_code) = run_cli_stdin(&rust_flowmark(), &["--inplace", "-"], "hello\n");
assert_eq!(rs_err, py_err, "D11: --inplace stdin error should match Python");
assert_eq!(rs_code, py_code, "D11: --inplace stdin exit code should match Python");
}
#[test]
#[cfg(feature = "cli")]
fn test_d11_output_multiple_files_error_matches_python() {
let (py_err, py_code) = run_cli(python_flowmark(), &["-o", "out.md", "/dev/null", "/dev/null"]);
let (rs_err, rs_code) = run_cli(&rust_flowmark(), &["-o", "out.md", "/dev/null", "/dev/null"]);
assert_eq!(rs_err, py_err, "D11: multi-file output error should match Python");
assert_eq!(rs_code, py_code, "D11: multi-file output exit code should match Python");
}
#[test]
#[cfg(feature = "cli")]
fn test_d11_nonexistent_file_error_format() {
let (py_err, _py_code) = run_cli(python_flowmark(), &["nonexistent.md"]);
let (rs_err, _rs_code) = run_cli(&rust_flowmark(), &["nonexistent.md"]);
assert!(
rs_err.starts_with("Error:"),
"D11: Rust nonexistent file error should start with 'Error:', got: {rs_err}"
);
assert!(
rs_err.contains("nonexistent.md"),
"D11: Rust error should mention the filename, got: {rs_err}"
);
assert!(
py_err.starts_with("Error:"),
"D11: Python nonexistent file error should start with 'Error:', got: {py_err}"
);
assert!(
py_err.contains("nonexistent.md"),
"D11: Python error should mention the filename, got: {py_err}"
);
}
#[test]
fn test_d12_paragraph_before_code_fence_tight() {
let input = "**Configuration Options**:\n```typescript\n{\n minTime: number,\n}\n```\n";
let python_output =
"**Configuration Options**:\n```typescript\n{\n minTime: number,\n}\n```\n";
let result = fmt(input);
assert_eq!(
result, python_output,
"D12/P6: Should not insert blank line before code fence when source is tight.\nGot:\n{result}"
);
}
#[test]
fn test_d12_inline_code_paragraph_before_code_fence() {
let input = "Add to root `package.json`:\n```json\n{\n \"scripts\": {}\n}\n```\n";
let python_output = "Add to root `package.json`:\n```json\n{\n \"scripts\": {}\n}\n```\n";
let result = fmt(input);
assert_eq!(
result, python_output,
"D12/P6: Inline-code paragraph should stay tight before code fence.\nGot:\n{result}"
);
}
#[test]
fn test_d12_multiple_tight_code_fences() {
let input =
"First block:\n```bash\necho hello\n```\n\nSecond block:\n```python\nprint(\"hi\")\n```\n";
let python_output =
"First block:\n```bash\necho hello\n```\n\nSecond block:\n```python\nprint(\"hi\")\n```\n";
let result = fmt(input);
assert_eq!(
result, python_output,
"D12/P6: Multiple tight code fences should not get extra blank lines.\nGot:\n{result}"
);
}
#[test]
fn test_d12b_mixed_loose_tight_list_code_fences() {
let input = "\
- [ ] Create output:
```bash
cd web
```
- [ ] Launch all:
```bash
cd web
pnpm batch
```
Note: If key not available, skip.
- [ ] Monitor:
```bash
watch ls
```
- [ ] Check failures:
```bash
echo check
```
";
let python_output = "\
- [ ] Create output:
```bash
cd web
```
- [ ] Launch all:
```bash
cd web
pnpm batch
```
Note: If key not available, skip.
- [ ] Monitor:
```bash
watch ls
```
- [ ] Check failures:
```bash
echo check
```
";
let result = fmt(input);
assert_eq!(
result, python_output,
"D12b/P6: Mixed loose/tight list should not add blank lines before tight code fences.\nGot:\n{result}"
);
}
#[test]
fn test_d16_adjacent_empty_code_blocks() {
let input = "\
Emergency commits:
```bash
git commit --no-verify -m \"WIP: emergency fix\"
````
```
```
Only use `--no-verify` when absolutely necessary.
";
let python_output = "\
Emergency commits:
```bash
git commit --no-verify -m \"WIP: emergency fix\"
```
```
```
Only use `--no-verify` when absolutely necessary.
";
let result = fmt(input);
assert_eq!(
result, python_output,
"D16: Adjacent empty code blocks should not have extra blank line between them.\nGot:\n{result}"
);
}
#[test]
fn test_d17_thematic_break_before_heading_tight() {
let input = "text\n\n* * *\n## Heading\n\nmore\n";
let python_output = "text\n\n* * *\n## Heading\n\nmore\n";
let result = fmt(input);
assert_eq!(
result, python_output,
"D17: tight thematic break → heading should stay tight.\nGot:\n{result}"
);
}
#[test]
fn test_d17_thematic_break_after_paragraph_tight() {
let input = "para\n* * *\n\nb\n";
let python_output = "para\n* * *\n\nb\n";
let result = fmt(input);
assert_eq!(
result, python_output,
"D17: tight paragraph → thematic break should stay tight.\nGot:\n{result}"
);
}
#[test]
fn test_d17_thematic_break_then_paragraph_tight() {
let input = "a\n\n* * *\npara\n\nb\n";
let python_output = "a\n\n* * *\npara\n\nb\n";
let result = fmt(input);
assert_eq!(
result, python_output,
"D17: tight thematic break → paragraph should stay tight.\nGot:\n{result}"
);
}
#[test]
fn test_d17_thematic_break_consecutive_tight() {
let input = "a\n\n* * *\n* * *\n\nb\n";
let python_output = "a\n\n* * *\n* * *\n\nb\n";
let result = fmt(input);
assert_eq!(
result, python_output,
"D17: consecutive tight thematic breaks should stay tight.\nGot:\n{result}"
);
}
#[test]
fn test_d17_thematic_break_loose_preserved() {
let input = "a\n\n* * *\n\n## Heading\n\nb\n";
let python_output = "a\n\n* * *\n\n## Heading\n\nb\n";
let result = fmt(input);
assert_eq!(
result, python_output,
"D17: loose thematic break spacing should be preserved.\nGot:\n{result}"
);
}
fn fmt_ref(body: &str) -> String {
fmt(&format!("{body}\n\n[foo]: https://example.com/x\n"))
}
#[test]
fn test_d18_shortcut_ref_normalized_to_collapsed() {
assert_eq!(fmt_ref("Use [foo]"), "Use [foo][]\n\n[foo]: https://example.com/x\n");
}
#[test]
fn test_d18_collapsed_ref_preserved() {
assert_eq!(fmt_ref("Use [foo][]"), "Use [foo][]\n\n[foo]: https://example.com/x\n");
}
#[test]
fn test_d18_full_ref_label_equals_text_collapsed() {
assert_eq!(fmt_ref("Use [foo][foo]"), "Use [foo][]\n\n[foo]: https://example.com/x\n");
}
#[test]
fn test_d18_full_ref_distinct_label_preserved() {
assert_eq!(fmt_ref("Use [bar][foo]"), "Use [bar][foo]\n\n[foo]: https://example.com/x\n");
}
#[test]
fn test_d18_uppercase_shortcut_expands_to_full() {
let input = "## [Unreleased]\n\n[unreleased]: https://example.com/c\n";
let expected = "## [Unreleased][unreleased]\n\n[unreleased]: https://example.com/c\n";
assert_eq!(fmt(input), expected);
}
#[test]
fn test_d18_label_normalized_to_lowercase() {
let input = "[Foo]\n\n[Foo]: https://example.com/x\n";
assert_eq!(fmt(input), "[Foo][foo]\n\n[foo]: https://example.com/x\n");
}
#[test]
fn test_d18_collapsed_ref_label_with_spaces_and_apostrophe() {
let input = "See [St. John's School][] here.\n\n[St. John's School]: https://example.com/x\n";
let expected = "See [St. John's School][st. john's school] here.\n\n[st. john's school]: https://example.com/x\n";
assert_eq!(fmt(input), expected);
}
#[test]
fn test_d18_shortcut_ref_label_with_spaces_and_apostrophe() {
let input = "See [St. John's School] here.\n\n[St. John's School]: https://example.com/x\n";
let expected = "See [St. John's School][st. john's school] here.\n\n[st. john's school]: https://example.com/x\n";
assert_eq!(fmt(input), expected);
}
#[test]
fn test_d18_collapsed_ref_label_lowercase_with_spaces_emits_collapsed() {
let input = "See [an example][] here.\n\n[an example]: https://example.com/x\n";
let expected = "See [an example][] here.\n\n[an example]: https://example.com/x\n";
assert_eq!(fmt(input), expected);
}
#[test]
fn test_d19_image_full_ref_inlined() {
let input = "![alt][img]\n\n[img]: https://example.com/img.png\n";
let expected = "\n\n[img]: https://example.com/img.png\n";
assert_eq!(fmt(input), expected);
}
#[test]
fn test_d19_image_collapsed_ref_inlined() {
let input = "![alt][]\n\n[alt]: https://example.com/img.png\n";
let expected = "\n\n[alt]: https://example.com/img.png\n";
assert_eq!(fmt(input), expected);
}
#[test]
fn test_d19_image_shortcut_ref_inlined() {
let input = "![alt]\n\n[alt]: https://example.com/img.png\n";
let expected = "\n\n[alt]: https://example.com/img.png\n";
assert_eq!(fmt(input), expected);
}
#[test]
fn test_d19_image_with_title_inlined() {
let input = "![alt][img]\n\n[img]: https://example.com/img.png \"My title\"\n";
let expected = "\n\n[img]: https://example.com/img.png \"My title\"\n";
assert_eq!(fmt(input), expected);
}
#[test]
fn test_d19_image_label_with_spaces_inlined() {
let input = "![Logo][company logo]\n\n[company logo]: https://example.com/logo.png\n";
let expected =
"\n\n[company logo]: https://example.com/logo.png\n";
assert_eq!(fmt(input), expected);
}
#[test]
fn test_d19_badge_pattern_image_inside_link() {
let input = "[![alt][img]][url]\n\n[img]: https://example.com/img.png\n[url]: https://example.com/page\n";
let expected = "[][url]\n\n[img]: https://example.com/img.png\n[url]: https://example.com/page\n";
assert_eq!(fmt(input), expected);
}
#[test]
fn test_d19_image_no_def_unchanged() {
let input = "Some ![alt][missing] text here.\n";
let result = fmt(input);
assert!(
!result.contains('\u{F000}') && !result.contains("696d67"),
"no-def image must not leak PUA or hex: {result}"
);
}
#[test]
fn test_d20_def_label_lowercased_on_render() {
let input = "[Logo][]\n\n[Logo]: https://example.com/logo.png\n";
let expected = "[Logo][logo]\n\n[logo]: https://example.com/logo.png\n";
assert_eq!(fmt(input), expected);
}
#[test]
fn test_d20_def_label_with_spaces_lowercased() {
let input = "[Company Logo][]\n\n[Company Logo]: https://example.com/logo.png\n";
let expected = "[Company Logo][company logo]\n\n[company logo]: https://example.com/logo.png\n";
assert_eq!(fmt(input), expected);
}
#[test]
fn test_d20_def_label_already_lowercase_unchanged() {
let input = "[link][example]\n\n[example]: https://example.com/page\n";
let expected = "[link][example]\n\n[example]: https://example.com/page\n";
assert_eq!(fmt(input), expected);
}
#[test]
fn test_d20_def_label_with_title_preserved_lowercased() {
let input = "[Page][Home]\n\n[Home]: https://example.com \"Welcome home\"\n";
let expected = "[Page][home]\n\n[home]: https://example.com \"Welcome home\"\n";
assert_eq!(fmt(input), expected);
}
#[test]
fn test_d18_shortcut_without_definition_unchanged() {
assert_eq!(fmt_ref("Use [bar]"), "Use [bar]\n\n[foo]: https://example.com/x\n");
}
#[test]
fn test_d18_multiple_shortcut_refs_on_one_line() {
assert_eq!(
fmt_ref("Both [foo] and [foo] here"),
"Both [foo][] and [foo][] here\n\n[foo]: https://example.com/x\n"
);
}
#[test]
fn test_d18_collapsed_form_is_idempotent() {
let once = fmt_ref("Use [foo]");
assert_eq!(fmt(&once), once, "D18: collapsed reference output must be idempotent");
}
#[test]
fn test_d18_inline_link_unaffected() {
let input = "See [foo](https://example.com/z) here.\n";
assert_eq!(fmt(input), "See [foo](https://example.com/z) here.\n");
}
#[test]
fn test_d13_blockquote_list_code_block_blank_line_indent() {
let input = "\
> 1. **Copy this file** to a dated version:
>
> ```
> template-process.md
> ```
>
> 2. **Review the previous** for context:
>
> - Check the section
";
let python_output = "\
> 1. **Copy this file** to a dated version:
> \n\
> ```
> template-process.md
> ```
>
> 2. **Review the previous** for context:
> \n\
> - Check the section
";
let result = fmt(input);
assert_eq!(
result, python_output,
"D13/P7: Blank lines between blockquote list items and children should have list indentation.\nGot:\n{result}"
);
}
#[test]
fn test_d13_blockquote_list_with_blank_continuation() {
let input = "> - Rules:\n>\n> 1. Look for duplicated code\n>\n> 2. Look for dead code\n";
let python_output =
"> - Rules:\n> \n> 1. Look for duplicated code\n>\n> 2. Look for dead code\n";
let result = fmt(input);
assert_eq!(
result, python_output,
"D13/P7: Blank line between blockquote list item and child should have list indent.\nGot:\n{result}"
);
}
#[test]
fn test_d14_escaped_backtick_in_table_inline_code() {
let input = "| Col1 | Col2 |\n| --- | --- |\n| swallowing | `throw new CLIError(\\`${msg}: ${error.message}\\`)` |\n";
let result = fmt(input);
assert!(
result.contains("\\`)` |") || result.contains("\\`)`|"),
"D14/P8: Escaped backtick at end of inline code in table should be preserved.\nGot:\n{result}"
);
}
fn fmt_auto(input: &str) -> String {
fill_markdown(input, true, 88, true, true, true, false, None, ListSpacing::Preserve)
}
#[test]
fn test_d15_smart_quote_after_code_ending_with_word_char() {
let input = "The `config`'s value is important.\n";
let result = fmt_auto(input);
assert!(
result.contains("`\u{2019}s"),
"D15/P9: Apostrophe after code ending with word char should be smart quote.\nGot:\n{result}"
);
}
#[test]
fn test_d15_no_smart_quote_after_code_ending_with_non_word_char() {
let input = "Call `foo()`'s result.\n";
let result = fmt_auto(input);
assert!(
result.contains("`'s"),
"D15/P9: Apostrophe after code ending with non-word char should stay ASCII.\nGot:\n{result}"
);
}
#[test]
fn test_d15_smart_quote_after_various_code_spans() {
let input = "Use `@react-spring/web`'s API and `x`'s type but `foo()`'s result.\n";
let result = fmt_auto(input);
assert!(
result.contains("web`\u{2019}s"),
"D15/P9: `web` ends with word char, apostrophe should be smart quote.\nGot:\n{result}"
);
assert!(
result.contains("x`\u{2019}s"),
"D15/P9: `x` ends with word char, apostrophe should be smart quote.\nGot:\n{result}"
);
assert!(
result.contains("foo()`'s"),
"D15/P9: `foo()` ends with non-word char, apostrophe should stay ASCII.\nGot:\n{result}"
);
}
#[test]
fn test_relative_path_link_preserved() {
let input = "See [docs/port-sync-playbook.md](docs/port-sync-playbook.md) for details.\n";
let result = fmt_semantic(input);
assert!(
result.contains("[docs/port-sync-playbook.md](docs/port-sync-playbook.md)"),
"Relative path link where text==URL should be preserved as explicit link, got:\n{result}"
);
}
#[test]
fn test_absolute_url_autolink_still_works() {
let input = "Visit https://example.com for info.\n";
let result = fmt(input);
assert!(
!result.contains("[https://example.com](https://example.com)"),
"Absolute URL autolink should render as bare text, got:\n{result}"
);
}