1use std::collections::HashMap;
15use std::path::Path;
16use std::sync::Arc;
17
18use anyhow::{bail, Context, Result};
19
20use crate::calamine::{open_workbook, Data as CData, Reader, Xlsx};
21use crate::directives::{parse_directive_cell, Direction, Directive};
22use crate::styles::{self, NumFmtKind, TemplateStyles};
23use crate::value::Value;
24
25#[derive(Debug, Default, Clone)]
26pub struct ConfigMeta {
27 pub values: HashMap<String, String>,
28}
29
30impl ConfigMeta {
31 pub fn get(&self, key: &str) -> Option<&str> {
32 self.values.get(key).map(String::as_str)
33 }
34 pub fn source_sheet(&self) -> Option<&str> {
35 self.get("source_sheet")
36 }
37 pub fn output_file_pattern(&self) -> Option<&str> {
38 self.get("output_file_pattern")
39 }
40 pub fn file_group_keys(&self) -> Vec<String> {
47 match self.output_file_pattern() {
48 Some(p) => extract_group_keys(p),
49 None => Vec::new(),
50 }
51 }
52 pub fn source_table(&self) -> SourceTable {
57 self.get("source_table")
58 .map(parse_source_table)
59 .unwrap_or(SourceTable::HeaderRow(1))
60 }
61}
62
63fn extract_group_keys(pattern: &str) -> Vec<String> {
64 let mut keys = Vec::new();
65 let mut rest = pattern;
66 while let Some(open) = rest.find("{{") {
67 let after_open = &rest[open + 2..];
68 let close = match after_open.find("}}") {
69 Some(c) => c,
70 None => break,
71 };
72 let expr = after_open[..close].trim();
73 rest = &after_open[close + 2..];
74 let raw = expr
77 .strip_prefix('[')
78 .and_then(|s| s.strip_suffix(']'))
79 .map(str::trim)
80 .unwrap_or(expr);
81 if raw.is_empty() {
82 continue;
83 }
84 if raw.starts_with('.') || raw.starts_with('_') {
85 continue;
86 }
87 if raw
90 .chars()
91 .any(|c| matches!(c, ' ' | '|' | '+' | '*' | '/' | '-' | '>' | '<' | '=' | '!' | '&' | '(' | ')' | '[' | ']' | ','))
92 {
93 continue;
94 }
95 if !keys.iter().any(|k: &String| k == raw) {
96 keys.push(raw.to_string());
97 }
98 }
99 keys
100}
101
102#[derive(Debug, Clone, PartialEq)]
104pub enum SourceTable {
105 HeaderRow(usize),
109 Range {
116 first_row: usize, last_row: Option<usize>, first_col: usize, last_col: Option<usize>, },
121}
122
123fn parse_source_table(raw: &str) -> SourceTable {
124 let s = raw.trim();
125 if let Ok(n) = s.parse::<usize>() {
126 return SourceTable::HeaderRow(n.max(1));
127 }
128 if let Some((a, b)) = s.split_once(':') {
129 let lhs = parse_a1_part(a.trim());
130 let rhs = parse_a1_part(b.trim());
131 if let (Some((Some(r1), Some(c1))), Some((r2, c2))) = (lhs, rhs) {
132 let (first_row, last_row) = match r2 {
133 Some(r2) => (r1.min(r2), Some(r1.max(r2))),
134 None => (r1, None),
135 };
136 let (first_col, last_col) = match c2 {
137 Some(c2) => (c1.min(c2), Some(c1.max(c2))),
138 None => (c1, None),
139 };
140 return SourceTable::Range {
141 first_row,
142 last_row,
143 first_col,
144 last_col,
145 };
146 }
147 }
148 SourceTable::HeaderRow(1)
149}
150
151fn parse_a1_part(s: &str) -> Option<(Option<usize>, Option<usize>)> {
155 if s.is_empty() {
156 return None;
157 }
158 let bytes = s.as_bytes();
159 let mut i = 0;
160 let mut col = 0usize;
161 while i < bytes.len() && bytes[i].is_ascii_alphabetic() {
162 let c = bytes[i].to_ascii_uppercase();
163 col = col * 26 + (c - b'A' + 1) as usize;
164 i += 1;
165 }
166 let col_opt = if col == 0 { None } else { Some(col) };
167 let row_opt = if i == bytes.len() {
168 None
169 } else {
170 let row: usize = std::str::from_utf8(&bytes[i..]).ok()?.parse().ok()?;
171 if row == 0 {
172 None
173 } else {
174 Some(row)
175 }
176 };
177 if col_opt.is_none() && row_opt.is_none() {
178 return None;
179 }
180 Some((row_opt, col_opt))
181}
182
183#[derive(Debug, Clone)]
184pub enum CellSource {
185 Empty,
186 Literal(Value),
187 Template {
198 text: String,
199 num_fmt: NumFmtKind,
200 format_code: Option<String>,
201 style_idx: Option<usize>,
202 },
203 Subtotal {
208 aggregate: String,
209 field: String,
210 },
211 CellFormula {
218 text: String,
219 cached: Value,
220 format_code: Option<String>,
221 style_idx: Option<usize>,
222 },
223}
224
225impl CellSource {
226 pub fn is_template(&self) -> bool {
227 matches!(self, CellSource::Template { .. })
228 }
229}
230
231fn parse_subtotal_cell(text: &str) -> Option<(String, String)> {
235 let trimmed = text.trim();
236 let inner = trimmed.strip_prefix("{{")?.strip_suffix("}}")?;
237 let body = inner.trim().strip_prefix("@subtotal")?.trim();
238 let paren_open = body.find('(')?;
239 let fn_name = body[..paren_open].trim();
240 let after = &body[paren_open + 1..];
241 let paren_close = after.rfind(')')?;
242 let arg = after[..paren_close].trim();
243 let field = arg
246 .strip_prefix('[')
247 .and_then(|s| s.strip_suffix(']'))
248 .map(str::trim)
249 .filter(|s| !s.is_empty())?;
250 Some((fn_name.to_ascii_uppercase(), field.to_string()))
251}
252
253#[derive(Debug, Clone)]
254pub enum RowPlan {
255 Static(Vec<CellSource>),
256 ExpandDown {
257 cells: Vec<CellSource>,
258 directives: Vec<Directive>,
259 subtotal_rows: Vec<Vec<CellSource>>,
263 side_rows: Vec<Vec<CellSource>>,
268 col_range: Option<(usize, usize)>,
273 },
274 ExpandRight {
278 cells: Vec<CellSource>,
279 directives: Vec<Directive>,
280 },
281}
282
283#[derive(Debug, Clone)]
284pub struct SheetPlan {
285 pub name: String,
286 pub rows: Vec<RowPlan>,
287 pub sub_blocks: Vec<SubBlock>,
291 pub n_cols: usize,
294}
295
296#[derive(Debug, Clone)]
300pub struct SubBlock {
301 pub col_first: usize,
302 pub col_last: usize,
303 pub rows: Vec<RowPlan>,
304}
305
306#[derive(Debug, Clone)]
307pub struct WorkbookPlan {
308 pub config: ConfigMeta,
309 pub sheets: Vec<SheetPlan>,
310 pub inputs: HashMap<String, Value>,
313 pub lists: HashMap<String, Vec<Value>>,
317 pub named_sources: HashMap<String, SourceDecl>,
321}
322
323#[derive(Debug, Clone)]
326pub struct SourceDecl {
327 pub sheet: String,
328 pub table: SourceTable,
329}
330
331const RESERVED_SHEETS: &[&str] = &["__config__", "__inputs__", "__lists__", "__sources__"];
332
333fn is_reserved_sheet(name: &str) -> bool {
334 RESERVED_SHEETS.contains(&name)
335}
336
337fn cell_is_template_text(s: &str) -> bool {
338 s.contains("{{")
342}
343
344fn compute_template_col_range(cells: &[CellSource]) -> Option<(usize, usize)> {
360 let mut first: Option<usize> = None;
363 let mut last: usize = 0;
364 for (i, c) in cells.iter().enumerate() {
365 let is_core = matches!(c, CellSource::Template { .. } | CellSource::Subtotal { .. });
366 if is_core {
367 first.get_or_insert(i);
368 last = i;
369 }
370 }
371 let (mut lo, mut hi) = (first?, last);
372 while hi + 1 < cells.len() {
380 match &cells[hi + 1] {
381 CellSource::CellFormula { .. } => hi += 1,
382 _ => break,
383 }
384 }
385 while lo > 0 {
386 match &cells[lo - 1] {
387 CellSource::CellFormula { .. } => lo -= 1,
388 _ => break,
389 }
390 }
391 Some((lo, hi))
392}
393
394fn cells_only_outside_range(cells: &[CellSource], range: (usize, usize)) -> bool {
398 let (lo, hi) = range;
399 let mut any_outside = false;
400 for (i, c) in cells.iter().enumerate() {
401 let inside = i >= lo && i <= hi;
402 match c {
403 CellSource::Empty => continue,
404 _ if inside => return false,
405 _ => any_outside = true,
406 }
407 }
408 any_outside
409}
410
411fn cells_isolate_outside(cells: &[CellSource], range: (usize, usize)) -> Vec<CellSource> {
412 let (lo, hi) = range;
413 cells
414 .iter()
415 .enumerate()
416 .map(|(i, c)| {
417 if i >= lo && i <= hi {
418 CellSource::Empty
419 } else {
420 c.clone()
421 }
422 })
423 .collect()
424}
425
426fn cells_isolate_inside(cells: &[CellSource], range: (usize, usize)) -> Vec<CellSource> {
427 let (lo, hi) = range;
428 cells
429 .iter()
430 .enumerate()
431 .map(|(i, c)| {
432 if i >= lo && i <= hi {
433 c.clone()
434 } else {
435 CellSource::Empty
436 }
437 })
438 .collect()
439}
440
441fn template_depends_on_source_row(s: &str, named_sources_to_exclude: &[&str]) -> bool {
442 let mut cleaned = s.to_string();
443 for ns in [
444 "__config__[",
445 "__inputs__[",
446 "__lists__[",
447 "__sources__[",
448 ] {
449 cleaned = cleaned.replace(ns, "");
450 }
451 for name in named_sources_to_exclude {
457 let prefix = format!("{name}[");
458 cleaned = cleaned.replace(&prefix, "");
459 }
460 cleaned.contains('[')
461}
462
463pub fn parse_template(path: &Path) -> Result<WorkbookPlan> {
464 let styles = styles::parse_template_styles(path).unwrap_or_default();
465 let wb: Xlsx<_> = open_workbook(path)
466 .with_context(|| format!("open template workbook at {}", path.display()))?;
467 parse_template_inner(wb, styles, None)
468}
469
470pub fn parse_template_bytes(bytes: &[u8]) -> Result<WorkbookPlan> {
473 parse_template_bytes_with_manifest(bytes, None)
474}
475
476pub fn parse_template_bytes_with_manifest(
481 bytes: &[u8],
482 manifest: Option<&crate::manifest::StyleManifest>,
483) -> Result<WorkbookPlan> {
484 let styles = styles::parse_template_styles_bytes(bytes).unwrap_or_default();
485 let cursor = std::io::Cursor::new(bytes.to_vec());
486 let wb: Xlsx<_> = Xlsx::new(cursor).context("open template workbook from bytes")?;
487 parse_template_inner(wb, styles, manifest)
488}
489
490fn parse_template_inner<R: std::io::Read + std::io::Seek>(
491 mut wb: Xlsx<R>,
492 styles: styles::TemplateStyles,
493 manifest: Option<&crate::manifest::StyleManifest>,
494) -> Result<WorkbookPlan> {
495 let named_source_names: Vec<String> = if sheet_names_set(&wb).contains("__sources__") {
499 if let Ok(range) = wb.worksheet_range("__sources__") {
500 let (rows, cols) = range.get_size();
501 if rows >= 2 && cols >= 1 {
502 (1..rows)
503 .filter_map(|r| match range.get((r, 0)) {
504 Some(CData::String(s)) if !s.is_empty() => Some(s.clone()),
505 _ => None,
506 })
507 .collect()
508 } else {
509 Vec::new()
510 }
511 } else {
512 Vec::new()
513 }
514 } else {
515 Vec::new()
516 };
517
518 let mut config = ConfigMeta::default();
519 let sheet_names = wb.sheet_names();
520
521 if sheet_names.iter().any(|n| n == "__config__") {
524 let range = wb
525 .worksheet_range("__config__")
526 .context("read __config__ sheet")?;
527 let (rows, cols) = range.get_size();
528 for r in 0..rows {
529 if cols < 2 {
530 break;
531 }
532 let key = match range.get((r, 0)) {
533 Some(CData::String(s)) if !s.is_empty() => s.clone(),
534 _ => continue,
535 };
536 let value = match range.get((r, 1)) {
537 Some(CData::String(s)) => s.clone(),
538 Some(CData::Float(f)) => format!("{f}"),
539 Some(CData::Int(i)) => format!("{i}"),
540 Some(CData::Bool(b)) => b.to_string(),
541 _ => String::new(),
542 };
543 config.values.insert(key, value);
544 }
545 }
546
547 let mut sheets = Vec::with_capacity(sheet_names.len());
548 for name in sheet_names {
549 if is_reserved_sheet(&name) {
550 continue;
551 }
552 let range = wb
553 .worksheet_range(&name)
554 .with_context(|| format!("read template sheet {name:?}"))?;
555 let formula_range = wb.worksheet_formula(&name).ok();
564 let (value_rows, value_cols) = range.get_size();
565 let value_start = range.start().unwrap_or((0, 0));
566 let (rows, cols) = if let Some(fr) = formula_range.as_ref() {
573 if let (Some(fr_start), Some(fr_end)) = (fr.start(), fr.end()) {
574 let needed_rows =
575 (fr_end.0 as i64 - value_start.0 as i64).max(value_rows as i64 - 1) + 1;
576 let needed_cols =
577 (fr_end.1 as i64 - value_start.1 as i64).max(value_cols as i64 - 1) + 1;
578 let _ = fr_start;
579 (needed_rows.max(0) as usize, needed_cols.max(0) as usize)
580 } else {
581 (value_rows, value_cols)
582 }
583 } else {
584 (value_rows, value_cols)
585 };
586
587 let mut block_ranges: Vec<(usize, usize)> = Vec::new();
592 for r in 0..rows {
593 for c in 0..cols {
594 if let Some(CData::String(s)) = range.get((r, c)) {
595 if let Some(dirs) = parse_directive_cell(s) {
596 for d in dirs {
597 if let Directive::Block { col_first, col_last } = d {
598 block_ranges.push((col_first, col_last));
599 }
600 }
601 }
602 }
603 }
604 }
605 block_ranges.sort();
606 block_ranges.dedup();
607
608 if !block_ranges.is_empty() {
609 let mut sub_blocks_out: Vec<SubBlock> = Vec::new();
610 for (col_first, col_last) in &block_ranges {
611 let sub_rows = build_row_plans_for_range(
612 &range,
613 formula_range.as_ref(),
614 &name,
615 rows,
616 *col_first,
617 *col_last,
618 &styles,
619 &named_source_names,
620 manifest,
621 )?;
622 sub_blocks_out.push(SubBlock {
623 col_first: *col_first,
624 col_last: *col_last,
625 rows: sub_rows,
626 });
627 }
628 sheets.push(SheetPlan {
629 name: name.clone(),
630 rows: Vec::new(),
631 sub_blocks: sub_blocks_out,
632 n_cols: cols,
633 });
634 continue;
635 }
636
637 let row_plans = build_row_plans_for_range(
638 &range,
639 formula_range.as_ref(),
640 &name,
641 rows,
642 0,
643 cols.saturating_sub(1),
644 &styles,
645 &named_source_names,
646 manifest,
647 )?;
648 sheets.push(SheetPlan {
649 name,
650 rows: row_plans,
651 sub_blocks: Vec::new(),
652 n_cols: cols,
653 });
654 }
655
656
657 if sheets.is_empty() {
659 bail!("template has no visible (non-reserved) sheets");
660 }
661
662 let mut inputs = HashMap::new();
666 if sheet_names_set(&wb).contains("__inputs__") {
667 if let Ok(range) = wb.worksheet_range("__inputs__") {
668 let (rows, cols) = range.get_size();
669 if rows >= 2 && cols >= 1 {
670 let mut headers: Vec<String> = Vec::new();
671 for c in 0..cols {
672 headers.push(match range.get((0, c)) {
673 Some(CData::String(s)) => s.clone(),
674 _ => String::new(),
675 });
676 }
677 let name_col = 0usize; let default_col = headers
679 .iter()
680 .position(|h| h.eq_ignore_ascii_case("default"));
681 if let Some(default_col) = default_col {
682 let mut input_ctx: HashMap<String, Value> = HashMap::new();
688 let config_map: HashMap<String, Value> = config
689 .values
690 .iter()
691 .map(|(k, v)| (k.clone(), Value::String(v.clone())))
692 .collect();
693 input_ctx
694 .insert("__config__".to_string(), Value::Map(Arc::new(config_map)));
695 for r in 1..rows {
696 let name = match range.get((r, name_col)) {
697 Some(CData::String(s)) if !s.is_empty() => s.clone(),
698 _ => continue,
699 };
700 let raw = range
701 .get((r, default_col))
702 .map(Value::from_calamine)
703 .unwrap_or(Value::Empty);
704 let evaluated = if let Value::String(s) = &raw {
705 if s.contains("{{") {
706 crate::eval::eval_cell(s, &input_ctx).unwrap_or(raw.clone())
707 } else {
708 raw
709 }
710 } else {
711 raw
712 };
713 inputs.insert(name, evaluated);
714 }
715 }
716 }
717 }
718 }
719
720 let mut lists: HashMap<String, Vec<Value>> = HashMap::new();
723 if sheet_names_set(&wb).contains("__lists__") {
724 if let Ok(range) = wb.worksheet_range("__lists__") {
725 let (rows, cols) = range.get_size();
726 for c in 0..cols {
727 let header = match range.get((0, c)) {
728 Some(CData::String(s)) if !s.is_empty() => s.clone(),
729 _ => continue,
730 };
731 let mut values = Vec::new();
732 for r in 1..rows {
733 match range.get((r, c)) {
734 Some(CData::Empty) | None => break,
735 Some(other) => values.push(Value::from_calamine(other)),
736 }
737 }
738 lists.insert(header, values);
739 }
740 }
741 }
742
743 let mut named_sources: HashMap<String, SourceDecl> = HashMap::new();
748 if sheet_names_set(&wb).contains("__sources__") {
749 if let Ok(range) = wb.worksheet_range("__sources__") {
750 let (rows, cols) = range.get_size();
751 if rows >= 2 && cols >= 1 {
752 let mut headers: Vec<String> = Vec::with_capacity(cols);
753 for c in 0..cols {
754 headers.push(match range.get((0, c)) {
755 Some(CData::String(s)) => s.clone(),
756 _ => String::new(),
757 });
758 }
759 let name_col = 0usize;
760 let sheet_col = headers
761 .iter()
762 .position(|h| h.eq_ignore_ascii_case("sheet"));
763 let table_col = headers
764 .iter()
765 .position(|h| h.eq_ignore_ascii_case("table"));
766 for r in 1..rows {
767 let name = match range.get((r, name_col)) {
768 Some(CData::String(s)) if !s.is_empty() => s.clone(),
769 _ => continue,
770 };
771 let sheet = sheet_col
772 .and_then(|c| range.get((r, c)))
773 .and_then(|d| match d {
774 CData::String(s) if !s.is_empty() => Some(s.clone()),
775 _ => None,
776 })
777 .unwrap_or_else(|| name.clone());
778 let table_raw = table_col
779 .and_then(|c| range.get((r, c)))
780 .map(|d| match d {
781 CData::String(s) => s.clone(),
782 CData::Float(f) => format!("{f}"),
783 CData::Int(i) => format!("{i}"),
784 _ => String::new(),
785 })
786 .unwrap_or_default();
787 let table = if table_raw.is_empty() {
788 SourceTable::HeaderRow(1)
789 } else {
790 parse_source_table(&table_raw)
791 };
792 named_sources.insert(name, SourceDecl { sheet, table });
793 }
794 }
795 }
796 }
797
798 Ok(WorkbookPlan {
799 config,
800 sheets,
801 inputs,
802 lists,
803 named_sources,
804 })
805}
806
807fn sheet_names_set<R: std::io::Read + std::io::Seek>(
808 wb: &Xlsx<R>,
809) -> std::collections::HashSet<String> {
810 wb.sheet_names().into_iter().collect()
811}
812
813fn build_row_plans_for_range(
818 range: &calamine::Range<CData>,
819 formulas: Option<&calamine::Range<String>>,
820 sheet_name: &str,
821 rows: usize,
822 col_first: usize,
823 col_last: usize,
824 styles: &TemplateStyles,
825 named_source_names: &[String],
826 manifest: Option<&crate::manifest::StyleManifest>,
827) -> Result<Vec<RowPlan>> {
828 let value_start = range.start().unwrap_or((0, 0));
835 let formula_at = |r: usize, c: usize| -> Option<String> {
836 formulas.and_then(|fr| {
837 let abs = (value_start.0 + r as u32, value_start.1 + c as u32);
838 fr.get_value(abs).and_then(|s| {
839 if s.is_empty() {
840 None
841 } else {
842 Some(s.clone())
843 }
844 })
845 })
846 };
847 let mut row_plans: Vec<RowPlan> = Vec::with_capacity(rows);
848 let mut pending_direction = Direction::Down;
849 let mut pending_directives: Vec<Directive> = Vec::new();
850 let cols_in_range = col_last.saturating_sub(col_first) + 1;
851 for r in 0..rows {
852 let mut row_cells = Vec::with_capacity(cols_in_range);
853 let mut has_source_template = false;
854 let mut has_subtotal = false;
855 let mut directive_only = true;
856 let mut any_cell = false;
857 let active_source: Option<&str> = pending_directives.iter().find_map(|d| match d {
858 Directive::Source(n) => Some(n.as_str()),
859 _ => None,
860 });
861 let exclude_named: Vec<&str> = named_source_names
862 .iter()
863 .filter(|n| Some(n.as_str()) != active_source)
864 .map(|s| s.as_str())
865 .collect();
866 for c_off in 0..cols_in_range {
867 let c = col_first + c_off;
868 let formula_here = formula_at(r, c);
875 let cell = match range.get((r, c)) {
876 None | Some(CData::Empty) if formula_here.is_some() => {
877 any_cell = true;
878 directive_only = false;
879 let formula_text = formula_here.unwrap();
880 let format_code = styles.format_code(sheet_name, r as u32, c as u32);
881 let style_idx = manifest.and_then(|m| {
882 m.cells
883 .get(sheet_name)
884 .and_then(|map| map.get(&(r as u32, c as u32)).copied())
885 });
886 CellSource::CellFormula {
887 text: formula_text,
888 cached: Value::Empty,
889 format_code,
890 style_idx,
891 }
892 }
893 None | Some(CData::Empty) => CellSource::Empty,
894 Some(CData::String(s)) if cell_is_template_text(s) => {
895 any_cell = true;
896 if let Some((aggregate, field)) = parse_subtotal_cell(s) {
897 directive_only = false;
898 has_subtotal = true;
899 CellSource::Subtotal { aggregate, field }
900 } else if parse_directive_cell(s).is_some() {
901 CellSource::Empty
902 } else {
903 directive_only = false;
904 if template_depends_on_source_row(s, &exclude_named) {
905 has_source_template = true;
906 }
907 let format_code = styles.format_code(sheet_name, r as u32, c as u32);
908 let num_fmt = format_code
909 .as_deref()
910 .map(styles::classify_num_fmt)
911 .unwrap_or(NumFmtKind::General);
912 let style_idx = manifest.and_then(|m| {
913 m.cells
914 .get(sheet_name)
915 .and_then(|map| map.get(&(r as u32, c as u32)).copied())
916 });
917 CellSource::Template {
918 text: s.clone(),
919 num_fmt,
920 format_code,
921 style_idx,
922 }
923 }
924 }
925 Some(other) => {
926 any_cell = true;
927 directive_only = false;
928 if let Some(formula_text) = formula_here {
933 let format_code = styles.format_code(sheet_name, r as u32, c as u32);
934 let style_idx = manifest.and_then(|m| {
935 m.cells
936 .get(sheet_name)
937 .and_then(|map| map.get(&(r as u32, c as u32)).copied())
938 });
939 CellSource::CellFormula {
940 text: formula_text,
941 cached: Value::from_calamine(other),
942 format_code,
943 style_idx,
944 }
945 } else {
946 CellSource::Literal(Value::from_calamine(other))
947 }
948 }
949 };
950 row_cells.push(cell);
951 }
952
953 if any_cell && directive_only {
954 for c_off in 0..cols_in_range {
955 let c = col_first + c_off;
956 if let Some(CData::String(s)) = range.get((r, c)) {
957 if let Some(directives) = parse_directive_cell(s) {
958 for d in directives {
959 match d {
960 Directive::Repeat(dir) => pending_direction = dir,
961 Directive::Block { .. } => {} other => pending_directives.push(other),
963 }
964 }
965 }
966 }
967 }
968 continue;
969 }
970
971 if !has_source_template && !has_subtotal {
972 if let Some(RowPlan::ExpandDown {
973 col_range: Some(range_inner),
974 side_rows,
975 ..
976 }) = row_plans.last_mut()
977 {
978 let range_inner = *range_inner;
979 if !any_cell {
980 side_rows.push(row_cells);
981 continue;
982 }
983 if cells_only_outside_range(&row_cells, range_inner) {
984 side_rows.push(row_cells);
985 continue;
986 }
987 let outside = cells_isolate_outside(&row_cells, range_inner);
988 let inside = cells_isolate_inside(&row_cells, range_inner);
989 let has_inside = inside.iter().any(|c| !matches!(c, CellSource::Empty));
990 let has_outside = outside.iter().any(|c| !matches!(c, CellSource::Empty));
991 if has_inside && has_outside {
992 side_rows.push(outside);
993 row_plans.push(RowPlan::Static(inside));
994 continue;
995 }
996 }
997 if !any_cell {
998 continue;
999 }
1000 }
1001
1002 if has_subtotal && !has_source_template {
1003 if let Some(RowPlan::ExpandDown { subtotal_rows, .. }) = row_plans.last_mut() {
1004 subtotal_rows.push(row_cells);
1005 continue;
1006 }
1007 }
1008
1009 let row_plan = if has_source_template {
1010 let directives = std::mem::take(&mut pending_directives);
1011 let col_range = compute_template_col_range(&row_cells);
1012 let plan = match pending_direction {
1013 Direction::Down => RowPlan::ExpandDown {
1014 cells: row_cells,
1015 directives,
1016 subtotal_rows: Vec::new(),
1017 side_rows: Vec::new(),
1018 col_range,
1019 },
1020 Direction::Right => RowPlan::ExpandRight {
1021 cells: row_cells,
1022 directives,
1023 },
1024 };
1025 pending_direction = Direction::Down;
1026 plan
1027 } else {
1028 if let Some(RowPlan::ExpandDown {
1029 col_range: Some(range_inner),
1030 side_rows,
1031 ..
1032 }) = row_plans.last_mut()
1033 {
1034 if cells_only_outside_range(&row_cells, *range_inner) {
1035 side_rows.push(row_cells);
1036 continue;
1037 }
1038 }
1039 RowPlan::Static(row_cells)
1040 };
1041 row_plans.push(row_plan);
1042 }
1043 Ok(row_plans)
1044}
1045
1046pub fn inputs_to_value(inputs: &HashMap<String, Value>) -> Value {
1051 Value::Map(Arc::new(inputs.clone()))
1052}
1053
1054pub fn lists_to_value(lists: &HashMap<String, Vec<Value>>) -> Value {
1058 let inner: HashMap<String, Value> = lists
1059 .iter()
1060 .map(|(k, v)| (k.clone(), Value::List(Arc::new(v.clone()))))
1061 .collect();
1062 Value::Map(Arc::new(inner))
1063}