#[macro_use]
#[path = "common/mod.rs"]
mod common;
use std::sync::LazyLock;
use mdtablefix::{lazy_regex, process_stream, textproc::leading_indent};
use proptest::prelude::*;
use regex::Regex;
use unicode_width::UnicodeWidthStr;
static BULLET_RE: LazyLock<Regex> = lazy_regex!(
r"^(\s*(?:[-*+]|\d+[.)])\s+(?:\[\s*(?:[xX]|\s)\s*\]\s*)?)(.*)",
"bullet pattern regex should compile",
);
fn list_continuation_indent(first_line: &str) -> Option<String> {
let prefix = BULLET_RE.captures(first_line)?.get(1)?.as_str();
let indent_str = leading_indent(prefix);
let prefix_width = UnicodeWidthStr::width(prefix);
let indent_width = UnicodeWidthStr::width(indent_str);
Some(format!(
"{}{}",
indent_str,
" ".repeat(prefix_width.saturating_sub(indent_width))
))
}
fn assert_no_spurious_leading_spaces(output: &[String]) {
assert!(!output.is_empty(), "output must not be empty");
let list_indent = output
.first()
.map(String::as_str)
.and_then(list_continuation_indent)
.unwrap_or_default();
for (index, line) in output.iter().enumerate() {
if line.is_empty() {
continue;
}
if index == 0 || list_indent.is_empty() {
assert!(
!(line.starts_with(' ') && list_indent.is_empty()),
"plain paragraph line must not begin with a space: {line:?}"
);
continue;
}
assert!(
line.starts_with(&list_indent),
"list continuation line must preserve indent {list_indent:?}: {line:?}"
);
let content = &line[list_indent.len()..];
assert!(
!content.starts_with(' '),
"spurious carry whitespace after list indent on line {index}: {line:?}"
);
}
}
#[test]
fn wrap_issue_291_execplan_fixture() {
let input: Vec<String> = include_lines!("data/issue_291_execplan_input.txt");
let expected: Vec<String> = include_lines!("data/issue_291_execplan_expected.txt");
let output = process_stream(&input);
assert_eq!(output, expected);
assert_no_spurious_leading_spaces(&output);
}
#[test]
fn wrap_issue_291_errors_fixture() {
let input: Vec<String> = include_lines!("data/issue_291_errors_input.txt");
let expected: Vec<String> = include_lines!("data/issue_291_errors_expected.txt");
let output = process_stream(&input);
assert_eq!(output, expected);
assert_no_spurious_leading_spaces(&output);
}
#[test]
fn wrap_issue_291_netsuke_list_fixture() {
let input: Vec<String> = include_lines!("data/issue_291_netsuke_list_input.txt");
let expected: Vec<String> = include_lines!("data/issue_291_netsuke_list_expected.txt");
let output = process_stream(&input);
assert_eq!(output, expected);
assert_no_spurious_leading_spaces(&output);
}
#[test]
fn wrap_issue_291_kani_list_fixture() {
let input: Vec<String> = include_lines!("data/issue_291_kani_list_input.txt");
let expected: Vec<String> = include_lines!("data/issue_291_kani_list_expected.txt");
let output = process_stream(&input);
assert_eq!(output, expected);
assert_no_spurious_leading_spaces(&output);
}
#[test]
fn wrap_issue_291_ordered_list_fixture() {
let input: Vec<String> = include_lines!("data/issue_291_ordered_list_input.txt");
let expected: Vec<String> = include_lines!("data/issue_291_ordered_list_expected.txt");
let output = process_stream(&input);
assert_eq!(output, expected);
assert_no_spurious_leading_spaces(&output);
}
#[test]
fn wrap_issue_291_nested_ordered_list_fixture() {
let input: Vec<String> = include_lines!("data/issue_291_nested_ordered_list_input.txt");
let expected: Vec<String> = include_lines!("data/issue_291_nested_ordered_list_expected.txt");
let output = process_stream(&input);
assert_eq!(output, expected);
assert_no_spurious_leading_spaces(&output);
}
fn prose_word_strategy() -> impl Strategy<Value = String> {
prop::collection::vec(
prop_oneof![
Just("alpha".to_string()),
Just("beta".to_string()),
Just("gamma".to_string()),
Just("delta".to_string()),
Just("epsilon".to_string()),
Just("zeta".to_string()),
Just("observation".to_string()),
Just("evidence".to_string()),
Just("discovery".to_string()),
],
8..20,
)
.prop_map(|words| words.join(" "))
}
fn inline_code_span_strategy() -> impl Strategy<Value = String> {
prop::collection::vec(
prop_oneof![
Just("Constraints".to_string()),
Just("Tolerances".to_string()),
Just("errors".to_string()),
Just("kani-smoke".to_string()),
Just("4.1.2".to_string()),
Just("4.2.*".to_string()),
Just("--file".to_string()),
],
1..4,
)
.prop_map(|labels| {
labels
.into_iter()
.map(|label| format!("`{label}`"))
.collect::<Vec<_>>()
.join(", ")
})
}
fn list_prefix_strategy() -> impl Strategy<Value = String> {
prop_oneof![
Just("- ".to_string()),
Just("* ".to_string()),
Just("+ ".to_string()),
Just("- [ ] ".to_string()),
Just("- [x] ".to_string()),
(1usize..3).prop_map(|n| format!("{n}. ")),
(10usize..20).prop_map(|n| format!("{n}. ")),
(1usize..3).prop_map(|n| format!("{} - ", " ".repeat(n))),
(1usize..3).prop_map(|n| format!("{} {}. ", " ".repeat(n), n + 1)),
]
}
proptest! {
#[test]
fn wrap_paragraphs_do_not_introduce_leading_spaces(
prefix in prose_word_strategy(),
code in inline_code_span_strategy(),
suffix in prose_word_strategy(),
) {
let body = format!("{prefix} {code} {suffix}");
let input = vec![body];
let output = process_stream(&input);
assert_no_spurious_leading_spaces(&output);
prop_assert!(output.iter().all(|line| line.len() <= 80));
}
#[test]
fn wrap_list_items_do_not_introduce_spurious_leading_spaces(
prefix in list_prefix_strategy(),
prose in prose_word_strategy(),
code in inline_code_span_strategy(),
tail in prose_word_strategy(),
) {
let body = format!("{prose} {code} {tail}");
let line = format!("{prefix}{body}");
let leading_spaces = line.chars().take_while(|&c| c == ' ').count();
prop_assume!(leading_spaces < 4);
prop_assume!(line.len() > 80);
let input = vec![line];
let expected_prefix = BULLET_RE
.captures(&input[0])
.and_then(|captures| captures.get(1))
.expect("wrapping must preserve a list prefix")
.as_str();
let output = process_stream(&input);
prop_assert!(output.len() > 1, "expected wrapping to occur");
prop_assert!(
output[0].starts_with(expected_prefix),
"first line must retain the canonical list prefix"
);
prop_assert!(
output.iter().all(|line| line.len() <= 80),
"wrapped lines must not exceed 80 columns"
);
assert_no_spurious_leading_spaces(&output);
}
}