1use crate::{Result, ShapeError};
29
30#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct ContentFormatSpec {
41 pub fg: Option<ColorSpec>,
42 pub bg: Option<ColorSpec>,
43 pub bold: bool,
44 pub italic: bool,
45 pub underline: bool,
46 pub dim: bool,
47 pub fixed_precision: Option<u8>,
48 pub border: Option<BorderStyleSpec>,
49 pub max_rows: Option<usize>,
50 pub align: Option<AlignSpec>,
51 pub chart_type: Option<ChartTypeSpec>,
53 pub x_column: Option<String>,
55 pub y_columns: Vec<String>,
57}
58
59impl Default for ContentFormatSpec {
60 fn default() -> Self {
61 Self {
62 fg: None,
63 bg: None,
64 bold: false,
65 italic: false,
66 underline: false,
67 dim: false,
68 fixed_precision: None,
69 border: None,
70 max_rows: None,
71 align: None,
72 chart_type: None,
73 x_column: None,
74 y_columns: vec![],
75 }
76 }
77}
78
79#[derive(Debug, Clone, PartialEq, Eq)]
81pub enum ColorSpec {
82 Named(NamedContentColor),
83 Rgb(u8, u8, u8),
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub enum NamedContentColor {
89 Red,
90 Green,
91 Blue,
92 Yellow,
93 Magenta,
94 Cyan,
95 White,
96 Default,
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101pub enum BorderStyleSpec {
102 Rounded,
103 Sharp,
104 Heavy,
105 Double,
106 Minimal,
107 None,
108}
109
110#[derive(Debug, Clone, Copy, PartialEq, Eq)]
112pub enum AlignSpec {
113 Left,
114 Center,
115 Right,
116}
117
118#[derive(Debug, Clone, PartialEq, Eq)]
120pub enum ChartTypeSpec {
121 Line,
122 Bar,
123 Scatter,
124 Area,
125 Histogram,
126}
127
128pub fn parse_content_format_spec(raw_spec: &str) -> Result<ContentFormatSpec> {
138 let mut spec = ContentFormatSpec::default();
139 let trimmed = raw_spec.trim();
140 if trimmed.is_empty() {
141 return Ok(spec);
142 }
143
144 for entry in split_top_level_commas(trimmed)? {
145 let entry = entry.trim();
146 if entry.is_empty() {
147 continue;
148 }
149
150 match entry {
152 "bold" => {
153 spec.bold = true;
154 continue;
155 }
156 "italic" => {
157 spec.italic = true;
158 continue;
159 }
160 "underline" => {
161 spec.underline = true;
162 continue;
163 }
164 "dim" => {
165 spec.dim = true;
166 continue;
167 }
168 _ => {}
169 }
170
171 if let Ok(color) = parse_color_spec(entry) {
173 spec.fg = Some(color);
174 continue;
175 }
176
177 if let Some(idx) = entry.find('(') {
179 if !entry.ends_with(')') {
180 return Err(ShapeError::RuntimeError {
181 message: format!("Unclosed parenthesis in content format spec '{}'", entry),
182 location: None,
183 });
184 }
185 let key = entry[..idx].trim();
186 let inner = entry[idx + 1..entry.len() - 1].trim();
187 match key {
188 "fg" => {
189 spec.fg = Some(parse_color_spec(inner)?);
190 }
191 "bg" => {
192 spec.bg = Some(parse_color_spec(inner)?);
193 }
194 "fixed" => {
195 spec.fixed_precision = Some(parse_u8_value(inner, "fixed precision")?);
196 }
197 "border" => {
198 spec.border = Some(parse_border_style_spec(inner)?);
199 }
200 "max_rows" => {
201 spec.max_rows = Some(parse_usize_value(inner, "max_rows")?);
202 }
203 "align" => {
204 spec.align = Some(parse_align_spec(inner)?);
205 }
206 "chart" => {
207 spec.chart_type = Some(parse_chart_type_spec(inner)?);
208 }
209 "x" => {
210 spec.x_column = Some(inner.to_string());
211 }
212 "y" => {
213 spec.y_columns = inner
215 .split(',')
216 .map(|s| s.trim().to_string())
217 .filter(|s| !s.is_empty())
218 .collect();
219 }
220 other => {
221 return Err(ShapeError::RuntimeError {
222 message: format!(
223 "Unknown content format key '{}'. Supported: fg, bg, bold, italic, underline, dim, fixed, border, max_rows, align, chart, x, y.",
224 other
225 ),
226 location: None,
227 });
228 }
229 }
230 continue;
231 }
232
233 return Err(ShapeError::RuntimeError {
234 message: format!(
235 "Unknown content format entry '{}'. Expected a flag (bold, italic, ...) or key(value).",
236 entry
237 ),
238 location: None,
239 });
240 }
241
242 Ok(spec)
243}
244
245pub fn parse_color_spec(s: &str) -> Result<ColorSpec> {
246 let s = s.trim();
247 if s.starts_with("rgb(") && s.ends_with(')') {
249 let inner = &s[4..s.len() - 1];
250 let parts: Vec<&str> = inner.split(',').map(|p| p.trim()).collect();
251 if parts.len() != 3 {
252 return Err(ShapeError::RuntimeError {
253 message: format!("rgb() expects 3 values, got {}", parts.len()),
254 location: None,
255 });
256 }
257 let r = parse_u8_value(parts[0], "red")?;
258 let g = parse_u8_value(parts[1], "green")?;
259 let b = parse_u8_value(parts[2], "blue")?;
260 return Ok(ColorSpec::Rgb(r, g, b));
261 }
262 match s {
264 "red" => Ok(ColorSpec::Named(NamedContentColor::Red)),
265 "green" => Ok(ColorSpec::Named(NamedContentColor::Green)),
266 "blue" => Ok(ColorSpec::Named(NamedContentColor::Blue)),
267 "yellow" => Ok(ColorSpec::Named(NamedContentColor::Yellow)),
268 "magenta" => Ok(ColorSpec::Named(NamedContentColor::Magenta)),
269 "cyan" => Ok(ColorSpec::Named(NamedContentColor::Cyan)),
270 "white" => Ok(ColorSpec::Named(NamedContentColor::White)),
271 "default" => Ok(ColorSpec::Named(NamedContentColor::Default)),
272 _ => Err(ShapeError::RuntimeError {
273 message: format!(
274 "Unknown color '{}'. Expected: red, green, blue, yellow, magenta, cyan, white, default, or rgb(r,g,b).",
275 s
276 ),
277 location: None,
278 }),
279 }
280}
281
282pub fn parse_border_style_spec(s: &str) -> Result<BorderStyleSpec> {
283 match s.trim() {
284 "rounded" => Ok(BorderStyleSpec::Rounded),
285 "sharp" => Ok(BorderStyleSpec::Sharp),
286 "heavy" => Ok(BorderStyleSpec::Heavy),
287 "double" => Ok(BorderStyleSpec::Double),
288 "minimal" => Ok(BorderStyleSpec::Minimal),
289 "none" => Ok(BorderStyleSpec::None),
290 _ => Err(ShapeError::RuntimeError {
291 message: format!(
292 "Unknown border style '{}'. Expected: rounded, sharp, heavy, double, minimal, none.",
293 s
294 ),
295 location: None,
296 }),
297 }
298}
299
300pub fn parse_align_spec(s: &str) -> Result<AlignSpec> {
301 match s.trim() {
302 "left" => Ok(AlignSpec::Left),
303 "center" => Ok(AlignSpec::Center),
304 "right" => Ok(AlignSpec::Right),
305 _ => Err(ShapeError::RuntimeError {
306 message: format!(
307 "Unknown align value '{}'. Expected: left, center, right.",
308 s
309 ),
310 location: None,
311 }),
312 }
313}
314
315pub fn parse_chart_type_spec(s: &str) -> Result<ChartTypeSpec> {
316 match s.trim().to_lowercase().as_str() {
317 "line" => Ok(ChartTypeSpec::Line),
318 "bar" => Ok(ChartTypeSpec::Bar),
319 "scatter" => Ok(ChartTypeSpec::Scatter),
320 "area" => Ok(ChartTypeSpec::Area),
321 "histogram" => Ok(ChartTypeSpec::Histogram),
322 _ => Err(ShapeError::RuntimeError {
323 message: format!(
324 "Unknown chart type '{}'. Expected: line, bar, scatter, area, histogram.",
325 s
326 ),
327 location: None,
328 }),
329 }
330}
331
332fn split_top_level_commas(s: &str) -> Result<Vec<&str>> {
337 let mut parts = Vec::new();
338 let mut start = 0usize;
339 let mut paren_depth = 0usize;
340 let mut brace_depth = 0usize;
341 let mut bracket_depth = 0usize;
342 let mut in_string: Option<char> = None;
343 let mut escaped = false;
344
345 for (idx, ch) in s.char_indices() {
346 if let Some(quote) = in_string {
347 if escaped {
348 escaped = false;
349 continue;
350 }
351 if ch == '\\' {
352 escaped = true;
353 continue;
354 }
355 if ch == quote {
356 in_string = None;
357 }
358 continue;
359 }
360
361 match ch {
362 '"' | '\'' => in_string = Some(ch),
363 '(' => paren_depth += 1,
364 ')' => paren_depth = paren_depth.saturating_sub(1),
365 '{' => brace_depth += 1,
366 '}' => brace_depth = brace_depth.saturating_sub(1),
367 '[' => bracket_depth += 1,
368 ']' => bracket_depth = bracket_depth.saturating_sub(1),
369 ',' if paren_depth == 0 && brace_depth == 0 && bracket_depth == 0 => {
370 parts.push(&s[start..idx]);
371 start = idx + 1;
372 }
373 _ => {}
374 }
375 }
376
377 if in_string.is_some() || paren_depth != 0 || brace_depth != 0 || bracket_depth != 0 {
378 return Err(ShapeError::RuntimeError {
379 message: "Unclosed delimiter in content format spec".to_string(),
380 location: None,
381 });
382 }
383
384 parts.push(&s[start..]);
385 Ok(parts)
386}
387
388fn parse_u8_value(value: &str, label: &str) -> Result<u8> {
389 value.parse::<u8>().map_err(|_| ShapeError::RuntimeError {
390 message: format!(
391 "Invalid {} '{}'. Expected an integer in range 0..=255.",
392 label, value
393 ),
394 location: None,
395 })
396}
397
398fn parse_usize_value(value: &str, label: &str) -> Result<usize> {
399 value
400 .parse::<usize>()
401 .map_err(|_| ShapeError::RuntimeError {
402 message: format!(
403 "Invalid {} '{}'. Expected a non-negative integer.",
404 label, value
405 ),
406 location: None,
407 })
408}
409
410#[cfg(test)]
415mod tests {
416 use super::*;
417
418 #[test]
419 fn parse_content_format_spec_bold() {
420 let spec = parse_content_format_spec("bold").unwrap();
421 assert!(spec.bold);
422 assert!(!spec.italic);
423 }
424
425 #[test]
426 fn parse_content_format_spec_multiple_flags() {
427 let spec = parse_content_format_spec("bold, italic, underline").unwrap();
428 assert!(spec.bold);
429 assert!(spec.italic);
430 assert!(spec.underline);
431 assert!(!spec.dim);
432 }
433
434 #[test]
435 fn parse_content_format_spec_fg_named() {
436 let spec = parse_content_format_spec("fg(red)").unwrap();
437 assert_eq!(spec.fg, Some(ColorSpec::Named(NamedContentColor::Red)));
438 }
439
440 #[test]
441 fn parse_content_format_spec_fg_rgb() {
442 let spec = parse_content_format_spec("fg(rgb(255, 128, 0))").unwrap();
443 assert_eq!(spec.fg, Some(ColorSpec::Rgb(255, 128, 0)));
444 }
445
446 #[test]
447 fn parse_content_format_spec_full() {
448 let spec = parse_content_format_spec(
449 "fg(green), bg(blue), bold, fixed(2), border(rounded), align(center)",
450 )
451 .unwrap();
452 assert_eq!(spec.fg, Some(ColorSpec::Named(NamedContentColor::Green)));
453 assert_eq!(spec.bg, Some(ColorSpec::Named(NamedContentColor::Blue)));
454 assert!(spec.bold);
455 assert_eq!(spec.fixed_precision, Some(2));
456 assert_eq!(spec.border, Some(BorderStyleSpec::Rounded));
457 assert_eq!(spec.align, Some(AlignSpec::Center));
458 }
459
460 #[test]
461 fn parse_content_format_spec_unknown_key_errors() {
462 let err = parse_content_format_spec("foo(bar)").unwrap_err();
463 assert!(err.to_string().contains("Unknown content format key"));
464 }
465
466 #[test]
467 fn parse_content_format_spec_chart_type() {
468 let spec = parse_content_format_spec("chart(bar)").unwrap();
469 assert_eq!(spec.chart_type, Some(ChartTypeSpec::Bar));
470 }
471
472 #[test]
473 fn parse_content_format_spec_chart_with_axes() {
474 let spec = parse_content_format_spec("chart(line), x(month), y(revenue, profit)").unwrap();
475 assert_eq!(spec.chart_type, Some(ChartTypeSpec::Line));
476 assert_eq!(spec.x_column, Some("month".to_string()));
477 assert_eq!(spec.y_columns, vec!["revenue", "profit"]);
478 }
479
480 #[test]
481 fn parse_content_format_spec_chart_invalid_type() {
482 let err = parse_content_format_spec("chart(pie)").unwrap_err();
483 assert!(err.to_string().contains("Unknown chart type"));
484 }
485
486 #[test]
487 fn parse_color_spec_named() {
488 assert_eq!(
489 parse_color_spec("red").unwrap(),
490 ColorSpec::Named(NamedContentColor::Red)
491 );
492 assert_eq!(
493 parse_color_spec("default").unwrap(),
494 ColorSpec::Named(NamedContentColor::Default)
495 );
496 }
497
498 #[test]
499 fn parse_color_spec_rgb() {
500 assert_eq!(
501 parse_color_spec("rgb(10, 20, 30)").unwrap(),
502 ColorSpec::Rgb(10, 20, 30)
503 );
504 }
505
506 #[test]
507 fn parse_color_spec_invalid() {
508 assert!(parse_color_spec("octarine").is_err());
509 assert!(parse_color_spec("rgb(1, 2)").is_err());
510 assert!(parse_color_spec("rgb(300, 0, 0)").is_err());
511 }
512
513 #[test]
514 fn parse_border_style_all() {
515 assert_eq!(parse_border_style_spec("rounded").unwrap(), BorderStyleSpec::Rounded);
516 assert_eq!(parse_border_style_spec("sharp").unwrap(), BorderStyleSpec::Sharp);
517 assert_eq!(parse_border_style_spec("heavy").unwrap(), BorderStyleSpec::Heavy);
518 assert_eq!(parse_border_style_spec("double").unwrap(), BorderStyleSpec::Double);
519 assert_eq!(parse_border_style_spec("minimal").unwrap(), BorderStyleSpec::Minimal);
520 assert_eq!(parse_border_style_spec("none").unwrap(), BorderStyleSpec::None);
521 assert!(parse_border_style_spec("triple").is_err());
522 }
523
524 #[test]
525 fn parse_align_all() {
526 assert_eq!(parse_align_spec("left").unwrap(), AlignSpec::Left);
527 assert_eq!(parse_align_spec("center").unwrap(), AlignSpec::Center);
528 assert_eq!(parse_align_spec("right").unwrap(), AlignSpec::Right);
529 assert!(parse_align_spec("justify").is_err());
530 }
531
532 #[test]
533 fn parse_chart_type_all() {
534 assert_eq!(parse_chart_type_spec("line").unwrap(), ChartTypeSpec::Line);
535 assert_eq!(parse_chart_type_spec("bar").unwrap(), ChartTypeSpec::Bar);
536 assert_eq!(parse_chart_type_spec("scatter").unwrap(), ChartTypeSpec::Scatter);
537 assert_eq!(parse_chart_type_spec("area").unwrap(), ChartTypeSpec::Area);
538 assert_eq!(parse_chart_type_spec("histogram").unwrap(), ChartTypeSpec::Histogram);
539 assert!(parse_chart_type_spec("pie").is_err());
540 }
541
542 #[test]
543 fn parse_content_format_spec_top_level_color_shorthand() {
544 let spec = parse_content_format_spec("red").unwrap();
546 assert_eq!(spec.fg, Some(ColorSpec::Named(NamedContentColor::Red)));
547 }
548
549 #[test]
550 fn parse_content_format_spec_bold_red_shorthand() {
551 let spec = parse_content_format_spec("bold, red").unwrap();
553 assert!(spec.bold);
554 assert_eq!(spec.fg, Some(ColorSpec::Named(NamedContentColor::Red)));
555 }
556}