use std::collections::HashMap;
use std::ops::Range;
use crate::emit::{
emit_content, emit_content_inline, escape_string, EmitError, Emission, SegmentMap,
};
use crate::SchemaMeta;
use quillmark_content::serial::from_canonical_value;
pub const HELPER_VERSION: &str = "0.1.0";
pub const HELPER_NAMESPACE: &str = "local";
pub const HELPER_NAME: &str = "quillmark-helper";
const LIB_TYP_TEMPLATE: &str = include_str!("lib.typ.template");
pub struct ContentMap {
pub path: String,
pub block: Range<usize>,
pub segments: Vec<SegmentMap>,
}
pub fn generate_lib_typ(
data: &serde_json::Value,
meta: &SchemaMeta,
) -> Result<(String, Vec<ContentMap>), EmitError> {
let mut cg = Codegen::new(meta);
let empty = serde_json::Map::new();
let data_obj = data.as_object().unwrap_or(&empty);
let data_literal = cg.emit_data(data_obj);
if let Some(e) = cg.emit_error.take() {
return Err(e);
}
let meta_literal = meta_literal(meta);
let plaintext_literal = plaintext_literal(&cg.plaintext);
let mut out = String::with_capacity(
LIB_TYP_TEMPLATE.len() + cg.blocks.len() + data_literal.len() + plaintext_literal.len(),
);
let mut cursor = 0usize;
let mut blocks_at = 0usize;
for (slot, value) in [
("{version}", HELPER_VERSION),
("{meta_literal}", meta_literal.as_str()),
("{content_blocks}", cg.blocks.as_str()),
("{data_literal}", data_literal.as_str()),
("{plaintext_literal}", plaintext_literal.as_str()),
] {
let rel = LIB_TYP_TEMPLATE[cursor..]
.find(slot)
.unwrap_or_else(|| panic!("lib.typ.template carries the {slot} slot in order"));
let at = cursor + rel;
out.push_str(&LIB_TYP_TEMPLATE[cursor..at]);
if slot == "{content_blocks}" {
blocks_at = out.len();
}
out.push_str(value);
cursor = at + slot.len();
}
out.push_str(&LIB_TYP_TEMPLATE[cursor..]);
let windows = cg
.windows
.into_iter()
.map(|(path, block, segments)| ContentMap {
path,
block: (block.start + blocks_at)..(block.end + blocks_at),
segments: segments
.into_iter()
.map(|s| rebase_segment(s, blocks_at))
.collect(),
})
.collect();
Ok((out, windows))
}
fn rebase_segment(mut s: SegmentMap, shift: usize) -> SegmentMap {
s.gen = (s.gen.start + shift)..(s.gen.end + shift);
for run in &mut s.runs {
run.1 = (run.1.start + shift)..(run.1.end + shift);
}
s
}
struct Codegen<'m> {
meta: &'m SchemaMeta,
blocks: String,
windows: Vec<(String, Range<usize>, Vec<SegmentMap>)>,
counter: usize,
date_counter: usize,
emit_error: Option<EmitError>,
plaintext: Vec<(String, String)>,
}
impl<'m> Codegen<'m> {
fn new(meta: &'m SchemaMeta) -> Self {
Self {
meta,
blocks: String::new(),
windows: Vec::new(),
counter: 0,
date_counter: 0,
emit_error: None,
plaintext: Vec::new(),
}
}
fn content_block(&mut self, path: &str, ec: Emission) -> String {
let id = format!("_qm_c{}", self.counter);
self.counter += 1;
self.blocks.push_str("#let ");
self.blocks.push_str(&id);
self.blocks.push_str(" = ");
let start = self.blocks.len();
self.blocks.push_str("[\n");
let markup_at = self.blocks.len();
self.blocks.push_str(&ec.markup);
self.blocks.push_str("\n]");
let end = self.blocks.len();
self.blocks.push('\n');
let segments = ec
.segments
.into_iter()
.map(|s| rebase_segment(s, markup_at))
.collect();
self.windows.push((path.to_string(), start..end, segments));
id
}
fn date_object(&mut self, path: &str, constructor: &str) -> String {
let id = format!("_qm_d{}", self.date_counter);
self.date_counter += 1;
self.blocks.push_str("#let ");
self.blocks.push_str(&id);
self.blocks.push_str(" = { let v = ");
self.blocks.push_str(constructor);
self.blocks.push_str("; (value: v, display: (..args) => ");
let text_start = self.blocks.len();
self.blocks.push_str("text(v.display(..args))");
let text_end = self.blocks.len();
self.blocks.push_str(") }\n");
self.windows
.push((path.to_string(), text_start..text_end, Vec::new()));
id
}
fn content_field(&mut self, path: &str, value: &serde_json::Value, inline: bool) -> String {
let emit = if inline {
emit_content_inline
} else {
emit_content
};
match from_canonical_value(value) {
Ok(rt) if !rt.is_blank() => {
let plain = quillmark_content::export::to_plaintext(&rt);
if !plain.is_empty() {
self.plaintext.push((path.to_string(), plain));
}
match emit(&rt) {
Ok(ec) => self.content_block(path, ec),
Err(e) => {
self.emit_error.get_or_insert(e);
"\"\"".to_string()
}
}
}
Ok(_) => "\"\"".to_string(),
Err(_) => lit(value),
}
}
fn emit_data(&mut self, obj: &serde_json::Map<String, serde_json::Value>) -> String {
let mut items = Vec::with_capacity(obj.len());
for (key, value) in sorted(obj) {
if key == "__meta__" {
continue;
}
if key == "$cards" {
if let Some(cards) = value.as_array() {
items.push(format!("\"$cards\": {}", self.emit_cards(cards)));
continue;
}
}
let is_content = self.meta.content_fields.iter().any(|f| f == key);
let date_kind = date_kind_of(&self.meta.date_fields, &self.meta.datetime_fields, key);
let is_inline = self.meta.inline_fields.iter().any(|f| f == key);
let expr = self.emit_field(key, value, is_content, date_kind, is_inline);
items.push(format!("\"{}\": {}", escape_string(key), expr));
}
wrap_dict(items)
}
fn emit_cards(&mut self, cards: &[serde_json::Value]) -> String {
let mut ordinals: HashMap<String, usize> = HashMap::new();
let mut out = Vec::with_capacity(cards.len());
for card in cards {
let obj = match card.as_object() {
Some(o) => o,
None => {
out.push(lit(card));
continue;
}
};
match obj.get("$kind").and_then(|v| v.as_str()) {
Some(kind) => {
let n = ordinals.entry(kind.to_string()).or_insert(0);
let prefix = format!("$cards.{kind}.{n}.");
*n += 1;
out.push(self.emit_card(obj, kind, &prefix));
}
None => out.push(lit(card)),
}
}
wrap_array(out)
}
fn emit_card(
&mut self,
obj: &serde_json::Map<String, serde_json::Value>,
kind: &str,
prefix: &str,
) -> String {
let content = card_names(&self.meta.card_content_fields, kind);
let dates = card_names(&self.meta.card_date_fields, kind);
let datetimes = card_names(&self.meta.card_datetime_fields, kind);
let inlines = card_names(&self.meta.card_inline_fields, kind);
let mut items = Vec::with_capacity(obj.len() + 1);
items.push(format!("\"$path\": \"{}\"", escape_string(prefix)));
for (key, value) in sorted(obj) {
if key == "$path" {
continue;
}
let is_content = content.iter().any(|f| f == key);
let date_kind = date_kind_of(&dates, &datetimes, key);
let is_inline = inlines.iter().any(|f| f == key);
let path = format!("{prefix}{key}");
let expr = self.emit_field(&path, value, is_content, date_kind, is_inline);
items.push(format!("\"{}\": {}", escape_string(key), expr));
}
wrap_dict(items)
}
fn emit_field(
&mut self,
path: &str,
value: &serde_json::Value,
is_content: bool,
date_kind: Option<DateKind>,
is_inline: bool,
) -> String {
if is_content {
match value {
serde_json::Value::Object(_) => self.content_field(path, value, is_inline),
serde_json::Value::Array(arr) => {
let items = arr
.iter()
.enumerate()
.map(|(i, elem)| match elem {
serde_json::Value::Object(_) => {
self.content_field(&format!("{path}.{i}"), elem, is_inline)
}
other => lit(other),
})
.collect();
wrap_array(items)
}
other => lit(other),
}
} else if let Some(kind) = date_kind {
match value {
serde_json::Value::String(s) => match datetime_constructor(s, kind) {
Some(ctor) => self.date_object(path, &ctor),
None => "none".to_string(),
},
serde_json::Value::Null => "none".to_string(),
other => lit(other),
}
} else {
lit(value)
}
}
}
fn date_kind_of(dates: &[String], datetimes: &[String], key: &str) -> Option<DateKind> {
if dates.iter().any(|f| f == key) {
Some(DateKind::Date)
} else if datetimes.iter().any(|f| f == key) {
Some(DateKind::DateTime)
} else {
None
}
}
#[derive(Clone, Copy)]
enum DateKind {
Date,
DateTime,
}
pub(crate) fn card_names(
table: &serde_json::Map<String, serde_json::Value>,
kind: &str,
) -> Vec<String> {
table
.get(kind)
.and_then(|v| v.as_array())
.map(|a| {
a.iter()
.filter_map(|s| s.as_str().map(str::to_string))
.collect()
})
.unwrap_or_default()
}
fn meta_literal(meta: &SchemaMeta) -> String {
let tables = serde_json::json!({
"fields": meta.fields,
"card_fields": meta.card_field_names,
"array_fields": meta.array_fields,
"card_array_fields": meta.card_array_fields,
});
lit(&tables)
}
fn plaintext_literal(entries: &[(String, String)]) -> String {
let mut sorted: Vec<&(String, String)> = entries.iter().collect();
sorted.sort_by(|a, b| a.0.cmp(&b.0));
wrap_dict(
sorted
.into_iter()
.map(|(path, text)| {
format!("\"{}\": \"{}\"", escape_string(path), escape_string(text))
})
.collect(),
)
}
fn datetime_constructor(s: &str, kind: DateKind) -> Option<String> {
match kind {
DateKind::Date => quillmark_core::quill::parse_date(s)
.map(|(year, month, day)| format!("datetime(year: {year}, month: {month}, day: {day})")),
DateKind::DateTime => {
quillmark_core::quill::parse_datetime(s).map(|(year, month, day, hour, minute, second)| {
format!(
"datetime(year: {year}, month: {month}, day: {day}, \
hour: {hour}, minute: {minute}, second: {second})"
)
})
}
}
}
fn lit(v: &serde_json::Value) -> String {
use serde_json::Value::*;
match v {
Null => "none".to_string(),
Bool(b) => b.to_string(),
Number(n) => {
if let Some(i) = n.as_i64() {
if i == i64::MIN {
"(-9223372036854775807 - 1)".to_string()
} else {
i.to_string()
}
} else {
let f = n.as_f64().expect("json numbers are finite");
format!("float(\"{f}\")")
}
}
String(s) => format!("\"{}\"", escape_string(s)),
Array(a) => wrap_array(a.iter().map(lit).collect()),
Object(o) => wrap_dict(
sorted(o)
.into_iter()
.map(|(k, v)| format!("\"{}\": {}", escape_string(k), lit(v)))
.collect(),
),
}
}
fn sorted(obj: &serde_json::Map<String, serde_json::Value>) -> Vec<(&String, &serde_json::Value)> {
let mut entries: Vec<_> = obj.iter().collect();
entries.sort_by(|a, b| a.0.cmp(b.0));
entries
}
fn wrap_array(items: Vec<String>) -> String {
if items.is_empty() {
"()".to_string()
} else {
format!("({},)", items.join(", "))
}
}
fn wrap_dict(items: Vec<String>) -> String {
if items.is_empty() {
"(:)".to_string()
} else {
format!("({},)", items.join(", "))
}
}
pub fn generate_typst_toml() -> String {
format!(
r#"[package]
name = "{name}"
version = "{version}"
namespace = "{namespace}"
entrypoint = "lib.typ"
"#,
name = HELPER_NAME,
version = HELPER_VERSION,
namespace = HELPER_NAMESPACE
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::emit::escape_markup;
use crate::emit::EscapeCtx;
use quillmark_core::quill::CONTENT_MEDIA_TYPE;
fn meta_from(schema: serde_json::Value) -> SchemaMeta {
SchemaMeta::from_schema_json(&schema)
}
fn content(markdown: &str) -> serde_json::Value {
let rt = quillmark_content::import::from_markdown(markdown).expect("import");
quillmark_content::serial::to_canonical_value(&rt)
}
fn richtext_field() -> serde_json::Value {
serde_json::json!({ "type": "object", "contentMediaType": CONTENT_MEDIA_TYPE })
}
fn inline_richtext_field() -> serde_json::Value {
serde_json::json!({
"type": "object",
"contentMediaType": CONTENT_MEDIA_TYPE,
"quillmark:inline": true
})
}
#[test]
fn inline_field_lowers_without_parbreak() {
let meta = meta_from(serde_json::json!({ "properties": {
"subject": inline_richtext_field(),
"intro": richtext_field(),
}}));
let data = serde_json::json!({
"subject": content("A **bold** subject"),
"intro": content("An intro."),
});
let (lib, _) = generate_lib_typ(&data, &meta).unwrap();
assert!(
lib.contains("[\nA #strong[bold] subject\n]"),
"inline block should carry no parbreak: {lib}"
);
assert!(
lib.contains("[\nAn intro.\n\n\n]"),
"block field keeps its terminator: {lib}"
);
}
#[test]
fn inline_array_lowers_each_element_without_parbreak() {
let meta = meta_from(serde_json::json!({ "properties": {
"cc": { "type": "array", "items": inline_richtext_field() }
}}));
let data = serde_json::json!({ "cc": [content("First"), content("Second **bold**")] });
let (lib, _) = generate_lib_typ(&data, &meta).unwrap();
assert!(lib.contains("[\nFirst\n]"), "{lib}");
assert!(lib.contains("[\nSecond #strong[bold]\n]"), "{lib}");
}
#[test]
fn content_field_becomes_a_referenced_block_with_a_bracket_window() {
let meta = meta_from(serde_json::json!({ "properties": { "intro": richtext_field() } }));
let data = serde_json::json!({ "intro": content("A **bold** intro.") });
let (lib, windows) = generate_lib_typ(&data, &meta).unwrap();
let block = "[\nA #strong[bold] intro.\n\n\n]";
assert!(lib.contains(&format!("#let _qm_c0 = {block}")));
assert!(lib.contains("\"intro\": _qm_c0"));
assert!(!lib.contains("eval("));
assert!(!lib.contains("json(bytes"));
assert!(!lib.contains("insert-content"));
assert!(!lib.contains("_parse-date"));
assert_eq!(windows.len(), 1);
assert_eq!(windows[0].path, "intro");
assert_eq!(&lib[windows[0].block.clone()], block);
}
#[test]
fn segment_maps_index_the_generated_lib_typ() {
let meta = meta_from(serde_json::json!({ "properties": { "intro": richtext_field() } }));
let rt = quillmark_content::import::from_markdown("Hello **bold**.\n\nSecond para.")
.expect("import");
let data =
serde_json::json!({ "intro": quillmark_content::serial::to_canonical_value(&rt) });
let (lib, windows) = generate_lib_typ(&data, &meta).unwrap();
assert_eq!(windows.len(), 1);
let cm = &windows[0];
assert_eq!(cm.segments.len(), 2);
let chars: Vec<char> = rt.text.chars().collect();
let mut saw_bold = false;
for seg in &cm.segments {
assert!(seg.gen.start <= seg.gen.end && seg.gen.end <= lib.len());
for (content_range, gen, ctx) in &seg.runs {
let src: String = chars[content_range.clone()].iter().collect();
let expect = match ctx {
EscapeCtx::Markup => escape_markup(&src),
EscapeCtx::StringLit => crate::emit::escape_string(&src),
};
assert_eq!(&lib[gen.clone()], expect, "run inverts to its content slice");
assert!(gen.start >= seg.gen.start && gen.end <= seg.gen.end);
if src == "bold" {
saw_bold = true;
}
}
}
assert!(saw_bold, "the strong-wrapped run maps back to \"bold\"");
}
#[test]
fn richtext_array_emits_a_block_per_nonblank_element() {
let meta = meta_from(serde_json::json!({
"properties": {
"sections": {
"type": "array",
"items": richtext_field()
}
}
}));
let data = serde_json::json!({
"sections": [content("one"), content(""), content("three")]
});
let (lib, windows) = generate_lib_typ(&data, &meta).unwrap();
let paths: Vec<&str> = windows.iter().map(|w| w.path.as_str()).collect();
assert_eq!(paths, vec!["sections.0", "sections.2"]);
assert!(lib.contains("\"sections\": (_qm_c0, \"\", _qm_c1,)"));
}
#[test]
fn content_fields_populate_the_plaintext_table() {
let meta = meta_from(serde_json::json!({ "properties": {
"subject": richtext_field(),
"refs": { "type": "array", "items": richtext_field() },
"blank": richtext_field(),
}}));
let data = serde_json::json!({
"subject": content("A **bold** subject"),
"refs": [content("first ref"), content("second _ref_")],
"blank": content(" "),
});
let (lib, _) = generate_lib_typ(&data, &meta).unwrap();
let table = plaintext_table(&lib);
assert!(table.contains(r#""subject": "A bold subject""#), "{table}");
assert!(table.contains(r#""refs.0": "first ref""#), "{table}");
assert!(table.contains(r#""refs.1": "second ref""#), "{table}");
assert!(!table.contains("blank"), "{table}");
assert!(lib.contains("#let plaintext(field) = _qm-plaintext.at(field"));
}
#[test]
fn island_only_content_has_no_plaintext_entry() {
let meta = meta_from(serde_json::json!({ "properties": { "fig": richtext_field() } }));
let data = serde_json::json!({ "fig": content("") });
let (lib, _) = generate_lib_typ(&data, &meta).unwrap();
assert_eq!(
plaintext_table(&lib),
"(:)",
"island-only field yields an empty plaintext table"
);
}
#[test]
fn card_content_plaintext_keyed_by_card_address() {
let meta = meta_from(serde_json::json!({
"properties": {},
"$defs": { "note_card": { "properties": { "$body": richtext_field() } } }
}));
let data = serde_json::json!({
"$cards": [ { "$kind": "note", "$body": content("Note **body**") } ]
});
let (lib, _) = generate_lib_typ(&data, &meta).unwrap();
assert!(
plaintext_table(&lib).contains(r#""$cards.note.0.$body": "Note body""#),
"{lib}"
);
}
fn plaintext_table(lib: &str) -> &str {
let start = lib
.find("#let _qm-plaintext = ")
.expect("plaintext table present")
+ "#let _qm-plaintext = ".len();
let rest = &lib[start..];
let end = rest.find('\n').unwrap_or(rest.len());
&rest[..end]
}
#[test]
fn date_and_datetime_fields_become_value_object_blocks() {
let meta = meta_from(serde_json::json!({
"properties": {
"issued": { "type": "string", "format": "date" },
"at": { "type": "string", "format": "date-time" },
"signed": { "type": "string", "format": "date" }
}
}));
let data = serde_json::json!({
"issued": "2026-07-03",
"at": "2026-07-03T09:30",
"signed": null
});
let (lib, windows) = generate_lib_typ(&data, &meta).unwrap();
assert!(
lib.contains(
"#let _qm_d0 = { let v = datetime(year: 2026, month: 7, day: 3, \
hour: 9, minute: 30, second: 0); \
(value: v, display: (..args) => text(v.display(..args))) }"
),
"{lib}"
);
assert!(
lib.contains(
"#let _qm_d1 = { let v = datetime(year: 2026, month: 7, day: 3); \
(value: v, display: (..args) => text(v.display(..args))) }"
),
"{lib}"
);
assert!(lib.contains("\"at\": _qm_d0"), "{lib}");
assert!(lib.contains("\"issued\": _qm_d1"), "{lib}");
assert!(lib.contains("\"signed\": none"), "{lib}");
let paths: Vec<&str> = windows.iter().map(|w| w.path.as_str()).collect();
assert!(paths.contains(&"issued"), "{paths:?}");
assert!(paths.contains(&"at"), "{paths:?}");
assert!(!paths.contains(&"signed"), "{paths:?}");
}
#[test]
fn cards_carry_path_and_per_kind_ordinals() {
let meta = meta_from(serde_json::json!({
"properties": {},
"$defs": {
"note_card": {
"properties": {
"$body": richtext_field(),
"on": { "type": "string", "format": "date" }
}
}
}
}));
let data = serde_json::json!({
"$cards": [
{ "$kind": "note", "$body": content("First body"), "on": "2026-01-02" },
{ "$kind": "note", "$body": content("Second body") }
]
});
let (lib, windows) = generate_lib_typ(&data, &meta).unwrap();
let paths: Vec<&str> = windows.iter().map(|w| w.path.as_str()).collect();
assert!(paths.contains(&"$cards.note.0.$body"), "{paths:?}");
assert!(paths.contains(&"$cards.note.1.$body"), "{paths:?}");
assert!(paths.contains(&"$cards.note.0.on"), "{paths:?}");
assert!(lib.contains("\"$path\": \"$cards.note.0.\""));
assert!(lib.contains("\"$path\": \"$cards.note.1.\""));
assert!(
lib.contains("let v = datetime(year: 2026, month: 1, day: 2);"),
"{lib}"
);
assert!(lib.contains("\"on\": _qm_d0"), "{lib}");
}
#[test]
fn meta_literal_carries_the_address_tables() {
let meta = meta_from(serde_json::json!({
"properties": {
"subject": { "type": "string" },
"refs": { "type": "array", "items": { "type": "string" } }
}
}));
let (lib, _) = generate_lib_typ(&serde_json::json!({}), &meta).unwrap();
assert!(lib.contains("#let _qm-meta = ("));
assert!(lib.contains("\"fields\": ("));
assert!(lib.contains("\"subject\""));
assert!(lib.contains("\"array_fields\": (\"refs\",)"));
}
#[test]
fn data_containing_placeholder_text_cannot_hijack_the_splice() {
let meta = SchemaMeta::default();
let data = serde_json::json!({
"note": "quoting the template: {content_blocks} and {data_literal}"
});
let (lib, _) = generate_lib_typ(&data, &meta).unwrap();
let payload = lib.find("quoting the template").expect("payload present");
let data_binding = lib.find("#let data =").expect("data binding present");
assert!(
payload > data_binding,
"the payload sits inside the data literal, after the real slot"
);
}
#[test]
fn reordered_input_emits_byte_identical_source() {
let meta = meta_from(serde_json::json!({
"properties": {
"body": richtext_field(),
"note": richtext_field(),
"issued": { "type": "string", "format": "date" },
"extra": { "type": "object" }
},
"$defs": {
"note_card": {
"properties": {
"$body": richtext_field(),
"on": { "type": "string", "format": "date" }
}
}
}
}));
let a = serde_json::json!({
"body": content("The body."),
"note": content("The note."),
"issued": "2026-01-02",
"extra": { "x": 1, "y": 2 },
"$cards": [ { "$kind": "note", "$body": content("Card body"), "on": "2027-03-04", "tag": "t" } ]
});
let b = serde_json::json!({
"$cards": [ { "tag": "t", "on": "2027-03-04", "$body": content("Card body"), "$kind": "note" } ],
"extra": { "y": 2, "x": 1 },
"issued": "2026-01-02",
"note": content("The note."),
"body": content("The body.")
});
let (lib_a, win_a) = generate_lib_typ(&a, &meta).unwrap();
let (lib_b, win_b) = generate_lib_typ(&b, &meta).unwrap();
assert_eq!(lib_a, lib_b, "reordered input must emit identical source");
let wa: Vec<_> = win_a.iter().map(|w| (&w.path, w.block.clone())).collect();
let wb: Vec<_> = win_b.iter().map(|w| (&w.path, w.block.clone())).collect();
assert_eq!(wa, wb, "windows must be identical too");
}
#[test]
fn lit_serializes_the_json_value_model() {
assert_eq!(lit(&serde_json::json!(null)), "none");
assert_eq!(lit(&serde_json::json!(true)), "true");
assert_eq!(lit(&serde_json::json!(42)), "42");
assert_eq!(
lit(&serde_json::json!(i64::MIN)),
"(-9223372036854775807 - 1)"
);
assert_eq!(lit(&serde_json::json!(1.5)), "float(\"1.5\")");
assert_eq!(lit(&serde_json::json!("hi")), "\"hi\"");
assert_eq!(lit(&serde_json::json!([])), "()");
assert_eq!(lit(&serde_json::json!([1])), "(1,)");
assert_eq!(lit(&serde_json::json!({})), "(:)");
assert_eq!(lit(&serde_json::json!({ "a": 1 })), "(\"a\": 1,)");
}
}