diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-19 11:48:30 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-19 11:48:30 +0200 |
| commit | 6762ce46af3cb12bc6ae37cda762c5d95add7903 (patch) | |
| tree | bc1d8c86050d0149ff961a9a4ff838f9c474ac2a /lib/render/emit_json.ml | |
| parent | 897c274fd28402159ca6d45eedc1257b1ce98696 (diff) | |
| parent | 390bc6ac5196a946c473d0dbe7760fa41837c428 (diff) | |
| download | colitur-6762ce46af3cb12bc6ae37cda762c5d95add7903.tar.gz colitur-6762ce46af3cb12bc6ae37cda762c5d95add7903.zip | |
feat: output, rendering and publishing
Gives colitur a publishable exit. Until now its only output was terminal
rows; it can now print an ordo booklet and a wall calendar, publish an
iCalendar feed people subscribe to, and serve a static JSON/XML API.
lib/render escaping (six flavours + RFC 5545 folding), a deliberately
logic-less template engine, the view model, and five
emitters (CSV, JSON, XML, iCalendar, S-expression)
CLI emit, table, render, publish -- all accepting --overlay
templates ordo booklet in six flavours, wall grid in three
schema day-v1.json and colitur-v1.xsd, the published contract
man colitur-templates.5, plus colitur.1 updates
The view model is why the engine can stay logic-less: a month grid needs
leading blank cells, week bucketing and an in-month test, and a
logic-less template can compute none of it. Shaping the data in OCaml
keeps the engine safe for untrusted templates and makes the grid
trivial.
Formats split by whether correctness is mechanical. Presentation goes
through templates; iCalendar and XML get dedicated emitters, because
folding, exclusive DTEND, stable UIDs and schema fidelity are rules a
template cannot enforce and each fails silently in a subscriber's
client rather than loudly at generation.
publish is deterministic and non-destructive: two runs produce a
byte-identical tree, and --prune removes only files a previous run
created, refusing any manifest entry that escapes the output directory.
No new dependencies. The kernel and rite modules are untouched, and
colitur day and colitur readings remain byte-identical.
Diffstat (limited to 'lib/render/emit_json.ml')
| -rw-r--r-- | lib/render/emit_json.ml | 47 |
1 files changed, 47 insertions, 0 deletions
diff --git a/lib/render/emit_json.ml b/lib/render/emit_json.ml new file mode 100644 index 0000000..272e4ca --- /dev/null +++ b/lib/render/emit_json.ml @@ -0,0 +1,47 @@ +(* emit_json.ml *) +module T = Template + +let escape_string s = + let b = Buffer.create (String.length s + 8) in + Buffer.add_char b '"'; + String.iter + (fun c -> + match c with + | '"' -> Buffer.add_string b "\\\"" + | '\\' -> Buffer.add_string b "\\\\" + | '\n' -> Buffer.add_string b "\\n" + | '\r' -> Buffer.add_string b "\\r" + | '\t' -> Buffer.add_string b "\\t" + | c when Char.code c < 0x20 -> Buffer.add_string b (Printf.sprintf "\\u%04x" (Char.code c)) + | c -> Buffer.add_char b c) + s; + Buffer.add_char b '"'; + Buffer.contents b + +(* Emit the view tree directly. Bools stay bools; everything else is a string, + an array or an object -- there are no numbers in the view, deliberately, so a + consumer never has to guess whether "week" is 2 or "2". *) +let rec write b (v : T.value) = + match v with + | T.Str s -> Buffer.add_string b (escape_string s) + | T.Bool x -> Buffer.add_string b (if x then "true" else "false") + | T.List l -> + Buffer.add_char b '['; + List.iteri (fun i x -> if i > 0 then Buffer.add_char b ','; write b x) l; + Buffer.add_char b ']' + | T.Obj kvs -> + Buffer.add_char b '{'; + List.iteri + (fun i (k, x) -> + if i > 0 then Buffer.add_char b ','; + Buffer.add_string b (escape_string k); + Buffer.add_char b ':'; + write b x) + kvs; + Buffer.add_char b '}' + +let year v = + let b = Buffer.create (64 * 1024) in + write b v; + Buffer.add_char b '\n'; + Buffer.contents b |
