use std::collections::HashMap;
use crate::config::CodeLang;
use crate::parser::Region;
use crate::sentence::SentenceSplitter;
#[derive(Default)]
pub struct ReflowConfig<'a> {
pub max_width: usize,
pub code: Option<&'a HashMap<String, CodeLang>>,
pub format_code: bool,
}
pub fn reflow(
regions: &[Region],
splitter: &dyn SentenceSplitter,
config: &ReflowConfig,
) -> String {
let mut output = String::new();
for (idx, region) in regions.iter().enumerate() {
match region {
Region::Structure(s) => output.push_str(s),
Region::BlankLines(s) => output.push_str(s),
Region::Code {
lang,
header,
body,
footer,
} => {
output.push_str(header);
let code_cfg = lang
.as_deref()
.and_then(|l| config.code.and_then(|m| m.get(l)));
let reflowed = if let Some(cfg) = code_cfg {
crate::code_block::reflow_code_body(body, cfg, splitter, config.format_code)
} else {
body.clone()
};
output.push_str(&reflowed);
output.push_str(footer);
}
Region::Prose(text) => {
let sentences = splitter.split(text);
for (i, sentence) in sentences.iter().enumerate() {
if config.max_width > 0 {
let wrapped = textwrap::fill(sentence, config.max_width);
output.push_str(&wrapped);
} else {
output.push_str(sentence);
}
if i < sentences.len() - 1 {
output.push('\n');
}
}
if !sentences.is_empty() {
let suppress = matches!(
regions.get(idx + 1),
Some(Region::Structure(s))
if s == "\n"
|| s.starts_with('}')
|| s.starts_with(']')
|| s.starts_with(')')
);
if !suppress {
output.push('\n');
}
}
}
}
}
output
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sentence::unicode::UnicodeSentenceSplitter;
fn reflow_text(input: &str) -> String {
let regions = vec![Region::Prose(input.to_string())];
let config = ReflowConfig::default();
reflow(®ions, &UnicodeSentenceSplitter::new(), &config)
}
#[test]
fn simple_reflow() {
let result = reflow_text("Hello world. This is a test. Another sentence.");
assert_eq!(result, "Hello world.\nThis is a test.\nAnother sentence.\n");
}
#[test]
fn idempotent() {
let input = "Hello world.\nThis is a test.\nAnother sentence.";
let first = reflow_text(input);
let second = reflow_text(&first);
assert_eq!(first, second, "reflow must be idempotent");
}
#[test]
fn preserves_structure() {
let regions = vec![
Region::Structure("#+TITLE: Test\n".to_string()),
Region::BlankLines("\n".to_string()),
Region::Prose("First sentence. Second sentence.".to_string()),
];
let config = ReflowConfig::default();
let result = reflow(®ions, &UnicodeSentenceSplitter::new(), &config);
assert_eq!(
result,
"#+TITLE: Test\n\nFirst sentence.\nSecond sentence.\n"
);
}
#[test]
fn max_width_wrapping() {
let regions = vec![Region::Prose(
"This is a very long sentence that should be wrapped at a reasonable width for readability in narrow terminals.".to_string(),
)];
let config = ReflowConfig {
max_width: 40,
..Default::default()
};
let result = reflow(®ions, &UnicodeSentenceSplitter::new(), &config);
for line in result.lines() {
assert!(
line.len() <= 40,
"Line too long: {} chars: {:?}",
line.len(),
line
);
}
}
}