1use std::path::Path;
8
9use anyhow::{Context, Result};
10
11use std::collections::HashMap;
12use std::sync::Arc;
13
14use crate::directives::Directive;
15use crate::eval::{
16 compare, eval_cell, eval_expression_str, inject_rownum, inject_rows, is_truthy, EvalContext,
17};
18use crate::output::{write_workbook_with_manifest, RenderedSheet};
19use crate::output_model::{OutputFile, XtlWarning};
20use crate::plan::{
21 inputs_to_value, lists_to_value, parse_template, CellSource, RowPlan, SheetPlan, WorkbookPlan,
22};
23use crate::source::{CalamineSourceReader, SourceData, SourceReader};
24use crate::styles::NumFmtKind;
25use crate::value::Value;
26
27pub fn render_from_paths(template: &Path, data: &Path) -> Result<Vec<u8>> {
34 first_file_bytes(render_from_paths_to_files(template, data)?)
35}
36
37pub fn render_from_paths_with_inputs(
42 template: &Path,
43 data: &Path,
44 host_inputs: &HashMap<String, Value>,
45) -> Result<Vec<u8>> {
46 first_file_bytes(render_from_paths_to_files_with_inputs(
47 template,
48 data,
49 host_inputs,
50 )?)
51}
52
53fn first_file_bytes(files: Vec<OutputFile>) -> Result<Vec<u8>> {
54 files
55 .into_iter()
56 .next()
57 .map(|f| f.data)
58 .ok_or_else(|| anyhow::anyhow!("renderer produced no output files"))
59}
60
61pub fn render_from_paths_to_files(template: &Path, data: &Path) -> Result<Vec<OutputFile>> {
66 render_from_paths_to_files_with_inputs(template, data, &HashMap::new())
67}
68
69pub fn render_from_paths_to_files_with_inputs(
70 template: &Path,
71 data: &Path,
72 host_inputs: &HashMap<String, Value>,
73) -> Result<Vec<OutputFile>> {
74 let plan = parse_template(template).context("parse template")?;
75 let source_reader = CalamineSourceReader::open(data).context("open source workbook")?;
76 render_with_reader(plan, source_reader, host_inputs)
77}
78
79pub fn render_from_bytes_to_files(
84 template_bytes: &[u8],
85 data_bytes: Vec<u8>,
86) -> Result<Vec<OutputFile>> {
87 render_from_bytes_to_files_with_inputs(template_bytes, data_bytes, &HashMap::new())
88}
89
90pub fn render_from_bytes_to_files_with_inputs(
91 template_bytes: &[u8],
92 data_bytes: Vec<u8>,
93 host_inputs: &HashMap<String, Value>,
94) -> Result<Vec<OutputFile>> {
95 render_from_bytes_to_files_full(template_bytes, data_bytes, host_inputs, None)
96}
97
98pub fn render_from_bytes_to_files_full(
105 template_bytes: &[u8],
106 data_bytes: Vec<u8>,
107 host_inputs: &HashMap<String, Value>,
108 manifest: Option<crate::manifest::StyleManifest>,
109) -> Result<Vec<OutputFile>> {
110 let plan = crate::plan::parse_template_bytes_with_manifest(template_bytes, manifest.as_ref())
115 .context("parse template")?;
116 let source_reader =
117 CalamineSourceReader::open_bytes(data_bytes).context("open source workbook")?;
118 render_with_reader_and_manifest(plan, source_reader, host_inputs, manifest)
119}
120
121fn render_with_reader(
122 plan: WorkbookPlan,
123 source_reader: CalamineSourceReader,
124 host_inputs: &HashMap<String, Value>,
125) -> Result<Vec<OutputFile>> {
126 render_with_reader_and_manifest(plan, source_reader, host_inputs, None)
127}
128
129fn render_with_reader_and_manifest(
130 mut plan: WorkbookPlan,
131 mut source_reader: CalamineSourceReader,
132 host_inputs: &HashMap<String, Value>,
133 manifest: Option<crate::manifest::StyleManifest>,
134) -> Result<Vec<OutputFile>> {
135 for (key, value) in host_inputs {
136 plan.inputs.insert(key.clone(), value.clone());
137 }
138 let source_sheet = match plan.config.source_sheet() {
139 Some(pattern) => source_reader.resolve_sheet_name(pattern).ok_or_else(|| {
140 anyhow::Error::from(crate::errors::XtlError::new(
143 crate::errors::code::SOURCE_SHEET_MISSING,
144 format!("Source sheet \"{pattern}\" was not found"),
145 ))
146 })?,
147 None => source_reader.first_sheet().ok_or_else(|| {
148 anyhow::Error::from(crate::errors::XtlError::new(
149 crate::errors::code::SOURCE_SHEET_MISSING,
150 "Source workbook is empty",
151 ))
152 })?,
153 };
154 let source_table = plan.config.source_table();
155 let source = source_reader.read(&source_sheet, &source_table)?;
156 let mut named_sources: HashMap<String, SourceData> = HashMap::new();
158 for (name, decl) in &plan.named_sources {
159 let data = source_reader.read(&decl.sheet, &decl.table)?;
160 named_sources.insert(name.clone(), data);
161 }
162 render_to_files_with_sources_and_manifest(&plan, &source, &named_sources, manifest.as_ref())
163}
164
165
166pub fn render(plan: &WorkbookPlan, source: &SourceData) -> Result<Vec<u8>> {
167 first_file_bytes(render_to_files(plan, source)?)
168}
169
170pub fn render_with_sources(
171 plan: &WorkbookPlan,
172 source: &SourceData,
173 named_sources: &HashMap<String, SourceData>,
174) -> Result<Vec<u8>> {
175 first_file_bytes(render_to_files_with_sources(plan, source, named_sources)?)
176}
177
178pub fn render_to_files(plan: &WorkbookPlan, source: &SourceData) -> Result<Vec<OutputFile>> {
179 render_to_files_with_sources(plan, source, &HashMap::new())
180}
181
182pub fn render_to_files_with_sources(
183 plan: &WorkbookPlan,
184 source: &SourceData,
185 named_sources: &HashMap<String, SourceData>,
186) -> Result<Vec<OutputFile>> {
187 render_to_files_with_sources_and_manifest(plan, source, named_sources, None)
188}
189
190pub fn render_to_files_with_sources_and_manifest(
191 plan: &WorkbookPlan,
192 source: &SourceData,
193 named_sources: &HashMap<String, SourceData>,
194 manifest: Option<&crate::manifest::StyleManifest>,
195) -> Result<Vec<OutputFile>> {
196 let group_keys = plan.config.file_group_keys();
197 if group_keys.is_empty() {
198 return Ok(vec![render_one_file(
199 plan,
200 source,
201 named_sources,
202 &HashMap::new(),
203 manifest,
204 )?]);
205 }
206 let mut groups: Vec<(Vec<String>, Vec<HashMap<String, Value>>)> = Vec::new();
211 for row in &source.rows {
212 let values: Vec<String> = group_keys
213 .iter()
214 .map(|k| row.get(k).cloned().unwrap_or(Value::Empty).canonical())
215 .collect();
216 if let Some(g) = groups.iter_mut().find(|g| g.0 == values) {
217 g.1.push(row.clone());
218 } else {
219 groups.push((values, vec![row.clone()]));
220 }
221 }
222 let mut out: Vec<OutputFile> = Vec::with_capacity(groups.len());
223 for (values, rows) in groups {
224 let group_ctx: HashMap<String, Value> = group_keys
225 .iter()
226 .cloned()
227 .zip(values.into_iter().map(Value::String))
228 .collect();
229 let group_source = SourceData {
230 name: source.name.clone(),
231 headers: source.headers.clone(),
232 rows,
233 };
234 out.push(render_one_file(
235 plan,
236 &group_source,
237 named_sources,
238 &group_ctx,
239 manifest,
240 )?);
241 }
242 Ok(out)
243}
244
245fn render_one_file(
246 plan: &WorkbookPlan,
247 source: &SourceData,
248 named_sources: &HashMap<String, SourceData>,
249 group_keys: &HashMap<String, Value>,
250 manifest: Option<&crate::manifest::StyleManifest>,
251) -> Result<OutputFile> {
252 let inputs_value = inputs_to_value(&plan.inputs);
253 let lists_value = lists_to_value(&plan.lists);
254 let named_source_handles: HashMap<String, Value> = named_sources
255 .iter()
256 .map(|(name, data)| {
257 let handle: Arc<Vec<HashMap<String, Value>>> = Arc::new(data.rows.clone());
258 (name.clone(), Value::Rows(handle))
259 })
260 .collect();
261 let mut out_sheets = Vec::with_capacity(plan.sheets.len());
262 for sheet in &plan.sheets {
263 if sheet.name.contains("{{") {
264 let groups = split_source_by_sheet_name(
267 &sheet.name,
268 source,
269 &inputs_value,
270 &lists_value,
271 &named_source_handles,
272 group_keys,
273 )?;
274 for (group_name, group_source) in groups {
275 let mut rs = render_sheet(
276 sheet,
277 &group_source,
278 &inputs_value,
279 &lists_value,
280 &named_source_handles,
281 group_keys,
282 )?;
283 rs.name = sanitize_sheet_name(&group_name);
284 out_sheets.push(rs);
285 }
286 } else {
287 out_sheets.push(render_sheet(
288 sheet,
289 source,
290 &inputs_value,
291 &lists_value,
292 &named_source_handles,
293 group_keys,
294 )?);
295 }
296 }
297 let bytes = write_workbook_with_manifest(&out_sheets, manifest)?;
298 let pattern = plan
299 .config
300 .output_file_pattern()
301 .map(str::to_string)
302 .unwrap_or_else(|| "output.xlsx".to_string());
303 let mut warnings: Vec<XtlWarning> = Vec::new();
308 let resolved = if pattern.contains("{{") {
309 let mut ctx: EvalContext = HashMap::new();
310 if let Some(row) = source.rows.first() {
311 ctx.extend(row.clone());
312 }
313 for (k, v) in group_keys {
314 ctx.insert(k.clone(), v.clone());
315 }
316 ctx.insert("__inputs__".to_string(), inputs_value.clone());
317 ctx.insert("__lists__".to_string(), lists_value.clone());
318 inject_named_sources(&mut ctx, &named_source_handles);
319 eval_cell(&pattern, &ctx)?.canonical()
320 } else {
321 pattern
322 };
323 let filename = sanitize_filename(&resolved, &mut warnings);
324 Ok(OutputFile {
325 filename,
326 data: bytes,
327 warnings,
328 })
329}
330
331fn sanitize_filename(name: &str, warnings: &mut Vec<XtlWarning>) -> String {
337 let cleaned: String = name
338 .chars()
339 .map(|c| match c {
340 '<' | '>' | ':' | '"' | '/' | '\\' | '|' | '?' | '*' => '_',
341 _ => c,
342 })
343 .collect();
344 if cleaned != name {
345 warnings.push(XtlWarning {
346 message: format!("Output filename \"{name}\" sanitized to \"{cleaned}\""),
347 });
348 }
349 cleaned
350}
351
352fn sanitize_sheet_name(s: &str) -> String {
357 let cleaned: String = s
358 .chars()
359 .map(|c| match c {
360 ':' | '\\' | '/' | '?' | '*' | '[' | ']' => '_',
361 _ => c,
362 })
363 .collect();
364 if cleaned.chars().count() <= 31 {
365 cleaned
366 } else {
367 cleaned.chars().take(31).collect()
368 }
369}
370
371fn split_source_by_sheet_name(
375 template: &str,
376 source: &SourceData,
377 inputs_value: &Value,
378 lists_value: &Value,
379 named_sources: &HashMap<String, Value>,
380 group_keys: &HashMap<String, Value>,
381) -> Result<Vec<(String, SourceData)>> {
382 let mut groups: Vec<(String, Vec<HashMap<String, Value>>)> = Vec::new();
383 for row in &source.rows {
384 let mut ctx: EvalContext = row.clone();
385 for (k, v) in group_keys {
386 ctx.insert(k.clone(), v.clone());
387 }
388 ctx.insert("__inputs__".to_string(), inputs_value.clone());
389 ctx.insert("__lists__".to_string(), lists_value.clone());
390 inject_named_sources(&mut ctx, named_sources);
391 let key_value = eval_cell(template, &ctx)?;
392 let raw_key = key_value.canonical();
393 let key = if raw_key.chars().all(char::is_whitespace) {
398 "(blank)".to_string()
399 } else {
400 raw_key
401 };
402 if let Some(g) = groups.iter_mut().find(|g| g.0 == key) {
403 g.1.push(row.clone());
404 } else {
405 groups.push((key, vec![row.clone()]));
406 }
407 }
408 Ok(groups
409 .into_iter()
410 .map(|(key, rows)| {
411 (
412 key,
413 SourceData {
414 name: source.name.clone(),
415 headers: source.headers.clone(),
416 rows,
417 },
418 )
419 })
420 .collect())
421}
422
423fn render_sheet(
424 plan: &SheetPlan,
425 source: &SourceData,
426 inputs_value: &Value,
427 lists_value: &Value,
428 named_sources: &HashMap<String, Value>,
429 group_keys: &HashMap<String, Value>,
430) -> Result<RenderedSheet> {
431 if !plan.sub_blocks.is_empty() {
434 let mut sub_outputs: Vec<(
435 usize,
436 usize,
437 Vec<Vec<Value>>,
438 Vec<Vec<Option<String>>>,
439 Vec<Vec<Option<usize>>>,
440 Vec<Vec<Option<String>>>,
441 )> = Vec::new();
442 for sub in &plan.sub_blocks {
443 let sub_plan = SheetPlan {
444 name: plan.name.clone(),
445 rows: sub.rows.clone(),
446 sub_blocks: Vec::new(),
447 n_cols: sub.col_last - sub.col_first + 1,
448 };
449 let sub_rendered = render_sheet(
450 &sub_plan,
451 source,
452 inputs_value,
453 lists_value,
454 named_sources,
455 group_keys,
456 )?;
457 sub_outputs.push((
458 sub.col_first,
459 sub.col_last,
460 sub_rendered.rows,
461 sub_rendered.formats,
462 sub_rendered.style_indices,
463 sub_rendered.formulas,
464 ));
465 }
466 let max_rows = sub_outputs
467 .iter()
468 .map(|(_, _, r, _, _, _)| r.len())
469 .max()
470 .unwrap_or(0);
471 let n_cols = plan.n_cols.max(1);
472 let mut merged: Vec<Vec<Value>> = (0..max_rows)
473 .map(|_| vec![Value::Empty; n_cols])
474 .collect();
475 let mut merged_formats: Vec<Vec<Option<String>>> = (0..max_rows)
476 .map(|_| vec![None; n_cols])
477 .collect();
478 let mut merged_style_indices: Vec<Vec<Option<usize>>> = (0..max_rows)
479 .map(|_| vec![None; n_cols])
480 .collect();
481 let mut merged_formulas: Vec<Vec<Option<String>>> = (0..max_rows)
482 .map(|_| vec![None; n_cols])
483 .collect();
484 for (col_first, _col_last, sub_rows, sub_formats, sub_styles, sub_formulas) in sub_outputs {
485 for (r_idx, sub_row) in sub_rows.iter().enumerate() {
486 for (c_off, v) in sub_row.iter().enumerate() {
487 let c = col_first + c_off;
488 if c < n_cols {
489 merged[r_idx][c] = v.clone();
490 }
491 }
492 if let Some(sub_fr) = sub_formats.get(r_idx) {
493 for (c_off, f) in sub_fr.iter().enumerate() {
494 let c = col_first + c_off;
495 if c < n_cols {
496 merged_formats[r_idx][c] = f.clone();
497 }
498 }
499 }
500 if let Some(sub_sr) = sub_styles.get(r_idx) {
501 for (c_off, s) in sub_sr.iter().enumerate() {
502 let c = col_first + c_off;
503 if c < n_cols {
504 merged_style_indices[r_idx][c] = *s;
505 }
506 }
507 }
508 if let Some(sub_fl) = sub_formulas.get(r_idx) {
509 for (c_off, f) in sub_fl.iter().enumerate() {
510 let c = col_first + c_off;
511 if c < n_cols {
512 merged_formulas[r_idx][c] = f.clone();
513 }
514 }
515 }
516 }
517 }
518 return Ok(RenderedSheet {
519 name: plan.name.clone(),
520 rows: merged,
521 formats: merged_formats,
522 style_indices: merged_style_indices,
523 formulas: merged_formulas,
524 });
525 }
526
527 let mut rows: Vec<Vec<Value>> = Vec::new();
528 let mut formats: Vec<Vec<Option<String>>> = Vec::new();
529 let mut style_indices: Vec<Vec<Option<usize>>> = Vec::new();
530 let mut formulas: Vec<Vec<Option<String>>> = Vec::new();
531 for row in &plan.rows {
532 match row {
533 RowPlan::Static(cells) => {
534 rows.push(render_static_row(
535 cells,
536 inputs_value,
537 lists_value,
538 named_sources,
539 group_keys,
540 )?);
541 formats.push(row_formats(cells));
542 style_indices.push(row_style_indices(cells));
543 formulas.push(row_formulas(cells));
544 }
545 RowPlan::ExpandDown {
546 cells,
547 directives,
548 subtotal_rows,
549 side_rows,
550 col_range,
551 } => {
552 let _ = col_range;
553 let block_rows = resolve_block_rows(directives, source, named_sources);
554 let effective =
555 apply_directives(&block_rows, directives, lists_value, named_sources)?;
556 let group_fields: Vec<String> = directives
557 .iter()
558 .find_map(|d| match d {
559 Directive::Group(fs) => Some(fs.clone()),
560 _ => None,
561 })
562 .unwrap_or_default();
563 let active_source: Option<String> = directives.iter().find_map(|d| match d {
564 Directive::Source(n) => Some(n.clone()),
565 _ => None,
566 });
567 let rows_handle: Arc<Vec<HashMap<String, Value>>> = Arc::new(effective.clone());
568
569 let mut global_idx = 0usize;
570 let emit_expansion =
571 |group_rows: &Vec<HashMap<String, Value>>,
572 rows: &mut Vec<Vec<Value>>,
573 formats: &mut Vec<Vec<Option<String>>>,
574 style_indices: &mut Vec<Vec<Option<usize>>>,
575 formulas: &mut Vec<Vec<Option<String>>>,
576 global_idx: &mut usize|
577 -> Result<()> {
578 for (iter_idx, source_row) in group_rows.iter().enumerate() {
579 *global_idx += 1;
580 let mut ctx: EvalContext = source_row.clone();
581 inject_rows(&mut ctx, Arc::clone(&rows_handle));
582 inject_rownum(&mut ctx, *global_idx);
583 ctx.insert("__inputs__".to_string(), inputs_value.clone());
584 ctx.insert("__lists__".to_string(), lists_value.clone());
585 if let Some(name) = &active_source {
586 ctx.insert(
587 name.clone(),
588 Value::Map(Arc::new(source_row.clone())),
589 );
590 }
591 inject_named_sources(&mut ctx, named_sources);
592 let effective_cells = compose_iteration_cells(
593 cells, side_rows, *col_range, iter_idx,
594 );
595 rows.push(render_template_row(&effective_cells, &ctx)?);
596 formats.push(row_formats(&effective_cells));
597 style_indices.push(row_style_indices(&effective_cells));
598 formulas.push(row_formulas(&effective_cells));
599 }
600 Ok(())
601 };
602
603 if group_fields.is_empty() {
604 emit_expansion(
605 &effective,
606 &mut rows,
607 &mut formats,
608 &mut style_indices,
609 &mut formulas,
610 &mut global_idx,
611 )?;
612 let consumed = effective.len().saturating_sub(1);
613 if side_rows.len() > consumed {
614 for extra in &side_rows[consumed..] {
615 rows.push(render_static_row(
616 extra,
617 inputs_value,
618 lists_value,
619 named_sources,
620 group_keys,
621 )?);
622 formats.push(row_formats(extra));
623 style_indices.push(row_style_indices(extra));
624 formulas.push(row_formulas(extra));
625 }
626 }
627 for subtotal_cells in subtotal_rows {
628 rows.push(render_subtotal_row(
629 subtotal_cells,
630 &rows_handle,
631 inputs_value,
632 lists_value,
633 named_sources,
634 )?);
635 formats.push(row_formats(subtotal_cells));
636 style_indices.push(row_style_indices(subtotal_cells));
637 formulas.push(row_formulas(subtotal_cells));
638 }
639 } else {
640 render_grouped(
641 &effective,
642 &group_fields,
643 0,
644 cells,
645 subtotal_rows,
646 side_rows,
647 *col_range,
648 &mut rows,
649 &mut formats,
650 &mut style_indices,
651 &mut formulas,
652 &mut global_idx,
653 &rows_handle,
654 inputs_value,
655 lists_value,
656 named_sources,
657 active_source.as_deref(),
658 )?;
659 }
660 }
661 RowPlan::ExpandRight { cells, directives } => {
662 let block_rows = resolve_block_rows(directives, source, named_sources);
663 let effective = apply_directives(&block_rows, directives, lists_value, named_sources)?;
664 rows.push(render_expand_right_row(
665 cells,
666 &effective,
667 inputs_value,
668 lists_value,
669 named_sources,
670 )?);
671 formats.push(row_formats_for_expand_right(cells, effective.len()));
672 style_indices
673 .push(row_style_indices_for_expand_right(cells, effective.len()));
674 formulas.push(row_formulas_for_expand_right(cells, effective.len()));
675 }
676 }
677 }
678 Ok(RenderedSheet {
679 name: plan.name.clone(),
680 rows,
681 formats,
682 style_indices,
683 formulas,
684 })
685}
686
687fn row_formats_for_expand_right(cells: &[CellSource], n_iters: usize) -> Vec<Option<String>> {
692 let mut out = Vec::with_capacity(cells.len() + n_iters);
693 for cell in cells {
694 match cell {
695 CellSource::Template { format_code, .. } => {
696 for _ in 0..n_iters {
697 out.push(format_code.clone());
698 }
699 }
700 _ => out.push(cell_format(cell)),
701 }
702 }
703 out
704}
705
706fn row_formulas_for_expand_right(cells: &[CellSource], n_iters: usize) -> Vec<Option<String>> {
710 let mut out = Vec::with_capacity(cells.len() + n_iters);
711 for cell in cells {
712 match cell {
713 CellSource::Template { .. } => {
714 for _ in 0..n_iters {
715 out.push(None);
716 }
717 }
718 _ => out.push(cell_formula(cell)),
719 }
720 }
721 out
722}
723
724fn resolve_block_rows(
730 directives: &[Directive],
731 default_source: &SourceData,
732 named_sources: &HashMap<String, Value>,
733) -> Vec<HashMap<String, Value>> {
734 if let Some(name) = directives.iter().find_map(|d| match d {
735 Directive::Source(n) => Some(n.as_str()),
736 _ => None,
737 }) {
738 match named_sources.get(name) {
739 Some(Value::Rows(handle)) => handle.as_ref().clone(),
740 _ => Vec::new(),
741 }
742 } else {
743 default_source.rows.clone()
744 }
745}
746
747fn partition_into_groups(
754 rows: &[HashMap<String, Value>],
755 field: Option<&str>,
756) -> Vec<Vec<HashMap<String, Value>>> {
757 let Some(field) = field else {
758 return vec![rows.to_vec()];
759 };
760 let mut out: Vec<Vec<HashMap<String, Value>>> = Vec::new();
761 let mut current_key: Option<Value> = None;
762 for row in rows {
763 let key = row.get(field).cloned().unwrap_or(Value::Empty);
764 let same = current_key
765 .as_ref()
766 .map(|prev| crate::eval::compare(prev, &key).map(|c| c == 0).unwrap_or(false))
767 .unwrap_or(false);
768 if same {
769 out.last_mut().unwrap().push(row.clone());
770 } else {
771 out.push(vec![row.clone()]);
772 current_key = Some(key);
773 }
774 }
775 out
776}
777
778fn render_grouped(
785 rows: &[HashMap<String, Value>],
786 group_fields: &[String],
787 depth: usize,
788 cells: &[CellSource],
789 subtotal_rows: &[Vec<CellSource>],
790 side_rows: &[Vec<CellSource>],
791 col_range: Option<(usize, usize)>,
792 out_rows: &mut Vec<Vec<Value>>,
793 out_formats: &mut Vec<Vec<Option<String>>>,
794 out_style_indices: &mut Vec<Vec<Option<usize>>>,
795 out_formulas: &mut Vec<Vec<Option<String>>>,
796 global_idx: &mut usize,
797 rows_handle: &Arc<Vec<HashMap<String, Value>>>,
798 inputs_value: &Value,
799 lists_value: &Value,
800 named_sources: &HashMap<String, Value>,
801 active_source: Option<&str>,
802) -> Result<()> {
803 if depth == group_fields.len() {
804 for (iter_idx, source_row) in rows.iter().enumerate() {
805 *global_idx += 1;
806 let mut ctx: EvalContext = source_row.clone();
807 inject_rows(&mut ctx, Arc::clone(rows_handle));
808 inject_rownum(&mut ctx, *global_idx);
809 ctx.insert("__inputs__".to_string(), inputs_value.clone());
810 ctx.insert("__lists__".to_string(), lists_value.clone());
811 if let Some(name) = active_source {
812 ctx.insert(name.to_string(), Value::Map(Arc::new(source_row.clone())));
813 }
814 inject_named_sources(&mut ctx, named_sources);
815 let effective_cells =
816 compose_iteration_cells(cells, side_rows, col_range, iter_idx);
817 out_rows.push(render_template_row(&effective_cells, &ctx)?);
818 out_formats.push(row_formats(&effective_cells));
819 out_style_indices.push(row_style_indices(&effective_cells));
820 out_formulas.push(row_formulas(&effective_cells));
821 }
822 return Ok(());
823 }
824 let groups = partition_into_groups(rows, Some(&group_fields[depth]));
825 for group in &groups {
826 render_grouped(
827 group,
828 group_fields,
829 depth + 1,
830 cells,
831 subtotal_rows,
832 side_rows,
833 col_range,
834 out_rows,
835 out_formats,
836 out_style_indices,
837 out_formulas,
838 global_idx,
839 rows_handle,
840 inputs_value,
841 lists_value,
842 named_sources,
843 active_source,
844 )?;
845 let slot = group_fields.len() - 1 - depth;
846 if slot < subtotal_rows.len() {
847 let group_handle: Arc<Vec<HashMap<String, Value>>> = Arc::new(group.clone());
848 out_rows.push(render_subtotal_row(
849 &subtotal_rows[slot],
850 &group_handle,
851 inputs_value,
852 lists_value,
853 named_sources,
854 )?);
855 out_formats.push(row_formats(&subtotal_rows[slot]));
856 out_style_indices.push(row_style_indices(&subtotal_rows[slot]));
857 out_formulas.push(row_formulas(&subtotal_rows[slot]));
858 }
859 }
860 Ok(())
861}
862
863fn render_subtotal_row(
864 cells: &[CellSource],
865 group_handle: &Arc<Vec<HashMap<String, Value>>>,
866 inputs_value: &Value,
867 lists_value: &Value,
868 named_sources: &HashMap<String, Value>,
869) -> Result<Vec<Value>> {
870 let mut ctx: EvalContext = HashMap::new();
875 inject_rows(&mut ctx, Arc::clone(group_handle));
876 ctx.insert("__inputs__".to_string(), inputs_value.clone());
877 ctx.insert("__lists__".to_string(), lists_value.clone());
878 inject_named_sources(&mut ctx, named_sources);
879 let mut out = Vec::with_capacity(cells.len());
880 for cell in cells {
881 let value = match cell {
882 CellSource::Empty => Value::Empty,
883 CellSource::Literal(v) => v.clone(),
884 CellSource::CellFormula { cached, .. } => cached.clone(),
885 CellSource::Template { text, num_fmt, .. } => {
886 coerce_for_num_fmt(eval_cell(text, &ctx)?, *num_fmt)
887 }
888 CellSource::Subtotal { aggregate, field } => {
889 let synthetic = format!("{aggregate}([{field}])");
894 eval_expression_str(&synthetic, &ctx)?
895 }
896 };
897 out.push(value);
898 }
899 Ok(out)
900}
901
902fn compose_iteration_cells(
911 cells: &[CellSource],
912 side_rows: &[Vec<CellSource>],
913 col_range: Option<(usize, usize)>,
914 iter_idx: usize,
915) -> Vec<CellSource> {
916 let Some((lo, hi)) = col_range else {
917 return cells.to_vec();
918 };
919 cells
920 .iter()
921 .enumerate()
922 .map(|(i, cell)| {
923 let inside = i >= lo && i <= hi;
924 if inside || iter_idx == 0 {
925 cell.clone()
926 } else {
927 side_rows
928 .get(iter_idx - 1)
929 .and_then(|r| r.get(i))
930 .cloned()
931 .unwrap_or(CellSource::Empty)
932 }
933 })
934 .collect()
935}
936
937fn split_inside_outside(
941 cells: &[CellSource],
942 range: (usize, usize),
943) -> (Vec<CellSource>, Vec<CellSource>, bool, bool) {
944 let (lo, hi) = range;
945 let mut outside = vec![CellSource::Empty; cells.len()];
946 let mut inside = vec![CellSource::Empty; cells.len()];
947 let mut has_outside = false;
948 let mut has_inside = false;
949 for (i, c) in cells.iter().enumerate() {
950 if matches!(c, CellSource::Empty) {
951 continue;
952 }
953 if i >= lo && i <= hi {
954 inside[i] = c.clone();
955 has_inside = true;
956 } else {
957 outside[i] = c.clone();
958 has_outside = true;
959 }
960 }
961 (outside, inside, has_outside, has_inside)
962}
963
964fn inject_named_sources(ctx: &mut EvalContext, named_sources: &HashMap<String, Value>) {
965 for (name, handle) in named_sources {
966 if !ctx.contains_key(name) {
970 ctx.insert(name.clone(), handle.clone());
971 }
972 }
973}
974
975fn apply_directives(
976 rows: &[HashMap<String, Value>],
977 directives: &[Directive],
978 lists_value: &Value,
979 named_sources: &HashMap<String, Value>,
980) -> Result<Vec<HashMap<String, Value>>> {
981 let mut current: Vec<HashMap<String, Value>> = rows.to_vec();
982 let mut ordered: Vec<&Directive> = directives.iter().collect();
987 {
988 let sort_positions: Vec<usize> = ordered
989 .iter()
990 .enumerate()
991 .filter(|(_, d)| matches!(d, Directive::Sort { .. }))
992 .map(|(i, _)| i)
993 .collect();
994 if sort_positions.len() > 1 {
995 let mut reversed = sort_positions.clone();
998 reversed.reverse();
999 let originals: Vec<&Directive> =
1000 sort_positions.iter().map(|&i| ordered[i]).collect();
1001 for (slot, src) in reversed.iter().zip(originals.iter()) {
1002 ordered[*slot] = *src;
1003 }
1004 }
1005 }
1006 for d in ordered {
1007 match d {
1008 Directive::Filter(expr) => {
1009 let mut kept = Vec::with_capacity(current.len());
1010 for row in current.drain(..) {
1011 let mut ctx = row.clone();
1015 ctx.insert("__lists__".to_string(), lists_value.clone());
1016 let v = eval_expression_str(expr, &ctx)?;
1017 if is_truthy(&v) {
1018 kept.push(row);
1019 }
1020 }
1021 current = kept;
1022 }
1023 Directive::Sort { field, ascending } => {
1024 let asc = *ascending;
1025 current.sort_by(|a, b| {
1026 let av = a.get(field).cloned().unwrap_or(Value::Empty);
1027 let bv = b.get(field).cloned().unwrap_or(Value::Empty);
1028 let ord = compare(&av, &bv).unwrap_or(0);
1029 let ordering = ord.cmp(&0);
1030 if asc {
1031 ordering
1032 } else {
1033 ordering.reverse()
1034 }
1035 });
1036 }
1037 Directive::Top(n) => {
1038 current.truncate(*n);
1039 }
1040 Directive::Join {
1041 source,
1042 match_field,
1043 primary_field,
1044 } => {
1045 let target_rows = match named_sources.get(source) {
1046 Some(Value::Rows(handle)) => Arc::clone(handle),
1047 _ => {
1048 anyhow::bail!(
1049 "@join source {source:?} is not declared in __sources__"
1050 );
1051 }
1052 };
1053 let mut index: HashMap<String, Arc<HashMap<String, Value>>> =
1060 HashMap::with_capacity(target_rows.len());
1061 for t in target_rows.iter() {
1062 if let Some(v) = t.get(match_field) {
1063 let key = v.canonical();
1064 index
1065 .entry(key)
1066 .or_insert_with(|| Arc::new(t.clone()));
1067 }
1068 }
1069 let mut joined = Vec::with_capacity(current.len());
1070 for mut row in current.drain(..) {
1071 let primary_val = row
1072 .get(primary_field)
1073 .cloned()
1074 .unwrap_or(Value::Empty);
1075 let key = primary_val.canonical();
1076 if let Some(m) = index.get(&key) {
1077 row.insert(
1082 source.clone(),
1083 Value::Map(Arc::clone(m)),
1084 );
1085 joined.push(row);
1086 }
1087 }
1088 current = joined;
1089 }
1090 Directive::Repeat(_)
1091 | Directive::Source(_)
1092 | Directive::Group(_)
1093 | Directive::Block { .. }
1094 | Directive::Unhandled(_) => {
1095 }
1100 }
1101 }
1102 Ok(current)
1103}
1104
1105fn render_expand_right_row(
1106 cells: &[CellSource],
1107 rows: &[HashMap<String, Value>],
1108 inputs_value: &Value,
1109 lists_value: &Value,
1110 named_sources: &HashMap<String, Value>,
1111) -> Result<Vec<Value>> {
1112 let mut out = Vec::with_capacity(cells.len() + rows.len());
1113 let mut emitted_expansion = false;
1114 let rows_handle: Arc<Vec<HashMap<String, Value>>> = Arc::new(rows.to_vec());
1115 for cell in cells {
1116 match cell {
1117 CellSource::Empty => out.push(Value::Empty),
1118 CellSource::Literal(v) => out.push(v.clone()),
1119 CellSource::CellFormula { cached, .. } => out.push(cached.clone()),
1120 CellSource::Template { text, num_fmt, .. } => {
1121 if emitted_expansion {
1122 anyhow::bail!(
1123 "multi-column @repeat right (two template cells in one expansion row) not yet supported"
1124 );
1125 }
1126 emitted_expansion = true;
1127 for (idx, source_row) in rows.iter().enumerate() {
1128 let mut ctx: EvalContext = source_row.clone();
1129 inject_rows(&mut ctx, Arc::clone(&rows_handle));
1130 inject_rownum(&mut ctx, idx + 1);
1131 ctx.insert("__inputs__".to_string(), inputs_value.clone());
1132 ctx.insert("__lists__".to_string(), lists_value.clone());
1133 inject_named_sources(&mut ctx, named_sources);
1134 out.push(coerce_for_num_fmt(eval_cell(text, &ctx)?, *num_fmt));
1135 }
1136 }
1137 CellSource::Subtotal { .. } => {
1138 out.push(Value::Empty);
1142 }
1143 }
1144 }
1145 Ok(out)
1146}
1147
1148fn cell_format(cell: &CellSource) -> Option<String> {
1152 match cell {
1153 CellSource::Template { format_code, .. } => format_code.clone(),
1154 CellSource::CellFormula { format_code, .. } => format_code.clone(),
1155 _ => None,
1156 }
1157}
1158
1159fn cell_style_idx(cell: &CellSource) -> Option<usize> {
1160 match cell {
1161 CellSource::Template { style_idx, .. } => *style_idx,
1162 CellSource::CellFormula { style_idx, .. } => *style_idx,
1163 _ => None,
1164 }
1165}
1166
1167fn cell_formula(cell: &CellSource) -> Option<String> {
1168 match cell {
1169 CellSource::CellFormula { text, .. } => Some(text.clone()),
1170 _ => None,
1171 }
1172}
1173
1174fn row_formats(cells: &[CellSource]) -> Vec<Option<String>> {
1175 cells.iter().map(cell_format).collect()
1176}
1177
1178fn row_style_indices(cells: &[CellSource]) -> Vec<Option<usize>> {
1179 cells.iter().map(cell_style_idx).collect()
1180}
1181
1182fn row_formulas(cells: &[CellSource]) -> Vec<Option<String>> {
1183 cells.iter().map(cell_formula).collect()
1184}
1185
1186fn row_style_indices_for_expand_right(
1187 cells: &[CellSource],
1188 n_iters: usize,
1189) -> Vec<Option<usize>> {
1190 let mut out = Vec::with_capacity(cells.len() + n_iters);
1191 for cell in cells {
1192 match cell {
1193 CellSource::Template { style_idx, .. } => {
1194 for _ in 0..n_iters {
1195 out.push(*style_idx);
1196 }
1197 }
1198 _ => out.push(cell_style_idx(cell)),
1199 }
1200 }
1201 out
1202}
1203
1204fn render_static_row(
1205 cells: &[CellSource],
1206 inputs_value: &Value,
1207 lists_value: &Value,
1208 named_sources: &HashMap<String, Value>,
1209 group_keys: &HashMap<String, Value>,
1210) -> Result<Vec<Value>> {
1211 let mut ctx: EvalContext = HashMap::new();
1217 for (k, v) in group_keys {
1218 ctx.insert(k.clone(), v.clone());
1219 }
1220 ctx.insert("__inputs__".to_string(), inputs_value.clone());
1221 ctx.insert("__lists__".to_string(), lists_value.clone());
1222 inject_named_sources(&mut ctx, named_sources);
1223 let mut out = Vec::with_capacity(cells.len());
1224 for c in cells {
1225 let value = match c {
1226 CellSource::Empty => Value::Empty,
1227 CellSource::Literal(v) => v.clone(),
1228 CellSource::CellFormula { cached, .. } => cached.clone(),
1229 CellSource::Template { text, num_fmt, .. } => {
1230 coerce_for_num_fmt(eval_cell(text, &ctx)?, *num_fmt)
1231 }
1232 CellSource::Subtotal { .. } => Value::Empty,
1233 };
1234 out.push(value);
1235 }
1236 Ok(out)
1237}
1238
1239fn render_template_row(cells: &[CellSource], ctx: &EvalContext) -> Result<Vec<Value>> {
1240 let mut out = Vec::with_capacity(cells.len());
1241 for cell in cells {
1242 match cell {
1243 CellSource::Empty => out.push(Value::Empty),
1244 CellSource::Literal(v) => out.push(v.clone()),
1245 CellSource::CellFormula { cached, .. } => out.push(cached.clone()),
1246 CellSource::Template { text, num_fmt, .. } => {
1247 out.push(coerce_for_num_fmt(eval_cell(text, ctx)?, *num_fmt))
1248 }
1249 CellSource::Subtotal { .. } => out.push(Value::Empty),
1250 }
1251 }
1252 Ok(out)
1253}
1254
1255fn coerce_for_num_fmt(value: Value, kind: NumFmtKind) -> Value {
1262 match kind {
1263 NumFmtKind::Numeric => match value {
1264 Value::String(s) => {
1265 let trimmed = s.trim();
1266 let cleaned: String = trimmed.chars().filter(|c| *c != ',').collect();
1267 match cleaned.parse::<f64>() {
1268 Ok(n) => Value::Number(n),
1269 Err(_) => Value::String(s),
1270 }
1271 }
1272 other => other,
1273 },
1274 NumFmtKind::Date => match value {
1275 Value::String(ref s) => {
1276 if let Some(serial) = parse_iso_date_to_serial(s.trim()) {
1277 Value::Number(serial)
1278 } else {
1279 value
1280 }
1281 }
1282 other => other,
1283 },
1284 NumFmtKind::Text => match value {
1285 Value::Number(n) => Value::String(canonical_number(n)),
1286 other => other,
1287 },
1288 NumFmtKind::General => value,
1289 }
1290}
1291
1292fn canonical_number(n: f64) -> String {
1293 crate::value::canonical_number(n)
1294}
1295
1296fn parse_iso_date_to_serial(s: &str) -> Option<f64> {
1297 let bytes = s.as_bytes();
1299 if bytes.len() < 10 {
1300 return None;
1301 }
1302 if bytes[4] != b'-' || bytes[7] != b'-' {
1303 return None;
1304 }
1305 let year: i32 = std::str::from_utf8(&bytes[..4]).ok()?.parse().ok()?;
1306 let month: u32 = std::str::from_utf8(&bytes[5..7]).ok()?.parse().ok()?;
1307 let day: u32 = std::str::from_utf8(&bytes[8..10]).ok()?.parse().ok()?;
1308 excel_date_to_serial(year, month, day)
1312}
1313
1314fn excel_date_to_serial(year: i32, month: u32, day: u32) -> Option<f64> {
1315 if !(1..=12).contains(&month) || !(1..=31).contains(&day) {
1318 return None;
1319 }
1320 let days = days_from_civil(year, month as i32, day as i32);
1321 let epoch = days_from_civil(1899, 12, 30);
1324 let mut serial = days - epoch;
1325 let leap_threshold = days_from_civil(1900, 3, 1);
1326 if days >= leap_threshold {
1327 } else if days >= days_from_civil(1900, 1, 1) {
1329 serial -= 1;
1330 }
1331 Some(serial as f64)
1332}
1333
1334fn days_from_civil(y: i32, m: i32, d: i32) -> i64 {
1338 let y = if m <= 2 { y - 1 } else { y };
1339 let era = if y >= 0 { y } else { y - 399 } / 400;
1340 let yoe = (y - era * 400) as i64;
1341 let doy = ((153 * (if m > 2 { m - 3 } else { m + 9 }) + 2) / 5 + d - 1) as i64;
1342 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
1343 era as i64 * 146097 + doe - 719468
1344}