1pub mod abbreviations;
35pub mod cli;
36pub mod config;
37pub mod diff;
38pub mod files;
39pub mod format;
40pub mod init;
41pub mod output;
42pub mod parser;
43pub mod reflow;
44pub mod sentence;
45
46use anyhow::Result;
47
48use crate::format::Format;
49use crate::parser::FormatParser;
50use crate::parser::latex::LatexParser;
51use crate::parser::markdown::MarkdownParser;
52use crate::parser::org::OrgParser;
53use crate::parser::plaintext::PlaintextParser;
54use crate::reflow::{ReflowConfig, reflow};
55use crate::sentence::SentenceSplitter;
56use crate::sentence::unicode::UnicodeSentenceSplitter;
57
58pub struct FormatConfig {
60 pub format: Format,
61 pub max_width: usize,
62 pub use_neural: bool,
63 pub extra_abbreviations: Vec<String>,
65}
66
67pub fn format_text(input: &str, config: &FormatConfig) -> Result<String> {
69 let parser: Box<dyn FormatParser> = match config.format {
70 Format::Org => Box::new(OrgParser),
71 Format::Latex => Box::new(LatexParser),
72 Format::Markdown => Box::new(MarkdownParser),
73 Format::Plaintext => Box::new(PlaintextParser),
74 };
75
76 let splitter: Box<dyn SentenceSplitter> = if config.use_neural {
77 #[cfg(feature = "neural")]
78 {
79 anyhow::bail!("Neural splitter not yet implemented");
81 }
82 #[cfg(not(feature = "neural"))]
83 {
84 anyhow::bail!(
85 "Neural sentence detection requires the 'neural' feature. \
86 Build with: cargo build --features neural"
87 );
88 }
89 } else if config.extra_abbreviations.is_empty() {
90 Box::new(UnicodeSentenceSplitter::new())
91 } else {
92 Box::new(UnicodeSentenceSplitter::with_extra_abbreviations(
93 &config.extra_abbreviations,
94 ))
95 };
96
97 let had_trailing_newline = input.ends_with('\n');
98 let uses_crlf = input.contains("\r\n");
99
100 let normalized;
102 let work_input = if uses_crlf {
103 normalized = input.replace("\r\n", "\n");
104 &normalized
105 } else {
106 input
107 };
108
109 let regions = parser.parse(work_input);
110 let reflow_config = ReflowConfig {
111 max_width: config.max_width,
112 };
113
114 let mut output = reflow(®ions, splitter.as_ref(), &reflow_config);
115
116 if had_trailing_newline && !output.ends_with('\n') {
118 output.push('\n');
119 } else if !had_trailing_newline {
120 while output.ends_with('\n') {
121 output.pop();
122 }
123 }
124
125 if uses_crlf {
127 output = output.replace('\n', "\r\n");
128 }
129
130 Ok(output)
131}
132
133pub fn format_range(
136 input: &str,
137 config: &FormatConfig,
138 start: usize,
139 end: usize,
140) -> Result<String> {
141 let lines: Vec<&str> = input.lines().collect();
142 let total = lines.len();
143
144 let start = start.max(1);
146 let end = end.min(total);
147
148 if start > total {
149 return Ok(input.to_string());
150 }
151
152 let range_text = lines[start - 1..end].join("\n");
154 let formatted = format_text(&range_text, config)?;
155
156 let mut result = String::new();
158 for (i, line) in lines.iter().enumerate() {
159 let line_num = i + 1;
160 if line_num < start {
161 result.push_str(line);
162 result.push('\n');
163 }
164 }
165 result.push_str(&formatted);
166 if !formatted.ends_with('\n') && end < total {
167 result.push('\n');
168 }
169 for (i, line) in lines.iter().enumerate() {
170 let line_num = i + 1;
171 if line_num > end {
172 result.push_str(line);
173 if line_num < total {
174 result.push('\n');
175 }
176 }
177 }
178
179 if input.ends_with('\n') && !result.ends_with('\n') {
181 result.push('\n');
182 } else if !input.ends_with('\n') {
183 while result.ends_with('\n') {
184 result.pop();
185 }
186 }
187
188 Ok(result)
189}