1use std::collections::HashMap;
2
3use crate::config::CodeLang;
4use crate::parser::Region;
5use crate::sentence::SentenceSplitter;
6
7#[derive(Default)]
9pub struct ReflowConfig<'a> {
10 pub max_width: usize,
12 pub code: Option<&'a HashMap<String, CodeLang>>,
14 pub format_code: bool,
16}
17
18#[cfg(feature = "cli")]
20const PARALLEL_REGION_THRESHOLD: usize = 32;
21
22pub fn reflow(
28 regions: &[Region],
29 splitter: &dyn SentenceSplitter,
30 config: &ReflowConfig,
31) -> String {
32 #[cfg(feature = "cli")]
33 {
34 if regions.len() >= PARALLEL_REGION_THRESHOLD {
35 return reflow_parallel(regions, splitter, config);
36 }
37 }
38 reflow_sequential(regions, splitter, config)
39}
40
41fn reflow_sequential(
42 regions: &[Region],
43 splitter: &dyn SentenceSplitter,
44 config: &ReflowConfig,
45) -> String {
46 let mut output = String::new();
47 for (idx, region) in regions.iter().enumerate() {
48 output.push_str(&reflow_one(region, idx, regions, splitter, config));
49 }
50 output
51}
52
53#[cfg(feature = "cli")]
54fn reflow_parallel(
55 regions: &[Region],
56 splitter: &dyn SentenceSplitter,
57 config: &ReflowConfig,
58) -> String {
59 use rayon::prelude::*;
60 let parts: Vec<String> = regions
62 .par_iter()
63 .enumerate()
64 .map(|(idx, region)| reflow_one(region, idx, regions, splitter, config))
65 .collect();
66 let mut output = String::new();
67 for p in parts {
68 output.push_str(&p);
69 }
70 output
71}
72
73fn reflow_one(
74 region: &Region,
75 idx: usize,
76 regions: &[Region],
77 splitter: &dyn SentenceSplitter,
78 config: &ReflowConfig,
79) -> String {
80 let mut output = String::new();
81 match region {
82 Region::Structure(s) => output.push_str(s),
83 Region::BlankLines(s) => output.push_str(s),
84 Region::Code {
85 lang,
86 header,
87 body,
88 footer,
89 } => {
90 output.push_str(header);
91 let code_cfg = lang
92 .as_deref()
93 .and_then(|l| config.code.and_then(|m| m.get(l)));
94 let reflowed = if let Some(cfg) = code_cfg {
95 crate::code_block::reflow_code_body(body, cfg, splitter, config.format_code)
96 } else {
97 body.clone()
98 };
99 output.push_str(&reflowed);
100 output.push_str(footer);
101 }
102 Region::Prose(text) => {
103 let sentences = splitter.split(text);
104 for (i, sentence) in sentences.iter().enumerate() {
105 if config.max_width > 0 {
106 let wrapped = textwrap::fill(sentence, config.max_width);
107 output.push_str(&wrapped);
108 } else {
109 output.push_str(sentence);
110 }
111 if i < sentences.len() - 1 {
112 output.push('\n');
113 }
114 }
115 if !sentences.is_empty() {
116 let suppress = matches!(
117 regions.get(idx + 1),
118 Some(Region::Structure(s))
119 if s == "\n"
120 || s.starts_with('}')
121 || s.starts_with(']')
122 || s.starts_with(')')
123 );
124 if !suppress {
125 output.push('\n');
126 }
127 }
128 }
129 }
130 output
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136 use crate::sentence::unicode::UnicodeSentenceSplitter;
137
138 fn reflow_text(input: &str) -> String {
139 let regions = vec![Region::Prose(input.to_string())];
140 let config = ReflowConfig::default();
141 reflow(®ions, &UnicodeSentenceSplitter::new(), &config)
142 }
143
144 #[test]
145 fn simple_reflow() {
146 let result = reflow_text("Hello world. This is a test. Another sentence.");
147 assert_eq!(result, "Hello world.\nThis is a test.\nAnother sentence.\n");
148 }
149
150 #[test]
151 fn idempotent() {
152 let input = "Hello world.\nThis is a test.\nAnother sentence.";
153 let first = reflow_text(input);
154 let second = reflow_text(&first);
155 assert_eq!(first, second, "reflow must be idempotent");
156 }
157
158 #[test]
159 fn preserves_structure() {
160 let regions = vec![
161 Region::Structure("#+TITLE: Test\n".to_string()),
162 Region::BlankLines("\n".to_string()),
163 Region::Prose("First sentence. Second sentence.".to_string()),
164 ];
165 let config = ReflowConfig::default();
166 let result = reflow(®ions, &UnicodeSentenceSplitter::new(), &config);
167 assert_eq!(
168 result,
169 "#+TITLE: Test\n\nFirst sentence.\nSecond sentence.\n"
170 );
171 }
172
173 #[test]
174 fn max_width_wrapping() {
175 let regions = vec![Region::Prose(
176 "This is a very long sentence that should be wrapped at a reasonable width for readability in narrow terminals.".to_string(),
177 )];
178 let config = ReflowConfig {
179 max_width: 40,
180 ..Default::default()
181 };
182 let result = reflow(®ions, &UnicodeSentenceSplitter::new(), &config);
183 for line in result.lines() {
185 assert!(
186 line.len() <= 40,
187 "Line too long: {} chars: {:?}",
188 line.len(),
189 line
190 );
191 }
192 }
193}