1use crate::ast::Expr;
4use crate::error::{PerlError, PerlResult};
5use crate::parser::parse_format_value_line;
6
7#[derive(Debug, Clone)]
9pub struct FormatTemplate {
10 pub records: Vec<FormatRecord>,
11}
12
13#[derive(Debug, Clone)]
14pub enum FormatRecord {
15 Literal(String),
17 Picture {
19 segments: Vec<PictureSegment>,
20 exprs: Vec<Expr>,
21 },
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum FieldAlign {
26 Left,
27 Right,
28 Center,
29 Numeric,
30 Multiline,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum FieldKind {
35 Text,
36 Numeric,
37 Multiline,
38}
39
40#[derive(Debug, Clone)]
41pub enum PictureSegment {
42 Literal(String),
43 Field {
44 width: usize,
45 align: FieldAlign,
46 kind: FieldKind,
47 },
48}
49
50pub fn parse_format_template(lines: &[String]) -> PerlResult<FormatTemplate> {
52 let mut records = Vec::new();
53 let mut i = 0;
54 while i < lines.len() {
55 let pic_line = &lines[i];
56 if !pic_line.contains('@') {
57 records.push(FormatRecord::Literal(pic_line.clone()));
58 i += 1;
59 continue;
60 }
61 let segments = parse_picture_segments(pic_line)?;
62 let n_fields = segments
63 .iter()
64 .filter(|s| matches!(s, PictureSegment::Field { .. }))
65 .count();
66 i += 1;
67 if i >= lines.len() {
68 return Err(PerlError::syntax(
69 "picture line with @ fields must be followed by a value line",
70 0,
71 ));
72 }
73 let exprs = parse_format_value_line(&lines[i])?;
74 if exprs.len() != n_fields {
75 return Err(PerlError::syntax(
76 format!(
77 "format: {} picture field(s) but {} value expression(s)",
78 n_fields,
79 exprs.len()
80 ),
81 0,
82 ));
83 }
84 records.push(FormatRecord::Picture { segments, exprs });
85 i += 1;
86 }
87 Ok(FormatTemplate { records })
88}
89
90fn parse_picture_segments(pic: &str) -> PerlResult<Vec<PictureSegment>> {
91 let mut out = Vec::new();
92 let mut lit = String::new();
93 let mut chars = pic.chars().peekable();
94 while let Some(c) = chars.next() {
95 if c == '@' {
96 if chars.peek() == Some(&'@') {
97 chars.next();
98 lit.push('@');
99 continue;
100 }
101 if !lit.is_empty() {
102 out.push(PictureSegment::Literal(std::mem::take(&mut lit)));
103 }
104 let mut width = 1usize;
106 let align = match chars.peek() {
107 Some('<') => {
108 while chars.peek() == Some(&'<') {
109 chars.next();
110 width += 1;
111 }
112 FieldAlign::Left
113 }
114 Some('>') => {
115 while chars.peek() == Some(&'>') {
116 chars.next();
117 width += 1;
118 }
119 FieldAlign::Right
120 }
121 Some('|') => {
122 while chars.peek() == Some(&'|') {
123 chars.next();
124 width += 1;
125 }
126 FieldAlign::Center
127 }
128 Some('#') => {
129 while chars.peek() == Some(&'#') {
130 chars.next();
131 width += 1;
132 }
133 FieldAlign::Numeric
134 }
135 Some('*') => {
136 while chars.peek() == Some(&'*') {
137 chars.next();
138 width += 1;
139 }
140 FieldAlign::Multiline
141 }
142 _ => {
143 width = 1;
144 FieldAlign::Left
145 }
146 };
147 let kind = match align {
148 FieldAlign::Numeric => FieldKind::Numeric,
149 FieldAlign::Multiline => FieldKind::Multiline,
150 _ => FieldKind::Text,
151 };
152 out.push(PictureSegment::Field { width, align, kind });
153 } else {
154 lit.push(c);
155 }
156 }
157 if !lit.is_empty() {
158 out.push(PictureSegment::Literal(lit));
159 }
160 Ok(out)
161}
162
163pub fn pad_field(s: &str, width: usize, align: FieldAlign) -> String {
165 let s = if s.chars().count() > width {
166 s.chars().take(width).collect::<String>()
167 } else {
168 s.to_string()
169 };
170 let len = s.chars().count();
171 match align {
172 FieldAlign::Left => {
173 let pad = width.saturating_sub(len);
174 format!("{}{}", s, " ".repeat(pad))
175 }
176 FieldAlign::Multiline => {
177 let first = s.lines().next().unwrap_or("");
178 let fl = first.chars().count();
179 let t = if fl > width {
180 first.chars().take(width).collect::<String>()
181 } else {
182 first.to_string()
183 };
184 let pad = width.saturating_sub(t.chars().count());
185 format!("{}{}", t, " ".repeat(pad))
186 }
187 FieldAlign::Right => {
188 let pad = width.saturating_sub(len);
189 format!("{}{}", " ".repeat(pad), s)
190 }
191 FieldAlign::Center => {
192 let pad = width.saturating_sub(len);
193 let left = pad / 2;
194 let right = pad - left;
195 format!("{}{}{}", " ".repeat(left), s, " ".repeat(right))
196 }
197 FieldAlign::Numeric => {
198 if let Ok(n) = s.parse::<i64>() {
199 format!("{n:>width$}", n = n, width = width)
200 } else if let Ok(f) = s.parse::<f64>() {
201 format!("{f:>width$}", f = f, width = width)
202 } else {
203 let pad = width.saturating_sub(len);
204 format!("{}{}", " ".repeat(pad), s)
205 }
206 }
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213
214 #[test]
215 fn parse_format_template_empty() {
216 let t = parse_format_template(&[]).expect("parse");
217 assert!(t.records.is_empty());
218 }
219
220 #[test]
221 fn parse_format_template_literal_only() {
222 let t = parse_format_template(&["no fields here".to_string()]).expect("parse");
223 assert_eq!(t.records.len(), 1);
224 assert!(matches!(
225 &t.records[0],
226 FormatRecord::Literal(s) if s == "no fields here"
227 ));
228 }
229
230 #[test]
231 fn parse_format_template_picture_and_value_line() {
232 let t =
233 parse_format_template(&["@<<<<".to_string(), r#"qq(ab)"#.to_string()]).expect("parse");
234 assert_eq!(t.records.len(), 1);
235 let FormatRecord::Picture { segments, exprs } = &t.records[0] else {
236 panic!("expected picture");
237 };
238 assert_eq!(exprs.len(), 1);
239 assert_eq!(segments.len(), 1);
240 assert!(matches!(
241 &segments[0],
242 PictureSegment::Field {
243 width: 5,
244 align: FieldAlign::Left,
245 kind: FieldKind::Text,
246 }
247 ));
248 }
249
250 #[test]
251 fn parse_format_template_doubled_at_is_literal_at() {
252 let t = parse_format_template(&["@@email".to_string(), "".to_string()]).expect("parse");
255 let FormatRecord::Picture { segments, exprs } = &t.records[0] else {
256 panic!("expected picture");
257 };
258 assert!(exprs.is_empty());
259 assert!(matches!(
260 segments.as_slice(),
261 [PictureSegment::Literal(s)] if s == "@email"
262 ));
263 }
264
265 #[test]
266 fn parse_format_template_picture_requires_value_line() {
267 let err = parse_format_template(&["@<<<<".to_string()]).expect_err("missing value");
268 assert!(err.to_string().contains("value line"));
269 }
270
271 #[test]
272 fn parse_format_template_field_count_mismatch() {
273 let err = parse_format_template(&["@<<, @<<".to_string(), "1".to_string()])
274 .expect_err("mismatch");
275 assert!(err.to_string().contains("picture field"));
276 }
277
278 #[test]
279 fn parse_format_template_two_fields_two_exprs() {
280 let t = parse_format_template(&["@<< @>>".to_string(), "1, 2".to_string()]).expect("parse");
281 assert_eq!(t.records.len(), 1);
282 let FormatRecord::Picture { exprs, .. } = &t.records[0] else {
283 panic!("expected picture");
284 };
285 assert_eq!(exprs.len(), 2);
286 }
287
288 #[test]
289 fn parse_format_value_line_qq_comma_qq_is_two_exprs() {
290 let v = parse_format_value_line("qq(x), qq(y)").expect("parse");
291 assert_eq!(
292 v.len(),
293 2,
294 "comma-separated qq() should be two value expressions"
295 );
296 }
297
298 #[test]
299 fn parse_format_value_line_rejects_extra_tokens_after_expr() {
300 let err = parse_format_value_line("42 junk").expect_err("extra tokens");
301 assert!(err.to_string().contains("Extra tokens"));
302 }
303
304 #[test]
305 fn parse_picture_numeric_field() {
306 let t = parse_format_template(&["@###".to_string(), "0".to_string()]).expect("parse");
307 let FormatRecord::Picture { segments, .. } = &t.records[0] else {
308 panic!("expected picture");
309 };
310 assert!(matches!(
311 &segments[0],
312 PictureSegment::Field {
313 width: 4,
314 align: FieldAlign::Numeric,
315 kind: FieldKind::Numeric,
316 }
317 ));
318 }
319
320 #[test]
321 fn parse_picture_right_center_multiline_and_bare_at() {
322 let t = parse_format_template(&["@>> @|| @** @".to_string(), "1, 2, 3, 4".to_string()])
323 .expect("parse");
324 let FormatRecord::Picture { segments, .. } = &t.records[0] else {
325 panic!("expected picture");
326 };
327 let fields: Vec<_> = segments
328 .iter()
329 .filter_map(|s| match s {
330 PictureSegment::Field { width, align, kind } => Some((*width, *align, *kind)),
331 _ => None,
332 })
333 .collect();
334 assert_eq!(fields.len(), 4);
335 assert!(matches!(fields[0], (3, FieldAlign::Right, FieldKind::Text)));
336 assert!(matches!(
337 fields[1],
338 (3, FieldAlign::Center, FieldKind::Text)
339 ));
340 assert!(matches!(
341 fields[2],
342 (3, FieldAlign::Multiline, FieldKind::Multiline)
343 ));
344 assert!(matches!(fields[3], (1, FieldAlign::Left, FieldKind::Text)));
345 }
346
347 #[test]
348 fn parse_picture_literal_between_fields() {
349 let t = parse_format_template(&["a@<<b".to_string(), "qq(z)".to_string()]).expect("parse");
350 let FormatRecord::Picture { segments, .. } = &t.records[0] else {
351 panic!("expected picture");
352 };
353 assert!(matches!(&segments[0], PictureSegment::Literal(s) if s == "a"));
354 assert!(matches!(
355 &segments[1],
356 PictureSegment::Field {
357 width: 3,
358 align: FieldAlign::Left,
359 kind: FieldKind::Text,
360 }
361 ));
362 assert!(matches!(&segments[2], PictureSegment::Literal(s) if s == "b"));
363 }
364
365 #[test]
366 fn parse_format_template_literal_after_picture() {
367 let t =
368 parse_format_template(&["@<<".to_string(), "qq(x)".to_string(), "footer".to_string()])
369 .expect("parse");
370 assert_eq!(t.records.len(), 2);
371 assert!(matches!(&t.records[1], FormatRecord::Literal(s) if s == "footer"));
372 }
373
374 #[test]
375 fn pad_field_left_aligns_and_pads() {
376 assert_eq!(pad_field("hi", 5, FieldAlign::Left), "hi ");
377 }
378
379 #[test]
380 fn pad_field_right_aligns() {
381 assert_eq!(pad_field("hi", 5, FieldAlign::Right), " hi");
382 }
383
384 #[test]
385 fn pad_field_center_aligns() {
386 assert_eq!(pad_field("hi", 5, FieldAlign::Center), " hi ");
387 }
388
389 #[test]
390 fn pad_field_numeric_right_aligns_integer() {
391 assert_eq!(pad_field("42", 5, FieldAlign::Numeric), " 42");
392 }
393
394 #[test]
395 fn pad_field_numeric_float() {
396 assert_eq!(pad_field("3.5", 6, FieldAlign::Numeric), " 3.5");
397 }
398
399 #[test]
400 fn pad_field_numeric_non_numeric_fallback_like_right() {
401 assert_eq!(pad_field("n/a", 5, FieldAlign::Numeric), " n/a");
402 }
403
404 #[test]
405 fn pad_field_truncates_to_width() {
406 assert_eq!(pad_field("abcdef", 3, FieldAlign::Left), "abc");
407 }
408
409 #[test]
410 fn pad_field_multiline_uses_first_line() {
411 assert_eq!(
412 pad_field("first\nsecond", 6, FieldAlign::Multiline),
413 "first "
414 );
415 }
416}