1pub mod abbreviations;
37pub mod cli;
38pub mod config;
39pub mod diff;
40pub mod files;
41pub mod format;
42pub mod git_diff;
43pub mod init;
44pub mod lsp;
45pub mod output;
46pub mod parser;
47pub mod reflow;
48pub mod sdiff;
49pub mod sentence;
50pub mod watch;
51
52use anyhow::Result;
53
54use crate::format::Format;
55use crate::parser::FormatParser;
56use crate::parser::latex::LatexParser;
57use crate::parser::markdown::MarkdownParser;
58use crate::parser::org::OrgParser;
59use crate::parser::plaintext::PlaintextParser;
60use crate::reflow::{ReflowConfig, reflow};
61use crate::sentence::SentenceSplitter;
62use crate::sentence::unicode::UnicodeSentenceSplitter;
63
64pub struct FormatConfig {
66 pub format: Format,
67 pub max_width: usize,
68 pub use_neural: bool,
69 pub neural_lang: String,
70 pub neural_model_path: Option<std::path::PathBuf>,
71 pub extra_abbreviations: Vec<String>,
72}
73
74pub fn build_splitter(config: &FormatConfig) -> Result<Box<dyn SentenceSplitter>> {
76 if config.use_neural {
77 let neural = if let Some(ref path) = config.neural_model_path {
78 sentence::neural::NeuralSentenceSplitter::from_path(path)
79 } else {
80 sentence::neural::NeuralSentenceSplitter::new(&config.neural_lang)
81 };
82 Ok(Box::new(neural.map_err(|e| anyhow::anyhow!("{e}"))?))
83 } else {
84 Ok(Box::new(UnicodeSentenceSplitter::for_lang(
85 &config.neural_lang,
86 &config.extra_abbreviations,
87 )))
88 }
89}
90
91pub fn format_text(input: &str, config: &FormatConfig) -> Result<String> {
93 let splitter = build_splitter(config)?;
94 format_text_with_splitter(input, config, splitter.as_ref())
95}
96
97pub fn format_text_with_splitter(
99 input: &str,
100 config: &FormatConfig,
101 splitter: &dyn SentenceSplitter,
102) -> Result<String> {
103 let parser: Box<dyn FormatParser> = match config.format {
104 Format::Org => Box::new(OrgParser),
105 Format::Latex => Box::new(LatexParser),
106 Format::Markdown => Box::new(MarkdownParser),
107 Format::Plaintext => Box::new(PlaintextParser),
108 };
109
110 let had_trailing_newline = input.ends_with('\n');
111 let uses_crlf = input.contains("\r\n");
112
113 let normalized;
115 let work_input = if uses_crlf {
116 normalized = input.replace("\r\n", "\n");
117 &normalized
118 } else {
119 input
120 };
121
122 let regions = parser.parse(work_input);
123 let reflow_config = ReflowConfig {
124 max_width: config.max_width,
125 };
126
127 let mut output = reflow(®ions, splitter, &reflow_config);
128
129 if had_trailing_newline && !output.ends_with('\n') {
131 output.push('\n');
132 } else if !had_trailing_newline {
133 while output.ends_with('\n') {
134 output.pop();
135 }
136 }
137
138 if uses_crlf {
140 output = output.replace('\n', "\r\n");
141 }
142
143 Ok(output)
144}
145
146pub fn format_range(
149 input: &str,
150 config: &FormatConfig,
151 start: usize,
152 end: usize,
153) -> Result<String> {
154 let lines: Vec<&str> = input.lines().collect();
155 let total = lines.len();
156
157 let start = start.max(1);
159 let end = end.min(total);
160
161 if start > total {
162 return Ok(input.to_string());
163 }
164
165 let range_text = lines[start - 1..end].join("\n");
167 let formatted = format_text(&range_text, config)?;
168
169 let mut result = String::new();
171 for (i, line) in lines.iter().enumerate() {
172 let line_num = i + 1;
173 if line_num < start {
174 result.push_str(line);
175 result.push('\n');
176 }
177 }
178 result.push_str(&formatted);
179 if !formatted.ends_with('\n') && end < total {
180 result.push('\n');
181 }
182 for (i, line) in lines.iter().enumerate() {
183 let line_num = i + 1;
184 if line_num > end {
185 result.push_str(line);
186 if line_num < total {
187 result.push('\n');
188 }
189 }
190 }
191
192 if input.ends_with('\n') && !result.ends_with('\n') {
194 result.push('\n');
195 } else if !input.ends_with('\n') {
196 while result.ends_with('\n') {
197 result.pop();
198 }
199 }
200
201 Ok(result)
202}