use std::collections::BTreeMap;
use crate::{BrandSpec, DocMeta};
use super::markdown::typst_string;
pub const CONTEXT_VIRTUAL_PATH: &str = "context.typ";
pub fn build_context_source(
meta: &DocMeta,
brand: &BrandSpec,
assets: &BTreeMap<String, String>,
) -> String {
let mut s = String::new();
s.push_str("#let doc-meta = (\n");
s.push_str(&dict_entry("title", meta.title.as_deref().unwrap_or("")));
s.push_str(&dict_entry("author", meta.author.as_deref().unwrap_or("")));
s.push_str(&dict_entry("date", meta.date.as_deref().unwrap_or("")));
for (k, v) in &meta.extra {
s.push_str(&dict_entry(k, v));
}
s.push_str(")\n\n");
s.push_str("#let brand = (\n");
s.push_str(&dict_entry("name", &brand.name));
s.push_str(&format!(" {}\n", nested_dict("palette", &brand.palette)));
s.push_str(&format!(" {}\n", nested_dict("fonts", &brand.fonts)));
s.push_str(")\n\n");
s.push_str("#let assets = (\n");
for (k, v) in assets {
s.push_str(&dict_entry(k, v));
}
s.push_str(")\n\n");
s.push_str(
"#let doc-meta-get(key, default: \"\") = if key in doc-meta { doc-meta.at(key) } else { default }\n",
);
s.push_str(
"#let brand-color(key, default: black) = if key in brand.palette { rgb(brand.palette.at(key)) } else { default }\n",
);
s.push_str(
"#let brand-font(key, default: none) = if key in brand.fonts { brand.fonts.at(key) } else { default }\n",
);
s.push_str(
"#let asset-path(key, default: none) = if key in assets { assets.at(key) } else { default }\n",
);
s
}
fn dict_entry(key: &str, value: &str) -> String {
format!(" {}: {},\n", typst_string(key), typst_string(value))
}
fn nested_dict(key: &str, entries: &BTreeMap<String, String>) -> String {
let mut s = format!("{}: (\n", typst_string(key));
for (k, v) in entries {
s.push_str(&format!(" {}: {},\n", typst_string(k), typst_string(v)));
}
s.push_str(" ),");
s
}
#[cfg(test)]
mod tests {
use super::*;
fn meta_with(title: Option<&str>, extra: &[(&str, &str)]) -> DocMeta {
DocMeta {
title: title.map(String::from),
author: None,
date: None,
extra: extra
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect(),
}
}
#[test]
fn empty_meta_and_brand_produce_valid_defaults() {
let out =
build_context_source(&DocMeta::default(), &BrandSpec::default(), &BTreeMap::new());
assert!(out.contains(r#""title": "","#));
assert!(out.contains(r#""author": "","#));
assert!(out.contains(r#""date": "","#));
assert!(out.contains("#let doc-meta-get"));
assert!(out.contains("#let brand-color"));
assert!(out.contains("#let brand-font"));
assert!(out.contains("#let assets = ("));
assert!(out.contains("#let asset-path"));
}
#[test]
fn extra_keys_flatten_into_doc_meta() {
let meta = meta_with(Some("Q3 Review"), &[("classification", "internal")]);
let out = build_context_source(&meta, &BrandSpec::default(), &BTreeMap::new());
assert!(out.contains(r#""title": "Q3 Review","#));
assert!(out.contains(r#""classification": "internal","#));
}
#[test]
fn brand_palette_and_fonts_are_nested_dicts() {
let brand = BrandSpec {
name: "F13".to_string(),
palette: BTreeMap::from([("navy".to_string(), "#243752".to_string())]),
fonts: BTreeMap::from([("sans".to_string(), "Montserrat".to_string())]),
..Default::default()
};
let out = build_context_source(&DocMeta::default(), &brand, &BTreeMap::new());
assert!(out.contains(r#""name": "F13","#));
assert!(out.contains(r#""palette": ("#));
assert!(out.contains(r##""navy": "#243752","##));
assert!(out.contains(r#""fonts": ("#));
assert!(out.contains(r#""sans": "Montserrat","#));
}
#[test]
fn special_characters_in_values_are_escaped() {
let meta = meta_with(Some("Title \"with\" quotes\nand a newline"), &[]);
let out = build_context_source(&meta, &BrandSpec::default(), &BTreeMap::new());
assert!(out.contains(r#"\"with\""#));
assert!(out.contains(r"\n"));
assert!(!out.contains("with\" quotes\nand"));
}
#[test]
fn assets_dict_maps_key_to_virtual_path() {
let assets = BTreeMap::from([("logo".to_string(), "/assets/logo.svg".to_string())]);
let out = build_context_source(&DocMeta::default(), &BrandSpec::default(), &assets);
assert!(out.contains(r#""logo": "/assets/logo.svg","#));
}
}