Hold a path's last level too, and reject an unfinished path

This commit is contained in:
M
2026-08-30 20:08:47 +02:00
committed by lilleman-tw
parent 8accca54b1
commit e05fed1190
2 changed files with 33 additions and 15 deletions
+6 -9
View File
@@ -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 // 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. // 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 { for i, seg := range a.tail {
if i < len(a.steps) { held, drew := bound.variant[a.steps[i]]
held, drew := bound.variant[a.steps[i]] if !drew {
if !drew { held = drawn(s, child(n, seg))
held = drawn(s, child(n, seg)) bound.variant[a.steps[i]] = held
bound.variant[a.steps[i]] = held
}
n = held
continue
} }
n = child(n, seg) n = held
} }
v := render(s, n) v := render(s, n)
bound.value[a.name] = v bound.value[a.name] = v
+27 -6
View File
@@ -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) continue // a root reference; its target is checked at New (see linkRefs)
} }
a := splitArm(name) a := splitArm(name)
if err := checkSegments(a); err != nil {
return fmt.Errorf("token {%s}: %w", t.body, err)
}
head, ok := fields[a.key] head, ok := fields[a.key]
if !ok { if !ok {
if isOption(a.key) { 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 name string // as written, and the key a bound draw's value is held under
key string key string
tail []string tail []string
// steps holds the key for each segment the walk passes *through* — the path // steps holds the key for each segment the walk descends to, so every level a
// prefixes below the head and above the leaf. The head is bound under key and // path passes through is held — including its last, which another path may
// the leaf's value under name, so a one-segment tail needs none of these. // name in full ({p.addr} beside {p.addr.city}).
steps []string steps []string
} }
@@ -197,13 +200,31 @@ func splitArm(name string) arm {
return arm{name: name, key: name} return arm{name: name, key: name}
} }
segs := strings.Split(tail, ".") segs := strings.Split(tail, ".")
var steps []string steps := make([]string, len(segs))
for i := 0; i < len(segs)-1; i++ { // every prefix except the leaf's own for i := range segs {
steps = append(steps, head+"."+strings.Join(segs[:i+1], ".")) steps[i] = head + "." + strings.Join(segs[:i+1], ".")
} }
return arm{name: name, key: head, tail: segs, steps: steps} 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. // splitArms splits a token body's '|' alternatives.
func splitArms(body string) []arm { func splitArms(body string) []arm {
parts := strings.Split(body, "|") parts := strings.Split(body, "|")