From cd46163fa347bbc49bf8008d70d408f62feabda5 Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Thu, 20 Aug 2026 10:11:48 +0200 Subject: fix(cli): close the flavour-list call sites Typst's addition exposed The task this branch of work set out to test was whether the template engine is genuinely flavour-agnostic outside lib/render/escape.ml. It is not, quite: bin/main.ml's own --flavour error/help text and both man pages hand-typed the flavour list and its count ("the six flavours") as literal prose, so adding Typst as a seventh left three places silently wrong (an incomplete flavour list in two error messages, and "six" where the true count is now seven) until this commit. bin/main.ml's two error messages (unknown --flavour value, and cannot infer a flavour from an extension) now build their flavour list by walking Escape.all/to_string instead of a literal string, so the next flavour added will not reintroduce the same drift; the static --help body text and colitur(1)/colitur-templates(5) still name flavours and their count by hand; there was no equivalent gain available for prose formatted this specifically, so those were updated by hand instead, along with colitur-templates(5)'s FLAVOURS section, which gained typst's own subsection alongside latex/groff/html/xml/ics/none. test/cli.t's two pinned error-text assertions are updated to match, and gained a new case: .typ inferred from extension, and a real shipped citation's hyphen surviving --flavour typst's own escaping live, the same shape the 2035 Ss. Petri et Pauli case already proves for latex's ampersand just above it. --- bin/main.ml | 37 ++++++++++++++++++++++++++++--------- man/colitur-templates.5 | 45 +++++++++++++++++++++++++++++++++++++++++---- man/colitur.1 | 11 ++++++----- test/cli.t | 15 ++++++++++++--- 4 files changed, 87 insertions(+), 21 deletions(-) diff --git a/bin/main.ml b/bin/main.ml index 0a09a63..cf6cb33 100644 --- a/bin/main.ml +++ b/bin/main.ml @@ -650,7 +650,27 @@ let extension path = fallback to [Escape.None_]: guessing the flavour wrong produces malformed output (unescaped LaTeX/HTML metacharacters) that looks fine until it does not -- the same "never silently substitute" discipline [data_dir]'s own - [COLITUR_DATA_DIR] handling documents above. *) + [COLITUR_DATA_DIR] handling documents above. + + Both error messages below list the flavours by walking [Escape.all] + rather than a hand-typed literal, on purpose: a hand-typed list is + exactly the kind of call site the design claims does not exist outside + escape.ml (spec section 5) -- adding Typst as a seventh flavour found + this one had drifted from that claim (it silently still said "the six + flavours" until then), so this is now future-proof against the same + drift the next flavour would otherwise reintroduce. *) +let flavour_names_comma () = + match List.map Colitur_render.Escape.to_string Colitur_render.Escape.all with + | [] -> "" + | [ x ] -> x + | xs -> ( + match List.rev xs with + | last :: rest -> String.concat ", " (List.rev rest) ^ " or " ^ last + | [] -> "") + +let flavour_names_bar () = + String.concat "|" (List.map Colitur_render.Escape.to_string Colitur_render.Escape.all) + let table_report ~lang ~template ~flavour_opt ~overlays y = let flavour = match flavour_opt with @@ -658,16 +678,14 @@ let table_report ~lang ~template ~flavour_opt ~overlays y = match Colitur_render.Escape.of_string name with | Some f -> f | None -> - Printf.eprintf "colitur: unknown flavour %S (want latex, groff, html, xml, ics or none)\n" - name; + Printf.eprintf "colitur: unknown flavour %S (want %s)\n" name (flavour_names_comma ()); exit 2) | None -> ( match Colitur_render.Escape.of_extension (extension template) with | Some f -> f | None -> - Printf.eprintf - "colitur: cannot infer a flavour from %S; pass --flavour latex|groff|html|xml|ics|none\n" - (extension template); + Printf.eprintf "colitur: cannot infer a flavour from %S; pass --flavour %s\n" + (extension template) (flavour_names_bar ()); exit 2) in match read_file template with @@ -1116,11 +1134,12 @@ rendering: --flavour X selects how interpolated VALUES are escaped (never the template's own literal markup, which is the author's). One of: - latex groff html xml ics none + latex typst groff html xml ics none Inferred from --template's extension when --flavour is omitted: .tex -> latex + .typ -> typst .ms .mom .me -> groff .html .htm -> html .xml -> xml @@ -1132,7 +1151,7 @@ rendering: them alone) An extension colitur does not recognise is a hard ERROR naming - the six flavours above, never a silent fallback to `none`: + the seven flavours above, never a silent fallback to `none`: guessing wrong produces output that looks fine until the metacharacters it silently failed to escape show up. @@ -1190,7 +1209,7 @@ Reading references only (e.g. "Jn 3:16"); never scripture text. See colitur(1) for the full description and the sources it computes against, colitur-overlay(5) for the overlay file format in full, colitur-templates(5) for the template format in full -- the syntax, the -remaining scope hazard, the six flavours' escaping, and the full view-model +remaining scope hazard, the seven flavours' escaping, and the full view-model field reference -- and colitur-config(5) for the config file's location and precedence in full.|} diff --git a/man/colitur-templates.5 b/man/colitur-templates.5 index 06ad583..e04560b 100644 --- a/man/colitur-templates.5 +++ b/man/colitur-templates.5 @@ -303,9 +303,9 @@ same file exactly as any other tag would). selects how an interpolated .I value is escaped before being written. It never touches the template's own literal -markup (the LaTeX, groff, HTML, XML or ICS surrounding a +markup (the LaTeX, Typst, groff, HTML, XML or ICS surrounding a .BR {{tag}} ), -which is the template author's and is trusted exactly as written. One of six: +which is the template author's and is trusted exactly as written. One of seven: .TP .B latex .BR \e " \(-> " \etextbackslash{} , @@ -320,6 +320,42 @@ which is the template author's and is trusted exactly as written. One of six: .BR ~ " \(-> " \etextasciitilde{} . Every LaTeX special character is covered; nothing else is touched. .TP +.B typst +.BR \e " \(-> " \e\e , +.BR # " \(-> " \e# , +.BR * " \(-> " \e* , +.BR _ " \(-> " \e_ , +.BR $ " \(-> " \e$ , +.BR @ " \(-> " \e@ , +.BR < " \(-> " \e< , +.BR > " \(-> " \e> , +.BR \(ga " \(-> " \e\(ga , +.BR ~ " \(-> " \e~ , +.BR \- " \(-> " \e\- . +Verified against the installed +.B typst +binary rather than assumed: each escaped character was confirmed to survive +as the literal character, and each was separately confirmed to do something +else when left bare +.RB ( # " opens code mode, " * / _ " toggle strong/emph, " $ " opens math, " +.B @ +opens a reference \(em a bare unresolved +.B @word +is a hard +.B typst +compile error, not merely mangled output \(em +.BR < / > " can close around a bare word into label syntax that swallows it, " +.B \(ga +opens raw text, and +.B ~ +is a non\-breaking space). A run of two or three unescaped hyphens becomes an +en or em dash; every +.B \- +is escaped unconditionally, not only inside a detected run, since escaping +is applied one character at a time with no lookahead \(em confirmed live that +escaping every hyphen independently still typesets as literal hyphens for a +run of any length. +.TP .B groff A backslash is escaped to .BR \ee , @@ -373,6 +409,7 @@ own file extension when the flag is omitted: .nf .I .tex \(-> latex +.I .typ \(-> typst .I .ms .mom .me \(-> groff .I .html .htm \(-> html .I .xml \(-> xml @@ -385,7 +422,7 @@ An extension .B colitur does not recognise is a hard .B ERROR -naming the six flavours above; it is +naming the seven flavours above; it is .I never a silent fallback to .BR none . @@ -401,7 +438,7 @@ flavour (selected for as well as .IR .txt ) passes every interpolated value through unchanged. This is a deliberate -choice, not a gap: unlike LaTeX, groff, HTML, XML or ICS, AsciiDoc and +choice, not a gap: unlike LaTeX, Typst, groff, HTML, XML or ICS, AsciiDoc and Markdown have no fixed, small metacharacter set that could be escaped mechanically \(em their own metacharacters are context\-dependent (a .B * diff --git a/man/colitur.1 b/man/colitur.1 index b9ce1d1..df7ad3e 100644 --- a/man/colitur.1 +++ b/man/colitur.1 @@ -265,7 +265,7 @@ below. .BI \-\-flavour " FLAVOUR" .RB ( "colitur table" " and " "colitur render" " only)" One of -.BR latex ", " groff ", " html ", " xml ", " ics " or " none . +.BR latex ", " typst ", " groff ", " html ", " xml ", " ics " or " none . Overrides the flavour that would otherwise be inferred from .BR \-\-template 's own extension. See @@ -602,7 +602,7 @@ literal markup, which is the author's and is trusted as\-is. One of: .RS .nf -latex groff html xml ics none +latex typst groff html xml ics none .fi .RE .PP @@ -615,6 +615,7 @@ own file extension: .nf .I .tex -> latex +.I .typ -> typst .I .ms .mom .me -> groff .I .html .htm -> html .I .xml -> xml @@ -630,7 +631,7 @@ leaving them alone. .PP An extension .B colitur -does not recognise is a hard error naming the six flavours above; it is +does not recognise is a hard error naming the seven flavours above; it is .I never a silent fallback to .BR none . @@ -641,7 +642,7 @@ document. .nf .B colitur table \-\-year 2027 \-\-template invite.wat -colitur: cannot infer a flavour from ".wat"; pass \-\-flavour latex|groff|html|xml|ics|none +colitur: cannot infer a flavour from ".wat"; pass \-\-flavour latex|typst|groff|html|xml|ics|none .fi .RE .PP @@ -1127,7 +1128,7 @@ shapes and worked examples. .BR colitur\-templates (5) for the template format used by .BR table ", " render " and " publish -\(em the four syntax forms, the six flavours and their escaping, the full +\(em the four syntax forms, the seven flavours and their escaping, the full view\-model field reference, and the one remaining scope hazard a template author can still hit. .PP diff --git a/test/cli.t b/test/cli.t index 610ce5b..ef1243e 100644 --- a/test/cli.t +++ b/test/cli.t @@ -621,18 +621,27 @@ An unknown extension with no --flavour is an error, not a silent fallback: $ printf 'x' > /tmp/t.wat $ colitur table --year 2027 --template /tmp/t.wat - colitur: cannot infer a flavour from ".wat"; pass --flavour latex|groff|html|xml|ics|none + colitur: cannot infer a flavour from ".wat"; pass --flavour latex|typst|groff|html|xml|ics|none [2] $ colitur table --year 2027 --template /tmp/t.wat --flavour none x -An unrecognised --flavour value is also an error naming the six valid ones: +An unrecognised --flavour value is also an error naming the seven valid ones: $ colitur table --year 2027 --template /tmp/t.txt --flavour bogus - colitur: unknown flavour "bogus" (want latex, groff, html, xml, ics or none) + colitur: unknown flavour "bogus" (want latex, typst, groff, html, xml, ics or none) [2] +Typst is inferred from .typ, like every other flavour's own extension. +Its metacharacters are escaped the same way latex's own is above -- here +'-', verified live (against the real typst binary) to collapse into an +en dash if left unescaped; a real shipped citation carries one: + + $ printf '{{#days}}{{first}}\n{{/days}}' > /tmp/t.typ + $ colitur table --year 2027 --template /tmp/t.typ | grep -c '11\\-16' + 1 + A malformed template is a clear error, not a crash: $ printf '{{#days}}oops' > /tmp/bad.txt -- cgit v1.3