From e05fed1190c225cf342c896659820730453f9c30 Mon Sep 17 00:00:00 2001 From: M Date: Sun, 30 Aug 2026 20:08:47 +0200 Subject: [PATCH] Hold a path's last level too, and reject an unfinished path --- render.go | 15 ++++++--------- template.go | 33 +++++++++++++++++++++++++++------ 2 files changed, 33 insertions(+), 15 deletions(-) diff --git a/render.go b/render.go index 5a493d1..e47d7dd 100644 --- a/render.go +++ b/render.go @@ -191,17 +191,14 @@ func resolve(s *session, arms []arm, t *template, bound *draws) string { } // Walk the tail, holding the draw at every level the path passes through, so // two paths sharing a prefix share every choice along it, not just the head. + // The last level is held too: another token may name it in full. for i, seg := range a.tail { - if i < len(a.steps) { - held, drew := bound.variant[a.steps[i]] - if !drew { - held = drawn(s, child(n, seg)) - bound.variant[a.steps[i]] = held - } - n = held - continue + held, drew := bound.variant[a.steps[i]] + if !drew { + held = drawn(s, child(n, seg)) + bound.variant[a.steps[i]] = held } - n = child(n, seg) + n = held } v := render(s, n) bound.value[a.name] = v diff --git a/template.go b/template.go index 74f1bb9..3130d64 100644 --- a/template.go +++ b/template.go @@ -143,6 +143,9 @@ func checkTokens(format string, fields map[string]node) error { continue // a root reference; its target is checked at New (see linkRefs) } a := splitArm(name) + if err := checkSegments(a); err != nil { + return fmt.Errorf("token {%s}: %w", t.body, err) + } head, ok := fields[a.key] if !ok { if isOption(a.key) { @@ -180,9 +183,9 @@ type arm struct { name string // as written, and the key a bound draw's value is held under key string tail []string - // steps holds the key for each segment the walk passes *through* — the path - // prefixes below the head and above the leaf. The head is bound under key and - // the leaf's value under name, so a one-segment tail needs none of these. + // steps holds the key for each segment the walk descends to, so every level a + // path passes through is held — including its last, which another path may + // name in full ({p.addr} beside {p.addr.city}). steps []string } @@ -197,13 +200,31 @@ func splitArm(name string) arm { return arm{name: name, key: name} } segs := strings.Split(tail, ".") - var steps []string - for i := 0; i < len(segs)-1; i++ { // every prefix except the leaf's own - steps = append(steps, head+"."+strings.Join(segs[:i+1], ".")) + steps := make([]string, len(segs)) + for i := range segs { + steps[i] = head + "." + strings.Join(segs[:i+1], ".") } return arm{name: name, key: head, tail: segs, steps: steps} } +// checkSegments rejects an unfinished path: "{a.}", "{.b}" and "{a..b}" each have +// a segment naming nothing. A field really named "" would otherwise make them +// resolve, so a typo would read as a path that worked. +func checkSegments(a arm) error { + if len(a.tail) == 0 { + return nil + } + if a.key == "" { + return fmt.Errorf("path has an empty segment") + } + for _, seg := range a.tail { + if seg == "" { + return fmt.Errorf("path has an empty segment") + } + } + return nil +} + // splitArms splits a token body's '|' alternatives. func splitArms(body string) []arm { parts := strings.Split(body, "|")