Reference sigils follow the filesystem: / the root, . this folder, .. the folder above
Tests / vet + fmt + tests (pull_request) Successful in 53s
Tests / vet + fmt + tests (pull_request) Successful in 53s
This commit is contained in:
+108
-24
@@ -6,26 +6,70 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// refPrefix marks a {..path} token: a reference to a node elsewhere in the data
|
||||
// root rather than a sibling field. The path is resolved across every loaded
|
||||
// directory (see linkRefs).
|
||||
const refPrefix = ".."
|
||||
// A reference names a node by path rather than as a sibling field: {/a.b} from the
|
||||
// data root, {.a} from the folder this file sits in, {..a} from the folder above.
|
||||
func isRef(name string) bool { return strings.HasPrefix(name, ".") || strings.HasPrefix(name, "/") }
|
||||
|
||||
func isRef(name string) bool { return strings.HasPrefix(name, refPrefix) }
|
||||
// refBinding is what a reference was bound to: the head key its category is held
|
||||
// under, and the tail read into it.
|
||||
type refBinding struct {
|
||||
key string
|
||||
tail []string
|
||||
}
|
||||
|
||||
// linkRefs resolves every {..path} reference in the assembled tree. The head of the
|
||||
// path — up to the category it names — is bound into the referring template's
|
||||
// fields, and the rest reads into it the way a sibling path does, so a reference
|
||||
// is held 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 reads a field not every variant carries fails here, keeping a bad reference a
|
||||
// New-time error, never a random render-time one.
|
||||
func linkRefs(root map[string]node) error {
|
||||
return walkNodes(root, func(path string, n node) error {
|
||||
t, ok := n.(*template)
|
||||
if !ok {
|
||||
return nil
|
||||
// refShape splits a reference into its sigil and the dotted path after it.
|
||||
func refShape(name string) (sigil, rest string, err error) {
|
||||
switch {
|
||||
case strings.HasPrefix(name, "/"):
|
||||
sigil, rest = "/", name[1:]
|
||||
case strings.HasPrefix(name, ".."):
|
||||
sigil, rest = "..", name[2:]
|
||||
default:
|
||||
sigil, rest = ".", name[1:]
|
||||
}
|
||||
if rest == "" {
|
||||
return "", "", fmt.Errorf("reference has no path")
|
||||
}
|
||||
if strings.HasPrefix(rest, ".") {
|
||||
return "", "", fmt.Errorf("a reference starts with / (the root), . (this folder) or .. (the folder above)")
|
||||
}
|
||||
for _, seg := range strings.Split(rest, ".") {
|
||||
if seg == "" {
|
||||
return "", "", fmt.Errorf("path has an empty segment")
|
||||
}
|
||||
}
|
||||
return sigil, rest, nil
|
||||
}
|
||||
|
||||
// refSegments resolves a reference written in folder to a path from the root.
|
||||
func refSegments(name string, folder []string) ([]string, error) {
|
||||
sigil, rest, err := refShape(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var base []string
|
||||
switch sigil {
|
||||
case ".":
|
||||
base = folder
|
||||
case "..":
|
||||
if len(folder) == 0 {
|
||||
return nil, fmt.Errorf("no folder above the root")
|
||||
}
|
||||
base = folder[:len(folder)-1]
|
||||
}
|
||||
return append(append([]string{}, base...), strings.Split(rest, ".")...), nil
|
||||
}
|
||||
|
||||
// linkRefs resolves every reference in the assembled tree. The head of the path —
|
||||
// up to the category it names — is bound into the referring template's fields
|
||||
// under its root path, and the rest reads into it the way a sibling path does, so
|
||||
// a reference is held like a sibling and two spellings of one target are one
|
||||
// draw. 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 reads a
|
||||
// field not every variant carries fails here, keeping a bad reference a New-time
|
||||
// error, never a random render-time one.
|
||||
func linkRefs(root map[string]node) error {
|
||||
return eachTemplate(root, func(folder []string, path string, t *template) error {
|
||||
names := refTokens(t.format)
|
||||
if len(names) == 0 {
|
||||
return nil
|
||||
@@ -33,18 +77,22 @@ func linkRefs(root map[string]node) error {
|
||||
if t.fields == nil {
|
||||
t.fields = map[string]node{}
|
||||
}
|
||||
t.refs = make(map[string]string, len(names))
|
||||
t.refs = make(map[string]refBinding, len(names))
|
||||
for _, name := range names {
|
||||
head, target, tail, err := resolveRef(root, strings.Split(name[len(refPrefix):], "."))
|
||||
segments, err := refSegments(name, folder)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: reference {%s}: %w", path, name, err)
|
||||
}
|
||||
key := refPrefix + strings.Join(head, ".")
|
||||
head, target, tail, err := resolveRef(root, segments)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: reference {%s}: %w", path, name, err)
|
||||
}
|
||||
key := "/" + strings.Join(head, ".")
|
||||
if err := checkPath(target, tail, key); err != nil {
|
||||
return fmt.Errorf("%s: reference {%s}: %w", path, name, err)
|
||||
}
|
||||
t.fields[key] = target
|
||||
t.refs[name] = key
|
||||
t.refs[name] = refBinding{key, tail}
|
||||
}
|
||||
if err := t.compileFormat(); err != nil {
|
||||
return fmt.Errorf("%s: %w", path, err)
|
||||
@@ -53,6 +101,42 @@ func linkRefs(root map[string]node) error {
|
||||
})
|
||||
}
|
||||
|
||||
// eachTemplate calls fn once per template, with the folder its category sits in
|
||||
// and the dot path reaching it, folders and names in sorted order.
|
||||
func eachTemplate(root map[string]node, fn func(folder []string, path string, t *template) error) error {
|
||||
var inCategory func(folder []string, path string, n node) error
|
||||
inCategory = func(folder []string, path string, n node) error {
|
||||
if t, ok := n.(*template); ok {
|
||||
if err := fn(folder, path, t); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, c := range contained(n) {
|
||||
if err := inCategory(folder, join(path, c.name), c.node); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
var inFolder func(folder []string, children map[string]node) error
|
||||
inFolder = func(folder []string, children map[string]node) error {
|
||||
for _, name := range sortedNames(children) {
|
||||
path := join(strings.Join(folder, "."), name)
|
||||
if g, ok := children[name].(*group); ok {
|
||||
if err := inFolder(append(folder[:len(folder):len(folder)], name), g.children); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err := inCategory(folder, path, children[name]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return inFolder(nil, root)
|
||||
}
|
||||
|
||||
// checkBoundLevelsHeld rejects every route to a held name except the ones that read
|
||||
// its draw. An expansion holds one draw of that name; anything else that renders it
|
||||
// draws again, and the two disagree. checkNoOverlap settles the spellings within one
|
||||
@@ -173,7 +257,7 @@ func cover(n node, into map[node]bool, inChoice bool) {
|
||||
// whole, so that draw fixes every value the render produced, and a second route to
|
||||
// any of them disagrees with it.
|
||||
//
|
||||
// The walk stops at a {..path} edge, which is where the operand's own value ends
|
||||
// The walk stops at a reference edge, which is where the operand's own value ends
|
||||
// and a shared source begins: two names referencing one category are two draws, the
|
||||
// same rule {word} {word} follows.
|
||||
func operandDraw(n node, into map[node]bool) {
|
||||
@@ -271,7 +355,7 @@ func contained(n node) []namedNode {
|
||||
}
|
||||
}
|
||||
|
||||
// named skips a bound {..path} key: it is a render edge, not containment, so using
|
||||
// named skips a bound {/path} key: it is a render edge, not containment, so using
|
||||
// it as a path segment would report a node under a path that does not reach it. Only
|
||||
// a template's fields hold bindings — loadDir skips a dot-prefixed entry, so a
|
||||
// group's children never carry the prefix — so this one skip serves both.
|
||||
@@ -317,7 +401,7 @@ func resolveRef(root map[string]node, segments []string) (head []string, target
|
||||
return segments[:i], n, segments[i:], nil
|
||||
}
|
||||
|
||||
// refTokens returns the {..path} names a format reads, as tokens or as operands.
|
||||
// refTokens returns the reference names a format reads, as tokens or as operands.
|
||||
func refTokens(format string) []string {
|
||||
var refs []string
|
||||
seen := map[string]bool{}
|
||||
|
||||
Reference in New Issue
Block a user