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::Rst => Box::new(parser::rst::RstParser),
108 Format::Plaintext => Box::new(PlaintextParser),
109 };
110
111 let had_trailing_newline = input.ends_with('\n');
112 let uses_crlf = input.contains("\r\n");
113
114 let normalized;
116 let work_input = if uses_crlf {
117 normalized = input.replace("\r\n", "\n");
118 &normalized
119 } else {
120 input
121 };
122
123 let regions = parser.parse(work_input);
124 let reflow_config = ReflowConfig {
125 max_width: config.max_width,
126 };
127
128 let mut output = reflow(®ions, splitter, &reflow_config);
129
130 if had_trailing_newline && !output.ends_with('\n') {
132 output.push('\n');
133 } else if !had_trailing_newline {
134 while output.ends_with('\n') {
135 output.pop();
136 }
137 }
138
139 if uses_crlf {
141 output = output.replace('\n', "\r\n");
142 }
143
144 Ok(output)
145}
146
147pub fn format_range(
150 input: &str,
151 config: &FormatConfig,
152 start: usize,
153 end: usize,
154) -> Result<String> {
155 let lines: Vec<&str> = input.lines().collect();
156 let total = lines.len();
157
158 let start = start.max(1);
160 let end = end.min(total);
161
162 if start > total {
163 return Ok(input.to_string());
164 }
165
166 let range_text = lines[start - 1..end].join("\n");
168 let formatted = format_text(&range_text, config)?;
169
170 let mut result = String::new();
172 for (i, line) in lines.iter().enumerate() {
173 let line_num = i + 1;
174 if line_num < start {
175 result.push_str(line);
176 result.push('\n');
177 }
178 }
179 result.push_str(&formatted);
180 if !formatted.ends_with('\n') && end < total {
181 result.push('\n');
182 }
183 for (i, line) in lines.iter().enumerate() {
184 let line_num = i + 1;
185 if line_num > end {
186 result.push_str(line);
187 if line_num < total {
188 result.push('\n');
189 }
190 }
191 }
192
193 if input.ends_with('\n') && !result.ends_with('\n') {
195 result.push('\n');
196 } else if !input.ends_with('\n') {
197 while result.ends_with('\n') {
198 result.pop();
199 }
200 }
201
202 Ok(result)
203}