1use crate::parser::Region;
2use crate::sentence::SentenceSplitter;
3
4pub struct ReflowConfig {
6 pub max_width: usize,
8}
9
10pub fn reflow(
12 regions: &[Region],
13 splitter: &dyn SentenceSplitter,
14 config: &ReflowConfig,
15) -> String {
16 let mut output = String::new();
17
18 for (idx, region) in regions.iter().enumerate() {
19 match region {
20 Region::Structure(s) => output.push_str(s),
21 Region::BlankLines(s) => output.push_str(s),
22 Region::Prose(text) => {
23 let sentences = splitter.split(text);
24 for (i, sentence) in sentences.iter().enumerate() {
25 if config.max_width > 0 {
26 let wrapped = textwrap::fill(sentence, config.max_width);
27 output.push_str(&wrapped);
28 } else {
29 output.push_str(sentence);
30 }
31 if i < sentences.len() - 1 {
32 output.push('\n');
33 }
34 }
35 if !sentences.is_empty() {
40 let suppress = matches!(
44 regions.get(idx + 1),
45 Some(Region::Structure(s)) if s == "\n"
46 );
47 if !suppress {
48 output.push('\n');
49 }
50 }
51 }
52 }
53 }
54
55 output
56}
57
58#[cfg(test)]
59mod tests {
60 use super::*;
61 use crate::sentence::unicode::UnicodeSentenceSplitter;
62
63 fn reflow_text(input: &str) -> String {
64 let regions = vec![Region::Prose(input.to_string())];
65 let config = ReflowConfig { max_width: 0 };
66 reflow(®ions, &UnicodeSentenceSplitter::new(), &config)
67 }
68
69 #[test]
70 fn simple_reflow() {
71 let result = reflow_text("Hello world. This is a test. Another sentence.");
72 assert_eq!(result, "Hello world.\nThis is a test.\nAnother sentence.\n");
73 }
74
75 #[test]
76 fn idempotent() {
77 let input = "Hello world.\nThis is a test.\nAnother sentence.";
78 let first = reflow_text(input);
79 let second = reflow_text(&first);
80 assert_eq!(first, second, "reflow must be idempotent");
81 }
82
83 #[test]
84 fn preserves_structure() {
85 let regions = vec![
86 Region::Structure("#+TITLE: Test\n".to_string()),
87 Region::BlankLines("\n".to_string()),
88 Region::Prose("First sentence. Second sentence.".to_string()),
89 ];
90 let config = ReflowConfig { max_width: 0 };
91 let result = reflow(®ions, &UnicodeSentenceSplitter::new(), &config);
92 assert_eq!(
93 result,
94 "#+TITLE: Test\n\nFirst sentence.\nSecond sentence.\n"
95 );
96 }
97
98 #[test]
99 fn max_width_wrapping() {
100 let regions = vec![Region::Prose(
101 "This is a very long sentence that should be wrapped at a reasonable width for readability in narrow terminals.".to_string(),
102 )];
103 let config = ReflowConfig { max_width: 40 };
104 let result = reflow(®ions, &UnicodeSentenceSplitter::new(), &config);
105 for line in result.lines() {
107 assert!(
108 line.len() <= 40,
109 "Line too long: {} chars: {:?}",
110 line.len(),
111 line
112 );
113 }
114 }
115}