1use std::io::Write;
2
3#[derive(Clone)]
5pub enum NumberingStyle {
6 All,
8 NonEmpty,
10 None,
12 Regex(regex::Regex),
14}
15
16#[derive(Clone, Copy, Debug, PartialEq)]
18pub enum NumberFormat {
19 Ln,
21 Rn,
23 Rz,
25}
26
27pub struct NlConfig {
29 pub body_style: NumberingStyle,
30 pub header_style: NumberingStyle,
31 pub footer_style: NumberingStyle,
32 pub section_delimiter: Vec<u8>,
33 pub line_increment: i64,
34 pub join_blank_lines: usize,
35 pub number_format: NumberFormat,
36 pub no_renumber: bool,
37 pub number_separator: Vec<u8>,
38 pub starting_line_number: i64,
39 pub number_width: usize,
40}
41
42impl Default for NlConfig {
43 fn default() -> Self {
44 Self {
45 body_style: NumberingStyle::NonEmpty,
46 header_style: NumberingStyle::None,
47 footer_style: NumberingStyle::None,
48 section_delimiter: vec![b'\\', b':'],
49 line_increment: 1,
50 join_blank_lines: 1,
51 number_format: NumberFormat::Rn,
52 no_renumber: false,
53 number_separator: vec![b'\t'],
54 starting_line_number: 1,
55 number_width: 6,
56 }
57 }
58}
59
60pub fn parse_numbering_style(s: &str) -> Result<NumberingStyle, String> {
62 match s {
63 "a" => Ok(NumberingStyle::All),
64 "t" => Ok(NumberingStyle::NonEmpty),
65 "n" => Ok(NumberingStyle::None),
66 _ if s.starts_with('p') => {
67 let pattern = &s[1..];
68 match regex::Regex::new(pattern) {
69 Ok(re) => Ok(NumberingStyle::Regex(re)),
70 Err(e) => Err(format!("invalid regular expression: {}", e)),
71 }
72 }
73 _ => Err(format!("invalid numbering style: '{}'", s)),
74 }
75}
76
77pub fn parse_number_format(s: &str) -> Result<NumberFormat, String> {
79 match s {
80 "ln" => Ok(NumberFormat::Ln),
81 "rn" => Ok(NumberFormat::Rn),
82 "rz" => Ok(NumberFormat::Rz),
83 _ => Err(format!("invalid line numbering: '{}'", s)),
84 }
85}
86
87#[derive(Clone, Copy, PartialEq)]
89enum Section {
90 Header,
91 Body,
92 Footer,
93}
94
95#[inline]
97fn check_section_delimiter(line: &[u8], delim: &[u8]) -> Option<Section> {
98 if delim.is_empty() {
99 return None;
100 }
101 let dlen = delim.len();
102
103 if line.len() == dlen * 3 {
105 let mut is_header = true;
106 for i in 0..3 {
107 if &line[i * dlen..(i + 1) * dlen] != delim {
108 is_header = false;
109 break;
110 }
111 }
112 if is_header {
113 return Some(Section::Header);
114 }
115 }
116
117 if line.len() == dlen * 2 && &line[..dlen] == delim && &line[dlen..] == delim {
119 return Some(Section::Body);
120 }
121
122 if line.len() == dlen && line == delim {
124 return Some(Section::Footer);
125 }
126
127 None
128}
129
130#[inline]
132fn format_number(num: i64, format: NumberFormat, width: usize, buf: &mut Vec<u8>) {
133 let mut num_buf = itoa::Buffer::new();
134 let num_str = num_buf.format(num);
135
136 match format {
137 NumberFormat::Ln => {
138 buf.extend_from_slice(num_str.as_bytes());
139 let pad = width.saturating_sub(num_str.len());
140 buf.resize(buf.len() + pad, b' ');
141 }
142 NumberFormat::Rn => {
143 let pad = width.saturating_sub(num_str.len());
144 buf.resize(buf.len() + pad, b' ');
145 buf.extend_from_slice(num_str.as_bytes());
146 }
147 NumberFormat::Rz => {
148 if num < 0 {
149 buf.push(b'-');
150 let abs_str = &num_str[1..];
151 let pad = width.saturating_sub(abs_str.len() + 1);
152 buf.resize(buf.len() + pad, b'0');
153 buf.extend_from_slice(abs_str.as_bytes());
154 } else {
155 let pad = width.saturating_sub(num_str.len());
156 buf.resize(buf.len() + pad, b'0');
157 buf.extend_from_slice(num_str.as_bytes());
158 }
159 }
160 }
161}
162
163#[inline]
165fn should_number(line: &[u8], style: &NumberingStyle) -> bool {
166 match style {
167 NumberingStyle::All => true,
168 NumberingStyle::NonEmpty => !line.is_empty(),
169 NumberingStyle::None => false,
170 NumberingStyle::Regex(re) => match std::str::from_utf8(line) {
171 Ok(s) => re.is_match(s),
172 Err(_) => false,
173 },
174 }
175}
176
177pub fn nl_to_vec(data: &[u8], config: &NlConfig) -> Vec<u8> {
179 let mut line_number = config.starting_line_number;
180 nl_to_vec_with_state(data, config, &mut line_number)
181}
182
183pub fn nl_to_vec_with_state(data: &[u8], config: &NlConfig, line_number: &mut i64) -> Vec<u8> {
186 if data.is_empty() {
187 return Vec::new();
188 }
189
190 let estimated_lines = memchr::memchr_iter(b'\n', data).count() + 1;
191 let prefix_size = config.number_width + config.number_separator.len() + 2;
192 let mut output = Vec::with_capacity(data.len() + estimated_lines * prefix_size);
193
194 let mut current_section = Section::Body;
195 let mut consecutive_blanks: usize = 0;
196
197 let mut start = 0;
198 let mut line_iter = memchr::memchr_iter(b'\n', data);
199
200 loop {
201 let (line, has_newline) = match line_iter.next() {
202 Some(pos) => (&data[start..pos], true),
203 None => {
204 if start < data.len() {
205 (&data[start..], false)
206 } else {
207 break;
208 }
209 }
210 };
211
212 if let Some(section) = check_section_delimiter(line, &config.section_delimiter) {
214 if !config.no_renumber {
215 *line_number = config.starting_line_number;
216 }
217 current_section = section;
218 consecutive_blanks = 0;
219 output.push(b'\n');
220 if has_newline {
221 start += line.len() + 1;
222 } else {
223 break;
224 }
225 continue;
226 }
227
228 let style = match current_section {
229 Section::Header => &config.header_style,
230 Section::Body => &config.body_style,
231 Section::Footer => &config.footer_style,
232 };
233
234 let is_blank = line.is_empty();
235
236 if is_blank {
237 consecutive_blanks += 1;
238 } else {
239 consecutive_blanks = 0;
240 }
241
242 let do_number = if is_blank && config.join_blank_lines > 1 {
243 if should_number(line, style) {
244 consecutive_blanks >= config.join_blank_lines
245 } else {
246 false
247 }
248 } else {
249 should_number(line, style)
250 };
251
252 if do_number {
253 if is_blank && config.join_blank_lines > 1 {
254 consecutive_blanks = 0;
255 }
256 format_number(
257 *line_number,
258 config.number_format,
259 config.number_width,
260 &mut output,
261 );
262 output.extend_from_slice(&config.number_separator);
263 output.extend_from_slice(line);
264 *line_number = line_number.wrapping_add(config.line_increment);
265 } else {
266 let total_pad = config.number_width + config.number_separator.len();
268 output.resize(output.len() + total_pad, b' ');
269 output.extend_from_slice(line);
270 }
271
272 if has_newline {
273 output.push(b'\n');
274 start += line.len() + 1;
275 } else {
276 output.push(b'\n');
279 break;
280 }
281 }
282
283 output
284}
285
286pub fn nl(data: &[u8], config: &NlConfig, out: &mut impl Write) -> std::io::Result<()> {
288 let output = nl_to_vec(data, config);
289 out.write_all(&output)
290}