1use shape_ast::error::{Result, ShapeError};
13use shape_value::ValueWord;
14use shape_value::content::{BorderStyle, ChartSeries, Color, ContentNode, NamedColor};
15
16pub fn call_content_method(
20 method_name: &str,
21 receiver: ValueWord,
22 args: Vec<ValueWord>,
23) -> Option<Result<ValueWord>> {
24 match method_name {
25 "fg" => Some(handle_fg(receiver, args)),
27 "bg" => Some(handle_bg(receiver, args)),
28 "bold" => Some(handle_bold(receiver, args)),
29 "italic" => Some(handle_italic(receiver, args)),
30 "underline" => Some(handle_underline(receiver, args)),
31 "dim" => Some(handle_dim(receiver, args)),
32 "border" => Some(handle_border(receiver, args)),
34 "max_rows" | "maxRows" => Some(handle_max_rows(receiver, args)),
35 "series" => Some(handle_series(receiver, args)),
37 "title" => Some(handle_title(receiver, args)),
38 "x_label" | "xLabel" => Some(handle_x_label(receiver, args)),
39 "y_label" | "yLabel" => Some(handle_y_label(receiver, args)),
40 _ => None,
41 }
42}
43
44fn parse_color(s: &str) -> Result<Color> {
46 match s.to_lowercase().as_str() {
47 "red" => Ok(Color::Named(NamedColor::Red)),
48 "green" => Ok(Color::Named(NamedColor::Green)),
49 "blue" => Ok(Color::Named(NamedColor::Blue)),
50 "yellow" => Ok(Color::Named(NamedColor::Yellow)),
51 "magenta" => Ok(Color::Named(NamedColor::Magenta)),
52 "cyan" => Ok(Color::Named(NamedColor::Cyan)),
53 "white" => Ok(Color::Named(NamedColor::White)),
54 "default" => Ok(Color::Named(NamedColor::Default)),
55 other => Err(ShapeError::RuntimeError {
56 message: format!(
57 "Unknown color '{}'. Expected: red, green, blue, yellow, magenta, cyan, white, default",
58 other
59 ),
60 location: None,
61 }),
62 }
63}
64
65fn extract_content(receiver: &ValueWord) -> Result<ContentNode> {
67 receiver
68 .as_content()
69 .cloned()
70 .ok_or_else(|| ShapeError::RuntimeError {
71 message: "Expected a ContentNode receiver".to_string(),
72 location: None,
73 })
74}
75
76fn require_string_arg(args: &[ValueWord], index: usize, label: &str) -> Result<String> {
78 args.get(index)
79 .and_then(|nb| nb.as_str().map(|s| s.to_string()))
80 .ok_or_else(|| ShapeError::RuntimeError {
81 message: format!("{} requires a string argument", label),
82 location: None,
83 })
84}
85
86fn handle_fg(receiver: ValueWord, args: Vec<ValueWord>) -> Result<ValueWord> {
87 let node = extract_content(&receiver)?;
88 let color_name = require_string_arg(&args, 0, "fg")?;
89 let color = parse_color(&color_name)?;
90 Ok(ValueWord::from_content(node.with_fg(color)))
91}
92
93fn handle_bg(receiver: ValueWord, args: Vec<ValueWord>) -> Result<ValueWord> {
94 let node = extract_content(&receiver)?;
95 let color_name = require_string_arg(&args, 0, "bg")?;
96 let color = parse_color(&color_name)?;
97 Ok(ValueWord::from_content(node.with_bg(color)))
98}
99
100fn handle_bold(receiver: ValueWord, _args: Vec<ValueWord>) -> Result<ValueWord> {
101 let node = extract_content(&receiver)?;
102 Ok(ValueWord::from_content(node.with_bold()))
103}
104
105fn handle_italic(receiver: ValueWord, _args: Vec<ValueWord>) -> Result<ValueWord> {
106 let node = extract_content(&receiver)?;
107 Ok(ValueWord::from_content(node.with_italic()))
108}
109
110fn handle_underline(receiver: ValueWord, _args: Vec<ValueWord>) -> Result<ValueWord> {
111 let node = extract_content(&receiver)?;
112 Ok(ValueWord::from_content(node.with_underline()))
113}
114
115fn handle_dim(receiver: ValueWord, _args: Vec<ValueWord>) -> Result<ValueWord> {
116 let node = extract_content(&receiver)?;
117 Ok(ValueWord::from_content(node.with_dim()))
118}
119
120fn handle_border(receiver: ValueWord, args: Vec<ValueWord>) -> Result<ValueWord> {
121 let node = extract_content(&receiver)?;
122 let style_name = require_string_arg(&args, 0, "border")?;
123 let border = match style_name.to_lowercase().as_str() {
124 "rounded" => BorderStyle::Rounded,
125 "sharp" => BorderStyle::Sharp,
126 "heavy" => BorderStyle::Heavy,
127 "double" => BorderStyle::Double,
128 "minimal" => BorderStyle::Minimal,
129 "none" => BorderStyle::None,
130 other => {
131 return Err(ShapeError::RuntimeError {
132 message: format!(
133 "Unknown border style '{}'. Expected: rounded, sharp, heavy, double, minimal, none",
134 other
135 ),
136 location: None,
137 });
138 }
139 };
140 match node {
141 ContentNode::Table(mut table) => {
142 table.border = border;
143 Ok(ValueWord::from_content(ContentNode::Table(table)))
144 }
145 other => Ok(ValueWord::from_content(other)),
146 }
147}
148
149fn handle_max_rows(receiver: ValueWord, args: Vec<ValueWord>) -> Result<ValueWord> {
150 let node = extract_content(&receiver)?;
151 let n = args
152 .first()
153 .and_then(|nb| nb.as_number_coerce())
154 .ok_or_else(|| ShapeError::RuntimeError {
155 message: "max_rows requires a numeric argument".to_string(),
156 location: None,
157 })? as usize;
158 match node {
159 ContentNode::Table(mut table) => {
160 table.max_rows = Some(n);
161 Ok(ValueWord::from_content(ContentNode::Table(table)))
162 }
163 other => Ok(ValueWord::from_content(other)),
164 }
165}
166
167fn handle_series(receiver: ValueWord, args: Vec<ValueWord>) -> Result<ValueWord> {
168 let node = extract_content(&receiver)?;
169 let label = require_string_arg(&args, 0, "series")?;
170 let data = if let Some(view) = args.get(1).and_then(|nb| nb.as_any_array()) {
172 let arr = view.to_generic();
173 arr.iter()
174 .filter_map(|item| {
175 if let Some(inner) = item.as_any_array() {
176 let inner = inner.to_generic();
177 if inner.len() >= 2 {
178 let x = inner[0].as_number_coerce()?;
179 let y = inner[1].as_number_coerce()?;
180 return Some((x, y));
181 }
182 }
183 None
184 })
185 .collect()
186 } else {
187 vec![]
188 };
189 match node {
190 ContentNode::Chart(mut spec) => {
191 spec.series.push(ChartSeries {
192 label,
193 data,
194 color: None,
195 });
196 Ok(ValueWord::from_content(ContentNode::Chart(spec)))
197 }
198 other => Ok(ValueWord::from_content(other)),
199 }
200}
201
202fn handle_title(receiver: ValueWord, args: Vec<ValueWord>) -> Result<ValueWord> {
203 let node = extract_content(&receiver)?;
204 let title = require_string_arg(&args, 0, "title")?;
205 match node {
206 ContentNode::Chart(mut spec) => {
207 spec.title = Some(title);
208 Ok(ValueWord::from_content(ContentNode::Chart(spec)))
209 }
210 other => Ok(ValueWord::from_content(other)),
211 }
212}
213
214fn handle_x_label(receiver: ValueWord, args: Vec<ValueWord>) -> Result<ValueWord> {
215 let node = extract_content(&receiver)?;
216 let label = require_string_arg(&args, 0, "x_label")?;
217 match node {
218 ContentNode::Chart(mut spec) => {
219 spec.x_label = Some(label);
220 Ok(ValueWord::from_content(ContentNode::Chart(spec)))
221 }
222 other => Ok(ValueWord::from_content(other)),
223 }
224}
225
226fn handle_y_label(receiver: ValueWord, args: Vec<ValueWord>) -> Result<ValueWord> {
227 let node = extract_content(&receiver)?;
228 let label = require_string_arg(&args, 0, "y_label")?;
229 match node {
230 ContentNode::Chart(mut spec) => {
231 spec.y_label = Some(label);
232 Ok(ValueWord::from_content(ContentNode::Chart(spec)))
233 }
234 other => Ok(ValueWord::from_content(other)),
235 }
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241 use shape_value::content::ContentTable;
242 use std::sync::Arc;
243
244 fn nb_str(s: &str) -> ValueWord {
245 ValueWord::from_string(Arc::new(s.to_string()))
246 }
247
248 #[test]
249 fn test_call_content_method_lookup() {
250 let node = ValueWord::from_content(ContentNode::plain("hello"));
251 assert!(call_content_method("bold", node.clone(), vec![]).is_some());
252 assert!(call_content_method("italic", node.clone(), vec![]).is_some());
253 assert!(call_content_method("underline", node.clone(), vec![]).is_some());
254 assert!(call_content_method("dim", node.clone(), vec![]).is_some());
255 assert!(call_content_method("unknown", node, vec![]).is_none());
256 }
257
258 #[test]
259 fn test_fg_method() {
260 let node = ValueWord::from_content(ContentNode::plain("text"));
261 let result = handle_fg(node, vec![nb_str("red")]).unwrap();
262 let content = result.as_content().unwrap();
263 match content {
264 ContentNode::Text(st) => {
265 assert_eq!(st.spans[0].style.fg, Some(Color::Named(NamedColor::Red)));
266 }
267 _ => panic!("expected Text"),
268 }
269 }
270
271 #[test]
272 fn test_bg_method() {
273 let node = ValueWord::from_content(ContentNode::plain("text"));
274 let result = handle_bg(node, vec![nb_str("blue")]).unwrap();
275 let content = result.as_content().unwrap();
276 match content {
277 ContentNode::Text(st) => {
278 assert_eq!(st.spans[0].style.bg, Some(Color::Named(NamedColor::Blue)));
279 }
280 _ => panic!("expected Text"),
281 }
282 }
283
284 #[test]
285 fn test_bold_method() {
286 let node = ValueWord::from_content(ContentNode::plain("text"));
287 let result = handle_bold(node, vec![]).unwrap();
288 let content = result.as_content().unwrap();
289 match content {
290 ContentNode::Text(st) => assert!(st.spans[0].style.bold),
291 _ => panic!("expected Text"),
292 }
293 }
294
295 #[test]
296 fn test_italic_method() {
297 let node = ValueWord::from_content(ContentNode::plain("text"));
298 let result = handle_italic(node, vec![]).unwrap();
299 let content = result.as_content().unwrap();
300 match content {
301 ContentNode::Text(st) => assert!(st.spans[0].style.italic),
302 _ => panic!("expected Text"),
303 }
304 }
305
306 #[test]
307 fn test_underline_method() {
308 let node = ValueWord::from_content(ContentNode::plain("text"));
309 let result = handle_underline(node, vec![]).unwrap();
310 let content = result.as_content().unwrap();
311 match content {
312 ContentNode::Text(st) => assert!(st.spans[0].style.underline),
313 _ => panic!("expected Text"),
314 }
315 }
316
317 #[test]
318 fn test_dim_method() {
319 let node = ValueWord::from_content(ContentNode::plain("text"));
320 let result = handle_dim(node, vec![]).unwrap();
321 let content = result.as_content().unwrap();
322 match content {
323 ContentNode::Text(st) => assert!(st.spans[0].style.dim),
324 _ => panic!("expected Text"),
325 }
326 }
327
328 #[test]
329 fn test_border_method_on_table() {
330 let table = ContentNode::Table(ContentTable {
331 headers: vec!["A".into()],
332 rows: vec![vec![ContentNode::plain("1")]],
333 border: BorderStyle::Rounded,
334 max_rows: None,
335 column_types: None,
336 total_rows: None,
337 sortable: false,
338 });
339 let node = ValueWord::from_content(table);
340 let result = handle_border(node, vec![nb_str("heavy")]).unwrap();
341 let content = result.as_content().unwrap();
342 match content {
343 ContentNode::Table(t) => assert_eq!(t.border, BorderStyle::Heavy),
344 _ => panic!("expected Table"),
345 }
346 }
347
348 #[test]
349 fn test_border_method_on_non_table() {
350 let node = ValueWord::from_content(ContentNode::plain("text"));
351 let result = handle_border(node, vec![nb_str("sharp")]).unwrap();
352 let content = result.as_content().unwrap();
353 match content {
354 ContentNode::Text(st) => assert_eq!(st.spans[0].text, "text"),
355 _ => panic!("expected Text passthrough"),
356 }
357 }
358
359 #[test]
360 fn test_max_rows_method() {
361 let table = ContentNode::Table(ContentTable {
362 headers: vec!["X".into()],
363 rows: vec![
364 vec![ContentNode::plain("1")],
365 vec![ContentNode::plain("2")],
366 vec![ContentNode::plain("3")],
367 ],
368 border: BorderStyle::default(),
369 max_rows: None,
370 column_types: None,
371 total_rows: None,
372 sortable: false,
373 });
374 let node = ValueWord::from_content(table);
375 let result = handle_max_rows(node, vec![ValueWord::from_i64(2)]).unwrap();
376 let content = result.as_content().unwrap();
377 match content {
378 ContentNode::Table(t) => assert_eq!(t.max_rows, Some(2)),
379 _ => panic!("expected Table"),
380 }
381 }
382
383 #[test]
384 fn test_parse_color_valid() {
385 assert_eq!(parse_color("red").unwrap(), Color::Named(NamedColor::Red));
386 assert_eq!(
387 parse_color("GREEN").unwrap(),
388 Color::Named(NamedColor::Green)
389 );
390 assert_eq!(parse_color("Blue").unwrap(), Color::Named(NamedColor::Blue));
391 }
392
393 #[test]
394 fn test_parse_color_invalid() {
395 assert!(parse_color("purple").is_err());
396 }
397
398 #[test]
399 fn test_fg_invalid_color() {
400 let node = ValueWord::from_content(ContentNode::plain("text"));
401 let result = handle_fg(node, vec![nb_str("purple")]);
402 assert!(result.is_err());
403 }
404
405 #[test]
406 fn test_style_chaining_via_methods() {
407 let node = ValueWord::from_content(ContentNode::plain("text"));
408 let bold_result = handle_bold(node, vec![]).unwrap();
409 let fg_result = handle_fg(bold_result, vec![nb_str("cyan")]).unwrap();
410 let content = fg_result.as_content().unwrap();
411 match content {
412 ContentNode::Text(st) => {
413 assert!(st.spans[0].style.bold);
414 assert_eq!(st.spans[0].style.fg, Some(Color::Named(NamedColor::Cyan)));
415 }
416 _ => panic!("expected Text"),
417 }
418 }
419
420 #[test]
421 fn test_chart_title_method() {
422 use shape_value::content::{ChartSpec, ChartType};
423 let chart = ContentNode::Chart(ChartSpec {
424 chart_type: ChartType::Line,
425 series: vec![],
426 title: None,
427 x_label: None,
428 y_label: None,
429 width: None,
430 height: None,
431 echarts_options: None,
432 interactive: true,
433 });
434 let node = ValueWord::from_content(chart);
435 let result = handle_title(node, vec![nb_str("Revenue")]).unwrap();
436 let content = result.as_content().unwrap();
437 match content {
438 ContentNode::Chart(spec) => assert_eq!(spec.title.as_deref(), Some("Revenue")),
439 _ => panic!("expected Chart"),
440 }
441 }
442
443 #[test]
444 fn test_chart_x_label_method() {
445 use shape_value::content::{ChartSpec, ChartType};
446 let chart = ContentNode::Chart(ChartSpec {
447 chart_type: ChartType::Bar,
448 series: vec![],
449 title: None,
450 x_label: None,
451 y_label: None,
452 width: None,
453 height: None,
454 echarts_options: None,
455 interactive: true,
456 });
457 let node = ValueWord::from_content(chart);
458 let result = handle_x_label(node, vec![nb_str("Time")]).unwrap();
459 let content = result.as_content().unwrap();
460 match content {
461 ContentNode::Chart(spec) => assert_eq!(spec.x_label.as_deref(), Some("Time")),
462 _ => panic!("expected Chart"),
463 }
464 }
465
466 #[test]
467 fn test_chart_y_label_method() {
468 use shape_value::content::{ChartSpec, ChartType};
469 let chart = ContentNode::Chart(ChartSpec {
470 chart_type: ChartType::Line,
471 series: vec![],
472 title: None,
473 x_label: None,
474 y_label: None,
475 width: None,
476 height: None,
477 echarts_options: None,
478 interactive: true,
479 });
480 let node = ValueWord::from_content(chart);
481 let result = handle_y_label(node, vec![nb_str("Value")]).unwrap();
482 let content = result.as_content().unwrap();
483 match content {
484 ContentNode::Chart(spec) => assert_eq!(spec.y_label.as_deref(), Some("Value")),
485 _ => panic!("expected Chart"),
486 }
487 }
488
489 #[test]
490 fn test_chart_series_method() {
491 use shape_value::content::{ChartSpec, ChartType};
492 let chart = ContentNode::Chart(ChartSpec {
493 chart_type: ChartType::Line,
494 series: vec![],
495 title: None,
496 x_label: None,
497 y_label: None,
498 width: None,
499 height: None,
500 echarts_options: None,
501 interactive: true,
502 });
503 let node = ValueWord::from_content(chart);
504 let data_points = ValueWord::from_array(Arc::new(vec![
505 ValueWord::from_array(Arc::new(vec![
506 ValueWord::from_f64(1.0),
507 ValueWord::from_f64(10.0),
508 ])),
509 ValueWord::from_array(Arc::new(vec![
510 ValueWord::from_f64(2.0),
511 ValueWord::from_f64(20.0),
512 ])),
513 ]));
514 let result = handle_series(node, vec![nb_str("Sales"), data_points]).unwrap();
515 let content = result.as_content().unwrap();
516 match content {
517 ContentNode::Chart(spec) => {
518 assert_eq!(spec.series.len(), 1);
519 assert_eq!(spec.series[0].label, "Sales");
520 assert_eq!(spec.series[0].data, vec![(1.0, 10.0), (2.0, 20.0)]);
521 }
522 _ => panic!("expected Chart"),
523 }
524 }
525
526 #[test]
527 fn test_chart_method_lookup() {
528 let node = ValueWord::from_content(ContentNode::plain("text"));
529 assert!(call_content_method("title", node.clone(), vec![nb_str("t")]).is_some());
530 assert!(call_content_method("series", node.clone(), vec![]).is_some());
531 assert!(call_content_method("xLabel", node.clone(), vec![nb_str("x")]).is_some());
532 assert!(call_content_method("yLabel", node, vec![nb_str("y")]).is_some());
533 }
534}