1use shape_ast::error::{Result, ShapeError};
15use shape_value::ValueWord;
16use shape_value::content::{
17 BorderStyle, ChartSpec, ChartType, ContentNode, ContentTable, NamedColor,
18};
19use std::sync::Arc;
20
21pub fn content_text(args: &[ValueWord]) -> Result<ValueWord> {
25 let text = args
26 .first()
27 .and_then(|nb| nb.as_str())
28 .ok_or_else(|| ShapeError::RuntimeError {
29 message: "Content.text() requires a string argument".to_string(),
30 location: None,
31 })?;
32 Ok(ValueWord::from_content(ContentNode::plain(text)))
33}
34
35pub fn content_table(args: &[ValueWord]) -> Result<ValueWord> {
39 let headers_arr = args
41 .first()
42 .and_then(|nb| nb.as_any_array())
43 .ok_or_else(|| ShapeError::RuntimeError {
44 message: "Content.table() requires an array of headers as first argument".to_string(),
45 location: None,
46 })?
47 .to_generic();
48
49 let mut headers = Vec::new();
50 for h in headers_arr.iter() {
51 let s = h.as_str().ok_or_else(|| ShapeError::RuntimeError {
52 message: "Table headers must be strings".to_string(),
53 location: None,
54 })?;
55 headers.push(s.to_string());
56 }
57
58 let rows_arr = args
60 .get(1)
61 .and_then(|nb| nb.as_any_array())
62 .ok_or_else(|| ShapeError::RuntimeError {
63 message: "Content.table() requires an array of rows as second argument".to_string(),
64 location: None,
65 })?
66 .to_generic();
67
68 let mut rows = Vec::new();
69 for row_nb in rows_arr.iter() {
70 let row_arr = row_nb
71 .as_any_array()
72 .ok_or_else(|| ShapeError::RuntimeError {
73 message: "Each table row must be an array".to_string(),
74 location: None,
75 })?
76 .to_generic();
77 let mut cells = Vec::new();
78 for cell in row_arr.iter() {
79 let text = if let Some(s) = cell.as_str() {
81 s.to_string()
82 } else {
83 format!("{}", cell)
84 };
85 cells.push(ContentNode::plain(text));
86 }
87 rows.push(cells);
88 }
89
90 Ok(ValueWord::from_content(ContentNode::Table(ContentTable {
91 headers,
92 rows,
93 border: BorderStyle::default(),
94 max_rows: None,
95 column_types: None,
96 total_rows: None,
97 sortable: false,
98 })))
99}
100
101pub fn content_chart(args: &[ValueWord]) -> Result<ValueWord> {
105 let type_name =
106 args.first()
107 .and_then(|nb| nb.as_str())
108 .ok_or_else(|| ShapeError::RuntimeError {
109 message: "Content.chart() requires a chart type string".to_string(),
110 location: None,
111 })?;
112
113 let chart_type = match type_name.to_lowercase().as_str() {
114 "line" => ChartType::Line,
115 "bar" => ChartType::Bar,
116 "scatter" => ChartType::Scatter,
117 "area" => ChartType::Area,
118 "candlestick" => ChartType::Candlestick,
119 "histogram" => ChartType::Histogram,
120 other => {
121 return Err(ShapeError::RuntimeError {
122 message: format!(
123 "Unknown chart type '{}'. Expected: line, bar, scatter, area, candlestick, histogram",
124 other
125 ),
126 location: None,
127 });
128 }
129 };
130
131 Ok(ValueWord::from_content(ContentNode::Chart(ChartSpec {
132 chart_type,
133 series: vec![],
134 title: None,
135 x_label: None,
136 y_label: None,
137 width: None,
138 height: None,
139 echarts_options: None,
140 interactive: true,
141 })))
142}
143
144pub fn content_code(args: &[ValueWord]) -> Result<ValueWord> {
148 let language = args.first().and_then(|nb| {
149 if nb.is_none() {
150 None
151 } else {
152 nb.as_str().map(|s| s.to_string())
153 }
154 });
155
156 let source =
157 args.get(1)
158 .and_then(|nb| nb.as_str())
159 .ok_or_else(|| ShapeError::RuntimeError {
160 message: "Content.code() requires a source string as second argument".to_string(),
161 location: None,
162 })?;
163
164 Ok(ValueWord::from_content(ContentNode::Code {
165 language,
166 source: source.to_string(),
167 }))
168}
169
170pub fn content_kv(args: &[ValueWord]) -> Result<ValueWord> {
175 let pairs_arr = args
176 .first()
177 .and_then(|nb| nb.as_any_array())
178 .ok_or_else(|| ShapeError::RuntimeError {
179 message: "Content.kv() requires an array of [key, value] pairs".to_string(),
180 location: None,
181 })?
182 .to_generic();
183
184 let mut pairs = Vec::new();
185 for pair_nb in pairs_arr.iter() {
186 let pair_arr = pair_nb
187 .as_any_array()
188 .ok_or_else(|| ShapeError::RuntimeError {
189 message: "Each kv pair must be a [key, value] array".to_string(),
190 location: None,
191 })?
192 .to_generic();
193 let key = pair_arr
194 .first()
195 .and_then(|nb| nb.as_str())
196 .ok_or_else(|| ShapeError::RuntimeError {
197 message: "Key in kv pair must be a string".to_string(),
198 location: None,
199 })?
200 .to_string();
201
202 let value_nb = pair_arr.get(1).ok_or_else(|| ShapeError::RuntimeError {
203 message: "Each kv pair must have both key and value".to_string(),
204 location: None,
205 })?;
206
207 let value = if let Some(content) = value_nb.as_content() {
209 content.clone()
210 } else if let Some(s) = value_nb.as_str() {
211 ContentNode::plain(s)
212 } else {
213 ContentNode::plain(format!("{}", value_nb))
214 };
215
216 pairs.push((key, value));
217 }
218
219 Ok(ValueWord::from_content(ContentNode::KeyValue(pairs)))
220}
221
222pub fn content_fragment(args: &[ValueWord]) -> Result<ValueWord> {
226 let parts_arr = args
227 .first()
228 .and_then(|nb| nb.as_any_array())
229 .ok_or_else(|| ShapeError::RuntimeError {
230 message: "Content.fragment() requires an array of content nodes".to_string(),
231 location: None,
232 })?
233 .to_generic();
234
235 let mut parts = Vec::new();
236 for part_nb in parts_arr.iter() {
237 if let Some(content) = part_nb.as_content() {
238 parts.push(content.clone());
239 } else if let Some(s) = part_nb.as_str() {
240 parts.push(ContentNode::plain(s));
241 } else {
242 parts.push(ContentNode::plain(format!("{}", part_nb)));
243 }
244 }
245
246 Ok(ValueWord::from_content(ContentNode::Fragment(parts)))
247}
248
249pub fn color_named(name: &str) -> Result<ValueWord> {
257 let _: NamedColor = match name.to_lowercase().as_str() {
259 "red" => NamedColor::Red,
260 "green" => NamedColor::Green,
261 "blue" => NamedColor::Blue,
262 "yellow" => NamedColor::Yellow,
263 "magenta" => NamedColor::Magenta,
264 "cyan" => NamedColor::Cyan,
265 "white" => NamedColor::White,
266 "default" => NamedColor::Default,
267 _ => {
268 return Err(ShapeError::RuntimeError {
269 message: format!("Unknown color name '{}'", name),
270 location: None,
271 });
272 }
273 };
274 Ok(ValueWord::from_string(Arc::new(name.to_lowercase())))
275}
276
277pub fn color_rgb(args: &[ValueWord]) -> Result<ValueWord> {
279 let r = args
280 .first()
281 .and_then(|nb| nb.as_number_coerce())
282 .ok_or_else(|| ShapeError::RuntimeError {
283 message: "Color.rgb() requires numeric r argument".to_string(),
284 location: None,
285 })? as u8;
286 let g = args
287 .get(1)
288 .and_then(|nb| nb.as_number_coerce())
289 .ok_or_else(|| ShapeError::RuntimeError {
290 message: "Color.rgb() requires numeric g argument".to_string(),
291 location: None,
292 })? as u8;
293 let b = args
294 .get(2)
295 .and_then(|nb| nb.as_number_coerce())
296 .ok_or_else(|| ShapeError::RuntimeError {
297 message: "Color.rgb() requires numeric b argument".to_string(),
298 location: None,
299 })? as u8;
300 Ok(ValueWord::from_string(Arc::new(format!(
301 "rgb({},{},{})",
302 r, g, b
303 ))))
304}
305
306pub fn border_named(name: &str) -> Result<ValueWord> {
309 match name.to_lowercase().as_str() {
310 "rounded" | "sharp" | "heavy" | "double" | "minimal" | "none" => {}
311 _ => {
312 return Err(ShapeError::RuntimeError {
313 message: format!("Unknown border style '{}'", name),
314 location: None,
315 });
316 }
317 }
318 Ok(ValueWord::from_string(Arc::new(name.to_lowercase())))
319}
320
321pub fn chart_type_named(name: &str) -> Result<ValueWord> {
324 match name.to_lowercase().as_str() {
325 "line" | "bar" | "scatter" | "area" | "candlestick" | "histogram" => {}
326 _ => {
327 return Err(ShapeError::RuntimeError {
328 message: format!("Unknown chart type '{}'", name),
329 location: None,
330 });
331 }
332 }
333 Ok(ValueWord::from_string(Arc::new(name.to_lowercase())))
334}
335
336pub fn align_named(name: &str) -> Result<ValueWord> {
339 match name.to_lowercase().as_str() {
340 "left" | "center" | "right" => {}
341 _ => {
342 return Err(ShapeError::RuntimeError {
343 message: format!("Unknown alignment '{}'", name),
344 location: None,
345 });
346 }
347 }
348 Ok(ValueWord::from_string(Arc::new(name.to_lowercase())))
349}
350
351#[cfg(test)]
352mod tests {
353 use super::*;
354 use shape_value::content::ChartType;
355 use std::sync::Arc;
356
357 fn nb_str(s: &str) -> ValueWord {
358 ValueWord::from_string(Arc::new(s.to_string()))
359 }
360
361 #[test]
362 fn test_content_text() {
363 let result = content_text(&[nb_str("hello")]).unwrap();
364 let node = result.as_content().unwrap();
365 assert_eq!(node.to_string(), "hello");
366 }
367
368 #[test]
369 fn test_content_text_missing_arg() {
370 assert!(content_text(&[]).is_err());
371 }
372
373 #[test]
374 fn test_content_table() {
375 let headers = ValueWord::from_array(Arc::new(vec![nb_str("Name"), nb_str("Value")]));
376 let row1 = ValueWord::from_array(Arc::new(vec![nb_str("a"), nb_str("1")]));
377 let rows = ValueWord::from_array(Arc::new(vec![row1]));
378 let result = content_table(&[headers, rows]).unwrap();
379 let node = result.as_content().unwrap();
380 match node {
381 ContentNode::Table(t) => {
382 assert_eq!(t.headers, vec!["Name", "Value"]);
383 assert_eq!(t.rows.len(), 1);
384 assert_eq!(t.border, BorderStyle::Rounded);
385 }
386 _ => panic!("expected Table"),
387 }
388 }
389
390 #[test]
391 fn test_content_chart() {
392 let result = content_chart(&[nb_str("line")]).unwrap();
393 let node = result.as_content().unwrap();
394 match node {
395 ContentNode::Chart(spec) => {
396 assert_eq!(spec.chart_type, ChartType::Line);
397 assert!(spec.series.is_empty());
398 }
399 _ => panic!("expected Chart"),
400 }
401 }
402
403 #[test]
404 fn test_content_chart_invalid_type() {
405 assert!(content_chart(&[nb_str("pie")]).is_err());
406 }
407
408 #[test]
409 fn test_content_code() {
410 let result = content_code(&[nb_str("rust"), nb_str("fn main() {}")]).unwrap();
411 let node = result.as_content().unwrap();
412 match node {
413 ContentNode::Code { language, source } => {
414 assert_eq!(language.as_deref(), Some("rust"));
415 assert_eq!(source, "fn main() {}");
416 }
417 _ => panic!("expected Code"),
418 }
419 }
420
421 #[test]
422 fn test_content_code_no_language() {
423 let result = content_code(&[ValueWord::none(), nb_str("plain text")]).unwrap();
424 let node = result.as_content().unwrap();
425 match node {
426 ContentNode::Code { language, source } => {
427 assert!(language.is_none());
428 assert_eq!(source, "plain text");
429 }
430 _ => panic!("expected Code"),
431 }
432 }
433
434 #[test]
435 fn test_content_kv() {
436 let pair1 = ValueWord::from_array(Arc::new(vec![nb_str("name"), nb_str("Alice")]));
437 let pair2 = ValueWord::from_array(Arc::new(vec![nb_str("age"), nb_str("30")]));
438 let pairs = ValueWord::from_array(Arc::new(vec![pair1, pair2]));
439 let result = content_kv(&[pairs]).unwrap();
440 let node = result.as_content().unwrap();
441 match node {
442 ContentNode::KeyValue(kv) => {
443 assert_eq!(kv.len(), 2);
444 assert_eq!(kv[0].0, "name");
445 assert_eq!(kv[1].0, "age");
446 }
447 _ => panic!("expected KeyValue"),
448 }
449 }
450
451 #[test]
452 fn test_content_fragment() {
453 let n1 = ValueWord::from_content(ContentNode::plain("hello "));
454 let n2 = ValueWord::from_content(ContentNode::plain("world"));
455 let parts = ValueWord::from_array(Arc::new(vec![n1, n2]));
456 let result = content_fragment(&[parts]).unwrap();
457 let node = result.as_content().unwrap();
458 match node {
459 ContentNode::Fragment(parts) => {
460 assert_eq!(parts.len(), 2);
461 assert_eq!(parts[0].to_string(), "hello ");
462 assert_eq!(parts[1].to_string(), "world");
463 }
464 _ => panic!("expected Fragment"),
465 }
466 }
467
468 #[test]
469 fn test_content_fragment_with_string_coercion() {
470 let parts = ValueWord::from_array(Arc::new(vec![nb_str("text")]));
471 let result = content_fragment(&[parts]).unwrap();
472 let node = result.as_content().unwrap();
473 match node {
474 ContentNode::Fragment(parts) => {
475 assert_eq!(parts.len(), 1);
476 assert_eq!(parts[0].to_string(), "text");
477 }
478 _ => panic!("expected Fragment"),
479 }
480 }
481
482 #[test]
483 fn test_color_named_valid() {
484 let result = color_named("red").unwrap();
485 assert_eq!(result.as_str().unwrap(), "red");
486 }
487
488 #[test]
489 fn test_color_named_case_insensitive() {
490 let result = color_named("GREEN").unwrap();
491 assert_eq!(result.as_str().unwrap(), "green");
492 }
493
494 #[test]
495 fn test_color_named_invalid() {
496 assert!(color_named("purple").is_err());
497 }
498
499 #[test]
500 fn test_color_rgb() {
501 let result = color_rgb(&[
502 ValueWord::from_f64(255.0),
503 ValueWord::from_f64(128.0),
504 ValueWord::from_f64(0.0),
505 ])
506 .unwrap();
507 assert_eq!(result.as_str().unwrap(), "rgb(255,128,0)");
508 }
509
510 #[test]
511 fn test_border_named_valid() {
512 assert_eq!(
513 border_named("rounded").unwrap().as_str().unwrap(),
514 "rounded"
515 );
516 assert_eq!(border_named("Heavy").unwrap().as_str().unwrap(), "heavy");
517 }
518
519 #[test]
520 fn test_border_named_invalid() {
521 assert!(border_named("dotted").is_err());
522 }
523
524 #[test]
525 fn test_chart_type_named_valid() {
526 assert_eq!(chart_type_named("line").unwrap().as_str().unwrap(), "line");
527 assert_eq!(chart_type_named("Bar").unwrap().as_str().unwrap(), "bar");
528 }
529
530 #[test]
531 fn test_chart_type_named_invalid() {
532 assert!(chart_type_named("pie").is_err());
533 }
534
535 #[test]
536 fn test_align_named_valid() {
537 assert_eq!(align_named("left").unwrap().as_str().unwrap(), "left");
538 assert_eq!(align_named("Center").unwrap().as_str().unwrap(), "center");
539 assert_eq!(align_named("RIGHT").unwrap().as_str().unwrap(), "right");
540 }
541
542 #[test]
543 fn test_align_named_invalid() {
544 assert!(align_named("justify").is_err());
545 }
546}