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