Add inline templates to the CLI and library #4

Merged
lilleman merged 23 commits from cli-inline-template into main 2026-09-03 21:20:26 +02:00
5 changed files with 15 additions and 19 deletions
Showing only changes of commit 1cd422352b - Show all commits
+9
View File
@@ -386,6 +386,15 @@ tokens add cost in proportion to the output.
instead of a leading-position special case. The JSON string is what makes the instead of a leading-position special case. The JSON string is what makes the
library's own advice reachable: the error for an object holding only a format library's own advice reachable: the error for an object holding only a format
names `"…"`, and that spelling has to work where it is printed. names `"…"`, and that spelling has to work where it is printed.
- **An inline template skips the cycle fence, and only that one.** `New` proves the
loaded tree acyclic, an inline node is a finite tree of its own, and nothing in
the tree can reference it, so no render of it reaches itself. Every other fence
runs over both, from one `checkScope`.
- **An inline template that does not compile is misuse (exit 2), including a
reference that resolves to nothing** — the whole argument is the spelling under
test, and `NewTemplate` compiles, links and validates as one step. An unknown
*path* stays a runtime error (exit 1): there the argument is well-formed and only
the data is absent.
- **A padded JSON argument is rejected, not trimmed.** Padding is the one place the - **A padded JSON argument is rejected, not trimmed.** Padding is the one place the
two readings disagree — a format string renders it, JSON drops it — so the two readings disagree — a format string renders it, JSON drops it — so the
spelling that renders is named rather than silently chosen. spelling that renders is named rather than silently chosen.
+2 -5
View File
@@ -270,11 +270,8 @@ const (
argTemplate argTemplate
) )
// classify reads what a positional argument names by its shape: a format string // classify reads what a positional argument names by its shape: a { token, or a
// carrying a { token, or a JSON object, array or string, is an inline template; // JSON object, array or string, is an inline template; anything else is a path.
// anything else is a path. A name may hold neither a brace nor a bracket nor a
// quote, so no path collides with any of those spellings, and the JSON gate is
// valid-JSON so a copied bracket names nothing rather than swallowing an argument.
func classify(arg string) (argKind, error) { func classify(arg string) (argKind, error) {
if strings.ContainsRune(arg, '{') || (isJSONStart(strings.TrimSpace(arg)) && json.Valid([]byte(arg))) { if strings.ContainsRune(arg, '{') || (isJSONStart(strings.TrimSpace(arg)) && json.Valid([]byte(arg))) {
return argTemplate, nil return argTemplate, nil
+1 -1
View File
@@ -20,7 +20,7 @@ func walkNodes(root map[string]node, fn func(path string, n node) error) error {
// eachNode visits n and every node contained within it once, passing the dot path // eachNode visits n and every node contained within it once, passing the dot path
// that reaches each. It never crosses a reference edge — a bound {/path} field is // that reaches each. It never crosses a reference edge — a bound {/path} field is
// skipped, as in walkNodes — so a single inline node is walked on its own. // skipped — so a single inline node is walked on its own.
func eachNode(n node, path string, fn func(path string, n node) error) error { func eachNode(n node, path string, fn func(path string, n node) error) error {
seen := map[node]bool{} seen := map[node]bool{}
var visit func(string, node) error var visit func(string, node) error
+2 -12
View File
@@ -27,10 +27,6 @@ func (t *Template) Fake() string {
// binds its references against the loaded tree, so repeated renders pay the // binds its references against the loaded tree, so repeated renders pay the
// compile and validation once. It shares [New]'s guarantees: a bad template errors // compile and validation once. It shares [New]'s guarantees: a bad template errors
// here, and rendering cannot fail. // here, and rendering cannot fail.
//
// checkNoCycles is the one fence loadData runs that an inline node does not need:
// the loaded tree is proven acyclic at [New], the node is a finite tree, and no
// tree node can reference it, so nothing it renders can reach itself.
func (f *Generator) NewTemplate(input string) (*Template, error) { func (f *Generator) NewTemplate(input string) (*Template, error) {
n, err := compileInput(input) n, err := compileInput(input)
if err != nil { if err != nil {
@@ -58,12 +54,7 @@ func (f *Generator) FakeTemplate(input string) (string, error) {
} }
// compileInput compiles an inline template: a JSON value, or a bare format string // compileInput compiles an inline template: a JSON value, or a bare format string
// when the input is not JSON. A JSON string literal and a bare string compile // when the input is not JSON.
// alike (both are a template with no fields); the other JSON scalars — a number,
// bool or null — are no template, so they are rejected here, as a data file that
// was one would be at load. Padding is where the two readings would disagree — a
// format string renders it, JSON drops it — so a padded JSON value is rejected
// naming the one that renders.
func compileInput(input string) (node, error) { func compileInput(input string) (node, error) {
var raw any var raw any
if err := json.Unmarshal([]byte(input), &raw); err != nil { if err := json.Unmarshal([]byte(input), &raw); err != nil {
@@ -76,8 +67,7 @@ func compileInput(input string) (node, error) {
} }
// linkNodeRefs binds the references in an inline node's templates against the // linkNodeRefs binds the references in an inline node's templates against the
// loaded tree. An inline template sits in no folder, so . and .. name nothing and // loaded tree.
// are rejected for the root spelling they would otherwise silently mean.
func linkNodeRefs(scope nodeScope, root map[string]node) error { func linkNodeRefs(scope nodeScope, root map[string]node) error {
return scope(func(path string, m node) error { return scope(func(path string, m node) error {
t, ok := m.(*template) t, ok := m.(*template)
+1 -1
View File
@@ -302,7 +302,7 @@ func repeatOf(m map[string]any) (int, error) {
if r == 1 { if r == 1 {
return 0, fmt.Errorf("repeat 1 is the default, so it has no effect; drop it") return 0, fmt.Errorf("repeat 1 is the default, so it has no effect; drop it")
} }
if r > MaxRepeat { // caps the renders one repeat asks for; checkRepeatReach bounds what nested ones multiply to if r > MaxRepeat { // caps the renders one repeat asks for; repeatCheck bounds what nested ones multiply to
return 0, fmt.Errorf("repeat %v exceeds the maximum %d", rv, MaxRepeat) return 0, fmt.Errorf("repeat %v exceeds the maximum %d", rv, MaxRepeat)
} }
return int(r), nil return int(r), nil