1pub mod abbreviations;
36#[cfg(feature = "cli")]
37pub mod cli;
38pub mod code_block;
39pub mod config;
40pub mod diff;
41#[cfg(not(target_arch = "wasm32"))]
42pub mod files;
43pub mod format;
44#[cfg(not(target_arch = "wasm32"))]
45pub mod git_diff;
46#[cfg(feature = "cli")]
47pub mod init;
48#[cfg(feature = "lsp")]
49pub mod lsp;
50#[cfg(feature = "mcp")]
51pub mod mcp;
52pub mod output;
53pub mod parser;
54pub mod reflow;
55#[cfg(not(target_arch = "wasm32"))]
56pub mod sdiff;
57pub mod sentence;
58#[cfg(feature = "wasm")]
59pub mod wasm;
60#[cfg(feature = "watch")]
61pub mod watch;
62
63use std::collections::HashMap;
64
65use anyhow::Result;
66
67use crate::config::CodeLang;
68use crate::format::Format;
69use crate::reflow::{ReflowConfig, reflow};
70use crate::sentence::SentenceSplitter;
71use crate::sentence::unicode::UnicodeSentenceSplitter;
72
73pub struct FormatConfig {
75 pub format: Format,
76 pub max_width: usize,
77 pub use_neural: bool,
78 pub neural_lang: String,
79 pub neural_model_path: Option<std::path::PathBuf>,
80 pub extra_abbreviations: Vec<String>,
81 pub use_pandoc: bool,
82 pub pandoc_format: Option<String>,
84 #[cfg(feature = "pandoc")]
87 pub pandoc_backend: parser::pandoc::PandocBackend,
88 pub code: HashMap<String, CodeLang>,
92 pub format_code: bool,
96 pub clause_breaks: bool,
100}
101
102impl Default for FormatConfig {
103 fn default() -> Self {
104 Self {
105 format: Format::Plaintext,
106 max_width: 0,
107 use_neural: false,
108 neural_lang: "en".to_string(),
109 neural_model_path: None,
110 extra_abbreviations: vec![],
111 use_pandoc: false,
112 pandoc_format: None,
113 #[cfg(feature = "pandoc")]
114 pandoc_backend: parser::pandoc::PandocBackend::default(),
115 code: HashMap::new(),
116 format_code: false,
117 clause_breaks: false,
118 }
119 }
120}
121
122pub fn build_splitter(config: &FormatConfig) -> Result<Box<dyn SentenceSplitter>> {
124 if config.use_neural {
125 #[cfg(feature = "neural")]
126 {
127 let neural = if let Some(ref path) = config.neural_model_path {
128 sentence::neural::NeuralSentenceSplitter::from_path_with_extras(
129 path,
130 &config.neural_lang,
131 &config.extra_abbreviations,
132 )
133 } else {
134 sentence::neural::NeuralSentenceSplitter::with_extras(
135 &config.neural_lang,
136 &config.extra_abbreviations,
137 )
138 };
139 Ok(Box::new(neural.map_err(|e| anyhow::anyhow!("{e}"))?))
140 }
141 #[cfg(not(feature = "neural"))]
142 {
143 Err(anyhow::anyhow!(
144 "neural sentence splitting requires the 'neural' feature"
145 ))
146 }
147 } else {
148 Ok(Box::new(UnicodeSentenceSplitter::for_lang(
149 &config.neural_lang,
150 &config.extra_abbreviations,
151 )))
152 }
153}
154
155pub fn format_text(input: &str, config: &FormatConfig) -> Result<String> {
157 let splitter = build_splitter(config)?;
158 format_text_with_splitter(input, config, splitter.as_ref())
159}
160
161pub fn format_text_with_splitter(
163 input: &str,
164 config: &FormatConfig,
165 splitter: &dyn SentenceSplitter,
166) -> Result<String> {
167 let had_trailing_newline = input.ends_with('\n');
168 let uses_crlf = input.contains("\r\n");
169
170 let normalized;
172 let work_input = if uses_crlf {
173 normalized = input.replace("\r\n", "\n");
174 &normalized
175 } else {
176 input
177 };
178
179 let regions = if config.use_pandoc {
183 #[cfg(feature = "pandoc")]
184 {
185 let pandoc_fmt = config
186 .pandoc_format
187 .as_deref()
188 .unwrap_or(match config.format {
189 Format::Org => "org",
190 Format::Latex => "latex",
191 Format::Markdown => "markdown",
192 Format::Rst => "rst",
193 Format::Plaintext => "markdown",
194 });
195 let parser =
196 parser::pandoc::PandocParser::with_backend(pandoc_fmt, config.pandoc_backend);
197 parser
199 .try_parse(work_input)
200 .map_err(|e| anyhow::anyhow!("{e}"))?
201 }
202 #[cfg(not(feature = "pandoc"))]
203 {
204 return Err(anyhow::anyhow!(
205 "pandoc backend requires the 'pandoc' feature"
206 ));
207 }
208 } else {
209 parser::parser_for_format(config.format).parse(work_input)
210 };
211
212 let reflow_config = ReflowConfig {
213 max_width: config.max_width,
214 code: Some(&config.code),
215 format_code: config.format_code,
216 clause_breaks: config.clause_breaks,
217 };
218
219 let mut output = reflow(®ions, splitter, &reflow_config);
220
221 if had_trailing_newline && !output.ends_with('\n') {
223 output.push('\n');
224 } else if !had_trailing_newline {
225 while output.ends_with('\n') {
226 output.pop();
227 }
228 }
229
230 if uses_crlf {
232 output = output.replace('\n', "\r\n");
233 }
234
235 Ok(output)
236}
237
238pub fn format_range(
241 input: &str,
242 config: &FormatConfig,
243 start: usize,
244 end: usize,
245) -> Result<String> {
246 let lines: Vec<&str> = input.lines().collect();
247 let total = lines.len();
248
249 let start = start.max(1);
251 let end = end.min(total);
252
253 if start > total {
254 return Ok(input.to_string());
255 }
256
257 let range_text = lines[start - 1..end].join("\n");
259 let formatted = format_text(&range_text, config)?;
260
261 let mut result = String::new();
263 for (i, line) in lines.iter().enumerate() {
264 let line_num = i + 1;
265 if line_num < start {
266 result.push_str(line);
267 result.push('\n');
268 }
269 }
270 result.push_str(&formatted);
271 if !formatted.ends_with('\n') && end < total {
272 result.push('\n');
273 }
274 for (i, line) in lines.iter().enumerate() {
275 let line_num = i + 1;
276 if line_num > end {
277 result.push_str(line);
278 if line_num < total {
279 result.push('\n');
280 }
281 }
282 }
283
284 if input.ends_with('\n') && !result.ends_with('\n') {
286 result.push('\n');
287 } else if !input.ends_with('\n') {
288 while result.ends_with('\n') {
289 result.pop();
290 }
291 }
292
293 Ok(result)
294}