use mdtablefix::{process::WRAP_COLS, wrap::wrap_text};
use proptest::prelude::*;
use unicode_width::UnicodeWidthStr;
fn has_md038_code_span(rendered: &str) -> bool {
let mut remaining = rendered;
while let Some(open_index) = remaining.find('`') {
let after_open = &remaining[open_index + 1..];
let Some(close_index) = after_open.find('`') else {
break;
};
let code = &after_open[..close_index];
if !code.is_empty() && (code.starts_with(' ') || code.ends_with(' ')) {
return true;
}
remaining = &after_open[close_index + 1..];
}
false
}
fn footnote_label_strategy() -> impl Strategy<Value = String> {
prop::collection::vec(
prop_oneof![
(b'a'..=b'z').prop_map(char::from),
(b'A'..=b'Z').prop_map(char::from),
(b'0'..=b'9').prop_map(char::from),
Just('-'),
Just('_')
],
1..16,
)
.prop_map(|chars| chars.into_iter().collect())
}
fn checklist_marker_count(lines: &[String]) -> usize {
lines
.iter()
.filter(|line| line.starts_with("- [ ] ") || line.starts_with("- [x] "))
.count()
}
fn path_segment_strategy() -> impl Strategy<Value = String> {
"[A-Za-z][A-Za-z0-9]{1,10}".prop_map(String::from)
}
proptest! {
#[test]
fn wrap_text_keeps_generated_footnote_references_atomic(
label in footnote_label_strategy(),
prefix_words in 2usize..30,
suffix_words in 0usize..=20,
width in 24usize..96,
) {
let marker = format!("[^{label}]");
let prefix = std::iter::repeat_n("prefix", prefix_words).collect::<Vec<_>>().join(" ");
let suffix = std::iter::repeat_n("suffix", suffix_words).collect::<Vec<_>>().join(" ");
let input = if suffix.is_empty() {
vec![format!("{prefix} xxxxxxxxxxxxxxxxxxxxx.{marker}")]
} else {
vec![format!("{prefix} xxxxxxxxxxxxxxxxxxxxx.{marker} {suffix}")]
};
let wrapped = wrap_text(&input, width);
let rendered = wrapped.join("\n");
prop_assert!(rendered.contains(&marker));
prop_assert!(!rendered.contains("[\n"));
prop_assert!(!rendered.contains("\n^"));
}
#[test]
fn wrap_text_never_orphans_closing_backtick(
content in "[a-z]{1,40}",
width in 20usize..=100,
) {
let input = vec![format!("- `{content}`")];
let output = wrap_text(&input, width);
for line in &output {
let stripped = line.strip_prefix(" ").unwrap_or(line);
if stripped.starts_with('`') {
prop_assert!(
stripped.ends_with('`'),
"orphaned closing backtick fragment on line: {line:?}"
);
}
}
}
#[test]
fn wrap_text_backslash_terminated_code_spans_stay_within_width(
vendor in path_segment_strategy(),
product in path_segment_strategy(),
scope in prop_oneof![Just("machine"), Just("user"), Just("system")],
) {
let code_span = format!("`C:\\Program Files\\{vendor}\\{product}\\bin\\`");
let input = vec![
format!("- Install the executable to {code_span} and"),
format!(" add that folder to PATH ({scope} scope)."),
];
let output = wrap_text(&input, WRAP_COLS);
prop_assert_eq!(wrap_text(&output, WRAP_COLS), output.clone());
let rendered = output.join("\n");
prop_assert!(
rendered.contains(&code_span),
"wrapped text no longer contains the expected inline code span {code_span:?}: {rendered:?}"
);
for line in &output {
let backtick_count = line.chars().filter(|&ch| ch == '`').count();
prop_assert_eq!(
backtick_count % 2,
0,
"line has an unclosed code span after wrapping: {:?}",
line
);
prop_assert!(
UnicodeWidthStr::width(line.as_str()) <= WRAP_COLS,
"line too wide ({} cols): {line:?}",
UnicodeWidthStr::width(line.as_str())
);
}
}
#[test]
fn wrap_text_deferred_span_closing_backtick_not_orphaned_for_generated_prefixes(
n in 1usize..=3,
prefix_kind in 0usize..3,
before in "[a-z]{1,20}",
inside in "[a-z][a-z ]{0,29}",
after in "[a-z]{1,20}",
width in 30usize..=120,
) {
let (line1_prefix, cont_prefix) = match prefix_kind {
0 => ("- ".to_owned(), " ".to_owned()),
1 => ("1. ".to_owned(), " ".to_owned()),
_ => ("> ".to_owned(), "> ".to_owned()),
};
let fence = "`".repeat(n);
let line1 = format!("{line1_prefix}{before} {fence}{inside}");
let line2 = format!("{cont_prefix}{inside}{fence} {after}");
let output = wrap_text(&[line1, line2], width);
for line in &output {
let body = line.trim_start_matches(|c: char| {
c.is_ascii_digit() || matches!(c, ' ' | '-' | '>' | '.')
});
if body.starts_with(fence.as_str()) {
prop_assert!(
body.len() > fence.len(),
"orphaned closing backtick on line: {line:?}"
);
}
}
}
#[test]
fn wrap_text_deferred_span_preserves_hard_break(
n in 1usize..=2,
prefix_kind in 0usize..3,
before in "[a-z]{1,15}",
inside in "[a-z][a-z ]{0,19}",
after in "[a-z]{1,15}",
width in 50usize..=120,
) {
let fence = "`".repeat(n);
let (line1_prefix, cont_prefix) = match prefix_kind {
0 => ("- ".to_owned(), " ".to_owned()),
1 => ("1. ".to_owned(), " ".to_owned()),
_ => ("> ".to_owned(), "> ".to_owned()),
};
let line1 = format!("{line1_prefix}{before} {fence}{inside}");
let line2 = format!("{cont_prefix}{inside}{fence} {after} ");
let output = wrap_text(&[line1, line2], width);
let rendered = output.join("\n");
prop_assert!(
output.iter().any(|l| l.ends_with(" ")),
"hard-break marker lost; rendered:\n{rendered}"
);
}
#[test]
fn wrap_text_deferred_blockquote_span_stays_atomic(
n in 1usize..=3,
before in "[a-z ]{1,25}",
inside in "[a-z][a-z ]{0,24}",
after in "[a-z ]{1,25}",
width in 40usize..=120,
) {
let fence = "`".repeat(n);
let line1 = format!("> {before} {fence}{inside}");
let line2 = format!("> {inside}{fence} {after}");
let output = wrap_text(&[line1, line2], width);
let bare_closer = format!("> {fence}");
for line in &output {
prop_assert!(
line != &bare_closer,
"bare closing fence on its own line: {line:?}"
);
}
let rendered = output.join("\n");
prop_assert!(rendered.contains(&fence), "fence lost; rendered:\n{rendered}");
}
#[test]
fn wrap_text_deferred_checklist_span_does_not_add_checklist_markers(
checked in any::<bool>(),
before in "[a-z][a-z ]{0,30}",
command in "[a-z][a-z0-9_-]{1,30}",
suffix in "[a-z][a-z ]{0,30}",
width in 30usize..=100,
) {
let marker = if checked { "- [x] " } else { "- [ ] " };
let input = vec![
format!("{marker}{before} `{command}"),
format!(" --flag` {suffix}"),
];
let output = wrap_text(&input, width);
prop_assert_eq!(
checklist_marker_count(&output),
1,
"wrapped checklist item gained markers: {:?}",
output
);
let rendered = output.join("\n");
prop_assert!(
!rendered.contains(format!("` {command} --flag").as_str()),
"wrapped checklist item inserted a space after the opening fence: {:?}",
output
);
prop_assert!(
!rendered.contains(format!("{command} `").as_str()),
"wrapped checklist item inserted a space before the closing fence: {:?}",
output
);
prop_assert!(
rendered.contains(format!("`{command} --flag`").as_str()),
"wrapped checklist item did not preserve the command span: {:?}",
output
);
}
#[test]
fn wrap_text_deferred_span_close_reopen_different_fence_lengths_correct(
n1 in 1usize..=3,
n2 in 1usize..=3,
before in "[a-z]{1,15}",
mid in "[a-z]{1,10}",
after in "[a-z]{1,15}",
width in 40usize..=120,
) {
prop_assume!(n1 != n2);
let fence1 = "`".repeat(n1);
let fence2 = "`".repeat(n2);
let input = vec![
format!("- [ ] {before} {fence1}{mid}"),
format!(" {mid}{fence1} {fence2}{after}{fence2}"),
];
let output = wrap_text(&input, width);
prop_assert_eq!(
checklist_marker_count(&output),
1,
"wrapped checklist item gained markers: {:?}",
output
);
for line in &output {
let trimmed = line.trim_start();
prop_assert!(
!trimmed.chars().all(|ch| ch == '`'),
"orphaned fence on line: {line:?}"
);
prop_assert!(
!trimmed.starts_with('`'),
"bare backtick run starts a continuation line: {line:?}"
);
}
let rendered = output.join("\n");
prop_assert!(
!rendered.contains(format!("{fence1} {mid}").as_str()),
"wrapped span inserted a space after the first opening fence: {:?}",
output
);
prop_assert!(
!rendered.contains(format!("{fence2} {after}").as_str()),
"wrapped span inserted a space after the second opening fence: {:?}",
output
);
}
#[test]
fn wrap_keeps_leading_hyphen_compound_atomic(
prefix in "\\p{L}{1,12}",
inner in "\\p{L}{1,12}",
width in 20usize..120,
) {
let compound = format!("{prefix}-`{inner}`");
let compound_width = UnicodeWidthStr::width(compound.as_str());
let sentence = format!(
"This sentence has the compound {compound} embedded within it and \
contains enough trailing prose to force the wrapping algorithm to \
reflow the text across multiple lines when the target width is \
sufficiently narrow."
);
let input = vec![sentence];
let output = wrap_text(&input, width);
if compound_width <= width {
let rendered = output.join("\n");
prop_assert!(
rendered.contains(&compound),
"compound {compound:?} (width {compound_width}) must appear intact \
at target width {width}: {output:?}"
);
} else {
prop_assert!(!output.is_empty(), "wrap_text must not panic or return empty output");
}
}
#[test]
fn wrap_text_opener_at_eol_does_not_create_md038_span(
body in "[A-Za-z_][A-Za-z0-9_:() .]{1,60}",
suffix in "[a-z ]{1,40}",
width in 40usize..=120,
) {
prop_assume!(!body.ends_with(' '));
let input = vec![
"4. Opens a code span at line end `".to_string(),
format!(" {body}`, {suffix}."),
];
let output = wrap_text(&input, width);
let rendered = output.join("\n");
prop_assert!(
!has_md038_code_span(&rendered),
"output must not contain MD038 code spans:\n{rendered}"
);
}
#[test]
fn wrap_text_spanning_code_continuations_respect_width_when_source_lines_fit(
prefix_kind in 0usize..3,
before in "[a-z]{1,16}",
part1 in "[a-z]{1,24}",
part2 in "[a-z]{1,24}",
) {
let (line1_prefix, cont_prefix) = match prefix_kind {
0 => ("- ".to_owned(), " ".to_owned()),
1 => ("1. ".to_owned(), " ".to_owned()),
_ => ("> ".to_owned(), "> ".to_owned()),
};
let line1 = format!("{line1_prefix}{before} `{part1}");
let line2 = format!("{cont_prefix}{part2}`");
let joined = format!("{line1_prefix}{before} `{part1} {part2}`");
let width = UnicodeWidthStr::width(line1.as_str())
.max(UnicodeWidthStr::width(line2.as_str()));
prop_assert!(UnicodeWidthStr::width(joined.as_str()) > width);
let output = wrap_text(&[line1, line2], width);
for line in &output {
prop_assert!(
UnicodeWidthStr::width(line.as_str()) <= width,
"line exceeds width {width}: {line:?}; output: {output:?}"
);
}
}
}