use std::collections::HashMap;
use rust_xlsxwriter::{Color, DocProperties, ExcelDateTime, Format, Formula, Workbook};
use serde::{Deserialize, Serialize};
use crate::cell_key;
use crate::resolve::{a1_to_zero_indexed_row_col, parse_a1};
use crate::sheet_ir::value::CellValue;
use crate::sheet_ir::RunResult;
fn writer_err(e: rust_xlsxwriter::XlsxError) -> RenderError {
RenderError::Writer(e.to_string())
}
pub mod layout;
pub use layout::*;
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum RenderError {
#[error("malformed cell address {addr} on sheet {sheet}")]
MalformedAddr {
sheet: String,
addr: String,
},
#[error("malformed merge range {range} on sheet {sheet}")]
MalformedMerge {
sheet: String,
range: String,
},
#[error("non-finite computed value at {cell}")]
NonFiniteValue {
cell: String,
},
#[error("xlsx writer error: {0}")]
Writer(String),
}
fn fixed_creation_datetime() -> Result<ExcelDateTime, RenderError> {
ExcelDateTime::from_ymd(2024, 1, 1)
.and_then(|d| d.and_hms(0, 0, 0))
.map_err(writer_err)
}
#[must_use]
pub fn normalize_formula_for_writer(f: &str) -> String {
if f.trim_start().starts_with('=') {
f.to_string()
} else {
format!("={f}")
}
}
fn argb_to_color(argb: &str) -> Option<Color> {
let hex = argb.trim();
let rgb_hex = match hex.len() {
8 => hex.get(2..)?,
6 => hex, _ => return None,
};
let rgb = u32::from_str_radix(rgb_hex, 16).ok()?;
Some(Color::RGB(rgb))
}
fn cell_format(cell: &CellLayout) -> Option<Format> {
if cell.number_format.is_none() && cell.fill_argb.is_none() && cell.font_argb.is_none() {
return None;
}
let mut fmt = Format::new();
if let Some(nf) = &cell.number_format {
fmt = fmt.set_num_format(nf.clone());
}
if let Some(fill) = cell.fill_argb.as_deref().and_then(argb_to_color) {
fmt = fmt.set_background_color(fill);
}
if let Some(font) = cell.font_argb.as_deref().and_then(argb_to_color) {
fmt = fmt.set_font_color(font);
}
Some(fmt)
}
type MergeInterior = std::collections::HashSet<(u32, u16)>;
fn replay_merges(
ws: &mut rust_xlsxwriter::Worksheet,
sheet: &SheetLayout,
top_left_text: &HashMap<(u32, u16), String>,
) -> Result<MergeInterior, RenderError> {
let mut interior = MergeInterior::new();
let blank = Format::new();
for range in &sheet.merges {
let (start, end) = range
.split_once(':')
.ok_or_else(|| RenderError::MalformedMerge {
sheet: sheet.name.clone(),
range: range.clone(),
})?;
let malformed = || RenderError::MalformedMerge {
sheet: sheet.name.clone(),
range: range.clone(),
};
let (r0, c0) = a1_to_zero_indexed_row_col(start.trim()).ok_or_else(malformed)?;
let (r1, c1) = a1_to_zero_indexed_row_col(end.trim()).ok_or_else(malformed)?;
let (row_lo, row_hi) = (r0.min(r1), r0.max(r1));
let (col_lo, col_hi) = (c0.min(c1), c0.max(c1));
if row_lo == row_hi && col_lo == col_hi {
return Err(malformed());
}
let text = top_left_text
.get(&(row_lo, col_lo))
.cloned()
.unwrap_or_default();
ws.merge_range(row_lo, col_lo, row_hi, col_hi, &text, &blank)
.map_err(writer_err)?;
for r in row_lo..=row_hi {
for c in col_lo..=col_hi {
interior.insert((r, c));
}
}
}
Ok(interior)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RenderMode {
#[default]
Filled,
InputsOnly,
}
pub fn render_xlsx(
layout: &LayoutDescriptor,
run: &RunResult,
mode: RenderMode,
) -> Result<Vec<u8>, RenderError> {
let mut wb = init_workbook()?;
for sheet in &layout.sheets {
let ws = wb.add_worksheet();
render_sheet(ws, sheet, run, mode)?;
}
wb.save_to_buffer().map_err(writer_err)
}
fn init_workbook() -> Result<Workbook, RenderError> {
let mut wb = Workbook::new();
let props = DocProperties::new()
.set_author("")
.set_creation_datetime(&fixed_creation_datetime()?);
wb.set_properties(&props);
Ok(wb)
}
fn render_sheet(
ws: &mut rust_xlsxwriter::Worksheet,
sheet: &SheetLayout,
run: &RunResult,
mode: RenderMode,
) -> Result<(), RenderError> {
apply_sheet_scaffold(ws, sheet)?;
let top_left_text = build_top_left_text(sheet, run)?;
let interior = replay_merges(ws, sheet, &top_left_text)?;
for cell in &sheet.cells {
write_cell(ws, sheet, run, cell, &interior, mode)?;
}
Ok(())
}
fn apply_sheet_scaffold(
ws: &mut rust_xlsxwriter::Worksheet,
sheet: &SheetLayout,
) -> Result<(), RenderError> {
ws.set_name(&sheet.name).map_err(writer_err)?;
if sheet.hidden {
ws.set_hidden(true);
}
for (col_1based, width) in &sheet.col_widths {
if let Some(col) = col_1based.checked_sub(1) {
ws.set_column_width(col, *width).map_err(writer_err)?;
}
}
for col_1based in &sheet.hidden_cols {
if let Some(col) = col_1based.checked_sub(1) {
ws.set_column_hidden(col).map_err(writer_err)?;
}
}
Ok(())
}
fn build_top_left_text(
sheet: &SheetLayout,
run: &RunResult,
) -> Result<HashMap<(u32, u16), String>, RenderError> {
let mut top_left_text: HashMap<(u32, u16), String> = HashMap::new();
for cell in &sheet.cells {
if a1_to_zero_indexed_row_col(&cell.addr).is_none() {
let _ = parse_a1(&cell.addr); return Err(RenderError::MalformedAddr {
sheet: sheet.name.clone(),
addr: cell.addr.clone(),
});
}
let key = cell_key(&sheet.name, &cell.addr);
let display = display_text(run, &key, cell)?;
if let (Some((r, c)), Some(text)) = (a1_to_zero_indexed_row_col(&cell.addr), display) {
top_left_text.insert((r, c), text);
}
}
Ok(top_left_text)
}
fn display_text(
run: &RunResult,
key: &str,
cell: &CellLayout,
) -> Result<Option<String>, RenderError> {
let display = match run.computed.get(key) {
Some(CellValue::Number(n)) if n.is_finite() => Some(format_number(*n)),
Some(CellValue::Number(_)) => {
return Err(RenderError::NonFiniteValue {
cell: key.to_string(),
})
},
Some(CellValue::Text(s)) => Some(s.clone()),
Some(CellValue::Bool(b)) => Some(b.to_string()),
_ => cell.value.clone(),
};
Ok(display)
}
fn write_cell(
ws: &mut rust_xlsxwriter::Worksheet,
sheet: &SheetLayout,
run: &RunResult,
cell: &CellLayout,
interior: &MergeInterior,
mode: RenderMode,
) -> Result<(), RenderError> {
let (row, col) =
a1_to_zero_indexed_row_col(&cell.addr).ok_or_else(|| RenderError::MalformedAddr {
sheet: sheet.name.clone(),
addr: cell.addr.clone(),
})?;
if interior.contains(&(row, col)) {
return Ok(()); }
let key = cell_key(&sheet.name, &cell.addr);
let computed = run.computed.get(&key);
let fmt = cell_format(cell);
write_computed_value(ws, row, col, cell, computed, key, fmt.as_ref(), mode)
}
fn write_computed_value(
ws: &mut rust_xlsxwriter::Worksheet,
row: u32,
col: u16,
cell: &CellLayout,
computed: Option<&CellValue>,
key: String,
fmt: Option<&Format>,
mode: RenderMode,
) -> Result<(), RenderError> {
match computed {
Some(CellValue::Number(n)) => {
if !n.is_finite() {
return Err(RenderError::NonFiniteValue { cell: key });
}
let n = *n;
write_formula_or_value(
ws,
row,
col,
&cell.formula,
format_number(n),
fmt,
mode,
|ws, fmt| write_number_literal(ws, row, col, n, fmt),
)?;
},
Some(CellValue::Text(s)) => {
write_formula_or_value(
ws,
row,
col,
&cell.formula,
s.clone(),
fmt,
mode,
|ws, fmt| write_string_cell(ws, row, col, s, fmt),
)?;
},
Some(CellValue::Bool(b)) => {
let literal = b.to_string();
let cached = if *b { "TRUE" } else { "FALSE" }.to_string();
write_formula_or_value(ws, row, col, &cell.formula, cached, fmt, mode, |ws, fmt| {
write_string_cell(ws, row, col, &literal, fmt)
})?;
},
_ => {
if let Some(v) = &cell.value {
write_string_cell(ws, row, col, v, fmt)?;
}
},
}
Ok(())
}
fn format_number(n: f64) -> String {
format!("{n}")
}
fn write_formula_or_value<W>(
ws: &mut rust_xlsxwriter::Worksheet,
row: u32,
col: u16,
formula: &Option<String>,
cached_result: String,
fmt: Option<&Format>,
mode: RenderMode,
write_literal: W,
) -> Result<(), RenderError>
where
W: FnOnce(&mut rust_xlsxwriter::Worksheet, Option<&Format>) -> Result<(), RenderError>,
{
match (formula, fmt) {
(Some(f), Some(fmt)) => {
let formula = build_formula(f, cached_result, mode);
ws.write_formula_with_format(row, col, formula, fmt)
.map_err(writer_err)?;
},
(Some(f), None) => {
let formula = build_formula(f, cached_result, mode);
ws.write_formula(row, col, formula).map_err(writer_err)?;
},
(None, _) => write_literal(ws, fmt)?,
}
Ok(())
}
fn build_formula(f: &str, cached_result: String, mode: RenderMode) -> Formula {
let formula = Formula::new(normalize_formula_for_writer(f));
match mode {
RenderMode::Filled => formula.set_result(cached_result),
RenderMode::InputsOnly => formula,
}
}
fn write_number_literal(
ws: &mut rust_xlsxwriter::Worksheet,
row: u32,
col: u16,
n: f64,
fmt: Option<&Format>,
) -> Result<(), RenderError> {
match fmt {
Some(fmt) => ws
.write_number_with_format(row, col, n, fmt)
.map_err(writer_err)?,
None => ws.write_number(row, col, n).map_err(writer_err)?,
};
Ok(())
}
fn write_string_cell(
ws: &mut rust_xlsxwriter::Worksheet,
row: u32,
col: u16,
s: &str,
fmt: Option<&Format>,
) -> Result<(), RenderError> {
match fmt {
Some(fmt) => ws
.write_string_with_format(row, col, s, fmt)
.map_err(writer_err)?,
None => ws.write_string(row, col, s).map_err(writer_err)?,
};
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::excel_error::ExcelError;
use std::collections::HashMap;
const ZIP_MAGIC: &[u8] = b"PK\x03\x04";
fn run_with(pairs: &[(&str, CellValue)]) -> RunResult {
let mut computed = HashMap::new();
for (k, v) in pairs {
computed.insert((*k).to_string(), v.clone());
}
RunResult {
computed,
traces: HashMap::new(),
}
}
fn cell(addr: &str, formula: Option<&str>, value: Option<&str>) -> CellLayout {
CellLayout {
addr: addr.to_string(),
formula: formula.map(str::to_string),
value: value.map(str::to_string),
number_format: None,
fill_argb: None,
font_argb: None,
}
}
fn extract_sheet_xml(buf: &[u8], sheet_path: &str) -> String {
use std::io::Read;
let reader = std::io::Cursor::new(buf.to_vec());
let mut archive = zip::ZipArchive::new(reader).expect("xlsx buffer is a valid ZIP");
let mut entry = archive
.by_name(sheet_path)
.unwrap_or_else(|_| panic!("worksheet entry {sheet_path} present in the xlsx"));
let mut xml = String::new();
entry
.read_to_string(&mut xml)
.expect("worksheet entry is UTF-8 XML");
xml
}
fn cell_xml<'a>(sheet_xml: &'a str, a1: &str) -> Option<&'a str> {
let needle = format!("<c r=\"{a1}\"");
let start = sheet_xml.find(&needle)?;
let rest = &sheet_xml[start..];
let close = rest.find("</c>").map(|i| i + "</c>".len());
let next_open = rest.find("<c ").unwrap_or(rest.len());
let self_close = rest[..next_open.min(rest.len())].find("/>").map(|i| i + 2);
let end = match (close, self_close) {
(Some(c), Some(s)) => c.min(s),
(Some(c), None) => c,
(None, Some(s)) => s,
(None, None) => return None,
};
Some(&rest[..end])
}
fn one_sheet(name: &str, cells: Vec<CellLayout>, merges: Vec<String>) -> LayoutDescriptor {
LayoutDescriptor {
descriptor_version: LAYOUT_DESCRIPTOR_VERSION,
source_workbook_hash: None,
sheets: vec![SheetLayout {
name: name.to_string(),
hidden: false,
cells,
merges,
col_widths: vec![],
hidden_cols: vec![],
}],
}
}
#[test]
fn render_xlsx_produces_valid_zip_container() {
let layout = one_sheet("7_Quote", vec![cell("C11", None, Some("0"))], vec![]);
let run = run_with(&[("7_Quote!C11", CellValue::Number(1594.93))]);
let bytes = render_xlsx(&layout, &run, RenderMode::Filled).expect("render");
assert!(!bytes.is_empty(), "non-empty output");
assert_eq!(
&bytes[..4],
ZIP_MAGIC,
"leads with the ZIP magic PK\\x03\\x04"
);
}
#[test]
fn render_xlsx_is_deterministic_byte_identical() {
let layout = one_sheet(
"7_Quote",
vec![cell("C11", Some("SUM(C9:C10)"), Some("0"))],
vec![],
);
let run = run_with(&[("7_Quote!C11", CellValue::Number(1594.93))]);
let a = render_xlsx(&layout, &run, RenderMode::Filled).expect("render a");
let b = render_xlsx(&layout, &run, RenderMode::Filled).expect("render b");
assert_eq!(
a, b,
"two Filled renders of the same input are byte-identical"
);
let io_a = render_xlsx(&layout, &run, RenderMode::InputsOnly).expect("render io a");
let io_b = render_xlsx(&layout, &run, RenderMode::InputsOnly).expect("render io b");
assert_eq!(
io_a, io_b,
"two InputsOnly renders of the same input are byte-identical"
);
}
#[test]
fn normalize_formula_for_writer_never_double_prefixes() {
assert_eq!(normalize_formula_for_writer("SUM(A1:A2)"), "=SUM(A1:A2)");
assert_eq!(normalize_formula_for_writer("=SUM(A1:A2)"), "=SUM(A1:A2)");
for f in ["SUM(A1:A2)", "=SUM(A1:A2)"] {
let out = normalize_formula_for_writer(f);
assert!(out.starts_with('='), "has a leading '='");
assert!(!out.starts_with("=="), "never double-prefixed");
}
}
#[test]
fn render_xlsx_rejects_non_finite_computed_value() {
let layout = one_sheet("7_Quote", vec![cell("C11", None, None)], vec![]);
for bad in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
let run = run_with(&[("7_Quote!C11", CellValue::Number(bad))]);
let err =
render_xlsx(&layout, &run, RenderMode::Filled).expect_err("non-finite must be Err");
assert!(
matches!(err, RenderError::NonFiniteValue { .. }),
"got {err:?}"
);
}
}
#[test]
fn render_xlsx_surfaces_malformed_addr_as_error_not_panic() {
let layout = one_sheet("7_Quote", vec![cell("1A", None, Some("x"))], vec![]);
let run = run_with(&[]);
let err =
render_xlsx(&layout, &run, RenderMode::Filled).expect_err("malformed addr must be Err");
assert!(
matches!(err, RenderError::MalformedAddr { .. }),
"got {err:?}"
);
}
#[test]
fn render_xlsx_writes_formula_with_finite_cached_result() {
let layout = one_sheet(
"7_Quote",
vec![cell("C11", Some("=SUM(C9:C10)"), None)],
vec![],
);
let run = run_with(&[("7_Quote!C11", CellValue::Number(1594.93))]);
let bytes = render_xlsx(&layout, &run, RenderMode::Filled).expect("render");
assert_eq!(&bytes[..4], ZIP_MAGIC);
}
#[test]
fn render_xlsx_replays_merge_top_left_only() {
let layout = one_sheet(
"7_Quote",
vec![
cell("A1", None, Some("merged")),
cell("A2", None, Some("interior")),
cell("B1", None, Some("interior")),
cell("B2", None, Some("interior")),
],
vec!["A1:B2".to_string()],
);
let run = run_with(&[("7_Quote!A1", CellValue::Text("merged".to_string()))]);
let bytes = render_xlsx(&layout, &run, RenderMode::Filled).expect("render with merge");
assert_eq!(
&bytes[..4],
ZIP_MAGIC,
"merge replay still yields a valid xlsx"
);
}
#[test]
fn render_xlsx_rejects_single_cell_merge() {
let layout = one_sheet(
"7_Quote",
vec![cell("A1", None, Some("x"))],
vec!["A1:A1".to_string()],
);
let run = run_with(&[]);
let err = render_xlsx(&layout, &run, RenderMode::Filled)
.expect_err("single-cell merge must be Err");
assert!(
matches!(err, RenderError::MalformedMerge { .. }),
"got {err:?}"
);
}
#[test]
fn render_xlsx_writes_text_and_bool_and_falls_back_on_error_value() {
let layout = one_sheet(
"7_Quote",
vec![
cell("A1", None, None),
cell("A2", None, None),
cell("A3", None, Some("orig")),
],
vec![],
);
let run = run_with(&[
("7_Quote!A1", CellValue::Text("hi".to_string())),
("7_Quote!A2", CellValue::Bool(true)),
("7_Quote!A3", CellValue::Error(ExcelError::DivZero)),
]);
let bytes = render_xlsx(&layout, &run, RenderMode::Filled).expect("render");
assert_eq!(&bytes[..4], ZIP_MAGIC);
}
#[test]
fn extract_sheet_xml_locates_formula_and_value_on_a_specific_cell() {
let layout = one_sheet(
"7_Quote",
vec![cell("C11", Some("SUM(C9:C10)"), Some("0"))],
vec![],
);
let run = run_with(&[("7_Quote!C11", CellValue::Number(1594.93))]);
let bytes = render_xlsx(&layout, &run, RenderMode::Filled).expect("render");
let sheet_xml = extract_sheet_xml(&bytes, "xl/worksheets/sheet1.xml");
let c11 = cell_xml(&sheet_xml, "C11").expect("the C11 cell element is present");
assert!(
c11.contains("<f>") || c11.contains("<f "),
"the numeric formula cell carries an <f> element within its own <c>"
);
assert!(
c11.contains("<v>"),
"the numeric formula cell carries a cached <v> within its own <c>"
);
assert!(
cell_xml(&sheet_xml, "Z99").is_none(),
"an absent cell address resolves to None, never a panic"
);
}
#[test]
fn render_xlsx_text_and_bool_formula_cells_carry_f_and_v_per_cell() {
let layout = one_sheet(
"3_Outputs",
vec![
cell(
"B6",
Some("IF(taxable_income>=40000,\"bracket_2\",\"bracket_1\")"),
None,
),
cell("B7", Some("taxable_income>0"), None),
],
vec![],
);
let run = run_with(&[
("3_Outputs!B6", CellValue::Text("bracket_2".to_string())),
("3_Outputs!B7", CellValue::Bool(true)),
]);
let bytes = render_xlsx(&layout, &run, RenderMode::Filled).expect("render");
let sheet_xml = extract_sheet_xml(&bytes, "xl/worksheets/sheet1.xml");
let b6 = cell_xml(&sheet_xml, "B6").expect("the B6 text-formula cell is present");
assert!(
b6.contains("<f>") || b6.contains("<f "),
"the TEXT formula cell carries an <f> element within its own <c>: {b6}"
);
assert!(
b6.contains("<v>"),
"the TEXT formula cell carries a cached <v> within its own <c>: {b6}"
);
let b7 = cell_xml(&sheet_xml, "B7").expect("the B7 bool-formula cell is present");
assert!(
b7.contains("<f>") || b7.contains("<f "),
"the BOOL formula cell carries an <f> element within its own <c>: {b7}"
);
assert!(
b7.contains("<v>"),
"the BOOL formula cell carries a cached <v> within its own <c>: {b7}"
);
}
#[test]
fn render_xlsx_non_formula_text_and_bool_remain_plain_literals() {
let layout = one_sheet(
"3_Outputs",
vec![cell("A1", None, None), cell("A2", None, None)],
vec![],
);
let run = run_with(&[
("3_Outputs!A1", CellValue::Text("plain".to_string())),
("3_Outputs!A2", CellValue::Bool(false)),
]);
let bytes = render_xlsx(&layout, &run, RenderMode::Filled).expect("render");
let sheet_xml = extract_sheet_xml(&bytes, "xl/worksheets/sheet1.xml");
let a1 = cell_xml(&sheet_xml, "A1").expect("A1 plain text cell present");
assert!(
!a1.contains("<f>") && !a1.contains("<f "),
"a non-formula text cell carries NO <f>: {a1}"
);
let a2 = cell_xml(&sheet_xml, "A2").expect("A2 plain bool cell present");
assert!(
!a2.contains("<f>") && !a2.contains("<f "),
"a non-formula bool cell carries NO <f>: {a2}"
);
}
#[test]
fn render_xlsx_inputs_only_emits_bare_formulas_no_cached_value_per_cell() {
let layout = one_sheet(
"3_Outputs",
vec![
cell("B5", Some("=SUM(B1:B4)"), None),
cell(
"B6",
Some("IF(taxable_income>=40000,\"bracket_2\",\"bracket_1\")"),
None,
),
cell("B7", Some("taxable_income>0"), None),
cell("A1", None, None),
],
vec![],
);
let run = run_with(&[
("3_Outputs!B5", CellValue::Number(123.0)),
("3_Outputs!B6", CellValue::Text("bracket_2".to_string())),
("3_Outputs!B7", CellValue::Bool(true)),
("3_Outputs!A1", CellValue::Number(40000.0)),
]);
let io_bytes =
render_xlsx(&layout, &run, RenderMode::InputsOnly).expect("inputs_only render");
let io_xml = extract_sheet_xml(&io_bytes, "xl/worksheets/sheet1.xml");
let filled_bytes = render_xlsx(&layout, &run, RenderMode::Filled).expect("filled render");
let filled_xml = extract_sheet_xml(&filled_bytes, "xl/worksheets/sheet1.xml");
let formula_cells = [("B5", "123"), ("B6", "bracket_2"), ("B7", "1")];
for (a1, exec_val) in formula_cells {
let io =
cell_xml(&io_xml, a1).unwrap_or_else(|| panic!("{a1} io formula cell present"));
assert!(
io.contains("<f>") || io.contains("<f "),
"InputsOnly: {a1} carries a bare <f>: {io}"
);
assert!(
io.contains("<v>0</v>"),
"InputsOnly: {a1} carries the NEUTRAL 0 placeholder, not the executor value: {io}"
);
assert!(
!io.contains(&format!("<v>{exec_val}</v>")) || exec_val == "0",
"InputsOnly: {a1} must NOT carry the executor's cached value '{exec_val}': {io}"
);
assert!(
!io.contains("t=\"str\"") && !io.contains("t=\"b\""),
"InputsOnly: {a1} emits NO value-type attr (bare formula, Excel recomputes): {io}"
);
let f =
cell_xml(&filled_xml, a1).unwrap_or_else(|| panic!("{a1} filled formula present"));
assert!(
f.contains("<f>") || f.contains("<f "),
"Filled: {a1} carries an <f>: {f}"
);
assert!(
f.contains(&format!("<v>{exec_val}</v>")),
"Filled: {a1} carries the executor's cached value '{exec_val}': {f}"
);
}
for (label, xml) in [("InputsOnly", &io_xml), ("Filled", &filled_xml)] {
let a1 = cell_xml(xml, "A1").expect("A1 seeded input present");
assert!(
!a1.contains("<f>") && !a1.contains("<f "),
"{label}: a seeded input cell has NO <f>: {a1}"
);
assert!(
a1.contains("<v>40000</v>"),
"{label}: a seeded input cell still carries its value <v>: {a1}"
);
}
}
#[test]
fn render_mode_deserializes_known_strings_and_rejects_unknown() {
let filled: RenderMode = serde_json::from_str("\"filled\"").expect("filled decodes");
assert_eq!(filled, RenderMode::Filled);
let io: RenderMode = serde_json::from_str("\"inputs_only\"").expect("inputs_only decodes");
assert_eq!(io, RenderMode::InputsOnly);
assert_eq!(
serde_json::to_string(&RenderMode::InputsOnly).expect("serialize"),
"\"inputs_only\""
);
assert_eq!(
RenderMode::default(),
RenderMode::Filled,
"Default is Filled"
);
let bogus = serde_json::from_str::<RenderMode>("\"bogus\"");
assert!(
bogus.is_err(),
"an unknown mode string is a decode Err, not a silent Filled: {bogus:?}"
);
}
#[test]
fn argb_to_color_non_ascii_eight_byte_input_is_none_not_a_panic() {
assert_eq!("€abcde".len(), 8, "the reproducer is byte-length 8");
assert_eq!(argb_to_color("€abcde"), None);
assert!(argb_to_color("FFE2EFDA").is_some());
assert!(argb_to_color("E2EFDA").is_some());
}
#[test]
fn render_xlsx_with_non_ascii_argb_renders_without_panic() {
let mut bad = cell("A1", None, Some("x"));
bad.fill_argb = Some("€abcde".to_string());
bad.font_argb = Some("€abcde".to_string());
let layout = one_sheet("7_Quote", vec![bad], vec![]);
let bytes = render_xlsx(&layout, &run_with(&[]), RenderMode::Filled).expect("render");
assert_eq!(&bytes[..4], ZIP_MAGIC);
}
}