use rumdl_lib::utils::text_reflow::*;
use std::time::Instant;
#[test]
fn test_list_item_trailing_whitespace_removal() {
let input = "1. First line with trailing spaces \n Second line with trailing spaces \n Third line\n";
let options = ReflowOptions {
line_length: 999999,
break_on_sentences: true, preserve_breaks: false,
sentence_per_line: false,
semantic_line_breaks: false,
abbreviations: None,
length_mode: ReflowLengthMode::default(),
attr_lists: false,
require_sentence_capital: true,
max_list_continuation_indent: None,
};
let result = reflow_markdown(input, &options);
assert!(
!result.contains(" "),
"Result should not contain 3+ consecutive spaces: {result:?}"
);
assert!(result.contains(" \n"), "Hard breaks should be preserved: {result:?}");
assert!(
result.lines().count() >= 2,
"Should have multiple lines (not reflowed due to hard breaks), got: {}",
result.lines().count()
);
}
#[test]
fn test_reflow_simple_text() {
let options = ReflowOptions {
line_length: 20,
..Default::default()
};
let input = "This is a very long line that needs to be wrapped";
let result = reflow_line(input, &options);
assert_eq!(result.len(), 3);
assert!(result[0].chars().count() <= 20);
}
#[test]
fn test_preserve_inline_code() {
let options = ReflowOptions {
line_length: 20,
..Default::default()
};
let input = "This line contains `some code` that should not be broken";
let result = reflow_line(input, &options);
assert!(result.iter().any(|line| line.contains("`some code`")));
}
#[test]
fn test_preserve_links() {
let options = ReflowOptions {
line_length: 30,
..Default::default()
};
let input = "Check out [this link](https://example.com) for more information on the topic";
let result = reflow_line(input, &options);
assert!(
result
.iter()
.any(|line| line.contains("[this link](https://example.com)"))
);
}
#[test]
fn test_reference_link_patterns_fixed() {
let options = ReflowOptions {
line_length: 80,
..Default::default()
};
let test_cases = vec![
(
"See [link][ref] for details",
vec!["[link][ref]"],
"reference link with label",
),
(
"Check [this][1] and [that][2] out",
vec!["[this][1]", "[that][2]"],
"multiple reference links",
),
(
"Visit [example.com][] today",
vec!["[example.com][]"],
"shortcut reference link",
),
(
"See [link] for more info [here][ref]",
vec!["[link]", "[here][ref]"],
"mixed reference styles",
),
];
for (input, expected_patterns, description) in test_cases {
let result = reflow_markdown(input, &options);
for pattern in expected_patterns {
assert!(
result.contains(pattern),
"Pattern '{pattern}' should be preserved in result for test: {description}\nInput: {input}\nResult: {result}"
);
}
}
}
#[test]
fn test_sentence_detection_basic() {
let text = "First sentence. Second sentence. Third sentence.";
let sentences = split_into_sentences(text);
assert_eq!(sentences.len(), 3);
assert_eq!(sentences[0], "First sentence.");
assert_eq!(sentences[1], "Second sentence.");
assert_eq!(sentences[2], "Third sentence.");
}
#[test]
fn test_sentence_detection_abbreviations() {
let text = "Talk to Dr. Smith. He is helpful.";
let sentences = split_into_sentences(text);
assert_eq!(sentences.len(), 2);
assert!(sentences[0].contains("Dr. Smith"));
}
#[test]
fn test_split_into_sentences() {
let text = "This is the first sentence. And this is the second! Is this the third?";
let sentences = split_into_sentences(text);
assert_eq!(sentences.len(), 3);
assert_eq!(sentences[0], "This is the first sentence.");
assert_eq!(sentences[1], "And this is the second!");
assert_eq!(sentences[2], "Is this the third?");
let text_no_punct = "This is a single sentence";
let sentences = split_into_sentences(text_no_punct);
assert_eq!(sentences.len(), 1);
assert_eq!(sentences[0], "This is a single sentence");
let sentences = split_into_sentences("");
assert_eq!(sentences.len(), 0);
}
#[test]
fn test_sentence_per_line_reflow() {
let options = ReflowOptions {
line_length: 0, break_on_sentences: true,
preserve_breaks: false,
sentence_per_line: true,
semantic_line_breaks: false,
abbreviations: None,
length_mode: ReflowLengthMode::default(),
attr_lists: false,
require_sentence_capital: true,
max_list_continuation_indent: None,
};
let input = "First sentence. Second sentence. Third sentence.";
let result = reflow_line(input, &options);
assert_eq!(result.len(), 3);
assert_eq!(result[0], "First sentence.");
assert_eq!(result[1], "Second sentence.");
assert_eq!(result[2], "Third sentence.");
let input_with_md = "This is `code`. And this is **bold**.";
let result = reflow_line(input_with_md, &options);
assert_eq!(result.len(), 2);
}
#[test]
fn test_sentence_per_line_with_backticks() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
semantic_line_breaks: false,
abbreviations: None,
..Default::default()
};
let input = "First sentence with `code`. Second sentence here.";
let result = reflow_line(input, &options);
assert_eq!(result.len(), 2);
assert_eq!(result[0], "First sentence with `code`.");
assert_eq!(result[1], "Second sentence here.");
}
#[test]
fn test_sentence_per_line_with_backticks_in_parens() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
semantic_line_breaks: false,
abbreviations: None,
..Default::default()
};
let input = "First sentence (with `code`). Second sentence here.";
let result = reflow_line(input, &options);
assert_eq!(result.len(), 2);
assert_eq!(result[0], "First sentence (with `code`).");
assert_eq!(result[1], "Second sentence here.");
}
#[test]
fn test_sentence_per_line_with_questions_exclamations() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
semantic_line_breaks: false,
abbreviations: None,
..Default::default()
};
let input = "Is this working? Yes it is! And a statement.";
let result = reflow_line(input, &options);
assert_eq!(result.len(), 3);
let lines = result;
assert_eq!(lines[0], "Is this working?");
assert_eq!(lines[1], "Yes it is!");
assert_eq!(lines[2], "And a statement.");
}
#[test]
fn test_split_sentences_issue_124() {
let text = "If you are sure ... on a `PyModule` instance. For example:";
let sentences = split_into_sentences(text);
assert_eq!(sentences.len(), 2, "Should detect 2 sentences");
}
#[test]
fn test_reference_link_edge_cases() {
let options = ReflowOptions {
line_length: 80,
..Default::default()
};
let input1 = "[link][ref] at the beginning of a line";
let result1 = reflow_markdown(input1, &options);
assert!(
result1.contains("[link][ref]"),
"Reference link at start should be preserved"
);
let input2 = "This is a reference to [link][ref]";
let result2 = reflow_markdown(input2, &options);
assert!(
result2.contains("[link][ref]"),
"Reference link at end should be preserved"
);
let input3 = "See [first][1] and [second][2] and [third][3] for details";
let result3 = reflow_markdown(input3, &options);
assert!(
result3.contains("[first][1]"),
"First reference link should be preserved"
);
assert!(
result3.contains("[second][2]"),
"Second reference link should be preserved"
);
assert!(
result3.contains("[third][3]"),
"Third reference link should be preserved"
);
let input4 = "Check out [example.com][] for more info";
let result4 = reflow_markdown(input4, &options);
assert!(
result4.contains("[example.com][]"),
"Shortcut reference link should be preserved"
);
let input5 = "See [link with [nested] brackets][ref] here";
let result5 = reflow_markdown(input5, &options);
assert!(
result5.contains("[link with [nested] brackets][ref]"),
"Reference link with nested brackets should be preserved"
);
}
#[test]
fn test_reflow_with_emphasis() {
let options = ReflowOptions {
line_length: 30,
..Default::default()
};
let input = "This line contains **bold text** and *italic text* that should be preserved";
let result = reflow_markdown(input, &options);
assert!(result.contains("**bold text**"));
assert!(result.contains("*italic text*"));
}
#[test]
fn test_image_patterns_preserved() {
let options = ReflowOptions {
line_length: 50,
..Default::default()
};
let test_cases = vec", "", "basic image"),
(
"",
"",
"image with URL",
),
(
"",
"",
"image with title",
),
("", "", "image without alt text"),
("![alt][ref]", "![alt][ref]", "reference-style image"),
];
for (input, expected_pattern, description) in test_cases {
let result = reflow_markdown(input, &options);
assert!(
result.contains(expected_pattern),
"Image pattern should be preserved for test: {description}\nInput: {input}\nResult: {result}"
);
}
}
#[test]
fn test_extended_markdown_patterns() {
let options = ReflowOptions {
line_length: 80,
..Default::default()
};
let input_strike = "This text has ~~strikethrough~~ formatting";
let result_strike = reflow_markdown(input_strike, &options);
assert!(result_strike.contains("~~strikethrough~~"));
let input_sub = "H~2~O is water";
let result_sub = reflow_markdown(input_sub, &options);
assert!(result_sub.contains("H~2~O"));
let input_sup = "E = mc^2^";
let result_sup = reflow_markdown(input_sup, &options);
assert!(result_sup.contains("mc^2^"));
let input_mark = "This is ==highlighted== text";
let result_mark = reflow_markdown(input_mark, &options);
assert!(result_mark.contains("==highlighted=="));
}
#[test]
fn test_complex_mixed_patterns() {
let options = ReflowOptions {
line_length: 100,
..Default::default()
};
let input = "This is a **bold link [example](https://example.com)** with `code` and an .";
let result = reflow_markdown(input, &options);
assert!(result.contains("**bold link [example](https://example.com)**"));
assert!(result.contains("`code`"));
assert!(result.contains(""));
}
#[test]
fn test_footnote_patterns_preserved() {
let options = ReflowOptions {
line_length: 80,
..Default::default()
};
let input_inline = "This is a sentence with a footnote^[This is the footnote text] in it.";
let result_inline = reflow_markdown(input_inline, &options);
assert!(result_inline.contains("^[This is the footnote text]"));
let input_ref = "This is a sentence with a reference footnote[^1] in it.";
let result_ref = reflow_markdown(input_ref, &options);
assert!(result_ref.contains("[^1]"));
let input_named = "This is a sentence with a named footnote[^note] in it.";
let result_named = reflow_markdown(input_named, &options);
assert!(result_named.contains("[^note]"));
}
#[test]
fn test_reflow_markdown_numbered_lists() {
let options = ReflowOptions {
line_length: 40,
..Default::default()
};
let input = "1. This is the first item in a numbered list\n2. This is the second item with a continuation that spans multiple lines\n3. Third item";
let result = reflow_markdown(input, &options);
assert!(result.contains("1. "), "Should have first list marker");
assert!(result.contains("2. "), "Should have second list marker");
assert!(result.contains("3. "), "Should have third list marker");
let lines: Vec<&str> = result.lines().collect();
let continuation_lines: Vec<&&str> = lines
.iter()
.filter(|l| l.starts_with(" ") && !l.starts_with(" that"))
.collect();
assert!(
!continuation_lines.is_empty(),
"Numbered list continuations should be indented with 3 spaces. Got:\n{result}"
);
}
#[test]
fn test_reflow_markdown_bullet_lists() {
let options = ReflowOptions {
line_length: 40,
..Default::default()
};
let input = "- This is the first bullet item\n- This is the second bullet with a continuation that spans multiple lines\n- Third item";
let result = reflow_markdown(input, &options);
assert!(result.contains("- This"), "Should have bullet markers");
let lines: Vec<&str> = result.lines().collect();
let continuation_lines: Vec<&&str> = lines
.iter()
.filter(|l| l.starts_with(" ") && !l.starts_with("- ") && !l.starts_with(" that"))
.collect();
assert!(
!continuation_lines.is_empty(),
"Bullet lists should preserve markers and indent continuations with 2 spaces. Got:\n{result}"
);
}
#[test]
fn test_ie_abbreviation_split_debug() {
let input = "This results in extracting directly from the input object, i.e. `obj.extract()`, rather than trying to access an item or attribute.";
let options = ReflowOptions {
line_length: 80,
break_on_sentences: true,
preserve_breaks: false,
sentence_per_line: true,
semantic_line_breaks: false,
abbreviations: None,
length_mode: ReflowLengthMode::default(),
attr_lists: false,
require_sentence_capital: true,
max_list_continuation_indent: None,
};
let result = reflow_line(input, &options);
assert_eq!(result.len(), 1, "Should not split after i.e. abbreviation");
}
#[test]
fn test_ie_abbreviation_paragraph() {
let input = "The `pyo3(transparent)` attribute can be used on structs with exactly one field.\nThis results in extracting directly from the input object, i.e. `obj.extract()`, rather than trying to access an item or attribute.\nThis behaviour is enabled per default for newtype structs and tuple-variants with a single field.";
let options = ReflowOptions {
line_length: 80,
break_on_sentences: true,
preserve_breaks: false,
sentence_per_line: true,
semantic_line_breaks: false,
abbreviations: None,
length_mode: ReflowLengthMode::default(),
attr_lists: false,
require_sentence_capital: true,
max_list_continuation_indent: None,
};
let result = reflow_markdown(input, &options);
assert!(
!result.contains("i.e.\n"),
"Should not break after i.e. abbreviation:\n{result}"
);
}
#[test]
fn test_definition_list_preservation() {
let options = ReflowOptions {
line_length: 80,
sentence_per_line: true,
semantic_line_breaks: false,
abbreviations: None,
..Default::default()
};
let content = "Term\n: Definition here";
let result = reflow_markdown(content, &options);
assert!(
result.contains(": Definition"),
"Definition list marker should be preserved"
);
}
#[test]
fn test_definition_list_multiline() {
let options = ReflowOptions {
line_length: 80,
sentence_per_line: true,
semantic_line_breaks: false,
abbreviations: None,
..Default::default()
};
let content = "Term\n: First sentence of definition. Second sentence.";
let result = reflow_markdown(content, &options);
assert!(result.contains("\n: First sentence of definition. Second sentence."));
}
#[test]
fn test_definition_list_multiple() {
let options = ReflowOptions {
line_length: 80,
sentence_per_line: true,
semantic_line_breaks: false,
abbreviations: None,
..Default::default()
};
let content = "Term 1\n: Definition 1\n: Another definition for term 1\n\nTerm 2\n: Definition 2";
let result = reflow_markdown(content, &options);
assert!(result.lines().filter(|l| l.trim_start().starts_with(": ")).count() >= 3);
}
#[test]
fn test_definition_list_with_paragraphs() {
let options = ReflowOptions {
line_length: 0, break_on_sentences: true,
preserve_breaks: false,
sentence_per_line: true,
semantic_line_breaks: false,
abbreviations: None,
length_mode: ReflowLengthMode::default(),
attr_lists: false,
require_sentence_capital: true,
max_list_continuation_indent: None,
};
let content = "Regular paragraph. With multiple sentences.\n\nTerm\n: Definition.\n\nAnother paragraph.";
let result = reflow_markdown(content, &options);
assert!(result.contains("Regular paragraph."));
assert!(result.contains("\nWith multiple sentences."));
assert!(result.contains("Term\n: Definition."));
assert!(result.contains("Another paragraph."));
}
#[test]
fn test_definition_list_edge_cases() {
let options = ReflowOptions::default();
let content1 = "Term\n : Indented definition";
let result1 = reflow_markdown(content1, &options);
assert!(result1.contains("\n : Indented definition"));
let content2 = "Term\n: Definition";
let result2 = reflow_markdown(content2, &options);
assert!(result2.contains("\n: Definition"));
let content3 = "Term\n:\tDefinition";
let result3 = reflow_markdown(content3, &options);
assert!(result3.contains("\n:\tDefinition"));
}
#[test]
fn test_abbreviation_false_positives_word_boundary() {
let options = ReflowOptions {
line_length: 80,
sentence_per_line: true,
semantic_line_breaks: false,
abbreviations: None,
..Default::default()
};
let false_positive_cases = vec![
("Why doesn't `rumdl` like the word paradigms?", 1),
("There are many programs?", 1),
("We have multiple items?", 1),
("The systems?", 1),
("Complex regex?", 1),
("These teams!", 1),
("Multiple schemes.", 1), ];
for (input, expected_sentences) in false_positive_cases {
let result = reflow_line(input, &options);
assert_eq!(
result.len(),
expected_sentences,
"Input '{input}' should be {expected_sentences} sentence(s), got {}: {:?}",
result.len(),
result
);
}
}
#[test]
fn test_abbreviation_period_vs_other_punctuation() {
let options = ReflowOptions {
line_length: 80,
sentence_per_line: true,
semantic_line_breaks: false,
abbreviations: None,
..Default::default()
};
let not_abbreviations = vec![
"Who is Dr?", "See Mr!", "What is Ms?", ];
for input in not_abbreviations {
let result = reflow_line(input, &options);
assert_eq!(
result.len(),
1,
"'{input}' should be 1 complete sentence (punctuation is not period)"
);
}
let actual_abbreviations = vec![
"See Dr. Smith today", "Use e.g. this example", "Call Mr. Jones", ];
for input in actual_abbreviations {
let sentences = split_into_sentences(input);
assert_eq!(
sentences.len(),
1,
"'{input}' should be 1 sentence (contains abbreviation with period)"
);
}
}
#[test]
fn test_abbreviation_true_positives() {
let text = "Talk to Dr. Smith. He is helpful. See also Mr. Jones.";
let sentences = split_into_sentences(text);
assert_eq!(sentences.len(), 3);
assert!(sentences[0].contains("Dr. Smith"));
assert!(sentences[2].contains("Mr. Jones"));
}
#[test]
fn test_issue_150_paradigms_with_question_mark() {
let text = "Why doesn't `rumdl` like the word paradigms? Next sentence.";
let sentences = split_into_sentences(text);
assert_eq!(sentences.len(), 2, "Should split at '?' (not an abbreviation)");
assert!(sentences[0].ends_with("paradigms?"));
assert_eq!(sentences[1], "Next sentence.");
}
#[test]
fn test_issue_150_exact_reproduction() {
let options = ReflowOptions {
line_length: 0, sentence_per_line: true,
semantic_line_breaks: false,
abbreviations: None,
..Default::default()
};
let input = "Why doesn't `rumdl` like the word paradigms?\nIf I remove the \"s\" from \"paradigms\", or if I replace \"paradigms\" with another word that ends in \"s\", this passes!";
let result = reflow_markdown(input, &options);
let lines: Vec<&str> = result.lines().collect();
assert_eq!(lines.len(), 2, "Should have 2 sentences on separate lines");
assert!(
lines[0].contains("paradigms?"),
"First line should contain 'paradigms?'"
);
assert!(lines[1].contains("passes!"), "Second line should contain 'passes!'");
}
#[test]
fn test_all_abbreviations_comprehensive() {
let all_abbreviations = ["i.e", "e.g", "Mr", "Mrs", "Dr", "Ms", "Prof", "Sr", "Jr"];
for abbr in all_abbreviations {
let with_period = format!("{abbr}.");
let sentences = split_into_sentences(&with_period);
assert_eq!(
sentences.len(),
1,
"Should detect '{with_period}' as complete (ends with abbreviation)"
);
let inline = format!("word {abbr}. next word");
let sentences = split_into_sentences(&inline);
assert_eq!(
sentences.len(),
1,
"'{inline}' should be 1 sentence (abbreviation doesn't end sentence)"
);
let with_content = format!("See {abbr}. Name here. Next sentence.");
let sentences = split_into_sentences(&with_content);
assert!(sentences.len() >= 2, "'{with_content}' should have multiple sentences");
}
}
#[test]
fn test_abbreviation_case_insensitivity() {
let case_variations = vec![
"Talk to dr. Smith. Next sentence.",
"Talk to Dr. Smith. Next sentence.",
"Talk to DR. Smith. Next sentence.",
"Talk to dR. Smith. Next sentence.",
];
for input in case_variations {
let sentences = split_into_sentences(input);
assert_eq!(sentences.len(), 2, "Case variation '{input}' should work correctly");
assert!(sentences[0].contains("Smith"), "First sentence should include 'Smith'");
}
}
#[test]
fn test_abbreviation_at_eof() {
let inputs = vec!["Talk to Dr.", "Use e.g.", "See Mr. Smith", "Prof. Jones", "It's vs."];
for input in inputs {
let sentences = split_into_sentences(input);
assert_eq!(
sentences.len(),
1,
"'{input}' should be 1 sentence (ends with abbreviation at EOF)"
);
}
}
#[test]
fn test_abbreviation_followed_by_sentence() {
let text = "See Dr. Smith went home. Another sentence here.";
let sentences = split_into_sentences(text);
assert_eq!(sentences.len(), 2, "Should detect 2 sentences");
assert!(
sentences[0].contains("Dr. Smith went home"),
"First sentence should include 'Dr. Smith went home'"
);
assert_eq!(sentences[1], "Another sentence here.");
}
#[test]
fn test_multiple_consecutive_spaces_with_abbreviations() {
let text = "Talk to Dr. Smith went home.";
let sentences = split_into_sentences(text);
assert_eq!(sentences.len(), 1, "Should be 1 sentence despite multiple spaces");
}
#[test]
fn test_all_false_positive_word_endings() {
let false_positive_words = vec![
("paradigms.", "ms"),
("programs.", "ms"),
("items.", "ms"),
("systems.", "ms"),
("teams.", "ms"),
("schemes.", "ms"),
("problems.", "ms"),
("algorithms.", "ms"),
("obviouslyvs.", "vs"), ("complex.", "ex"),
("index.", "ex"),
("regex.", "ex"),
("vertex.", "ex"),
("cortex.", "ex"),
("cookie.", "ie"),
("movie.", "ie"),
("zombie.", "ie"),
("nutmeg.", "eg"),
("peg.", "eg"),
("usr.", "sr"), ("mjr.", "jr"), ];
for (word, _pattern) in false_positive_words {
let text = format!("{word} Next sentence.");
let sentences = split_into_sentences(&text);
assert_eq!(
sentences.len(),
2,
"'{word}' should NOT be detected as abbreviation (should split into 2 sentences)"
);
}
}
#[test]
fn test_abbreviations_in_sentence_per_line_integration() {
let options = ReflowOptions {
line_length: 0, sentence_per_line: true,
semantic_line_breaks: false,
abbreviations: None,
..Default::default()
};
let content = r#"Talk to Dr. Smith about the research. The experiment uses e.g. neural networks. Meet Prof. Jones and Mr. Wilson tomorrow. This is important, i.e. very critical. Compare apples vs. oranges in the study. See also Sr. Developer position. Contact Jr. Analyst for details. Use etc. for additional items. Check ex. references in appendix. Define ie. for clarity. Consider eg. alternative approaches."#;
let result = reflow_markdown(content, &options);
let lines: Vec<&str> = result.lines().collect();
assert_eq!(lines.len(), 11, "Should have 11 sentences on separate lines");
assert!(result.contains("Dr. Smith"));
assert!(result.contains("e.g. neural"));
assert!(result.contains("Prof. Jones"));
assert!(result.contains("Mr. Wilson"));
assert!(result.contains("i.e. very"));
assert!(result.contains("vs. oranges"));
assert!(result.contains("Sr. Developer"));
assert!(result.contains("Jr. Analyst"));
assert!(result.contains("etc. for"));
assert!(result.contains("ex. references"));
}
#[test]
fn test_abbreviations_inside_parentheses() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
semantic_line_breaks: false,
abbreviations: None,
..Default::default()
};
let input = "In addition, not all platforms (e.g. Wasm) are supported by `inventory`, which is used in the implementation of the feature.";
let result = reflow_line(input, &options);
assert_eq!(
result.len(),
1,
"e.g. inside parentheses should not split sentence: {result:?}"
);
let input = "This marks code for the unlimited Python API (i.e. PyO3's `abi3` feature is not enabled).";
let result = reflow_line(input, &options);
assert_eq!(
result.len(),
1,
"i.e. inside parentheses should not split sentence: {result:?}"
);
let input = "See the documentation [e.g. Chapter 5] for more details about this feature.";
let result = reflow_line(input, &options);
assert_eq!(
result.len(),
1,
"e.g. inside brackets should not split sentence: {result:?}"
);
let input = "The doctor (Dr. Smith) performed the surgery successfully.";
let result = reflow_line(input, &options);
assert_eq!(
result.len(),
1,
"Dr. inside parentheses should not split sentence: {result:?}"
);
let text = "Not all platforms (e.g. Wasm) are supported.";
let sentences = split_into_sentences(text);
assert_eq!(
sentences.len(),
1,
"split_into_sentences should not split at (e.g.: {sentences:?}"
);
}
#[test]
fn test_issue_150_all_reported_variations() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
semantic_line_breaks: false,
abbreviations: None,
..Default::default()
};
let paradigms = "Why doesn't `rumdl` like the word paradigms?\nNext sentence.";
let result = reflow_markdown(paradigms, &options);
assert!(result.contains("paradigms?"), "Should handle 'paradigms'");
let paradigm = "Why doesn't `rumdl` like the word paradigm?\nNext sentence.";
let result = reflow_markdown(paradigm, &options);
assert!(result.contains("paradigm?"), "Should handle 'paradigm'");
let programs = "Why doesn't `rumdl` like programs?\nNext sentence.";
let result = reflow_markdown(programs, &options);
assert!(result.contains("programs?"), "Should handle 'programs'");
let items = "Why doesn't `rumdl` like items?\nNext sentence.";
let result = reflow_markdown(items, &options);
assert!(result.contains("items?"), "Should handle 'items'");
}
#[test]
fn test_performance_no_hang_on_false_positives() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
semantic_line_breaks: false,
abbreviations: None,
..Default::default()
};
let test_cases = vec![
"paradigms?",
"programs!",
"items.",
"systems?",
"teams!",
"complex.",
"regex?",
"cookie.",
"vertex!",
];
for case in test_cases {
let start = Instant::now();
let _result = reflow_line(case, &options);
let elapsed = start.elapsed();
assert!(
elapsed.as_millis() < 100,
"'{case}' took {elapsed:?} (should be <100ms)"
);
}
}
#[test]
fn test_reflow_preserves_colon_after_code() {
let options = ReflowOptions {
line_length: 20,
..Default::default()
};
let input = "This has `code`: followed by text";
let result = reflow_line(input, &options);
let joined = result.join("\n");
assert!(
joined.contains("`code`:"),
"Colon should stay attached to code span, got: {joined:?}"
);
assert!(
!joined.contains("`code` :"),
"Should not have space before colon, got: {joined:?}"
);
}
#[test]
fn test_reflow_preserves_comma_after_code() {
let options = ReflowOptions {
line_length: 30,
..Default::default()
};
let input = "List: `a`, `b`, `c`.";
let result = reflow_line(input, &options);
let joined = result.join("\n");
assert!(
joined.contains("`a`,"),
"Comma should stay attached to code span, got: {joined:?}"
);
assert!(
!joined.contains("`a` ,"),
"Should not have space before comma, got: {joined:?}"
);
}
#[test]
fn test_reflow_preserves_closing_paren_after_code() {
let options = ReflowOptions {
line_length: 40,
..Default::default()
};
let input = "And (`parens`) here";
let result = reflow_line(input, &options);
let joined = result.join("\n");
assert!(
joined.contains("`parens`)"),
"Closing paren should stay attached, got: {joined:?}"
);
assert!(
!joined.contains("`parens` )"),
"Should not have space before closing paren, got: {joined:?}"
);
}
#[test]
fn test_reflow_no_space_after_opening_paren() {
let options = ReflowOptions {
line_length: 80,
..Default::default()
};
let input = "titles (`Mr`, `Mrs`, `Ms`)";
let result = reflow_line(input, &options);
let joined = result.join("\n");
assert!(
joined.contains("(`Mr`"),
"No space after opening paren, got: {joined:?}"
);
assert!(
!joined.contains("( `Mr`"),
"Should not have space after opening paren, got: {joined:?}"
);
}
#[test]
fn test_reflow_punctuation_never_starts_line() {
let options = ReflowOptions {
line_length: 10,
..Default::default()
};
let input = "List: `a`, `b`, `c`.";
let result = reflow_line(input, &options);
for line in &result {
let trimmed = line.trim_start();
assert!(!trimmed.starts_with(','), "Line should not start with comma: {line:?}");
assert!(!trimmed.starts_with('.'), "Line should not start with period: {line:?}");
assert!(
!trimmed.starts_with(')'),
"Line should not start with closing paren: {line:?}"
);
}
}
#[test]
fn test_reflow_complex_punctuation_case() {
let options = ReflowOptions {
line_length: 200,
..Default::default()
};
let input = "- `abbreviations`: Custom abbreviations for sentence-per-line mode (optional). Periods are optional - both `\"Dr\"` and `\"Dr.\"` work the same. Custom abbreviations are added to the built-in defaults: titles (`Mr`, `Mrs`, `Ms`, `Dr`, `Prof`, `Sr`, `Jr`) and Latin (`i.e`, `e.g`).";
let result = reflow_markdown(input, &options);
assert!(
!result.contains("` :"),
"No space before colon after backtick: {result:?}"
);
assert!(
!result.contains("` ,"),
"No space before comma after backtick: {result:?}"
);
assert!(
!result.contains("` )"),
"No space before paren after backtick: {result:?}"
);
assert!(
!result.contains("( `"),
"No space after opening paren before backtick: {result:?}"
);
}
mod issue_170_nested_link_image {
use super::*;
#[test]
fn test_pattern1_inline_inline_simple() {
let options = ReflowOptions {
line_length: 80,
..Default::default()
};
let input = "[](https://example.com) some text here";
let result = reflow_markdown(input, &options);
assert!(
!result.contains("]\n(") && !result.contains("]\n["),
"Linked image should not be broken: {result:?}"
);
assert!(
result.contains("[](https://example.com)"),
"Full structure should be preserved: {result:?}"
);
}
#[test]
fn test_pattern1_inline_inline_long_url() {
let options = ReflowOptions {
line_length: 80,
..Default::default()
};
let input = "[](https://github.com/user/repo/actions) text";
let result = reflow_markdown(input, &options);
assert!(
!result.contains("]\n(") && !result.contains("]\n["),
"Long linked image should not be broken: {result:?}"
);
}
#[test]
fn test_pattern1_inline_inline_with_text() {
let options = ReflowOptions {
line_length: 80,
..Default::default()
};
let input = "prefix: [](https://crates.io/crates/mypackage) This is descriptive text that continues after";
let result = reflow_markdown(input, &options);
assert!(!result.contains("]\n("), "Badge should not be broken: {result:?}");
assert!(
result.contains(
"[](https://crates.io/crates/mypackage)"
),
"Full badge structure should be preserved: {result:?}"
);
}
#[test]
fn test_pattern1_multiple_badges() {
let options = ReflowOptions {
line_length: 80,
..Default::default()
};
let input = "[](https://l1.com) [](https://l2.com) [](https://l3.com)";
let result = reflow_markdown(input, &options);
assert!(
!result.contains("]\n("),
"Badge structures should not be broken: {result:?}"
);
}
#[test]
fn test_pattern2_ref_inline_simple() {
let options = ReflowOptions {
line_length: 80,
..Default::default()
};
let input = "[![Badge][badge-img]](https://example.com) some text here that might wrap";
let result = reflow_markdown(input, &options);
assert!(
!result.contains("]\n(") && !result.contains("]\n["),
"Linked image with ref should not be broken: {result:?}"
);
assert!(
result.contains("[![Badge][badge-img]](https://example.com)"),
"Full structure should be preserved: {result:?}"
);
}
#[test]
fn test_pattern2_ref_inline_long() {
let options = ReflowOptions {
line_length: 80,
..Default::default()
};
let input = "[![GitHub Actions Status][github-actions-badge]](https://github.com/user/repo/actions/workflows/ci.yml) text after";
let result = reflow_markdown(input, &options);
assert!(
!result.contains("]\n(") && !result.contains("]\n["),
"Long ref-inline linked image should not be broken: {result:?}"
);
}
#[test]
fn test_pattern3_inline_ref_simple() {
let options = ReflowOptions {
line_length: 80,
..Default::default()
};
let input = "[][link-ref] some text here to wrap";
let result = reflow_markdown(input, &options);
assert!(
!result.contains("]\n(") && !result.contains("]\n["),
"Linked image with ref link should not be broken: {result:?}"
);
assert!(
result.contains("[][link-ref]"),
"Full structure should be preserved: {result:?}"
);
}
#[test]
fn test_pattern3_inline_ref_long() {
let options = ReflowOptions {
line_length: 80,
..Default::default()
};
let input = "[][ci-link] text";
let result = reflow_markdown(input, &options);
assert!(
!result.contains("]\n(") && !result.contains("]\n["),
"Long inline-ref linked image should not be broken: {result:?}"
);
}
#[test]
fn test_pattern4_ref_ref_simple() {
let options = ReflowOptions {
line_length: 80,
..Default::default()
};
let input = "[![Badge][badge-img]][badge-link] some text here that might need to wrap";
let result = reflow_markdown(input, &options);
assert!(
!result.contains("]\n(") && !result.contains("]\n["),
"Double-ref linked image should not be broken: {result:?}"
);
assert!(
result.contains("[![Badge][badge-img]][badge-link]"),
"Full structure should be preserved: {result:?}"
);
}
#[test]
fn test_pattern4_ref_ref_long() {
let options = ReflowOptions {
line_length: 80,
..Default::default()
};
let input = "[![GitHub Actions Badge][github-actions-img]][github-actions-link] text after the badge";
let result = reflow_markdown(input, &options);
assert!(
!result.contains("]\n(") && !result.contains("]\n["),
"Long double-ref linked image should not be broken: {result:?}"
);
}
#[test]
fn test_url_with_parentheses() {
let options = ReflowOptions {
line_length: 80,
..Default::default()
};
let input = "[](https://en.wikipedia.org/wiki/Rust_(language)) text";
let result = reflow_markdown(input, &options);
assert!(
!result.contains("]\n("),
"URL with parentheses should not break badge: {result:?}"
);
}
#[test]
fn test_empty_alt_text() {
let options = ReflowOptions {
line_length: 80,
..Default::default()
};
let input = "[](https://example.com) text after";
let result = reflow_markdown(input, &options);
assert!(
!result.contains("]\n("),
"Empty alt text badge should not be broken: {result:?}"
);
}
#[test]
fn test_special_chars_in_alt() {
let options = ReflowOptions {
line_length: 80,
..Default::default()
};
let input = "[](https://example.com) text";
let result = reflow_markdown(input, &options);
assert!(
!result.contains("]\n("),
"Special chars in alt should not break badge: {result:?}"
);
}
#[test]
fn test_mixed_patterns_on_line() {
let options = ReflowOptions {
line_length: 120,
..Default::default()
};
let input = "[](https://l1.com) [][ref] more text here";
let result = reflow_markdown(input, &options);
assert!(
!result.contains("]\n(") && !result.contains("]\n["),
"Mixed patterns should all be preserved: {result:?}"
);
}
#[test]
fn test_hugo_shortcode_preserved() {
let options = ReflowOptions {
line_length: 80,
sentence_per_line: true,
..Default::default()
};
let input = r#"{{< figure src="image.png" alt="Description. More text." >}}"#;
let result = reflow_markdown(input, &options);
assert!(
result.contains(r#"{{< figure src="image.png" alt="Description. More text." >}}"#),
"Hugo shortcode should be preserved as atomic unit: {result:?}"
);
}
#[test]
fn test_hugo_percent_shortcode_preserved() {
let options = ReflowOptions {
line_length: 80,
sentence_per_line: true,
..Default::default()
};
let input = r#"{{% notice tip %}}This is a tip. It has periods.{{% /notice %}}"#;
let result = reflow_markdown(input, &options);
assert!(
result.contains(r#"{{% notice tip %}}"#),
"Hugo template shortcode should be preserved: {result:?}"
);
}
#[test]
fn test_hugo_shortcode_no_duplication() {
let options = ReflowOptions {
line_length: 80,
sentence_per_line: true,
..Default::default()
};
let input = r#"{{< figure src="images/test.png" alt="Grid of three rows. Each comparing." >}}"#;
let result1 = reflow_markdown(input, &options);
let result2 = reflow_markdown(&result1, &options);
assert_eq!(
result1.len(),
result2.len(),
"Hugo shortcode reflow should be idempotent. Got: first={}, second={}",
result1.len(),
result2.len()
);
let original_shortcode_count = input.matches("{{<").count();
let result_shortcode_count = result2.matches("{{<").count();
assert_eq!(
original_shortcode_count, result_shortcode_count,
"Number of shortcodes should not change: original={original_shortcode_count}, result={result_shortcode_count}"
);
}
#[test]
fn test_hugo_shortcode_multiline() {
let options = ReflowOptions {
line_length: 80,
sentence_per_line: true,
..Default::default()
};
let input =
r#"{{< figure src="test.png" alt="Line one. Line two. Line three." caption="A caption. With periods." >}}"#;
let result = reflow_markdown(input, &options);
assert!(
result.contains("{{<") && result.contains(">}}"),
"Hugo shortcode delimiters should be preserved: {result:?}"
);
assert_eq!(
result.matches("test.png").count(),
1,
"Image path should appear exactly once: {result:?}"
);
}
#[test]
fn test_hugo_shortcode_with_text_before_after() {
let options = ReflowOptions {
line_length: 80,
sentence_per_line: true,
..Default::default()
};
let input = r#"Some text before. {{< shortcode param="value. with period." >}} And text after."#;
let result = reflow_markdown(input, &options);
assert!(
result.contains(r#"{{< shortcode param="value. with period." >}}"#),
"Shortcode should be preserved: {result:?}"
);
}
}
mod issue_251_emphasis_continuation {
use super::*;
#[test]
fn test_underscore_italic_parsing() {
let options = ReflowOptions {
line_length: 80,
..Default::default()
};
let input = "This has _italic text_ in it";
let result = reflow_markdown(input, &options);
assert!(
result.contains("_italic text_"),
"Underscore italic should be preserved: {result:?}"
);
}
#[test]
fn test_underscore_bold_parsing() {
let options = ReflowOptions {
line_length: 80,
..Default::default()
};
let input = "This has __bold text__ in it";
let result = reflow_markdown(input, &options);
assert!(
result.contains("__bold text__"),
"Underscore bold should be preserved: {result:?}"
);
}
#[test]
fn test_mixed_emphasis_markers() {
let options = ReflowOptions {
line_length: 80,
..Default::default()
};
let input = "Text with *asterisk italic* and _underscore italic_ mixed";
let result = reflow_markdown(input, &options);
assert!(
result.contains("*asterisk italic*"),
"Asterisk italic preserved: {result:?}"
);
assert!(
result.contains("_underscore italic_"),
"Underscore italic preserved: {result:?}"
);
}
#[test]
fn test_asterisk_italic_sentence_continuation() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = "*Sentence one. Sentence two. Sentence three.*";
let result = reflow_line(input, &options);
assert_eq!(result.len(), 3, "Should have 3 lines: {result:?}");
assert!(
result[0].starts_with('*') && result[0].ends_with('*'),
"First line should have italic markers: {:?}",
result[0]
);
assert!(
result[1].starts_with('*') && result[1].ends_with('*'),
"Second line should have italic markers: {:?}",
result[1]
);
assert!(
result[2].starts_with('*') && result[2].ends_with('*'),
"Third line should have italic markers: {:?}",
result[2]
);
}
#[test]
fn test_underscore_italic_sentence_continuation() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = "_Sentence one. Sentence two. Sentence three._";
let result = reflow_line(input, &options);
assert_eq!(result.len(), 3, "Should have 3 lines: {result:?}");
assert!(
result[0].starts_with('_') && result[0].ends_with('_'),
"First line should have underscore markers: {:?}",
result[0]
);
assert!(
result[1].starts_with('_') && result[1].ends_with('_'),
"Second line should have underscore markers: {:?}",
result[1]
);
assert!(
result[2].starts_with('_') && result[2].ends_with('_'),
"Third line should have underscore markers: {:?}",
result[2]
);
}
#[test]
fn test_bold_sentence_continuation() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = "**Sentence one. Sentence two.**";
let result = reflow_line(input, &options);
assert_eq!(result.len(), 2, "Should have 2 lines: {result:?}");
assert!(
result[0].starts_with("**") && result[0].ends_with("**"),
"First line should have bold markers: {:?}",
result[0]
);
assert!(
result[1].starts_with("**") && result[1].ends_with("**"),
"Second line should have bold markers: {:?}",
result[1]
);
}
#[test]
fn test_underscore_bold_sentence_continuation() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = "__Sentence one. Sentence two.__";
let result = reflow_line(input, &options);
assert_eq!(result.len(), 2, "Should have 2 lines: {result:?}");
assert!(
result[0].starts_with("__") && result[0].ends_with("__"),
"First line should have underscore bold markers: {:?}",
result[0]
);
assert!(
result[1].starts_with("__") && result[1].ends_with("__"),
"Second line should have underscore bold markers: {:?}",
result[1]
);
}
#[test]
fn test_issue_251_quoted_citation() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = r#"He said, _"There is this whole spectrum of crazy futures. But the one that I feel we're almost guaranteed to get. It's the same either way"_ [^ref]."#;
let result = reflow_markdown(input, &options);
let lines: Vec<&str> = result.lines().collect();
assert!(
lines.len() >= 3,
"Should have at least 3 lines for 3 sentences: {result:?}"
);
assert!(
lines[0].contains("_\"There is this whole spectrum"),
"First line should have opening quote with emphasis: {:?}",
lines[0]
);
for line in &lines[1..lines.len() - 1] {
if !line.trim().is_empty() && !line.starts_with("He said") {
assert!(
line.trim().starts_with('_') || line.contains("_\""),
"Middle line should start with emphasis: {line:?}"
);
}
}
let last_line = lines.last().unwrap();
assert!(
last_line.contains("\"_") || last_line.ends_with('_'),
"Last line should have closing emphasis: {last_line:?}"
);
}
#[test]
fn test_issue_251_simplified() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = r#"_"First sentence. Second sentence."_"#;
let result = reflow_line(input, &options);
assert_eq!(result.len(), 2, "Should have 2 lines: {result:?}");
assert!(
result[0].starts_with("_\"") && result[0].ends_with('_'),
"First line: {:?}",
result[0]
);
assert!(
result[1].starts_with('_') && result[1].ends_with("\"_"),
"Second line: {:?}",
result[1]
);
}
#[test]
fn test_emphasis_with_trailing_text() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = "Intro: *Sentence one. Sentence two.* And then more text.";
let result = reflow_markdown(input, &options);
let lines: Vec<&str> = result.lines().collect();
assert!(
lines.iter().any(|l| l.contains("And then more text")),
"Non-emphasized text should be preserved: {result:?}"
);
}
#[test]
fn test_emphasis_single_sentence_no_change() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = "*Just one sentence here.*";
let result = reflow_line(input, &options);
assert_eq!(result.len(), 1, "Single sentence should stay one line");
assert_eq!(result[0], "*Just one sentence here.*");
}
#[test]
fn test_emphasis_with_abbreviations() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = "*Talk to Dr. Smith about the results. Then report back.*";
let result = reflow_line(input, &options);
assert_eq!(result.len(), 2, "Should have 2 lines: {result:?}");
assert!(
result[0].contains("Dr. Smith"),
"First sentence should contain Dr. Smith"
);
}
#[test]
fn test_nested_emphasis_sentence_split() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = "**First bold sentence. Second bold sentence.**";
let result = reflow_line(input, &options);
assert_eq!(result.len(), 2, "Should have 2 lines: {result:?}");
for (i, line) in result.iter().enumerate() {
assert!(
line.starts_with("**") && line.ends_with("**"),
"Line {i} should have bold markers: {line:?}"
);
}
}
#[test]
fn test_emphasis_idempotence() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = "*Sentence one. Sentence two.*";
let result1 = reflow_markdown(input, &options);
let result2 = reflow_markdown(&result1, &options);
assert_eq!(
result1, result2,
"Reflow should be idempotent.\nFirst: {result1:?}\nSecond: {result2:?}"
);
}
#[test]
fn test_multiple_emphasis_spans_on_line() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = "*First italic.* Normal text. *Second italic.*";
let result = reflow_markdown(input, &options);
let lines: Vec<&str> = result.lines().collect();
assert_eq!(lines.len(), 3, "Should have 3 lines: {result:?}");
assert!(lines[0].contains("*First italic.*"));
assert!(lines[1].contains("Normal text."));
assert!(lines[2].contains("*Second italic.*"));
}
#[test]
fn test_marker_type_preserved_asterisk() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = "*Sentence one. Sentence two.*";
let result = reflow_line(input, &options);
for line in &result {
assert!(
!line.contains('_'),
"Asterisk emphasis should not become underscore: {line:?}"
);
assert!(
line.starts_with('*') && line.ends_with('*'),
"Should use asterisk markers: {line:?}"
);
}
}
#[test]
fn test_marker_type_preserved_underscore() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = "_Sentence one. Sentence two._";
let result = reflow_line(input, &options);
for line in &result {
assert!(
line.starts_with('_') && line.ends_with('_'),
"Should use underscore markers: {line:?}"
);
}
}
#[test]
fn test_nested_italic_containing_bold_asterisk() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = "*Sentence one. **Bold sentence.** Sentence three.*";
let result = reflow_line(input, &options);
assert!(
result.len() >= 2,
"Should have at least 2 lines (bold is inside italic): {result:?}"
);
assert!(result[0].starts_with('*'), "First line should start with *: {result:?}");
let all_text = result.join("\n");
assert!(
all_text.contains("**Bold sentence.**") || all_text.contains("**"),
"Bold markers should be preserved: {result:?}"
);
}
#[test]
fn test_nested_italic_containing_bold_underscore() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = "_Sentence one. __Bold sentence.__ Sentence three._";
let result = reflow_line(input, &options);
assert!(result.len() >= 2, "Should have at least 2 lines: {result:?}");
assert!(result[0].starts_with('_'), "First line should start with _: {result:?}");
}
#[test]
fn test_mixed_nested_emphasis() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = "*Text with __bold__ inside.*";
let result = reflow_line(input, &options);
assert_eq!(result.len(), 1, "Single sentence should be one line: {result:?}");
assert!(
result[0].contains("__bold__"),
"Nested bold should be preserved: {result:?}"
);
}
#[test]
fn test_double_asterisk_not_confused_with_single() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: false,
..Default::default()
};
let input = "Text with **bold** content.";
let result = reflow_line(input, &options);
assert_eq!(result.len(), 1, "Should be single line");
assert!(result[0].contains("**bold**"), "Bold should be preserved: {result:?}");
}
#[test]
fn test_adjacent_emphasis_markers() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: false,
..Default::default()
};
let input = "Here is *italic* and **bold** text.";
let result = reflow_line(input, &options);
assert_eq!(result.len(), 1);
assert!(
result[0].contains("*italic*") && result[0].contains("**bold**"),
"Both emphasis should be preserved: {result:?}"
);
}
#[test]
fn test_sentence_boundary_after_emphasis() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = "Normal text. *Italic sentence.* Another sentence.";
let result = reflow_line(input, &options);
assert_eq!(result.len(), 3, "Should have 3 sentences: {result:?}");
assert_eq!(result[0], "Normal text.");
assert_eq!(result[1], "*Italic sentence.*");
assert_eq!(result[2], "Another sentence.");
}
#[test]
fn test_sentence_boundary_before_emphasis() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = "Plain sentence. *Italic sentence.* More text.";
let result = reflow_line(input, &options);
assert_eq!(result.len(), 3, "Should have 3 sentences: {result:?}");
assert_eq!(result[0], "Plain sentence.");
assert_eq!(result[1], "*Italic sentence.*");
assert_eq!(result[2], "More text.");
}
#[test]
fn test_sentence_boundary_bold_emphasis() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = "Before. **Bold sentence.** After.";
let result = reflow_line(input, &options);
assert_eq!(result.len(), 3, "Should have 3 sentences: {result:?}");
assert_eq!(result[0], "Before.");
assert_eq!(result[1], "**Bold sentence.**");
assert_eq!(result[2], "After.");
}
#[test]
fn test_sentence_boundary_underscore_emphasis() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = "Before. _Underscore sentence._ After.";
let result = reflow_line(input, &options);
assert_eq!(result.len(), 3, "Should have 3 sentences: {result:?}");
assert_eq!(result[0], "Before.");
assert_eq!(result[1], "_Underscore sentence._");
assert_eq!(result[2], "After.");
}
#[test]
fn test_sentence_boundary_underscore_bold() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = "Before. __Bold sentence.__ After.";
let result = reflow_line(input, &options);
assert_eq!(result.len(), 3, "Should have 3 sentences: {result:?}");
assert_eq!(result[0], "Before.");
assert_eq!(result[1], "__Bold sentence.__");
assert_eq!(result[2], "After.");
}
#[test]
fn test_sentence_boundary_exclamation() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = "Normal! *Excited!* More.";
let result = reflow_line(input, &options);
assert_eq!(result.len(), 3, "Should have 3 sentences: {result:?}");
assert_eq!(result[0], "Normal!");
assert_eq!(result[1], "*Excited!*");
assert_eq!(result[2], "More.");
}
#[test]
fn test_sentence_boundary_question() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = "Really? *Is it?* Yes.";
let result = reflow_line(input, &options);
assert_eq!(result.len(), 3, "Should have 3 sentences: {result:?}");
assert_eq!(result[0], "Really?");
assert_eq!(result[1], "*Is it?*");
assert_eq!(result[2], "Yes.");
}
#[test]
fn test_cjk_chinese_ideographic_full_stop() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = "这是第一句。这是第二句。";
let result = reflow_line(input, &options);
assert_eq!(result.len(), 2, "Should have 2 sentences: {result:?}");
assert_eq!(result[0], "这是第一句。");
assert_eq!(result[1], "这是第二句。");
}
#[test]
fn test_cjk_fullwidth_exclamation() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = "太棒了!继续努力!";
let result = reflow_line(input, &options);
assert_eq!(result.len(), 2, "Should have 2 sentences: {result:?}");
assert_eq!(result[0], "太棒了!");
assert_eq!(result[1], "继续努力!");
}
#[test]
fn test_cjk_fullwidth_question() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = "你好吗?我很好。";
let result = reflow_line(input, &options);
assert_eq!(result.len(), 2, "Should have 2 sentences: {result:?}");
assert_eq!(result[0], "你好吗?");
assert_eq!(result[1], "我很好。");
}
#[test]
fn test_cjk_japanese_mixed() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = "これは日本語です。もう一文。";
let result = reflow_line(input, &options);
assert_eq!(result.len(), 2, "Should have 2 sentences: {result:?}");
assert_eq!(result[0], "これは日本語です。");
assert_eq!(result[1], "もう一文。");
}
#[test]
fn test_mixed_cjk_and_english() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = "Hello。你好。World.";
let result = reflow_line(input, &options);
assert_eq!(result.len(), 3, "Should have 3 sentences: {result:?}");
assert_eq!(result[0], "Hello。");
assert_eq!(result[1], "你好。");
assert_eq!(result[2], "World.");
}
#[test]
fn test_cjk_with_emphasis() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = "普通文字。*强调文字。* 更多文字。";
let result = reflow_line(input, &options);
assert_eq!(result.len(), 3, "Should have 3 sentences: {result:?}");
assert_eq!(result[0], "普通文字。");
assert_eq!(result[1], "*强调文字。*");
assert_eq!(result[2], "更多文字。");
}
#[test]
fn test_url_inside_emphasis() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = "Check *https://example.com* for details. More text.";
let result = reflow_line(input, &options);
assert!(
result[0].contains("https://example.com"),
"URL should be preserved: {result:?}"
);
}
#[test]
fn test_code_span_inside_emphasis() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: false,
..Default::default()
};
let input = "Use *the `code` function* to process.";
let result = reflow_line(input, &options);
assert_eq!(result.len(), 1);
assert!(
result[0].contains("`code`"),
"Code span should be preserved: {result:?}"
);
}
#[test]
fn test_link_inside_emphasis() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: false,
..Default::default()
};
let input = "See *[the link](https://example.com)* for info.";
let result = reflow_line(input, &options);
assert_eq!(result.len(), 1);
assert!(result[0].contains("[the link]"), "Link should be preserved: {result:?}");
}
#[test]
fn test_very_long_emphasis_text() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let long_sentence = "This is a sentence. ".repeat(49) + "This is a sentence.";
let input = format!("*{long_sentence}*");
let result = reflow_line(&input, &options);
assert_eq!(result.len(), 50, "Should have 50 sentences");
for line in &result {
assert!(
line.starts_with('*') && line.ends_with('*'),
"Each line should have emphasis: {line}"
);
}
}
#[test]
fn test_consecutive_emphasis_markers() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: false,
..Default::default()
};
let input = "*italic* **bold** *more italic*";
let result = reflow_line(input, &options);
assert_eq!(result.len(), 1);
assert!(
result[0].contains("*italic*") && result[0].contains("**bold**") && result[0].contains("*more italic*"),
"All emphasis should be preserved: {result:?}"
);
}
#[test]
fn test_emphasis_at_line_boundaries() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = "*Start sentence.* Middle. *End sentence.*";
let result = reflow_line(input, &options);
assert_eq!(result.len(), 3, "Should have 3 sentences: {result:?}");
assert_eq!(result[0], "*Start sentence.*");
assert_eq!(result[1], "Middle.");
assert_eq!(result[2], "*End sentence.*");
}
#[test]
fn test_single_character_emphasis() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: false,
..Default::default()
};
let input = "Press *x* to continue.";
let result = reflow_line(input, &options);
assert_eq!(result.len(), 1);
assert!(
result[0].contains("*x*"),
"Single char emphasis should be preserved: {result:?}"
);
}
#[test]
fn test_empty_emphasis_handled() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: false,
..Default::default()
};
let input = "Text ** more text";
let result = reflow_line(input, &options);
assert_eq!(result.len(), 1);
}
#[test]
fn test_limitation_lowercase_after_period() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = "Use e.g. this method. And this.";
let result = reflow_line(input, &options);
assert!(!result.is_empty(), "Should have at least 1 line: {result:?}");
}
#[test]
fn test_limitation_triple_emphasis() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: false,
..Default::default()
};
let input = "This is ***bold italic*** text.";
let result = reflow_line(input, &options);
assert_eq!(result.len(), 1);
assert!(
result[0].contains("bold italic"),
"Content should be preserved: {result:?}"
);
}
}
#[test]
fn test_utf8_numbered_list_with_chinese_characters() {
let options = ReflowOptions {
line_length: 80,
..Default::default()
};
let input = "1. 你好世界 - Hello World in Chinese\n2. 日本語 - Japanese text\n";
let result = reflow_markdown(input, &options);
assert!(result.contains("你好世界"), "Chinese characters should be preserved");
assert!(result.contains("日本語"), "Japanese characters should be preserved");
assert!(result.contains("1."), "List numbering should be preserved");
assert!(result.contains("2."), "List numbering should be preserved");
}
#[test]
fn test_utf8_bullet_list_with_emoji() {
let options = ReflowOptions {
line_length: 80,
..Default::default()
};
let input = "- 🎉 Party time!\n- 🚀 Rocket launch\n- 🌟 Starry night\n";
let result = reflow_markdown(input, &options);
assert!(result.contains("🎉"), "Emoji should be preserved");
assert!(result.contains("🚀"), "Emoji should be preserved");
assert!(result.contains("🌟"), "Emoji should be preserved");
}
#[test]
fn test_utf8_indented_list_with_cyrillic() {
let options = ReflowOptions {
line_length: 80,
..Default::default()
};
let input = " - Привет мир (Hello World in Russian)\n - Добрый день (Good day)\n";
let result = reflow_markdown(input, &options);
assert!(result.contains("Привет"), "Cyrillic should be preserved");
assert!(result.contains("Добрый"), "Cyrillic should be preserved");
}
#[test]
fn test_utf8_blockquote_with_arabic() {
let options = ReflowOptions {
line_length: 80,
..Default::default()
};
let input = "> مرحبا بالعالم - Hello World in Arabic\n";
let result = reflow_markdown(input, &options);
assert!(result.contains("مرحبا"), "Arabic text should be preserved");
assert!(result.starts_with('>'), "Blockquote marker should be preserved");
}
#[test]
fn test_utf8_blockquote_with_leading_spaces_and_unicode() {
let options = ReflowOptions {
line_length: 80,
..Default::default()
};
let input = " > 日本語テキスト with some English\n";
let result = reflow_markdown(input, &options);
assert!(result.contains("日本語"), "Japanese should be preserved");
assert!(result.contains('>'), "Blockquote marker should be preserved");
}
#[test]
fn test_utf8_mixed_scripts_in_numbered_list() {
let options = ReflowOptions {
line_length: 80,
..Default::default()
};
let input = "1. Hello 你好 🌍 World\n2. مرحبا Привет 🎉 Mixed\n3. Normal ASCII text\n";
let result = reflow_markdown(input, &options);
assert!(result.contains("Hello"), "Latin preserved");
assert!(result.contains("你好"), "Chinese preserved");
assert!(result.contains("🌍"), "Emoji preserved");
assert!(result.contains("مرحبا"), "Arabic preserved");
assert!(result.contains("Привет"), "Cyrillic preserved");
}
#[test]
fn test_utf8_list_marker_after_multibyte_indent() {
let options = ReflowOptions {
line_length: 80,
..Default::default()
};
let input = " 1. 日本語 text after marker\n";
let result = reflow_markdown(input, &options);
assert!(result.contains("日本語"), "Content after marker preserved");
assert!(result.contains("1."), "List marker preserved");
}
#[test]
fn test_utf8_multiple_spaces_after_marker_with_unicode() {
let options = ReflowOptions {
line_length: 80,
..Default::default()
};
let input = "- 🎉 Extra spaces before emoji\n1. 日本語 Extra spaces before Japanese\n";
let result = reflow_markdown(input, &options);
assert!(result.contains("🎉"), "Emoji preserved after extra spaces");
assert!(result.contains("日本語"), "Japanese preserved after extra spaces");
}
#[test]
fn test_utf8_very_long_unicode_line_reflow() {
let options = ReflowOptions {
line_length: 40,
..Default::default()
};
let input = "这是一个很长的中文句子,包含了很多汉字,需要被正确地换行处理。";
let result = reflow_line(input, &options);
assert!(!result.is_empty(), "Should produce output");
let joined = result.join("");
assert!(joined.contains("中文"), "Chinese text preserved after reflow");
}
#[test]
fn test_utf8_combining_characters() {
let options = ReflowOptions {
line_length: 80,
..Default::default()
};
let input = "- Café résumé naïve\n";
let result = reflow_markdown(input, &options);
assert!(
result.contains("Café") || result.contains("Cafe"),
"Accented text preserved"
);
}
#[test]
fn test_utf8_zero_width_characters() {
let options = ReflowOptions {
line_length: 80,
..Default::default()
};
let input = "1. Text\u{200B}with\u{200D}invisible\n";
let result = reflow_markdown(input, &options);
assert!(result.contains("Text"), "Base text preserved");
assert!(result.contains("invisible"), "Text after zero-width preserved");
}
#[test]
fn test_sentence_split_when_next_sentence_starts_with_quote() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = r#"Builders create significant business value. "AI native" workers set the AI vision."#;
let result = reflow_markdown(input, &options);
let lines: Vec<&str> = result.lines().collect();
assert_eq!(lines.len(), 2, "Should split into 2 sentences: {result:?}");
assert!(lines[0].ends_with("value."), "First sentence ends with 'value.'");
assert!(
lines[1].starts_with("\"AI"),
"Second sentence starts with opening quote"
);
}
#[test]
fn test_sentence_split_when_period_inside_closing_quote() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = r#"Users electable "to make Gemini helpful." Personal context is provided."#;
let result = reflow_markdown(input, &options);
let lines: Vec<&str> = result.lines().collect();
assert_eq!(lines.len(), 2, "Should split into 2 sentences: {result:?}");
assert!(
lines[0].ends_with("helpful.\""),
"First sentence ends with closing quote after period: {:?}",
lines[0]
);
assert!(
lines[1].starts_with("Personal"),
"Second sentence starts with 'Personal'"
);
}
#[test]
fn test_curly_quotes_sentence_boundary() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = "First sentence.\u{201C}Second sentence.\u{201D} Third sentence.";
let result = reflow_markdown(input, &options);
let lines: Vec<&str> = result.lines().collect();
assert!(lines.len() >= 2, "Should split at sentence boundaries: {result:?}");
}
#[test]
fn test_exclamation_with_quotes() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = r#"She said "Amazing!" He replied "Incredible!""#;
let result = reflow_markdown(input, &options);
let lines: Vec<&str> = result.lines().collect();
assert_eq!(lines.len(), 2, "Should split at exclamation: {result:?}");
assert!(
lines[0].ends_with("Amazing!\""),
"First sentence should end with exclamation and quote"
);
}
#[test]
fn test_question_with_quotes() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = r#"He asked "Really?" She answered yes."#;
let result = reflow_markdown(input, &options);
let lines: Vec<&str> = result.lines().collect();
assert_eq!(lines.len(), 2, "Should split at question mark: {result:?}");
assert!(
lines[0].ends_with("Really?\""),
"First sentence should end with question and quote"
);
}
#[test]
fn test_single_quote_sentence_boundary() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = "The character said 'Done.' Next line follows.";
let result = reflow_markdown(input, &options);
let lines: Vec<&str> = result.lines().collect();
assert_eq!(lines.len(), 2, "Should split at period with single quote: {result:?}");
assert!(lines[0].ends_with("Done.'"), "First sentence ends with single quote");
}
#[test]
fn test_mixed_quotes_and_emphasis() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = r#"He wrote *"Important text."* Then continued."#;
let result = reflow_markdown(input, &options);
let lines: Vec<&str> = result.lines().collect();
assert_eq!(
lines.len(),
2,
"Should split with mixed emphasis and quotes: {result:?}"
);
}
#[test]
fn test_email_autolink_not_treated_as_html_tag() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = "First sentence here. Reach me at <test@example.com>.";
let result = reflow_markdown(input, &options);
let lines: Vec<&str> = result.lines().collect();
assert_eq!(lines.len(), 2, "Should split into 2 sentences: {result:?}");
assert_eq!(lines[0], "First sentence here.");
assert_eq!(lines[1], "Reach me at <test@example.com>.");
}
#[test]
fn test_email_autolink_at_end_of_sentence() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = "Contact us at <support@company.com>. We respond within 24 hours.";
let result = reflow_markdown(input, &options);
let lines: Vec<&str> = result.lines().collect();
assert_eq!(lines.len(), 2, "Should split into 2 sentences: {result:?}");
assert_eq!(lines[0], "Contact us at <support@company.com>.");
assert_eq!(lines[1], "We respond within 24 hours.");
}
#[test]
fn test_email_autolink_mid_sentence() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = "Email <admin@test.org> for more info. Thank you.";
let result = reflow_markdown(input, &options);
let lines: Vec<&str> = result.lines().collect();
assert_eq!(lines.len(), 2, "Should split into 2 sentences: {result:?}");
assert_eq!(lines[0], "Email <admin@test.org> for more info.");
assert_eq!(lines[1], "Thank you.");
}
#[test]
fn test_email_autolink_complex_domain() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = "Reach me at <user.name+tag@sub.domain.example.com>. Thanks!";
let result = reflow_markdown(input, &options);
let lines: Vec<&str> = result.lines().collect();
assert_eq!(lines.len(), 2, "Should split into 2 sentences: {result:?}");
assert!(lines[0].contains("<user.name+tag@sub.domain.example.com>"));
}
#[test]
fn test_url_autolinks_still_work() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = "Visit <https://example.com> for details. See you there.";
let result = reflow_markdown(input, &options);
let lines: Vec<&str> = result.lines().collect();
assert_eq!(lines.len(), 2, "Should split into 2 sentences: {result:?}");
assert_eq!(lines[0], "Visit <https://example.com> for details.");
assert_eq!(lines[1], "See you there.");
}
#[test]
fn test_html_tag_vs_email_autolink_distinction() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = "Use the <code>command</code> here. It's simple.";
let result = reflow_markdown(input, &options);
let lines: Vec<&str> = result.lines().collect();
assert_eq!(lines.len(), 2, "Should split into 2 sentences: {result:?}");
assert!(lines[0].contains("<code>"));
assert!(lines[0].contains("</code>"));
}
#[test]
fn test_email_autolink_no_content_duplication() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = "Prefix text <test@example.com> suffix text.";
let result = reflow_markdown(input, &options);
assert_eq!(
result.matches("Prefix text").count(),
1,
"Prefix should appear exactly once: {result:?}"
);
assert_eq!(
result.matches("<test@example.com>").count(),
1,
"Email should appear exactly once: {result:?}"
);
}
#[test]
fn test_multiple_emails_in_sentence() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = "Contact <sales@example.com> or <support@example.com> for help. Thanks!";
let result = reflow_markdown(input, &options);
let lines: Vec<&str> = result.lines().collect();
assert_eq!(lines.len(), 2, "Should split into 2 sentences: {result:?}");
assert!(lines[0].contains("<sales@example.com>"));
assert!(lines[0].contains("<support@example.com>"));
}
#[test]
fn test_email_and_html_tags_mixed() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = "Use <code>git</code> or email <dev@example.com> for help. Done.";
let result = reflow_markdown(input, &options);
let lines: Vec<&str> = result.lines().collect();
assert_eq!(lines.len(), 2, "Should split into 2 sentences: {result:?}");
assert!(lines[0].contains("<dev@example.com>"));
assert!(lines[0].contains("<code>"));
assert!(lines[0].contains("</code>"));
}
#[test]
fn test_email_and_url_autolinks_mixed() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = "Visit <https://example.com> or email <info@example.com> for details. Bye.";
let result = reflow_markdown(input, &options);
let lines: Vec<&str> = result.lines().collect();
assert_eq!(lines.len(), 2, "Should split into 2 sentences: {result:?}");
assert!(lines[0].contains("<https://example.com>"));
assert!(lines[0].contains("<info@example.com>"));
}
#[test]
fn test_email_with_long_tld() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = "Contact <curator@art.museum> for exhibitions. Welcome!";
let result = reflow_markdown(input, &options);
let lines: Vec<&str> = result.lines().collect();
assert_eq!(lines.len(), 2, "Should split into 2 sentences: {result:?}");
assert!(lines[0].contains("<curator@art.museum>"));
}
#[test]
fn test_email_with_numbers_in_local_part() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = "Email <user123@test99.example.com> for access. Thanks!";
let result = reflow_markdown(input, &options);
let lines: Vec<&str> = result.lines().collect();
assert_eq!(lines.len(), 2, "Should split into 2 sentences: {result:?}");
assert!(lines[0].contains("<user123@test99.example.com>"));
}
#[test]
fn test_email_with_percent_encoding_chars() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = "Email <user%40special@example.com> if needed. Done!";
let result = reflow_markdown(input, &options);
let lines: Vec<&str> = result.lines().collect();
assert_eq!(lines.len(), 2, "Should split into 2 sentences: {result:?}");
assert!(lines[0].contains("<user%40special@example.com>"));
}
#[test]
fn test_invalid_email_single_char_tld_treated_as_html() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = "Check <a@b.c> for testing. Done!";
let result = reflow_markdown(input, &options);
let lines: Vec<&str> = result.lines().collect();
assert_eq!(lines.len(), 2, "Should split into 2 sentences: {result:?}");
assert!(lines[0].contains("<a@b.c>"));
}
#[test]
fn test_invalid_email_no_tld_treated_as_html() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = "Use <user@localhost> locally. Done!";
let result = reflow_markdown(input, &options);
let lines: Vec<&str> = result.lines().collect();
assert_eq!(lines.len(), 2, "Should split into 2 sentences: {result:?}");
assert!(lines[0].contains("<user@localhost>"));
}
#[test]
fn test_email_at_very_start_of_text() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = "<start@example.com> is the contact. Use it.";
let result = reflow_markdown(input, &options);
let lines: Vec<&str> = result.lines().collect();
assert_eq!(lines.len(), 2, "Should split into 2 sentences: {result:?}");
assert!(lines[0].starts_with("<start@example.com>"));
}
#[test]
fn test_email_as_only_content() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = "<only@example.com>";
let result = reflow_markdown(input, &options);
assert_eq!(result, "<only@example.com>");
}
#[test]
fn test_consecutive_emails() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = "<first@example.com><second@example.com> are contacts. Done.";
let result = reflow_markdown(input, &options);
let lines: Vec<&str> = result.lines().collect();
assert_eq!(lines.len(), 2, "Should split into 2 sentences: {result:?}");
assert!(lines[0].contains("<first@example.com>"));
assert!(lines[0].contains("<second@example.com>"));
}
#[test]
fn test_email_idempotency() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = "Contact <test@example.com> for help. Thank you for reading.";
let first_pass = reflow_markdown(input, &options);
let second_pass = reflow_markdown(&first_pass, &options);
assert_eq!(first_pass, second_pass, "Reflow should be idempotent");
}
#[test]
fn test_email_with_hyphen_in_domain() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = "Email <contact@my-company.example.com> for info. Thanks!";
let result = reflow_markdown(input, &options);
let lines: Vec<&str> = result.lines().collect();
assert_eq!(lines.len(), 2, "Should split into 2 sentences: {result:?}");
assert!(lines[0].contains("<contact@my-company.example.com>"));
}
#[test]
fn test_html_entity_extraction_no_duplication() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = "Use for spacing. Done!";
let result = reflow_markdown(input, &options);
assert_eq!(
result.matches("Use").count(),
1,
"Prefix should appear exactly once: {result:?}"
);
assert_eq!(
result.matches(" ").count(),
1,
"Entity should appear exactly once: {result:?}"
);
}
#[test]
fn test_hugo_shortcode_extraction_no_duplication() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = "Include {{< figure src=\"test.png\" >}} here. Done!";
let result = reflow_markdown(input, &options);
assert_eq!(
result.matches("Include").count(),
1,
"Prefix should appear exactly once: {result:?}"
);
assert_eq!(
result.matches("{{< figure").count(),
1,
"Shortcode should appear exactly once: {result:?}"
);
}
#[test]
fn test_emphasis_multiple_sentences_idempotent() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = "**First sentence. Second sentence.**";
let result = reflow_line(input, &options);
assert_eq!(result.len(), 2, "Should produce 2 lines: {result:?}");
assert_eq!(result[0], "**First sentence.**");
assert_eq!(result[1], "**Second sentence.**");
let joined = result.join("\n");
let second_result = reflow_markdown(&joined, &options);
let second_joined = second_result.trim_end();
assert_eq!(
joined, second_joined,
"Reflow should be idempotent. First: {joined:?}, Second: {second_joined:?}"
);
for line in second_result.lines() {
assert!(!line.starts_with(' '), "Line should not start with space: {line:?}");
}
}
#[test]
fn test_emphasis_idempotent_all_types() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let test_cases = vec![
("**Bold one. Bold two.**", "**Bold one.**", "**Bold two.**"),
("*Italic one. Italic two.*", "*Italic one.*", "*Italic two.*"),
("~~Strike one. Strike two.~~", "~~Strike one.~~", "~~Strike two.~~"),
("__Bold underscore. Second.__", "__Bold underscore.__", "__Second.__"),
("_Italic underscore. Second._", "_Italic underscore._", "_Second._"),
];
for (input, expected_first, expected_second) in test_cases {
let result = reflow_line(input, &options);
assert_eq!(result.len(), 2, "Input {input:?} should produce 2 lines: {result:?}");
assert_eq!(result[0], expected_first, "First line mismatch for {input:?}");
assert_eq!(result[1], expected_second, "Second line mismatch for {input:?}");
let joined = result.join("\n");
let second_result = reflow_markdown(&joined, &options);
let second_joined = second_result.trim_end();
assert_eq!(
joined, second_joined,
"Reflow should be idempotent for {input:?}. First: {joined:?}, Second: {second_joined:?}"
);
for (i, line) in second_result.lines().enumerate() {
assert!(
!line.starts_with(' '),
"Line {i} should not start with space for {input:?}: {line:?}"
);
}
}
}
#[test]
fn test_emphasis_idempotent_convergence_stress() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = "**First. Second. Third.**";
let mut current = input.to_string();
for iteration in 0..10 {
let result = reflow_markdown(¤t, &options);
let next = result.trim_end().to_string();
if next == current {
assert!(iteration <= 2, "Should converge within 2 iterations, took {iteration}");
return;
}
current = next;
}
panic!("Reflow did not converge after 10 iterations. Final state: {current:?}");
}
#[test]
fn test_whitespace_only_text_not_accumulated() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let already_formatted = "**First.**\n**Second.**";
let result = reflow_markdown(already_formatted, &options);
for line in result.lines() {
assert!(!line.starts_with(' '), "Line should not start with space: {line:?}");
assert!(
!line.starts_with(" **"),
"Emphasis should not have leading space: {line:?}"
);
}
}
#[test]
fn test_semantic_basic_sentence_splitting() {
let options = ReflowOptions {
line_length: 80,
semantic_line_breaks: true,
..Default::default()
};
let input = "First sentence here. Second sentence there.";
let result = reflow_line(input, &options);
assert_eq!(result, vec!["First sentence here.", "Second sentence there."]);
}
#[test]
fn test_semantic_short_lines_no_cascade() {
let options = ReflowOptions {
line_length: 80,
semantic_line_breaks: true,
..Default::default()
};
let input = "Short one. Short two. Short three.";
let result = reflow_line(input, &options);
assert_eq!(result, vec!["Short one.", "Short two.", "Short three."]);
}
#[test]
fn test_semantic_clause_punctuation_cascade() {
let options = ReflowOptions {
line_length: 50,
semantic_line_breaks: true,
..Default::default()
};
let input = "The quick brown fox dog, and the lazy cow jumped over the shining moon tonight.";
let result = reflow_line(input, &options);
assert!(result.len() >= 2, "Should split long sentence: {result:?}");
assert!(
result[0].ends_with(','),
"First part should end at clause punctuation: {:?}",
result[0]
);
}
#[test]
fn test_semantic_break_word_cascade() {
let options = ReflowOptions {
line_length: 40,
semantic_line_breaks: true,
..Default::default()
};
let input = "The implementation handles errors and provides meaningful feedback to users.";
let result = reflow_line(input, &options);
assert!(result.len() >= 2, "Should split at break-word: {result:?}");
let joined = result.join(" ");
assert!(
joined.contains("errors") && joined.contains("provides"),
"Content should be preserved: {result:?}"
);
}
#[test]
fn test_semantic_full_cascade_all_levels() {
let options = ReflowOptions {
line_length: 50,
semantic_line_breaks: true,
..Default::default()
};
let input = "First sentence is short. The second sentence is quite long with a comma, \
and it also has break-words which make it even longer than the limit allows.";
let result = reflow_line(input, &options);
assert_eq!(result[0], "First sentence is short.");
assert!(
result.len() >= 3,
"Long second sentence should be split further: {result:?}"
);
}
#[test]
fn test_semantic_markdown_link_preservation() {
let options = ReflowOptions {
line_length: 50,
semantic_line_breaks: true,
..Default::default()
};
let input = "See the [documentation link](https://example.com/very/long/path) for details.";
let result = reflow_line(input, &options);
let joined = result.join("\n");
assert!(
joined.contains("[documentation link](https://example.com/very/long/path)"),
"Link should not be broken: {result:?}"
);
}
#[test]
fn test_semantic_code_span_preservation() {
let options = ReflowOptions {
line_length: 50,
semantic_line_breaks: true,
..Default::default()
};
let input = "Use the `very_long_function_name_here()` method, and then call `another_function()` after.";
let result = reflow_line(input, &options);
let joined = result.join("\n");
assert!(
joined.contains("`very_long_function_name_here()`"),
"Code span should not be broken: {result:?}"
);
assert!(
joined.contains("`another_function()`"),
"Second code span should not be broken: {result:?}"
);
}
#[test]
fn test_semantic_em_dash_splitting() {
let options = ReflowOptions {
line_length: 40,
semantic_line_breaks: true,
..Default::default()
};
let input = "The feature\u{2014}which was requested by many users\u{2014}is now available.";
let result = reflow_line(input, &options);
assert!(result.len() >= 2, "Should split at em dash: {result:?}");
}
#[test]
fn test_semantic_line_length_zero_sentence_only() {
let options = ReflowOptions {
line_length: 0,
semantic_line_breaks: true,
..Default::default()
};
let input = "First sentence with a very long clause, and another clause, and even more text that goes on and on. Second sentence.";
let result = reflow_line(input, &options);
assert_eq!(result.len(), 2, "Should only have sentence splits: {result:?}");
assert!(result[0].ends_with('.'), "First line should end at sentence boundary");
assert_eq!(result[1], "Second sentence.");
}
#[test]
fn test_semantic_abbreviations_respected() {
let options = ReflowOptions {
line_length: 80,
semantic_line_breaks: true,
..Default::default()
};
let input = "Dr. Smith went to the store. He bought milk.";
let result = reflow_line(input, &options);
assert_eq!(result, vec!["Dr. Smith went to the store.", "He bought milk."]);
}
#[test]
fn test_semantic_idempotency() {
let options = ReflowOptions {
line_length: 60,
semantic_line_breaks: true,
..Default::default()
};
let input = "All human beings are born free and equal in dignity and rights. They are endowed with reason and conscience and should act towards one another in a spirit of brotherhood.";
let first_pass = reflow_line(input, &options);
let first_result = first_pass.join(" ");
let second_pass = reflow_line(&first_result, &options);
assert_eq!(
first_pass, second_pass,
"Second reflow pass should produce same result.\nFirst: {first_pass:?}\nSecond: {second_pass:?}"
);
}
#[test]
fn test_semantic_single_sentence_no_split() {
let options = ReflowOptions {
line_length: 80,
semantic_line_breaks: true,
..Default::default()
};
let input = "Just a single short sentence.";
let result = reflow_line(input, &options);
assert_eq!(result, vec!["Just a single short sentence."]);
}
#[test]
fn test_semantic_semicolon_split() {
let options = ReflowOptions {
line_length: 40,
semantic_line_breaks: true,
..Default::default()
};
let input = "The first clause is here; the second clause follows after the semicolon.";
let result = reflow_line(input, &options);
assert!(result.len() >= 2, "Should split at semicolon: {result:?}");
assert!(
result[0].ends_with(';'),
"First part should end at semicolon: {:?}",
result[0]
);
}
#[test]
fn test_semantic_word_wrap_fallback() {
let options = ReflowOptions {
line_length: 30,
semantic_line_breaks: true,
..Default::default()
};
let input = "Supercalifragilisticexpialidocious documentation reference manual.";
let result = reflow_line(input, &options);
assert!(result.len() >= 2, "Should use word wrap fallback: {result:?}");
}
#[test]
fn test_semantic_multiple_sentences_with_cascade() {
let options = ReflowOptions {
line_length: 50,
semantic_line_breaks: true,
..Default::default()
};
let input = "Short sentence. A longer sentence that contains a comma, \
and additional clauses that push it beyond the limit. Another short one.";
let result = reflow_line(input, &options);
assert_eq!(result[0], "Short sentence.");
assert_eq!(result.last().unwrap().trim(), "Another short one.");
assert!(result.len() >= 4, "Middle sentence should be cascade-split: {result:?}");
}
#[test]
fn test_semantic_break_word_which() {
let options = ReflowOptions {
line_length: 50,
semantic_line_breaks: true,
..Default::default()
};
let input = "The new feature which was requested by many users improves the overall experience.";
let result = reflow_line(input, &options);
assert!(result.len() >= 2, "Should split at break-word: {result:?}");
}
#[test]
fn test_semantic_break_word_because() {
let options = ReflowOptions {
line_length: 50,
semantic_line_breaks: true,
..Default::default()
};
let input = "This approach is preferred because it provides better performance and maintainability.";
let result = reflow_line(input, &options);
assert!(result.len() >= 2, "Should split at 'because': {result:?}");
}
#[test]
fn test_semantic_break_word_not_inside_words() {
let options = ReflowOptions {
line_length: 80,
semantic_line_breaks: true,
..Default::default()
};
let input = "The author organized the information for the editor.";
let result = reflow_line(input, &options);
assert_eq!(result.len(), 1, "Short line should not be split: {result:?}");
assert_eq!(result[0], input);
}
#[test]
fn test_semantic_break_word_boundary_check() {
let options = ReflowOptions {
line_length: 60,
semantic_line_breaks: true,
..Default::default()
};
let input = "The normalized format works because the authors organized all information for distribution purposes.";
let result = reflow_line(input, &options);
assert!(result.len() >= 2, "Should split: {result:?}");
for line in &result {
assert!(
!line.ends_with("auth") && !line.ends_with('f') && !line.ends_with("inf"),
"Should not break inside words: {result:?}"
);
}
}
#[test]
fn test_semantic_em_dash_no_spaces() {
let options = ReflowOptions {
line_length: 40,
semantic_line_breaks: true,
..Default::default()
};
let input = "The implementation\u{2014}which was carefully designed\u{2014}handles all the edge cases properly.";
let result = reflow_line(input, &options);
assert!(result.len() >= 2, "Should split at em dash: {result:?}");
}
#[test]
fn test_semantic_break_word_inside_link() {
let options = ReflowOptions {
line_length: 60,
semantic_line_breaks: true,
..Default::default()
};
let input = "See the [documentation for beginners](https://example.com) and the [guide for experts](https://example.com/experts) today.";
let result = reflow_line(input, &options);
for line in &result {
if line.contains("[documentation") {
assert!(line.contains("example.com)"), "Link should not be split: {result:?}");
}
if line.contains("[guide") {
assert!(line.contains("experts)"), "Link should not be split: {result:?}");
}
}
}
#[test]
fn test_semantic_multiple_break_words_prefers_latest() {
let options = ReflowOptions {
line_length: 70,
semantic_line_breaks: true,
..Default::default()
};
let input = "The system handles errors and warnings and notifications and alerts when processing large batches.";
let result = reflow_line(input, &options);
assert!(result.len() >= 2, "Should split: {result:?}");
assert!(
result[0].chars().count() > 30,
"Should prefer latest break-word for longer first line: {result:?}"
);
}
#[test]
fn test_semantic_break_word_at_start_of_text() {
let options = ReflowOptions {
line_length: 40,
semantic_line_breaks: true,
..Default::default()
};
let input = "and then the rest of the very long sentence continues beyond the line length limit here.";
let result = reflow_line(input, &options);
assert!(!result[0].is_empty(), "First line should not be empty: {result:?}");
}
#[test]
fn test_semantic_short_clause_punct_skipped() {
let options = ReflowOptions {
line_length: 80,
semantic_line_breaks: true,
..Default::default()
};
let input = "A, this is a very long sentence that goes on and on with many words but no more commas in the rest.";
let result = reflow_line(input, &options);
assert!(result.len() >= 2, "Should split: {result:?}");
assert!(
result[0].chars().count() >= 16,
"First line should not be unreasonably short (min 20% of line_length): got '{}' ({} chars)",
result[0],
result[0].chars().count()
);
}
#[test]
fn test_semantic_short_colon_skipped() {
let options = ReflowOptions {
line_length: 80,
semantic_line_breaks: true,
..Default::default()
};
let input = "Note: the configuration system provides multiple options for customizing behavior and the settings persist across sessions.";
let result = reflow_line(input, &options);
assert!(result.len() >= 2, "Should split: {result:?}");
assert!(
result[0].chars().count() >= 16,
"First line should not be unreasonably short: got '{}' ({} chars)",
result[0],
result[0].chars().count()
);
}
#[test]
fn test_semantic_valid_clause_punct_still_works() {
let options = ReflowOptions {
line_length: 80,
semantic_line_breaks: true,
..Default::default()
};
let input = "The author organized the information for the editor, and the format worked well because it was properly normalized for distribution.";
let result = reflow_line(input, &options);
assert!(result.len() >= 2, "Should split: {result:?}");
assert!(result[0].ends_with(','), "Should split at comma: {result:?}");
}
#[test]
fn test_semantic_nested_elements_bold_in_link() {
let options = ReflowOptions {
line_length: 50,
semantic_line_breaks: true,
..Default::default()
};
let input = "Check the **[important guide](https://example.com/guide)** for more details and information.";
let result = reflow_line(input, &options);
let joined = result.join("\n");
assert!(
joined.contains("**[important guide](https://example.com/guide)**"),
"Nested bold+link should not be split: {result:?}"
);
}
#[test]
fn test_semantic_shortcode_adjacent_to_text() {
let options = ReflowOptions {
line_length: 80,
semantic_line_breaks: true,
..Default::default()
};
let input = "If you are running a version of Kubernetes other than v{{< skew currentVersion >}}, check the documentation for that version.";
let result = reflow_line(input, &options);
let joined = result.join("\n");
assert!(
joined.contains("v{{< skew currentVersion >}}"),
"v must not be separated from adjacent shortcode: {result:?}"
);
assert!(
joined.contains("v{{< skew currentVersion >}},"),
"comma must stay attached to shortcode: {result:?}"
);
for line in &result {
assert!(
line.trim() != "v",
"\"v\" should not appear alone on a line: {result:?}"
);
}
}
#[test]
fn test_semantic_shortcode_with_surrounding_text() {
let options = ReflowOptions {
line_length: 60,
semantic_line_breaks: true,
..Default::default()
};
let input =
"Kubernetes {{< skew currentVersion >}} requires that you use a runtime that conforms with the specification.";
let result = reflow_line(input, &options);
let joined = result.join("\n");
assert!(
joined.contains("{{< skew currentVersion >}}"),
"Shortcode should not be split: {result:?}"
);
}
#[test]
fn test_word_wrap_adjacent_element_no_break() {
let options = ReflowOptions {
line_length: 60,
break_on_sentences: false,
..Default::default()
};
let input = "If you are running a version other than v{{< skew currentVersion >}}, check docs.";
let result = reflow_line(input, &options);
let joined = result.join("\n");
assert!(
joined.contains("v{{< skew currentVersion >}}"),
"v must not be separated from adjacent shortcode in word-wrap mode: {result:?}"
);
}
#[test]
fn test_word_wrap_code_adjacent_to_text() {
let options = ReflowOptions {
line_length: 40,
break_on_sentences: false,
..Default::default()
};
let input = "The configuration uses myconfig`value` for all operations in the system.";
let result = reflow_line(input, &options);
let joined = result.join("\n");
assert!(
joined.contains("myconfig`value`"),
"Text must stay attached to adjacent code span: {result:?}"
);
}
#[test]
fn test_autolink_not_broken_at_colon_issue_416() {
let options = ReflowOptions {
line_length: 40,
semantic_line_breaks: true,
..Default::default()
};
let input = "Visit <https://example.com/long/path/to/resource> for more info.";
let result = reflow_markdown(input, &options);
assert!(
result.contains("<https://example.com/long/path/to/resource>"),
"Autolink must remain intact, not split at colon. Got: {result:?}"
);
}
#[test]
fn test_autolink_email_not_broken_issue_417() {
let options = ReflowOptions {
line_length: 30,
semantic_line_breaks: true,
..Default::default()
};
let input = "Contact <user@example.com> for help with the project.";
let result = reflow_markdown(input, &options);
assert!(
result.contains("<user@example.com>"),
"Email autolink must remain intact. Got: {result:?}"
);
}
#[test]
fn test_autolink_preserved_in_default_reflow() {
let options = ReflowOptions {
line_length: 40,
..Default::default()
};
let input = "See <https://example.com/path> for details about this topic.";
let result = reflow_markdown(input, &options);
assert!(
result.contains("<https://example.com/path>"),
"Autolink must remain intact in default reflow mode. Got: {result:?}"
);
}
#[test]
fn test_link_text_spaces_not_used_for_split_issue_412() {
let options = ReflowOptions {
line_length: 40,
..Default::default()
};
let input = "Text with [a link that has many words](https://example.com) and more.";
let result = reflow_markdown(input, &options);
assert!(
result.contains("[a link that has many words](https://example.com)"),
"Link must not be broken at spaces in link text. Got: {result:?}"
);
}
#[test]
fn test_long_link_text_not_split_at_space_issue_412() {
let options = ReflowOptions {
line_length: 60,
..Default::default()
};
let input = "See [very long link text with many words inside it that should not be split](https://example.com/path) for details.";
let result = reflow_markdown(input, &options);
assert!(
result.contains(
"[very long link text with many words inside it that should not be split](https://example.com/path)"
),
"Link must remain intact regardless of length. Got: {result:?}"
);
}
#[test]
fn test_inline_html_tag_not_split_issue_413() {
let options = ReflowOptions {
line_length: 50,
..Default::default()
};
let input = "Click <a href=\"https://example.com\" target=\"_blank\">here</a> for info.";
let result = reflow_markdown(input, &options);
assert!(
result.contains("<a href=\"https://example.com\" target=\"_blank\">"),
"HTML tag must remain intact. Got: {result:?}"
);
assert!(
result.contains("</a>"),
"Closing HTML tag must be present. Got: {result:?}"
);
}
#[test]
fn test_visual_width_cjk_reflow_issue_414() {
let options = ReflowOptions {
line_length: 20,
length_mode: ReflowLengthMode::Visual,
..Default::default()
};
let input = "测试十个中文字符号呢 additional text here.";
let result = reflow_markdown(input, &options);
let lines: Vec<&str> = result.trim().lines().collect();
assert!(
lines.len() > 1,
"CJK text should be wrapped based on visual width. Got: {result:?}"
);
}
#[test]
fn test_visual_width_vs_char_count_issue_414() {
let visual_options = ReflowOptions {
line_length: 40,
length_mode: ReflowLengthMode::Visual,
..Default::default()
};
let char_options = ReflowOptions {
line_length: 40,
length_mode: ReflowLengthMode::Chars,
..Default::default()
};
let input = "开始测试这段文字的视觉宽度和字符宽度之间的差异 end.";
let visual_result = reflow_markdown(input, &visual_options);
let char_result = reflow_markdown(input, &char_options);
let visual_lines: Vec<&str> = visual_result.trim().lines().collect();
let char_lines: Vec<&str> = char_result.trim().lines().collect();
assert!(
visual_lines.len() >= char_lines.len(),
"Visual mode should wrap CJK text earlier than char mode. Visual: {visual_lines:?}, Char: {char_lines:?}"
);
}
#[test]
fn test_autolink_clause_punctuation_not_triggered() {
let options = ReflowOptions {
line_length: 30,
semantic_line_breaks: true,
..Default::default()
};
let input = "See <https://example.com> for info.";
let result = reflow_markdown(input, &options);
for line in result.lines() {
assert!(
!line.trim_start().starts_with("//"),
"Autolink was broken at colon in URL scheme. Got: {result:?}"
);
}
}
#[test]
fn test_multiple_autolinks_preserved() {
let options = ReflowOptions {
line_length: 60,
semantic_line_breaks: true,
..Default::default()
};
let input = "First link <https://example.com/a> and second link <https://example.com/b> in the same paragraph.";
let result = reflow_markdown(input, &options);
assert!(
result.contains("<https://example.com/a>"),
"First autolink must be preserved. Got: {result:?}"
);
assert!(
result.contains("<https://example.com/b>"),
"Second autolink must be preserved. Got: {result:?}"
);
}
#[test]
fn test_image_link_not_broken_at_alt_text_spaces() {
let options = ReflowOptions {
line_length: 40,
..Default::default()
};
let input = "See  for reference.";
let result = reflow_markdown(input, &options);
assert!(
result.contains(""),
"Image link must remain intact. Got: {result:?}"
);
}
#[test]
fn test_reflow_paragraph_at_line_uses_visual_width() {
let content = "Hello 你好世界测试文本在这里显示出来 world.\n";
let result = reflow_paragraph_at_line(content, 1, 30);
assert!(result.is_some(), "Should reflow the paragraph");
}
#[test]
fn test_reflow_paragraph_at_line_blockquote_explicit_target() {
let content = "> This is a long quoted line that should be wrapped by manual paragraph reflow for blockquotes.\n";
let result = reflow_paragraph_at_line(content, 1, 40).expect("Expected blockquote reflow");
let lines: Vec<&str> = result.reflowed_text.lines().collect();
assert!(!lines.is_empty(), "Expected reflowed output");
assert!(
lines.iter().all(|line| line.starts_with("> ")),
"Expected explicit quote prefix on all lines: {lines:?}",
);
}
#[test]
fn test_reflow_paragraph_at_line_blockquote_lazy_target() {
let content = "> This quoted paragraph begins explicitly and should still be detected when selecting a lazy continuation line.\nlazy continuation should be part of the same quoted paragraph for manual reflow.\n";
let result = reflow_paragraph_at_line(content, 2, 44).expect("Expected lazy continuation reflow");
let lines: Vec<&str> = result.reflowed_text.lines().collect();
assert!(!lines.is_empty(), "Expected reflowed output");
assert!(lines[0].starts_with("> "));
assert!(
lines.iter().skip(1).any(|line| !line.starts_with('>')),
"Expected at least one lazy continuation line: {lines:?}",
);
}
#[test]
fn test_semantic_line_breaks_sibling_list_items_preserved() {
let options = ReflowOptions {
line_length: 60,
semantic_line_breaks: true,
..Default::default()
};
let input = "- [AlphaTool](https://example.com/alpha) - This is a long description that definitely exceeds the line length limit and needs wrapping.\n- [BetaTool](https://example.com/beta) - Short description.\n- [GammaTool](https://example.com/gamma) - Short description.\n";
let result = reflow_markdown(input, &options);
assert!(
result.contains("\n- [BetaTool]"),
"BetaTool must remain a top-level sibling list item, not indented. Got:\n{result}"
);
assert!(
result.contains("\n- [GammaTool]"),
"GammaTool must remain a top-level sibling list item. Got:\n{result}"
);
}
#[test]
fn test_semantic_line_breaks_numbered_list_sibling_preserved() {
let options = ReflowOptions {
line_length: 60,
semantic_line_breaks: true,
..Default::default()
};
let input = "1. First item with a very long description that exceeds the line length and requires proper semantic line breaking.\n2. Second item should stay as its own list item.\n3. Third item should also stay independent.\n";
let result = reflow_markdown(input, &options);
assert!(
result.contains("\n2. Second item"),
"Second numbered item must remain a sibling. Got:\n{result}"
);
assert!(
result.contains("\n3. Third item"),
"Third numbered item must remain a sibling. Got:\n{result}"
);
}
#[test]
fn test_semantic_line_breaks_list_continuation_indented() {
let options = ReflowOptions {
line_length: 50,
semantic_line_breaks: true,
..Default::default()
};
let input =
"- This is a list item with a very long sentence that should be wrapped with continuation indentation.\n";
let result = reflow_markdown(input, &options);
let lines: Vec<&str> = result.trim().lines().collect();
assert!(lines.len() > 1, "Long list item should be wrapped. Got:\n{result}");
for line in &lines[1..] {
assert!(
line.starts_with(" "),
"Continuation line must be indented: {line:?}. Full:\n{result}"
);
}
}
#[test]
fn test_semantic_line_breaks_adjacent_long_list_items() {
let options = ReflowOptions {
line_length: 50,
semantic_line_breaks: true,
..Default::default()
};
let input = "- First list item with text that is long enough to exceed the configured line length limit.\n- Second list item also with text long enough to exceed the configured line length limit.\n";
let result = reflow_markdown(input, &options);
let top_markers: Vec<&str> = result.lines().filter(|l| l.starts_with("- ")).collect();
assert_eq!(
top_markers.len(),
2,
"Must have exactly 2 top-level list items. Got {}: {top_markers:?}\nFull:\n{result}",
top_markers.len()
);
}
#[test]
fn test_semantic_line_breaks_nested_list_structure_preserved() {
let options = ReflowOptions {
line_length: 60,
semantic_line_breaks: true,
..Default::default()
};
let input = "- Parent item with some text that may be long enough to require wrapping.\n - Child item one.\n - Child item two.\n- Another parent item.\n";
let result = reflow_markdown(input, &options);
assert!(
result.contains("\n - Child item one."),
"Child items must remain nested. Got:\n{result}"
);
assert!(
result.contains("\n - Child item two."),
"Child items must remain nested. Got:\n{result}"
);
assert!(
result.contains("\n- Another parent"),
"Second parent must remain top-level. Got:\n{result}"
);
}
#[test]
fn test_default_reflow_sibling_list_items_preserved() {
let options = ReflowOptions {
line_length: 60,
..Default::default()
};
let input = "- First long item that exceeds the line length and needs to be wrapped properly by the reflow engine.\n- Second item should stay as sibling.\n";
let result = reflow_markdown(input, &options);
assert!(
result.contains("\n- Second item"),
"Second item must remain a sibling in default reflow. Got:\n{result}"
);
}
#[test]
fn test_autolink_with_query_params() {
let options = ReflowOptions {
line_length: 40,
semantic_line_breaks: true,
..Default::default()
};
let input = "See <https://example.com/api?key=value&format=json> for the API.";
let result = reflow_markdown(input, &options);
assert!(
result.contains("<https://example.com/api?key=value&format=json>"),
"Autolink with query params must be preserved. Got: {result:?}"
);
}
#[test]
fn test_autolink_with_fragment() {
let options = ReflowOptions {
line_length: 40,
semantic_line_breaks: true,
..Default::default()
};
let input = "See <https://example.com/page#section-heading> for details.";
let result = reflow_markdown(input, &options);
assert!(
result.contains("<https://example.com/page#section-heading>"),
"Autolink with fragment must be preserved. Got: {result:?}"
);
}
#[test]
fn test_multiple_adjacent_autolinks() {
let options = ReflowOptions {
line_length: 80,
semantic_line_breaks: true,
..Default::default()
};
let input = "Links: <https://example.com/first><https://example.com/second> are both here.";
let result = reflow_markdown(input, &options);
assert!(
result.contains("<https://example.com/first>"),
"First adjacent autolink must be preserved. Got: {result:?}"
);
assert!(
result.contains("<https://example.com/second>"),
"Second adjacent autolink must be preserved. Got: {result:?}"
);
}
#[test]
fn test_autolink_mixed_with_markdown_link() {
let options = ReflowOptions {
line_length: 50,
semantic_line_breaks: true,
..Default::default()
};
let input = "See [the docs](https://example.com/docs) or <https://example.com/api> for info.";
let result = reflow_markdown(input, &options);
assert!(
result.contains("[the docs](https://example.com/docs)"),
"Markdown link must be preserved. Got: {result:?}"
);
assert!(
result.contains("<https://example.com/api>"),
"Autolink must be preserved. Got: {result:?}"
);
}
#[test]
fn test_autolink_in_sentence_per_line_mode() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
..Default::default()
};
let input = "First sentence about <https://example.com/path>. Second sentence here.";
let result = reflow_markdown(input, &options);
assert!(
result.contains("<https://example.com/path>"),
"Autolink must be preserved in sentence-per-line mode. Got: {result:?}"
);
}
#[test]
fn test_autolink_ftp_and_mailto_schemes() {
let options = ReflowOptions {
line_length: 40,
semantic_line_breaks: true,
..Default::default()
};
let input = "Download from <ftp://files.example.com/package.tar.gz> or email <mailto:admin@example.com> for help.";
let result = reflow_markdown(input, &options);
assert!(
result.contains("<ftp://files.example.com/package.tar.gz>"),
"FTP autolink must be preserved. Got: {result:?}"
);
assert!(
result.contains("<mailto:admin@example.com>"),
"Mailto autolink must be preserved. Got: {result:?}"
);
}
#[test]
fn test_html_tag_with_many_attributes_not_split() {
let options = ReflowOptions {
line_length: 40,
..Default::default()
};
let input = r#"Click <a href="https://example.com" target="_blank" rel="noopener noreferrer" class="link">here</a> for details."#;
let result = reflow_markdown(input, &options);
assert!(
result.contains(r#"<a href="https://example.com" target="_blank" rel="noopener noreferrer" class="link">"#),
"HTML tag with many attributes must not be split. Got: {result:?}"
);
}
#[test]
fn test_code_span_with_spaces_not_split() {
let options = ReflowOptions {
line_length: 30,
..Default::default()
};
let input = "Use `some command with args` to run the task.";
let result = reflow_markdown(input, &options);
assert!(
result.contains("`some command with args`"),
"Code span with spaces must not be split. Got: {result:?}"
);
}
#[test]
fn test_reflow_idempotent_with_autolinks() {
let options = ReflowOptions {
line_length: 60,
semantic_line_breaks: true,
..Default::default()
};
let input = "See the documentation at <https://example.com/very/long/path/to/documentation> for more details about the configuration.";
let first = reflow_markdown(input, &options);
let second = reflow_markdown(&first, &options);
assert_eq!(
first, second,
"Reflow must be idempotent.\nFirst: {first:?}\nSecond: {second:?}"
);
}
#[test]
fn test_reflow_idempotent_with_links() {
let options = ReflowOptions {
line_length: 60,
semantic_line_breaks: true,
..Default::default()
};
let input = "Reference: [Widget Entrypoint Location](https://example.com/docs/widget-entrypoint-location) for the configuration guide.";
let first = reflow_markdown(input, &options);
let second = reflow_markdown(&first, &options);
assert_eq!(
first, second,
"Reflow with links must be idempotent.\nFirst: {first:?}\nSecond: {second:?}"
);
}
#[test]
fn test_visual_width_reflow_idempotent() {
let options = ReflowOptions {
line_length: 40,
length_mode: ReflowLengthMode::Visual,
..Default::default()
};
let input = "Test with CJK 这是一个测试句子 and more English text after.";
let first = reflow_markdown(input, &options);
let second = reflow_markdown(&first, &options);
assert_eq!(
first, second,
"Visual width reflow must be idempotent.\nFirst: {first:?}\nSecond: {second:?}"
);
}
#[test]
fn test_bytes_length_mode_reflow() {
let options = ReflowOptions {
line_length: 40,
length_mode: ReflowLengthMode::Bytes,
..Default::default()
};
let input = "Hello 你好世界 this is a test line that should wrap.";
let result = reflow_markdown(input, &options);
let lines: Vec<&str> = result.trim().lines().collect();
assert!(
lines.len() > 1,
"Bytes mode should wrap multibyte content. Got:\n{result}"
);
}
#[test]
fn test_rfind_safe_space_empty_spans() {
let options = ReflowOptions {
line_length: 20,
..Default::default()
};
let input = "Simple text without any special elements here.";
let result = reflow_markdown(input, &options);
assert!(
result.trim().lines().count() > 1,
"Should wrap long plain text. Got: {result:?}"
);
}
#[test]
fn test_link_at_end_with_trailing_punctuation() {
let options = ReflowOptions {
line_length: 50,
..Default::default()
};
for punct in ['.', ',', ';', '!', '?', ')'] {
let input = format!("See [the documentation page](https://example.com/docs){punct} More text follows here.");
let result = reflow_markdown(&input, &options);
assert!(
result.contains("[the documentation page](https://example.com/docs)"),
"Link must be preserved with trailing '{punct}'. Got: {result:?}"
);
}
}
#[test]
fn test_autolink_exceeding_line_length_preserved() {
let options = ReflowOptions {
line_length: 30,
semantic_line_breaks: true,
..Default::default()
};
let input = "See <https://example.com/very/long/path/that/exceeds/the/line/length/limit> for details.";
let result = reflow_markdown(input, &options);
assert!(
result.contains("<https://example.com/very/long/path/that/exceeds/the/line/length/limit>"),
"Autolink exceeding line length must remain intact. Got: {result:?}"
);
}
#[test]
fn test_semantic_merge_does_not_exceed_line_length() {
let options = ReflowOptions {
line_length: 80,
semantic_line_breaks: true,
..Default::default()
};
let short = "Short sentence here.";
let long = "This is a somewhat longer sentence that pushes close to the limit of eighty chars.";
let input = format!("{short}\n{long}");
let result = reflow_markdown(&input, &options);
for line in result.lines() {
let len = line.len();
assert!(
len <= 80,
"Reflow produced line exceeding line_length (80): {len} chars: {line:?}"
);
}
}
#[test]
fn test_semantic_merge_short_trailing_at_exact_limit() {
let options = ReflowOptions {
line_length: 80,
semantic_line_breaks: true,
..Default::default()
};
let line1 = "This is a long line that fills up most of the available space and";
let line2 = "then merges";
assert!(line2.len() < 24, "Second line must be short enough to trigger merge");
assert!(line1.len() + 1 + line2.len() <= 80, "Combined must fit within limit");
let input = format!("{line1}\n{line2}");
let result = reflow_markdown(&input, &options);
let line_count = result.lines().count();
assert_eq!(
line_count, 1,
"Short trailing line at exact limit should be merged. Got {line_count} lines: {result:?}"
);
}
#[test]
fn test_blockquote_continuation_style_all_explicit() {
let lines = vec![
BlockquoteLineData::explicit("First line.".to_string(), "> ".to_string()),
BlockquoteLineData::explicit("Second line.".to_string(), "> ".to_string()),
BlockquoteLineData::explicit("Third line.".to_string(), "> ".to_string()),
];
assert_eq!(
blockquote_continuation_style(&lines),
BlockquoteContinuationStyle::Explicit
);
}
#[test]
fn test_blockquote_continuation_style_all_lazy() {
let lines = vec![
BlockquoteLineData::explicit("First line.".to_string(), "> ".to_string()),
BlockquoteLineData::lazy("Second line.".to_string()),
BlockquoteLineData::lazy("Third line.".to_string()),
];
assert_eq!(blockquote_continuation_style(&lines), BlockquoteContinuationStyle::Lazy);
}
#[test]
fn test_blockquote_continuation_style_tie_resolves_to_explicit() {
let lines = vec![
BlockquoteLineData::explicit("First.".to_string(), "> ".to_string()),
BlockquoteLineData::explicit("Second.".to_string(), "> ".to_string()),
BlockquoteLineData::lazy("Third.".to_string()),
];
assert_eq!(
blockquote_continuation_style(&lines),
BlockquoteContinuationStyle::Explicit
);
}
#[test]
fn test_blockquote_continuation_style_single_element() {
let lines = vec![BlockquoteLineData::explicit("Only line.".to_string(), "> ".to_string())];
assert_eq!(
blockquote_continuation_style(&lines),
BlockquoteContinuationStyle::Explicit
);
}
#[test]
fn test_dominant_blockquote_prefix_single_variant() {
let lines = vec![
BlockquoteLineData::explicit("a".to_string(), "> ".to_string()),
BlockquoteLineData::explicit("b".to_string(), "> ".to_string()),
BlockquoteLineData::lazy("c".to_string()),
];
assert_eq!(dominant_blockquote_prefix(&lines, ">> "), "> ");
}
#[test]
fn test_dominant_blockquote_prefix_majority_wins() {
let lines = vec![
BlockquoteLineData::explicit("a".to_string(), "> ".to_string()),
BlockquoteLineData::explicit("b".to_string(), ">> ".to_string()),
BlockquoteLineData::explicit("c".to_string(), ">> ".to_string()),
];
assert_eq!(dominant_blockquote_prefix(&lines, "> "), ">> ");
}
#[test]
fn test_dominant_blockquote_prefix_tie_chooses_earliest() {
let lines = vec![
BlockquoteLineData::explicit("a".to_string(), "> ".to_string()), BlockquoteLineData::explicit("b".to_string(), ">> ".to_string()), ];
assert_eq!(dominant_blockquote_prefix(&lines, ">>> "), "> ");
}
#[test]
fn test_dominant_blockquote_prefix_no_explicit_uses_fallback() {
let lines = vec![
BlockquoteLineData::lazy("a".to_string()),
BlockquoteLineData::lazy("b".to_string()),
];
assert_eq!(dominant_blockquote_prefix(&lines, "> "), "> ");
}
#[test]
fn test_reflow_blockquote_content_explicit_style() {
let lines = vec![BlockquoteLineData::explicit(
"This is a long blockquote line that exceeds the limit.".to_string(),
"> ".to_string(),
)];
let options = ReflowOptions {
line_length: 30,
..Default::default()
};
let result = reflow_blockquote_content(&lines, "> ", BlockquoteContinuationStyle::Explicit, &options);
assert!(
result.iter().all(|l| l.starts_with("> ")),
"All lines must carry explicit prefix: {result:?}"
);
assert!(
result.iter().all(|l| l.len() <= 36), "Lines must be wrapped: {result:?}"
);
}
#[test]
fn test_reflow_blockquote_content_lazy_style() {
let lines = vec![
BlockquoteLineData::explicit("First line.".to_string(), "> ".to_string()),
BlockquoteLineData::lazy("Second line.".to_string()),
];
let options = ReflowOptions {
line_length: 80,
..Default::default()
};
let result = reflow_blockquote_content(&lines, "> ", BlockquoteContinuationStyle::Lazy, &options);
assert!(result[0].starts_with("> "), "First line must be explicit: {result:?}");
if result.len() > 1 {
assert!(
!result[1].starts_with("> "),
"Lazy continuation must not carry prefix: {result:?}"
);
}
}
#[test]
fn test_reflow_blockquote_content_hard_break_preserved() {
let lines = vec![BlockquoteLineData::explicit(
"Short line with hard break.\\".to_string(),
"> ".to_string(),
)];
let options = ReflowOptions {
line_length: 80,
..Default::default()
};
let result = reflow_blockquote_content(&lines, "> ", BlockquoteContinuationStyle::Explicit, &options);
assert!(
result.last().is_some_and(|l| l.ends_with('\\')),
"Hard break marker must be on the last output line: {result:?}"
);
}
#[test]
fn test_reflow_blockquote_content_force_explicit_for_structural_lines() {
let lines = vec![
BlockquoteLineData::explicit("Normal line.".to_string(), "> ".to_string()),
BlockquoteLineData::explicit("# Heading".to_string(), "> ".to_string()),
];
let options = ReflowOptions {
line_length: 13,
..Default::default()
};
let result = reflow_blockquote_content(&lines, "> ", BlockquoteContinuationStyle::Lazy, &options);
assert!(
result.iter().any(|l| l.starts_with("> # ")),
"Heading content must carry explicit prefix even in lazy mode: {result:?}"
);
}
#[test]
fn test_relaxed_sentences_lowercase_after_period() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
require_sentence_capital: false,
..Default::default()
};
let input = "lets add some periods. like this we can see if it works. and another sentence here.";
let result = reflow_line(input, &options);
assert_eq!(result.len(), 3, "Should split into 3 sentences: {result:?}");
assert_eq!(result[0], "lets add some periods.");
assert_eq!(result[1], "like this we can see if it works.");
assert_eq!(result[2], "and another sentence here.");
}
#[test]
fn test_relaxed_sentences_mixed_case() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
require_sentence_capital: false,
..Default::default()
};
let input = "first sentence. Second sentence. third sentence. Fourth sentence.";
let result = reflow_line(input, &options);
assert_eq!(result.len(), 4, "Should split into 4 sentences: {result:?}");
assert_eq!(result[0], "first sentence.");
assert_eq!(result[1], "Second sentence.");
assert_eq!(result[2], "third sentence.");
assert_eq!(result[3], "Fourth sentence.");
}
#[test]
fn test_relaxed_sentences_abbreviations_still_work() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
require_sentence_capital: false,
..Default::default()
};
let input = "Use e.g. this method and i.e. that one. then continue.";
let result = reflow_line(input, &options);
assert_eq!(
result.len(),
2,
"Should split into 2 sentences (e.g. and i.e. are not boundaries): {result:?}"
);
assert_eq!(result[0], "Use e.g. this method and i.e. that one.");
assert_eq!(result[1], "then continue.");
}
#[test]
fn test_relaxed_sentences_vs_abbreviation() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
require_sentence_capital: false,
..Default::default()
};
let input = "Python vs. ruby is a common comparison. try both.";
let result = reflow_line(input, &options);
assert_eq!(result.len(), 2, "vs. should not split: {result:?}");
assert_eq!(result[0], "Python vs. ruby is a common comparison.");
assert_eq!(result[1], "try both.");
}
#[test]
fn test_relaxed_sentences_exclamation_and_question() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
require_sentence_capital: true, ..Default::default()
};
let input = "does this work? yes it does! and another.";
let result = reflow_line(input, &options);
assert_eq!(result.len(), 3, "! and ? should always split: {result:?}");
assert_eq!(result[0], "does this work?");
assert_eq!(result[1], "yes it does!");
assert_eq!(result[2], "and another.");
}
#[test]
fn test_relaxed_sentences_initials_not_split() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
require_sentence_capital: false,
..Default::default()
};
let input = "Written by J. K. Rowling in the nineties.";
let result = reflow_line(input, &options);
assert_eq!(result.len(), 1, "Initials should not split: {result:?}");
}
#[test]
fn test_relaxed_sentences_decimal_not_split() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
require_sentence_capital: false,
..Default::default()
};
let input = "The value is 3.14 and it matters. check it.";
let result = reflow_line(input, &options);
assert_eq!(result.len(), 2, "Decimals should not split: {result:?}");
assert_eq!(result[0], "The value is 3.14 and it matters.");
assert_eq!(result[1], "check it.");
}
#[test]
fn test_relaxed_sentences_issue_514_exact_case() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
require_sentence_capital: false,
..Default::default()
};
let input = "lets write a whole bunch of words to make a sentence and then lets add some periods some places without capitalization after them. like this we can see if it works or not. we can also test it again and again and then try another one with capitalization. Like this one probably will work correctly, based on my understanding";
let result = reflow_line(input, &options);
assert_eq!(result.len(), 4, "Should split into 4 sentences: {result:?}");
assert!(result[0].ends_with("after them."), "First sentence: {}", result[0]);
assert!(result[1].ends_with("works or not."), "Second sentence: {}", result[1]);
assert!(result[2].ends_with("capitalization."), "Third sentence: {}", result[2]);
assert!(result[3].starts_with("Like this"), "Fourth sentence: {}", result[3]);
}
#[test]
fn test_require_sentence_capital_preserves_old_behavior() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
require_sentence_capital: true,
..Default::default()
};
let input = "first sentence. second sentence. Third sentence.";
let result = reflow_line(input, &options);
assert_eq!(
result.len(),
2,
"Strict mode should only split at uppercase: {result:?}"
);
assert!(
result[1].starts_with("Third"),
"Second line should start with Third: {result:?}"
);
}
#[test]
fn test_relaxed_sentences_fig_no_abbreviations() {
let options = ReflowOptions {
line_length: 0,
sentence_per_line: true,
require_sentence_capital: false,
..Default::default()
};
let input = "See fig. 3 for details and no. 5 for more. then continue reading.";
let result = reflow_line(input, &options);
assert_eq!(result.len(), 2, "fig. and no. should not split: {result:?}");
assert!(result[0].contains("fig. 3"), "fig. should stay: {result:?}");
assert!(result[0].contains("no. 5"), "no. should stay: {result:?}");
}
#[test]
fn test_relaxed_sentences_semantic_line_breaks() {
let options = ReflowOptions {
line_length: 80,
sentence_per_line: false,
semantic_line_breaks: true,
require_sentence_capital: false,
..Default::default()
};
let input = "first sentence is here. second sentence follows it. Third sentence too.";
let result = reflow_line(input, &options);
assert!(
result.len() >= 3,
"Semantic should split at all sentence boundaries: {result:?}"
);
}
fn reflow_options_at(line_length: usize) -> ReflowOptions {
ReflowOptions {
line_length,
..Default::default()
}
}
fn semantic_options_at(line_length: usize) -> ReflowOptions {
ReflowOptions {
line_length,
break_on_sentences: true,
semantic_line_breaks: true,
require_sentence_capital: true,
..Default::default()
}
}
#[test]
fn test_checkbox_list_continuation_indent_unchecked() {
let options = reflow_options_at(40);
let input = "- [ ] This is a checkbox item that is long enough to require wrapping\n";
let result = reflow_markdown(input, &options);
let lines: Vec<&str> = result.lines().collect();
assert!(lines.len() >= 2, "Should wrap into multiple lines. Got: {result:?}");
assert!(
lines[0].starts_with("- [ ] "),
"First line must start with '- [ ] '. Got: {:?}",
lines[0]
);
for line in &lines[1..] {
assert!(
line.starts_with(" "),
"Continuation line must be indented 6 spaces to align under checkbox content. Got: {line:?}"
);
let trimmed = line.trim_start();
let indent = line.len() - trimmed.len();
assert_eq!(
indent, 6,
"Continuation indent should be exactly 6 spaces. Got {indent} in: {line:?}"
);
}
}
#[test]
fn test_checkbox_list_continuation_indent_checked() {
let options = reflow_options_at(40);
let input = "- [x] This is a completed checkbox item that is long enough to require wrapping\n";
let result = reflow_markdown(input, &options);
let lines: Vec<&str> = result.lines().collect();
assert!(lines.len() >= 2, "Should wrap. Got: {result:?}");
assert!(
lines[0].starts_with("- [x] "),
"First line must preserve checked state. Got: {:?}",
lines[0]
);
for line in &lines[1..] {
let indent = line.len() - line.trim_start().len();
assert_eq!(
indent, 6,
"Continuation indent should be 6 for '- [x] '. Got {indent} in: {line:?}"
);
}
}
#[test]
fn test_checkbox_list_continuation_indent_uppercase_x() {
let options = reflow_options_at(40);
let input = "- [X] This is a completed checkbox item that is long enough to require wrapping\n";
let result = reflow_markdown(input, &options);
let lines: Vec<&str> = result.lines().collect();
assert!(lines.len() >= 2, "Should wrap. Got: {result:?}");
assert!(
lines[0].starts_with("- [X] "),
"First line must preserve uppercase X. Got: {:?}",
lines[0]
);
for line in &lines[1..] {
let indent = line.len() - line.trim_start().len();
assert_eq!(
indent, 6,
"Continuation indent should be 6 for '- [X] '. Got {indent} in: {line:?}"
);
}
}
#[test]
fn test_checkbox_list_with_star_marker() {
let options = reflow_options_at(40);
let input = "* [ ] This is a checkbox item that is long enough to require wrapping\n";
let result = reflow_markdown(input, &options);
let lines: Vec<&str> = result.lines().collect();
assert!(lines.len() >= 2, "Should wrap. Got: {result:?}");
assert!(
lines[0].starts_with("* [ ] "),
"First line must preserve '* [ ]' marker. Got: {:?}",
lines[0]
);
for line in &lines[1..] {
let indent = line.len() - line.trim_start().len();
assert_eq!(
indent, 6,
"Continuation indent should be 6 for '* [ ] '. Got {indent} in: {line:?}"
);
}
}
#[test]
fn test_checkbox_list_with_plus_marker() {
let options = reflow_options_at(40);
let input = "+ [ ] This is a checkbox item that is long enough to require wrapping\n";
let result = reflow_markdown(input, &options);
let lines: Vec<&str> = result.lines().collect();
assert!(lines.len() >= 2, "Should wrap. Got: {result:?}");
assert!(
lines[0].starts_with("+ [ ] "),
"First line must preserve '+ [ ]' marker. Got: {:?}",
lines[0]
);
for line in &lines[1..] {
let indent = line.len() - line.trim_start().len();
assert_eq!(
indent, 6,
"Continuation indent should be 6 for '+ [ ] '. Got {indent} in: {line:?}"
);
}
}
#[test]
fn test_checkbox_list_indented_nested() {
let options = reflow_options_at(40);
let input = " - [ ] This nested checkbox item is long enough to need wrapping here\n";
let result = reflow_markdown(input, &options);
let lines: Vec<&str> = result.lines().collect();
assert!(lines.len() >= 2, "Should wrap. Got: {result:?}");
assert!(
lines[0].starts_with(" - [ ] "),
"First line must preserve indent + checkbox. Got: {:?}",
lines[0]
);
for line in &lines[1..] {
let indent = line.len() - line.trim_start().len();
assert_eq!(
indent, 8,
"Continuation indent should be 8 for ' - [ ] '. Got {indent} in: {line:?}"
);
}
}
#[test]
fn test_checkbox_list_existing_continuation_collected() {
let options = reflow_options_at(60);
let input = "- [ ] First part of the text.\n Second part continues here and goes on.\n";
let result = reflow_markdown(input, &options);
let lines: Vec<&str> = result.lines().collect();
assert!(
lines[0].starts_with("- [ ] "),
"First line must start with checkbox. Got: {:?}",
lines[0]
);
for line in &lines[1..] {
if !line.trim().is_empty() {
let indent = line.len() - line.trim_start().len();
assert_eq!(
indent, 6,
"Continuation must be indented 6 spaces. Got {indent} in: {line:?}"
);
}
}
}
#[test]
fn test_checkbox_list_does_not_strip_to_zero_indent() {
let options = reflow_options_at(80);
let input = "- [ ] whatever long line which goes on and on and on and on and on and on and on and on and on and on.\n A continuation line which should get formatted with proper indentation.\n";
let result = reflow_markdown(input, &options);
let lines: Vec<&str> = result.lines().collect();
for (i, line) in lines.iter().enumerate() {
if i == 0 {
continue;
}
if line.trim().is_empty() {
continue;
}
assert!(
!line.starts_with(|c: char| c.is_alphabetic()),
"Continuation line must NOT start at column 0. Got: {line:?}"
);
let indent = line.len() - line.trim_start().len();
assert_eq!(indent, 6, "Continuation must be indented 6. Got {indent} in: {line:?}");
}
}
#[test]
fn test_checkbox_list_line_length_accounts_for_checkbox() {
let options = reflow_options_at(30);
let input = "- [ ] Word word word word word word word word\n";
let result = reflow_markdown(input, &options);
let lines: Vec<&str> = result.lines().collect();
for line in &lines {
assert!(
line.len() <= 32, "Line exceeds target length (accounting for checkbox prefix). Line len={}, line: {line:?}",
line.len()
);
}
}
#[test]
fn test_checkbox_short_content_no_wrap() {
let options = reflow_options_at(80);
let input = "- [ ] Short task\n";
let result = reflow_markdown(input, &options);
assert_eq!(
result.trim(),
"- [ ] Short task",
"Short checkbox should remain single line. Got: {result:?}"
);
}
#[test]
fn test_checkbox_preserves_check_state_after_reflow() {
let options = reflow_options_at(40);
for marker in &["[ ]", "[x]", "[X]"] {
let input = format!("- {marker} This is a long task item that needs to be wrapped across lines\n");
let result = reflow_markdown(&input, &options);
assert!(
result.starts_with(&format!("- {marker} ")),
"Checkbox state '{marker}' must be preserved. Got: {result:?}"
);
}
}
#[test]
fn test_checkbox_multiple_items_each_indented_correctly() {
let options = reflow_options_at(40);
let input = "\
- [ ] First task which is long enough to require wrapping across lines
- [x] Second task which is also long enough to require wrapping across lines
- [ ] Third short task
";
let result = reflow_markdown(input, &options);
let mut current_marker: Option<&str> = None;
for line in result.lines() {
if line.starts_with("- [") {
current_marker = Some(if line.starts_with("- [ ]") { "- [ ] " } else { "- [x] " });
} else if !line.trim().is_empty() {
let indent = line.len() - line.trim_start().len();
assert_eq!(
indent, 6,
"Continuation of {current_marker:?} must be indented 6. Got {indent} in: {line:?}",
);
}
}
}
#[test]
fn test_checkbox_semantic_line_breaks() {
let options = semantic_options_at(120);
let input =
"- [ ] First sentence of the task. Second sentence continues the description. Third sentence wraps up.\n";
let result = reflow_markdown(input, &options);
let lines: Vec<&str> = result.lines().collect();
assert!(
lines[0].starts_with("- [ ] "),
"First line must have checkbox. Got: {:?}",
lines[0]
);
for line in &lines[1..] {
if !line.trim().is_empty() {
let indent = line.len() - line.trim_start().len();
assert_eq!(
indent, 6,
"Semantic continuation must be indented 6. Got {indent} in: {line:?}"
);
}
}
}
#[test]
fn test_checkbox_mixed_with_regular_list_items() {
let options = reflow_options_at(40);
let input = "\
- Regular item that is long enough to need wrapping across multiple lines
- [ ] Checkbox item that is also long enough to need wrapping across lines
- Another regular item that is long enough to wrap
";
let result = reflow_markdown(input, &options);
let mut expect_checkbox_continuation = false;
for line in result.lines() {
if line.starts_with("- [ ]") || line.starts_with("- [x]") {
expect_checkbox_continuation = true;
} else if line.starts_with("- ") {
expect_checkbox_continuation = false;
} else if !line.trim().is_empty() {
let indent = line.len() - line.trim_start().len();
if expect_checkbox_continuation {
assert_eq!(
indent, 6,
"Checkbox continuation should be 6 spaces. Got {indent} in: {line:?}"
);
} else {
assert_eq!(
indent, 2,
"Regular list continuation should be 2 spaces. Got {indent} in: {line:?}"
);
}
}
}
}
#[test]
fn test_checkbox_not_confused_with_link_reference() {
let options = reflow_options_at(40);
let input = "- Start with bracket [x] in the middle of a long list item text\n";
let result = reflow_markdown(input, &options);
let lines: Vec<&str> = result.lines().collect();
for line in &lines[1..] {
if !line.trim().is_empty() {
let indent = line.len() - line.trim_start().len();
assert_eq!(
indent, 2,
"Non-checkbox list continuation should be 2 spaces. Got {indent} in: {line:?}"
);
}
}
}
#[test]
fn test_checkbox_ordered_list_continuation_indent() {
let options = reflow_options_at(40);
let input = "1. [ ] This is an ordered checkbox item that is long enough to need wrapping\n";
let result = reflow_markdown(input, &options);
let lines: Vec<&str> = result.lines().collect();
assert!(lines.len() >= 2, "Should wrap. Got: {result:?}");
assert!(
lines[0].starts_with("1. [ ] "),
"First line must start with '1. [ ] '. Got: {:?}",
lines[0]
);
for line in &lines[1..] {
if !line.trim().is_empty() {
let indent = line.len() - line.trim_start().len();
assert_eq!(
indent, 7,
"Ordered checkbox continuation should be 7 spaces (for '1. [ ] '). Got {indent} in: {line:?}"
);
}
}
}
#[test]
fn test_ordered_list_without_checkbox_unchanged() {
let options = reflow_options_at(40);
let input = "1. This is an ordered list item that is long enough to need wrapping\n";
let result = reflow_markdown(input, &options);
let lines: Vec<&str> = result.lines().collect();
for line in &lines[1..] {
if !line.trim().is_empty() {
let indent = line.len() - line.trim_start().len();
assert_eq!(
indent, 3,
"Regular ordered list continuation should be 3 spaces. Got {indent} in: {line:?}"
);
}
}
}
#[test]
fn test_checkbox_multi_digit_ordered_list() {
let options = reflow_options_at(40);
let input = "10. [ ] This is a multi-digit ordered checkbox item long enough to wrap\n";
let result = reflow_markdown(input, &options);
let lines: Vec<&str> = result.lines().collect();
assert!(lines.len() >= 2, "Should wrap. Got: {result:?}");
assert!(
lines[0].starts_with("10. [ ] "),
"First line must start with '10. [ ] '. Got: {:?}",
lines[0]
);
for line in &lines[1..] {
if !line.trim().is_empty() {
let indent = line.len() - line.trim_start().len();
assert_eq!(
indent, 8,
"Multi-digit ordered checkbox continuation should be 8 spaces (for '10. [ ] '). Got {indent} in: {line:?}"
);
}
}
}
#[test]
fn test_checkbox_idempotent_reflow() {
let options = reflow_options_at(50);
let input = "- [ ] First part of the task\n continues on this line\n";
let result1 = reflow_markdown(input, &options);
let result2 = reflow_markdown(&result1, &options);
assert_eq!(
result1, result2,
"Reflow should be idempotent.\nFirst: {result1:?}\nSecond: {result2:?}"
);
}
#[test]
fn test_checkbox_with_inline_code() {
let options = reflow_options_at(40);
let input = "- [ ] Run `cargo test` and verify that all tests pass successfully\n";
let result = reflow_markdown(input, &options);
let lines: Vec<&str> = result.lines().collect();
if lines.len() >= 2 {
for line in &lines[1..] {
if !line.trim().is_empty() {
let indent = line.len() - line.trim_start().len();
assert_eq!(
indent, 6,
"Checkbox + code continuation should be 6 spaces. Got {indent} in: {line:?}"
);
}
}
}
}
#[test]
fn test_checkbox_no_content_after_marker() {
let options = reflow_options_at(80);
let input = "- [ ]\n";
let result = reflow_markdown(input, &options);
assert_eq!(
result.trim(),
"- [ ]",
"Empty checkbox should pass through unchanged. Got: {result:?}"
);
}
#[test]
fn test_checkbox_lazy_continuation_with_low_indent() {
let options = reflow_options_at(80);
let input = "- [ ] whatever long line which goes on and on and on and on and on and on and on.\n A continuation line which should get formatted with proper indentation.\n";
let result = reflow_markdown(input, &options);
assert!(
!result.contains("\nA "),
"Continuation must not appear at column 0. Got:\n{result}"
);
for (i, line) in result.lines().enumerate() {
if i > 0 && !line.trim().is_empty() {
let indent = line.len() - line.trim_start().len();
assert_eq!(
indent, 6,
"Lazy continuation must be re-indented to 6 spaces. Got {indent} in: {line:?}"
);
}
}
}
#[test]
fn test_checkbox_lazy_continuation_two_space_indent() {
let options = reflow_options_at(40);
let input = "- [ ] A checkbox item with long text here.\n Continuation indented only two spaces.\n";
let result = reflow_markdown(input, &options);
for (i, line) in result.lines().enumerate() {
if i > 0 && !line.trim().is_empty() {
let indent = line.len() - line.trim_start().len();
assert_eq!(
indent, 6,
"2-space lazy continuation must be re-indented to 6. Got {indent} in: {line:?}"
);
}
}
}
#[test]
fn test_checkbox_lazy_continuation_idempotent() {
let options = reflow_options_at(50);
let input = "- [ ] A checkbox item with long enough text to wrap.\n Lazily indented continuation line.\n";
let result1 = reflow_markdown(input, &options);
let result2 = reflow_markdown(&result1, &options);
assert_eq!(
result1, result2,
"Lazy continuation reflow must be idempotent.\nFirst: {result1:?}\nSecond: {result2:?}"
);
for (i, line) in result1.lines().enumerate() {
if i > 0 && !line.trim().is_empty() {
let indent = line.len() - line.trim_start().len();
assert_eq!(indent, 6, "Should have 6-space indent. Got {indent} in: {line:?}");
}
}
}
#[test]
fn test_checkbox_no_trailing_space_after_brackets() {
let options = reflow_options_at(40);
let input = "- [ ]text that is long enough to need to be wrapped across multiple lines here\n";
let result = reflow_markdown(input, &options);
let lines: Vec<&str> = result.lines().collect();
for line in &lines[1..] {
if !line.trim().is_empty() {
let indent = line.len() - line.trim_start().len();
assert_eq!(
indent, 2,
"Invalid checkbox (no space) should use regular 2-space indent. Got {indent} in: {line:?}"
);
}
}
}
#[test]
fn test_checkbox_invalid_marker_content() {
let options = reflow_options_at(40);
let input = "- [y] This has an invalid checkbox state that is long enough to wrap across lines\n";
let result = reflow_markdown(input, &options);
let lines: Vec<&str> = result.lines().collect();
for line in &lines[1..] {
if !line.trim().is_empty() {
let indent = line.len() - line.trim_start().len();
assert_eq!(
indent, 2,
"Invalid checkbox char should use regular 2-space indent. Got {indent} in: {line:?}"
);
}
}
}
#[test]
fn test_checkbox_idempotent_reflow_with_ordered_list() {
let options = reflow_options_at(50);
let input = "1. [ ] First part of this task\n continues on this line here\n";
let result1 = reflow_markdown(input, &options);
let result2 = reflow_markdown(&result1, &options);
assert_eq!(
result1, result2,
"Ordered checkbox reflow should be idempotent.\nFirst: {result1:?}\nSecond: {result2:?}"
);
}
#[test]
fn test_reflow_markdown_checkbox_with_max_list_indent() {
let options = ReflowOptions {
line_length: 60,
max_list_continuation_indent: Some(4),
..ReflowOptions::default()
};
let input = "- [ ] This checkbox item has a long description that needs wrapping to multiple lines.\n";
let result = reflow_markdown(input, &options);
for line in result.lines().skip(1) {
if !line.is_empty() {
let indent = line.len() - line.trim_start().len();
assert_eq!(
indent, 4,
"Checkbox with max_list_indent=4 should use 4-space continuation, got {indent} in: {line:?}"
);
}
}
}
#[test]
fn test_reflow_markdown_nested_checkbox_with_max_list_indent() {
let options = ReflowOptions {
line_length: 60,
max_list_continuation_indent: Some(4),
..ReflowOptions::default()
};
let input = "- Parent\n - [ ] Nested checkbox with a long description that needs wrapping to multiple lines.\n";
let result = reflow_markdown(input, &options);
for line in result.lines() {
if line.starts_with(" ") && !line.trim_start().starts_with('-') {
let indent = line.len() - line.trim_start().len();
assert_eq!(
indent, 8,
"Nested checkbox with max_list_indent=4 should use 8-space (4 nesting + 4), got {indent} in: {line:?}"
);
}
}
}
fn semantic_slb(line_length: usize) -> ReflowOptions {
ReflowOptions {
line_length,
semantic_line_breaks: true,
require_sentence_capital: true,
..Default::default()
}
}
#[test]
fn test_slb_parenthetical_exact_example_from_issue() {
let options = semantic_slb(120);
let input = "You can also run the whole stack in docker-compose. \
This has the advantage that the app runs behind a proxy \
(traefik, see `docker-compose.yml`) and the app will be \
available under a host and path prefix, similar as if deployed:";
let result = reflow_line(input, &options);
assert!(
result[0].contains("docker-compose."),
"First sentence must be first line. Got:\n{result:#?}"
);
for line in &result {
assert!(
!line.ends_with("(traefik,"),
"Must not split inside the parenthetical at the comma. Got line: {line:?}"
);
}
assert!(
result.iter().any(|l| l.contains("(traefik, see `docker-compose.yml`)")),
"The parenthetical must be intact on a single line. Got:\n{result:#?}"
);
}
#[test]
fn test_slb_parenthetical_full_isolation_shorter_limit() {
let options = semantic_slb(90);
let input = "You can also run the whole stack in docker-compose. \
This has the advantage that the app runs behind a proxy \
(traefik, see `docker-compose.yml`) and the app will be \
available under a host and path prefix, similar as if deployed:";
let result = reflow_line(input, &options);
assert!(
result.iter().any(|l| l.trim() == "(traefik, see `docker-compose.yml`)"),
"Parenthetical must be isolated when continuation also exceeds limit. Got:\n{result:#?}"
);
let paren_idx = result
.iter()
.position(|l| l.trim() == "(traefik, see `docker-compose.yml`)")
.unwrap();
assert!(
paren_idx > 0 && result[paren_idx - 1].trim().ends_with("proxy"),
"The line before the parenthetical must end with 'proxy'. Got:\n{result:#?}"
);
}
#[test]
fn test_slb_single_word_paren_not_split() {
let options = semantic_slb(60);
let input = "This configures the feature (optional) and enables the extended functionality for your project.";
let result = reflow_line(input, &options);
assert!(
!result.iter().any(|l| l.trim() == "(optional)"),
"Single-word parens must not be isolated. Got:\n{result:#?}"
);
}
#[test]
fn test_slb_multi_word_paren_split_before_open() {
let options = semantic_slb(50);
let input = "The system supports multiple backends (Redis, Memcached) for caching purposes.";
let result = reflow_line(input, &options);
assert!(
result.iter().any(|l| l.trim().ends_with("backends")),
"Line must break before '('. Got:\n{result:#?}"
);
assert!(
result.iter().any(|l| l.trim().starts_with("(Redis,")),
"Parenthetical must start a new line. Got:\n{result:#?}"
);
}
#[test]
fn test_slb_leading_parenthetical_split_after_close() {
let options = semantic_slb(80);
let input = "(traefik, see `docker-compose.yml`) and the app will be available \
under a host and path prefix, similar as if deployed:";
let result = reflow_line(input, &options);
assert_eq!(
result[0].trim(),
"(traefik, see `docker-compose.yml`)",
"Leading parenthetical must be first line. Got:\n{result:#?}"
);
assert!(result.len() >= 2, "Must produce at least 2 lines. Got:\n{result:#?}");
}
#[test]
fn test_slb_comma_inside_parens_not_clause_split() {
let options = semantic_slb(60);
let input = "The cluster supports several storage drivers (overlay2, devicemapper, btrfs) \
and each has different performance characteristics.";
let result = reflow_line(input, &options);
for line in &result {
let trimmed = line.trim();
assert!(
!(trimmed.ends_with("(overlay2,") || trimmed.ends_with("overlay2,")),
"Must not split inside parenthetical at comma. Got line: {line:?}\nFull:\n{result:#?}"
);
}
assert!(
result.iter().any(|l| l.contains("(overlay2, devicemapper, btrfs)")),
"Parenthetical group must remain on one line. Got:\n{result:#?}"
);
}
#[test]
fn test_slb_comma_outside_parens_still_clause_splits() {
let options = semantic_slb(50);
let input = "First clause, second clause that is long enough to require wrapping here.";
let result = reflow_line(input, &options);
assert!(
result.len() > 1,
"Comma outside parens must still be a split point. Got:\n{result:#?}"
);
}
#[test]
fn test_slb_nested_parens_treated_as_unit() {
let options = semantic_slb(50);
let input = "See the function signature (foo(bar, baz) returns nothing) for the details.";
let result = reflow_line(input, &options);
let paren_lines: Vec<&String> = result.iter().filter(|l| l.contains("foo(bar")).collect();
assert!(
!paren_lines.is_empty(),
"Parenthetical must appear in output. Got:\n{result:#?}"
);
for line in paren_lines {
let open_count = line.chars().filter(|&c| c == '(').count();
let close_count = line.chars().filter(|&c| c == ')').count();
assert_eq!(
open_count, close_count,
"Nested parens must be balanced on the same line. Got line: {line:?}"
);
}
}
#[test]
fn test_slb_paren_inside_link_not_split_point() {
let options = semantic_slb(60);
let input = "Visit [the documentation](https://example.com/docs) for more details \
and comprehensive usage examples.";
let result = reflow_line(input, &options);
assert!(
result
.iter()
.any(|l| l.contains("[the documentation](https://example.com/docs)")),
"Link parens must not trigger a split. Got:\n{result:#?}"
);
}
#[test]
fn test_slb_paren_inside_code_span_not_split_point() {
let options = semantic_slb(60);
let input = "Call the function with `connect(host, port)` to establish a connection \
to the remote server endpoint.";
let result = reflow_line(input, &options);
assert!(
result.iter().any(|l| l.contains("`connect(host, port)`")),
"Code-span parens must not trigger a split. Got:\n{result:#?}"
);
}
#[test]
fn test_slb_short_paren_abbreviations_not_split() {
let options = semantic_slb(50);
for abbr in &["(e.g.)", "(i.e.)", "(2024)", "(optional)"] {
let input = format!("This feature is useful {abbr} for processing large amounts of data efficiently.");
let result = reflow_line(&input, &options);
assert!(
!result.iter().any(|l| l.trim() == *abbr),
"Single-word paren {abbr:?} must not be isolated. Got:\n{result:#?}"
);
}
}
#[test]
fn test_slb_two_word_paren_is_valid_semantic_unit() {
let options = semantic_slb(50);
let input = "This feature is useful (see above) for processing large amounts of data efficiently.";
let result = reflow_line(input, &options);
assert!(
result.iter().any(|l| l.trim() == "(see above)"),
"(see above) must be kept as an intact semantic unit. Got:\n{result:#?}"
);
}
#[test]
fn test_slb_parenthetical_with_code_span_inside() {
let options = semantic_slb(80);
let input = "The proxy is configurable (see `traefik.toml` for details) \
and supports multiple backends in production environments.";
let result = reflow_line(input, &options);
assert!(
result.iter().any(|l| l.trim().starts_with("(see `traefik.toml`")),
"Parenthetical with code span must be isolated. Got:\n{result:#?}"
);
}
#[test]
fn test_slb_trailing_comma_stays_with_paren() {
let options = semantic_slb(60);
let input = "rumdl loads configuration from config files \
(with per-directory resolution when available), \
then applies CLI overrides on top.";
let result = reflow_line(input, &options);
for line in &result {
assert!(
!line.trim().starts_with(','),
"No line must start with a bare comma. Got line: {line:?}\nFull:\n{result:#?}"
);
}
assert!(
result.iter().any(|l| l.contains("),")),
"Trailing comma must sit on the same line as the closing ')'. Got:\n{result:#?}"
);
}
#[test]
fn test_slb_multiple_parentheticals_last_valid_used() {
let options = semantic_slb(80);
let input = "First group (alpha, beta) and second group (gamma, delta, epsilon) continue here \
with more text.";
let result = reflow_line(input, &options);
assert!(result.len() > 1, "Should produce multiple lines. Got:\n{result:#?}");
assert!(
result.iter().all(|l| l.len() < input.len()),
"Must actually split. Got:\n{result:#?}"
);
}
#[test]
fn test_slb_break_word_inside_paren_not_split_point() {
let options = semantic_slb(40);
let input = "Text with a clause (foo and bar and baz) then more text here.";
let result = reflow_line(input, &options);
let lines_with_foo: Vec<&String> = result.iter().filter(|l| l.contains("foo")).collect();
assert!(
!lines_with_foo.is_empty(),
"Parenthetical content must appear in output. Got:\n{result:#?}"
);
for line in lines_with_foo {
assert!(
line.contains("foo") && line.contains("baz"),
"foo and baz must be on the same line (paren not split). Got line: {line:?}\nFull:\n{result:#?}"
);
}
}
#[test]
fn test_slb_standalone_paren_not_merged_back() {
let options = semantic_slb(80);
let input = "Configuration is described elsewhere \
(see Section 5.2 for details) and applies globally.";
let result = reflow_line(input, &options);
assert!(
result.iter().any(|l| l.trim().starts_with("(see Section")),
"Multi-word parenthetical must not be merged back into prior line. Got:\n{result:#?}"
);
assert!(
!result
.iter()
.any(|l| l.contains("elsewhere") && l.contains("(see Section")),
"Parenthetical must be on its own line, not merged with 'elsewhere'. Got:\n{result:#?}"
);
}