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}
97
98impl Default for FormatConfig {
99 fn default() -> Self {
100 Self {
101 format: Format::Plaintext,
102 max_width: 0,
103 use_neural: false,
104 neural_lang: "en".to_string(),
105 neural_model_path: None,
106 extra_abbreviations: vec![],
107 use_pandoc: false,
108 pandoc_format: None,
109 #[cfg(feature = "pandoc")]
110 pandoc_backend: parser::pandoc::PandocBackend::default(),
111 code: HashMap::new(),
112 format_code: false,
113 }
114 }
115}
116
117pub fn build_splitter(config: &FormatConfig) -> Result<Box<dyn SentenceSplitter>> {
119 if config.use_neural {
120 #[cfg(feature = "neural")]
121 {
122 let neural = if let Some(ref path) = config.neural_model_path {
123 sentence::neural::NeuralSentenceSplitter::from_path_with_extras(
124 path,
125 &config.neural_lang,
126 &config.extra_abbreviations,
127 )
128 } else {
129 sentence::neural::NeuralSentenceSplitter::with_extras(
130 &config.neural_lang,
131 &config.extra_abbreviations,
132 )
133 };
134 Ok(Box::new(neural.map_err(|e| anyhow::anyhow!("{e}"))?))
135 }
136 #[cfg(not(feature = "neural"))]
137 {
138 Err(anyhow::anyhow!(
139 "neural sentence splitting requires the 'neural' feature"
140 ))
141 }
142 } else {
143 Ok(Box::new(UnicodeSentenceSplitter::for_lang(
144 &config.neural_lang,
145 &config.extra_abbreviations,
146 )))
147 }
148}
149
150pub fn format_text(input: &str, config: &FormatConfig) -> Result<String> {
152 let splitter = build_splitter(config)?;
153 format_text_with_splitter(input, config, splitter.as_ref())
154}
155
156pub fn format_text_with_splitter(
158 input: &str,
159 config: &FormatConfig,
160 splitter: &dyn SentenceSplitter,
161) -> Result<String> {
162 let had_trailing_newline = input.ends_with('\n');
163 let uses_crlf = input.contains("\r\n");
164
165 let normalized;
167 let work_input = if uses_crlf {
168 normalized = input.replace("\r\n", "\n");
169 &normalized
170 } else {
171 input
172 };
173
174 let regions = if config.use_pandoc {
178 #[cfg(feature = "pandoc")]
179 {
180 let pandoc_fmt = config
181 .pandoc_format
182 .as_deref()
183 .unwrap_or(match config.format {
184 Format::Org => "org",
185 Format::Latex => "latex",
186 Format::Markdown => "markdown",
187 Format::Rst => "rst",
188 Format::Plaintext => "markdown",
189 });
190 let parser =
191 parser::pandoc::PandocParser::with_backend(pandoc_fmt, config.pandoc_backend);
192 parser
194 .try_parse(work_input)
195 .map_err(|e| anyhow::anyhow!("{e}"))?
196 }
197 #[cfg(not(feature = "pandoc"))]
198 {
199 return Err(anyhow::anyhow!(
200 "pandoc backend requires the 'pandoc' feature"
201 ));
202 }
203 } else {
204 parser::parser_for_format(config.format).parse(work_input)
205 };
206
207 let reflow_config = ReflowConfig {
208 max_width: config.max_width,
209 code: Some(&config.code),
210 format_code: config.format_code,
211 };
212
213 let mut output = reflow(®ions, splitter, &reflow_config);
214
215 if had_trailing_newline && !output.ends_with('\n') {
217 output.push('\n');
218 } else if !had_trailing_newline {
219 while output.ends_with('\n') {
220 output.pop();
221 }
222 }
223
224 if uses_crlf {
226 output = output.replace('\n', "\r\n");
227 }
228
229 Ok(output)
230}
231
232pub fn format_range(
235 input: &str,
236 config: &FormatConfig,
237 start: usize,
238 end: usize,
239) -> Result<String> {
240 let lines: Vec<&str> = input.lines().collect();
241 let total = lines.len();
242
243 let start = start.max(1);
245 let end = end.min(total);
246
247 if start > total {
248 return Ok(input.to_string());
249 }
250
251 let range_text = lines[start - 1..end].join("\n");
253 let formatted = format_text(&range_text, config)?;
254
255 let mut result = String::new();
257 for (i, line) in lines.iter().enumerate() {
258 let line_num = i + 1;
259 if line_num < start {
260 result.push_str(line);
261 result.push('\n');
262 }
263 }
264 result.push_str(&formatted);
265 if !formatted.ends_with('\n') && end < total {
266 result.push('\n');
267 }
268 for (i, line) in lines.iter().enumerate() {
269 let line_num = i + 1;
270 if line_num > end {
271 result.push_str(line);
272 if line_num < total {
273 result.push('\n');
274 }
275 }
276 }
277
278 if input.ends_with('\n') && !result.ends_with('\n') {
280 result.push('\n');
281 } else if !input.ends_with('\n') {
282 while result.ends_with('\n') {
283 result.pop();
284 }
285 }
286
287 Ok(result)
288}