Add {..path} references: render a node from elsewhere in the merged data root
This commit is contained in:
+134
-2
@@ -175,8 +175,9 @@ func expand(r rng, format string, fields map[string]node) string {
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// resolve renders a "{token}" body: one or more field names separated by '|',
|
||||
// of which one is chosen at random. checkTokens guarantees every name exists.
|
||||
// resolve renders a "{token}" body: one or more names separated by '|', one
|
||||
// picked at random. A name is a sibling field or a {..path} reference, which
|
||||
// linkRefs bound into fields too; checkTokens and linkRefs guarantee both exist.
|
||||
func resolve(r rng, token string, fields map[string]node) string {
|
||||
names := strings.Split(token, "|")
|
||||
return render(r, fields[names[r.IntN(len(names))]])
|
||||
@@ -366,6 +367,12 @@ func checkTokens(format string, fields map[string]node) error {
|
||||
}
|
||||
} else {
|
||||
for _, name := range strings.Split(body, "|") {
|
||||
if isRef(name) {
|
||||
if name == refPrefix {
|
||||
return fmt.Errorf("token {%s}: reference has no path", body)
|
||||
}
|
||||
continue // a root reference; its target is checked at New (see linkRefs)
|
||||
}
|
||||
if _, ok := fields[name]; !ok {
|
||||
return fmt.Errorf("token {%s}: no field %q", body, name)
|
||||
}
|
||||
@@ -377,6 +384,131 @@ func checkTokens(format string, fields map[string]node) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// refPrefix marks a {..path} token: a reference to a node elsewhere in the data
|
||||
// root rather than a sibling field. The path after it is the same dot path Fake
|
||||
// takes, resolved across every loaded directory (see linkRefs).
|
||||
const refPrefix = ".."
|
||||
|
||||
func isRef(name string) bool { return strings.HasPrefix(name, refPrefix) }
|
||||
|
||||
// linkRefs resolves every {..path} reference in the assembled tree, binding the
|
||||
// target node into the referring template's fields under the token's key so the
|
||||
// ordinary resolver renders it like a sibling. It runs once, after all data is
|
||||
// merged, so a reference sees the final (override-resolved) tree. A path that is
|
||||
// unknown, names a folder, or steps through a multi-variant choice fails here,
|
||||
// keeping a bad reference a New-time error, never a random render-time one. The
|
||||
// seen set both skips shared nodes and stops a cyclic reference looping the walk.
|
||||
func linkRefs(root map[string]node) error {
|
||||
seen := map[node]bool{}
|
||||
var visit func(node) error
|
||||
visit = func(n node) error {
|
||||
if n == nil || seen[n] {
|
||||
return nil
|
||||
}
|
||||
seen[n] = true
|
||||
switch n := n.(type) {
|
||||
case *group:
|
||||
for _, c := range n.children {
|
||||
if err := visit(c); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case *choice:
|
||||
for _, it := range n.items {
|
||||
if err := visit(it); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case *template:
|
||||
for _, name := range refTokens(n.format) {
|
||||
target, err := lookup(root, strings.Split(name[len(refPrefix):], "."))
|
||||
if err != nil {
|
||||
return fmt.Errorf("reference {%s}: %w", name, err)
|
||||
}
|
||||
n.fields[name] = target
|
||||
}
|
||||
for _, c := range n.fields {
|
||||
if err := visit(c); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
for _, c := range root {
|
||||
if err := visit(c); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// lookup finds the single node a reference path names, walking groups and
|
||||
// template fields by segment and descending a single-variant choice as a
|
||||
// transparent wrapper. A missing segment, a folder target, or a step through a
|
||||
// multi-variant choice (which has no one value to bind) is an error.
|
||||
func lookup(root map[string]node, segments []string) (node, error) {
|
||||
var n node = &group{children: root}
|
||||
for i := 0; i < len(segments); i++ {
|
||||
switch c := n.(type) {
|
||||
case *group:
|
||||
child, ok := c.children[segments[i]]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no entry %q", segments[i])
|
||||
}
|
||||
n = child
|
||||
case *template:
|
||||
child, ok := c.fields[segments[i]]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no field %q", segments[i])
|
||||
}
|
||||
n = child
|
||||
case *choice:
|
||||
if len(c.items) != 1 {
|
||||
return nil, fmt.Errorf("%q steps through a %d-way choice", segments[i], len(c.items))
|
||||
}
|
||||
n, i = c.items[0], i-1 // a choice consumes no segment; reprocess it unwrapped
|
||||
default:
|
||||
return nil, fmt.Errorf("cannot descend into %T at %q", n, segments[i])
|
||||
}
|
||||
}
|
||||
if _, ok := n.(*group); ok {
|
||||
return nil, fmt.Errorf("names a folder, not a value")
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// refTokens returns the reference names ({..path}, including ones inside a
|
||||
// {a|..path} alternation) used in a format string, scanning braces the way
|
||||
// expand does. Function tokens carry no references.
|
||||
func refTokens(format string) []string {
|
||||
var refs []string
|
||||
rs := []rune(format)
|
||||
for i := 0; i < len(rs); i++ {
|
||||
switch rs[i] {
|
||||
case '#':
|
||||
i++
|
||||
case '{':
|
||||
end := i + 1
|
||||
for end < len(rs) && rs[end] != '}' {
|
||||
end++
|
||||
}
|
||||
if end >= len(rs) {
|
||||
return refs // checkTokens already rejected the unterminated brace
|
||||
}
|
||||
if body := string(rs[i+1 : end]); strings.IndexByte(body, '(') < 0 {
|
||||
for _, name := range strings.Split(body, "|") {
|
||||
if isRef(name) {
|
||||
refs = append(refs, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
i = end
|
||||
}
|
||||
}
|
||||
return refs
|
||||
}
|
||||
|
||||
// repeatOf reads a template's "repeat" (default 1): how many times its format
|
||||
// is rendered and concatenated. A present one must be a positive integer.
|
||||
func repeatOf(m map[string]any) (int, error) {
|
||||
|
||||
Reference in New Issue
Block a user