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