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