Render a repeat iteration in no group, refuse a group that names nothing, key group names by category, and own reference overlap in one fence
Tests / vet + fmt + tests (pull_request) Successful in 1m6s
Tests / vet + fmt + tests (pull_request) Successful in 1m6s
This commit is contained in:
@@ -443,9 +443,10 @@ Renders e.g. `Hej, Pat Smith!`. A reference path into a category is held like a
|
|||||||
record — rather than one format: `{.person.femalefirst} {.person.last}` name one
|
record — rather than one format: `{.person.femalefirst} {.person.last}` name one
|
||||||
person, as do the same two references in sibling fields or a nested template, and
|
person, as do the same two references in sibling fields or a nested template, and
|
||||||
`{lowercase(.person.femalefirst)}` reads that same draw. Each `repeat` iteration is
|
`{lowercase(.person.femalefirst)}` reads that same draw. Each `repeat` iteration is
|
||||||
a render of its own, so it draws anew, and a [group](#group) holds a draw apart. A
|
a render of its own, in no group, so it draws anew, and a [group](#group) holds a
|
||||||
bare reference names no field and draws each time: `{/misc.uuid} {/misc.uuid}` is
|
draw apart. A bare reference names no field and makes its own picks each time —
|
||||||
two draws. Rejected at `New`: a path that is
|
`{/misc.uuid} {/misc.uuid}` is two draws — while the reference paths inside what it
|
||||||
|
renders still read the render's draws. Rejected at `New`: a path that is
|
||||||
unknown, names a folder, has no folder above, or reads a field not every variant
|
unknown, names a folder, has no folder above, or reads a field not every variant
|
||||||
of a choice carries, and a reference that leads back to its own value, directly,
|
of a choice carries, and a reference that leads back to its own value, directly,
|
||||||
mutually or through a chain.
|
mutually or through a chain.
|
||||||
@@ -453,8 +454,10 @@ mutually or through a chain.
|
|||||||
### Group
|
### Group
|
||||||
|
|
||||||
A template may carry `group` to hold its reference draws apart: every reference path
|
A template may carry `group` to hold its reference draws apart: every reference path
|
||||||
it renders, however deep, reads the draw of that group, and the templates naming one
|
it renders, however deep short of a `repeat`, reads the draw of that group, and the
|
||||||
group in a render read one draw. A nested `group` names another.
|
templates of one category naming one group read one draw. A group name is local to
|
||||||
|
its category, so a category another one references never joins its groups by name;
|
||||||
|
the unnamed group spans them all.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{ "format": "{payer} pays {payee}; signed {signature}",
|
{ "format": "{payer} pays {payee}; signed {signature}",
|
||||||
@@ -464,9 +467,11 @@ group in a render read one draw. A nested `group` names another.
|
|||||||
```
|
```
|
||||||
|
|
||||||
Renders e.g. `Sara Eriksson pays Ebba Lind; signed Eriksson`: the signature reads the
|
Renders e.g. `Sara Eriksson pays Ebba Lind; signed Eriksson`: the signature reads the
|
||||||
payer's draw, while the payee is drawn apart. Rejected at load: a `group` of `""` (the
|
payer's draw, while the payee is drawn apart. Rejected at load, each naming nothing: a
|
||||||
default), one on a template that renders no reference path, and a path reading into a
|
`group` of `""` (the default); one naming the group its template already draws in; one
|
||||||
level that carries one.
|
on a template that renders no reference path short of a `repeat`, on a `repeat` itself —
|
||||||
|
each iteration renders in no group — or on an inline template's root, which nothing
|
||||||
|
references. So is a path reading into a level that carries a `group`.
|
||||||
|
|
||||||
### Correlated fields
|
### Correlated fields
|
||||||
|
|
||||||
@@ -673,6 +678,11 @@ tokens add cost in proportion to the output.
|
|||||||
`person` rather than two copies of it. Only references share: a sibling field is
|
`person` rather than two copies of it. Only references share: a sibling field is
|
||||||
local to its own expansion, so a `first` column does not silently bind to a
|
local to its own expansion, so a `first` column does not silently bind to a
|
||||||
`first` in the column next to it.
|
`first` in the column next to it.
|
||||||
|
- **A group name is local to its category.** A category's groups are its own
|
||||||
|
entities, so a caller naming a group the same way never joins them by accident,
|
||||||
|
and renaming a group inside one file changes no render elsewhere. The unnamed
|
||||||
|
group still spans categories, since facts that belong together across categories
|
||||||
|
must agree.
|
||||||
- **A record's column set is fixed before the first draw.** Only a category-level
|
- **A record's column set is fixed before the first draw.** Only a category-level
|
||||||
template is a record: a path descending into a field, or naming a folder or a
|
template is a record: a path descending into a field, or naming a folder or a
|
||||||
choice, errors. A tail may pass through a choice whose variants carry different
|
choice, errors. A tail may pass through a choice whose variants carry different
|
||||||
|
|||||||
@@ -31,10 +31,10 @@ func renderOnce(s *session, n node) string {
|
|||||||
return render(s, n, drawScope{set: &set})
|
return render(s, n, drawScope{set: &set})
|
||||||
}
|
}
|
||||||
|
|
||||||
// in is the scope t renders in: the group it names, else its caller's.
|
// in is the scope t renders in: its group where it names one, else its caller's.
|
||||||
func (sc drawScope) in(t *template) drawScope {
|
func (sc drawScope) in(t *template) drawScope {
|
||||||
if t.drawGroup != "" {
|
if t.groupKey != "" {
|
||||||
sc.group = t.drawGroup
|
sc.group = t.groupKey
|
||||||
}
|
}
|
||||||
return sc
|
return sc
|
||||||
}
|
}
|
||||||
@@ -55,8 +55,9 @@ func (sc drawScope) draws() *draws {
|
|||||||
return d
|
return d
|
||||||
}
|
}
|
||||||
|
|
||||||
// groupOf reads a template's "group" (default "").
|
// groupOf reads a template's "group" (default ""), which a repeat cannot carry: each iteration
|
||||||
func groupOf(m map[string]any) (string, error) {
|
// renders in no group.
|
||||||
|
func groupOf(m map[string]any, repeat int) (string, error) {
|
||||||
v, ok := m["group"]
|
v, ok := m["group"]
|
||||||
if !ok {
|
if !ok {
|
||||||
return "", nil
|
return "", nil
|
||||||
@@ -67,29 +68,73 @@ func groupOf(m map[string]any) (string, error) {
|
|||||||
return "", fmt.Errorf("group must be a string, got %T", v)
|
return "", fmt.Errorf("group must be a string, got %T", v)
|
||||||
case name == "":
|
case name == "":
|
||||||
return "", fmt.Errorf(`group "" is the default, so it has no effect; drop it`)
|
return "", fmt.Errorf(`group "" is the default, so it has no effect; drop it`)
|
||||||
|
case repeat > 1:
|
||||||
|
return "", fmt.Errorf("group %q on a repeat names nothing, since each iteration is a render of its own; drop it", name)
|
||||||
}
|
}
|
||||||
return name, nil
|
return name, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// drawCheck fences each template of a scope as a render of its own, remembering which nodes
|
// keyGroup keys t's group by the category t sits in, "" for an inline template, so a group name is
|
||||||
// read a reference path.
|
// local to its category.
|
||||||
|
func (t *template) keyGroup(category string) {
|
||||||
|
if t.group != "" {
|
||||||
|
t.groupKey = category + "/" + t.group
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkNestedGroup refuses a template beneath one drawing in group that names group again, short of
|
||||||
|
// a repeat or another group.
|
||||||
|
func checkNestedGroup(fields map[string]node, group string) error {
|
||||||
|
if group == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var walk func(path string, n node) error
|
||||||
|
walk = func(path string, n node) error {
|
||||||
|
t, isTemplate := n.(*template)
|
||||||
|
switch {
|
||||||
|
case isTemplate && t.group == group:
|
||||||
|
return fmt.Errorf("%q names group %q, the group this template draws in already; drop it", path, group)
|
||||||
|
case isTemplate && (t.group != "" || t.repeat > 1):
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for _, c := range contained(n) {
|
||||||
|
if err := walk(join(path, c.name), c.node); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for _, name := range sortedNames(fields) {
|
||||||
|
if err := walk(name, fields[name]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// drawCheck fences each template of a scope as a render of its own, remembering which nodes read a
|
||||||
|
// reference path.
|
||||||
type drawCheck struct {
|
type drawCheck struct {
|
||||||
reads map[node]bool
|
reads map[node]bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *drawCheck) check(path string, n node) error {
|
// checkGroup refuses a group that splits nothing: one whose render reads no reference path short of
|
||||||
|
// a repeat.
|
||||||
|
func (c *drawCheck) checkGroup(path string, n node) error {
|
||||||
|
if t, ok := n.(*template); ok && t.group != "" && !c.readsPath(t) {
|
||||||
|
return fmt.Errorf("%s: group %q splits nothing, since nothing it renders reads a reference path; drop it", path, t.group)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *drawCheck) checkDraws(path string, n node) error {
|
||||||
t, ok := n.(*template)
|
t, ok := n.(*template)
|
||||||
switch {
|
if !ok || !c.readsPath(t) {
|
||||||
case !ok:
|
|
||||||
return nil
|
|
||||||
case !c.readsPath(t) && t.drawGroup != "":
|
|
||||||
return fmt.Errorf("%s: group %q splits nothing, since nothing it renders reads a reference path; drop it", path, t.drawGroup)
|
|
||||||
case !c.readsPath(t):
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
w := newDrawWalk(nil)
|
w := newDrawWalk(nil)
|
||||||
for _, e := range renderEdges(t) {
|
for _, e := range renderEdges(t) {
|
||||||
w.edge(t, e, t.drawGroup, drawRoute{e.reached(), e.label}, "", false)
|
w.edge(t, e, drawAt{group: t.groupKey, route: drawRoute{e.reached(), e.label}})
|
||||||
}
|
}
|
||||||
if err := w.check(); err != nil {
|
if err := w.check(); err != nil {
|
||||||
return fmt.Errorf("%s: %w", path, err)
|
return fmt.Errorf("%s: %w", path, err)
|
||||||
@@ -97,14 +142,15 @@ func (c *drawCheck) check(path string, n node) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// readsPath reports whether rendering n reads a reference path, however deep.
|
// readsPath reports whether rendering n reads a reference path, short of a repeat, which renders over
|
||||||
|
// draws of its own.
|
||||||
func (c *drawCheck) readsPath(n node) bool {
|
func (c *drawCheck) readsPath(n node) bool {
|
||||||
if r, done := c.reads[n]; done {
|
if r, done := c.reads[n]; done {
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
r := false
|
r := false
|
||||||
for _, e := range renderEdges(n) {
|
for _, e := range renderEdges(n) {
|
||||||
if a, _, isRef := refRead(n, e.label); (isRef && len(a.tail) > 0) || c.readsPath(e.to) {
|
if a, _, isRef := refRead(n, e.label); (isRef && len(a.tail) > 0) || (!repeats(e.to) && c.readsPath(e.to)) {
|
||||||
r = true
|
r = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -116,8 +162,13 @@ func (c *drawCheck) readsPath(n node) bool {
|
|||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
// refRead is the reference an edge of n reads, and the node it is bound to; false when the
|
func repeats(n node) bool {
|
||||||
// edge reads none.
|
t, isTemplate := n.(*template)
|
||||||
|
return isTemplate && t.repeat > 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// refRead is the reference an edge of n reads, and the node it is bound to; false when the edge
|
||||||
|
// reads none.
|
||||||
func refRead(n node, label string) (arm, node, bool) {
|
func refRead(n node, label string) (arm, node, bool) {
|
||||||
t, isTemplate := n.(*template)
|
t, isTemplate := n.(*template)
|
||||||
if !isTemplate {
|
if !isTemplate {
|
||||||
@@ -131,14 +182,13 @@ func refRead(n node, label string) (arm, node, bool) {
|
|||||||
func checkColumnDraws(t *template, columns []string) error {
|
func checkColumnDraws(t *template, columns []string) error {
|
||||||
w := newDrawWalk(t)
|
w := newDrawWalk(t)
|
||||||
for _, name := range columns {
|
for _, name := range columns {
|
||||||
w.walk(t.fields[name], t.drawGroup, drawRoute{spelling: fmt.Sprintf("column %q", name)}, "", false)
|
w.walk(t.fields[name], drawAt{group: t.groupKey, route: drawRoute{spelling: fmt.Sprintf("column %q", name)}})
|
||||||
}
|
}
|
||||||
return w.check()
|
return w.check()
|
||||||
}
|
}
|
||||||
|
|
||||||
// drawWalk gathers what one render reads through its draws and what it draws afresh, each by
|
// drawWalk gathers what one render reads by reference and what it draws afresh, each by group, for
|
||||||
// group, for check to compare. record is set for a record's columns, which may not read the
|
// check to compare. record is set for a record's columns, which may not read the record back.
|
||||||
// record back.
|
|
||||||
type drawWalk struct {
|
type drawWalk struct {
|
||||||
record *template
|
record *template
|
||||||
reads []pathRead
|
reads []pathRead
|
||||||
@@ -148,21 +198,28 @@ type drawWalk struct {
|
|||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// drawAt is where a walk stands: the group it draws in, how the render's root reached it, the
|
||||||
|
// reference it last crossed, and whether it renders inside a reference path's draw.
|
||||||
|
type drawAt struct {
|
||||||
|
group string
|
||||||
|
route drawRoute
|
||||||
|
via string
|
||||||
|
held bool
|
||||||
|
}
|
||||||
|
|
||||||
// drawRoute is how a render reaches a draw: as its author spells it, and the root edge's label.
|
// drawRoute is how a render reaches a draw: as its author spells it, and the root edge's label.
|
||||||
type drawRoute struct{ spelling, label string }
|
type drawRoute struct{ spelling, label string }
|
||||||
|
|
||||||
|
// pathRead is one reference a render reads: a path, or a bare reference with no tail.
|
||||||
type pathRead struct {
|
type pathRead struct {
|
||||||
group string
|
at drawAt
|
||||||
route drawRoute
|
|
||||||
a arm
|
a arm
|
||||||
target node
|
target node
|
||||||
}
|
}
|
||||||
|
|
||||||
type freshDraw struct {
|
type freshDraw struct {
|
||||||
group string
|
at drawAt
|
||||||
route drawRoute
|
n node
|
||||||
via string
|
|
||||||
n node
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type drawVisit struct {
|
type drawVisit struct {
|
||||||
@@ -180,88 +237,105 @@ func newDrawWalk(record *template) *drawWalk {
|
|||||||
return &drawWalk{record: record, read: map[drawKey]bool{}, seen: map[drawVisit]bool{}}
|
return &drawWalk{record: record, read: map[drawKey]bool{}, seen: map[drawVisit]bool{}}
|
||||||
}
|
}
|
||||||
|
|
||||||
// walk follows what rendering n renders. held says n renders inside a reference path's draw, so
|
// walk follows what rendering n renders. A repeat renders over draws of its own, so the walk stops
|
||||||
// what it draws belongs to that draw; a repeat renders over draws of its own, so the walk stops there.
|
// there.
|
||||||
func (w *drawWalk) walk(n node, group string, route drawRoute, via string, held bool) {
|
func (w *drawWalk) walk(n node, at drawAt) {
|
||||||
v := drawVisit{n, group, held}
|
v := drawVisit{n, at.group, at.held}
|
||||||
if w.seen[v] || w.err != nil {
|
if w.seen[v] || w.err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.seen[v] = true
|
w.seen[v] = true
|
||||||
if !held {
|
if !at.held {
|
||||||
w.fresh = append(w.fresh, freshDraw{group, route, via, n})
|
w.fresh = append(w.fresh, freshDraw{at, n})
|
||||||
}
|
}
|
||||||
t, isTemplate := n.(*template)
|
if repeats(n) {
|
||||||
if isTemplate && t.repeat > 1 {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if isTemplate && t.drawGroup != "" {
|
if t, isTemplate := n.(*template); isTemplate && t.groupKey != "" {
|
||||||
group = t.drawGroup
|
at.group = t.groupKey
|
||||||
}
|
}
|
||||||
for _, e := range renderEdges(n) {
|
for _, e := range renderEdges(n) {
|
||||||
w.edge(n, e, group, route, via, held)
|
w.edge(n, e, at)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *drawWalk) edge(from node, e renderEdge, group string, route drawRoute, via string, held bool) {
|
func (w *drawWalk) edge(from node, e renderEdge, at drawAt) {
|
||||||
a, target, reads := refRead(from, e.label)
|
a, target, reads := refRead(from, e.label)
|
||||||
switch {
|
switch {
|
||||||
case !reads:
|
case !reads:
|
||||||
w.walk(e.to, group, route, via, held)
|
w.walk(e.to, at)
|
||||||
|
return
|
||||||
case w.record != nil && target == node(w.record):
|
case w.record != nil && target == node(w.record):
|
||||||
w.err = fmt.Errorf("%s reads {%s}, which points back at this record; a column cannot read another column — move the shared value into its own category and reference that", route.spelling, a.name)
|
w.err = fmt.Errorf("%s reads {%s}, which points back at this record; a column cannot read another column — move the shared value into its own category and reference that", at.route.spelling, a.name)
|
||||||
case len(a.tail) > 0:
|
return
|
||||||
if k := (drawKey{group, a.path}); !w.read[k] {
|
|
||||||
w.read[k] = true
|
|
||||||
w.reads = append(w.reads, pathRead{group, route, a, target})
|
|
||||||
}
|
|
||||||
w.walk(e.to, group, route, a.name, true)
|
|
||||||
default:
|
|
||||||
w.walk(e.to, group, route, a.name, false)
|
|
||||||
}
|
}
|
||||||
|
if k := (drawKey{at.group, a.path}); !w.read[k] {
|
||||||
|
w.read[k] = true
|
||||||
|
w.reads = append(w.reads, pathRead{at, a, target})
|
||||||
|
}
|
||||||
|
at.via, at.held = a.name, len(a.tail) > 0
|
||||||
|
w.walk(e.to, at)
|
||||||
}
|
}
|
||||||
|
|
||||||
// check refuses what one draw per reference path cannot answer for: a path read into a level
|
// check refuses what one draw per reference path cannot answer for, reads compared in path order so
|
||||||
// another read renders, and a node drawn afresh beside a path whose draw holds it.
|
// which pair is reported does not vary.
|
||||||
func (w *drawWalk) check() error {
|
func (w *drawWalk) check() error {
|
||||||
if w.err != nil {
|
if w.err != nil {
|
||||||
return w.err
|
return w.err
|
||||||
}
|
}
|
||||||
sort.SliceStable(w.reads, func(i, j int) bool {
|
sort.SliceStable(w.reads, func(i, j int) bool {
|
||||||
if w.reads[i].group != w.reads[j].group {
|
if w.reads[i].at.group != w.reads[j].at.group {
|
||||||
return w.reads[i].group < w.reads[j].group
|
return w.reads[i].at.group < w.reads[j].at.group
|
||||||
}
|
}
|
||||||
return w.reads[i].a.path < w.reads[j].a.path
|
return w.reads[i].a.path < w.reads[j].a.path
|
||||||
})
|
})
|
||||||
pins := map[drawKey]pathRead{}
|
if err := w.checkOverlaps(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return w.checkFreshDraws()
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkOverlaps refuses a read of a level beside a path another read takes into it.
|
||||||
|
func (w *drawWalk) checkOverlaps() error {
|
||||||
for i, level := range w.reads {
|
for i, level := range w.reads {
|
||||||
for _, into := range w.reads[i+1:] {
|
for _, into := range w.reads[i+1:] {
|
||||||
if into.group == level.group && strings.HasPrefix(into.a.path, level.a.path+".") {
|
if into.at.group == level.at.group && strings.HasPrefix(into.a.path, level.a.path+".") {
|
||||||
return overlap(level.route, level.a.name, into)
|
return overlapError(level.at.route, level.a.name, into)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
held := map[node]bool{}
|
|
||||||
coverPath(level.target, level.a.tail, held)
|
|
||||||
for n := range held {
|
|
||||||
if _, pinned := pins[drawKey{level.group, n}]; !pinned {
|
|
||||||
pins[drawKey{level.group, n}] = level
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, f := range w.fresh {
|
|
||||||
if r, pinned := pins[drawKey{f.group, f.n}]; pinned {
|
|
||||||
return overlap(f.route, f.via, r)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func overlap(route drawRoute, ref string, into pathRead) error {
|
// checkFreshDraws refuses a node drawn afresh where a reference path's draw holds it.
|
||||||
return fmt.Errorf("%s renders a level that %s reads a path into; name the fields you want instead, or draw them apart with a group", route.spelled(ref), into.route.spelled(into.a.name))
|
func (w *drawWalk) checkFreshDraws() error {
|
||||||
|
pins := map[drawKey]pathRead{}
|
||||||
|
for _, r := range w.reads {
|
||||||
|
if len(r.a.tail) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
held := map[node]bool{}
|
||||||
|
coverPath(r.target, r.a.tail, held)
|
||||||
|
for n := range held {
|
||||||
|
if _, pinned := pins[drawKey{r.at.group, n}]; !pinned {
|
||||||
|
pins[drawKey{r.at.group, n}] = r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, f := range w.fresh {
|
||||||
|
if r, pinned := pins[drawKey{f.at.group, f.n}]; pinned {
|
||||||
|
return overlapError(f.at.route, f.at.via, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// spelled names the route, and the reference it reaches a draw by where its root edge is not
|
func overlapError(route drawRoute, ref string, into pathRead) error {
|
||||||
// that reference.
|
return fmt.Errorf("%s renders a level that %s reads a path into; name the fields you want instead, or draw them apart with a group", route.spelled(ref), into.at.route.spelled(into.a.name))
|
||||||
|
}
|
||||||
|
|
||||||
|
// spelled names the route, and the reference it reaches a draw by where its root edge is not that
|
||||||
|
// reference.
|
||||||
func (r drawRoute) spelled(ref string) string {
|
func (r drawRoute) spelled(ref string) string {
|
||||||
if ref == "" || ref == r.label {
|
if ref == "" || ref == r.label {
|
||||||
return r.spelling
|
return r.spelling
|
||||||
|
|||||||
@@ -196,7 +196,11 @@ func checkRenders(s nodeScope) error {
|
|||||||
if err := s(heldCheck); err != nil {
|
if err := s(heldCheck); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := s((&drawCheck{}).check); err != nil {
|
fence := &drawCheck{}
|
||||||
|
if err := s(fence.checkGroup); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := s(fence.checkDraws); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return s((&valueProof{}).checkDatatype)
|
return s((&valueProof{}).checkDatatype)
|
||||||
|
|||||||
@@ -6,11 +6,12 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// heldCheck rejects every route to a held name except the ones that read its draw.
|
// heldCheck rejects every route to a held sibling name except the ones that read its
|
||||||
// An expansion holds one draw of that name; anything else that renders it draws
|
// 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
|
// again, and the two disagree. checkNoOverlap settles the spellings within one format
|
||||||
// format (a token, an operand); this settles the rest — a reference, whether it
|
// (a token, an operand); this settles the rest — a reference, whether it sits in that
|
||||||
// sits in that format or in anything the format renders, however deep.
|
// format or in anything the format renders, however deep. A reference path is held
|
||||||
|
// for the whole render instead, which drawCheck fences.
|
||||||
func heldCheck(path string, n node) error {
|
func heldCheck(path string, n node) error {
|
||||||
t, ok := n.(*template)
|
t, ok := n.(*template)
|
||||||
if !ok || len(t.held) == 0 {
|
if !ok || len(t.held) == 0 {
|
||||||
@@ -19,7 +20,7 @@ func heldCheck(path string, n node) error {
|
|||||||
readers := boundReaders(t.format, t.bound, t.refs)
|
readers := boundReaders(t.format, t.bound, t.refs)
|
||||||
for _, head := range heldHeads(t) {
|
for _, head := range heldHeads(t) {
|
||||||
if _, isPath := t.bound[head]; isPath && isRef(head) {
|
if _, isPath := t.bound[head]; isPath && isRef(head) {
|
||||||
continue // held for the whole render, which drawCheck fences
|
continue
|
||||||
}
|
}
|
||||||
if err := checkHeadHeld(t, head, readers); err != nil {
|
if err := checkHeadHeld(t, head, readers); err != nil {
|
||||||
return fmt.Errorf("%s: %w", path, err)
|
return fmt.Errorf("%s: %w", path, err)
|
||||||
@@ -178,12 +179,11 @@ func renders(n node, want, seen map[node]bool) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkNoOverlap rejects a format that both renders a level and reads a path into
|
// checkNoOverlap rejects a format that both renders a sibling level and reads a path
|
||||||
// it — {p} beside {p.first}, {p.addr} beside {p.addr.city}, {.p} beside
|
// into it — {p} beside {p.first}, {p.addr} beside {p.addr.city}. The path reads the
|
||||||
// {/sv_SE.p.first}. The path reads the level's held draw while rendering the level
|
// level's held draw while rendering the level expands it afresh, so their values would
|
||||||
// expands it afresh, so their values would disagree. Reads are compared by their
|
// disagree. Reads are compared in sorted order, so which pair is reported does not
|
||||||
// one spelling, in sorted order, so which pair is reported depends neither on how
|
// depend on where the tokens sit.
|
||||||
// a reference was written nor on where the tokens sit.
|
|
||||||
func checkNoOverlap(format string, bound map[string]string, refs map[string]refBinding) error {
|
func checkNoOverlap(format string, bound map[string]string, refs map[string]refBinding) error {
|
||||||
names := boundReaders(format, bound, refs)
|
names := boundReaders(format, bound, refs)
|
||||||
// Stable over one format-order scan, so two readers of one name (a token and a
|
// Stable over one format-order scan, so two readers of one name (a token and a
|
||||||
@@ -203,7 +203,7 @@ func checkNoOverlap(format string, bound map[string]string, refs map[string]refB
|
|||||||
// spelling, and how to name it.
|
// spelling, and how to name it.
|
||||||
type reader struct{ name, path, label string }
|
type reader struct{ name, path, label string }
|
||||||
|
|
||||||
// boundReaders lists every way a format reaches a bound field, in the order the
|
// boundReaders lists every way a format reaches a bound sibling field, in the order the
|
||||||
// format writes them. An operand renders its field, so it names a level exactly
|
// format writes them. An operand renders its field, so it names a level exactly
|
||||||
// as a token does; one scan finds both, which is what puts them in one order.
|
// as a token does; one scan finds both, which is what puts them in one order.
|
||||||
func boundReaders(format string, bound map[string]string, refs map[string]refBinding) []reader {
|
func boundReaders(format string, bound map[string]string, refs map[string]refBinding) []reader {
|
||||||
@@ -215,14 +215,14 @@ func boundReaders(format string, bound map[string]string, refs map[string]refBin
|
|||||||
if fn, _, isFunc := funcCall(t.body); isFunc {
|
if fn, _, isFunc := funcCall(t.body); isFunc {
|
||||||
for _, operand := range tokenOperands(t.body) {
|
for _, operand := range tokenOperands(t.body) {
|
||||||
a := splitArm(operand, refs)
|
a := splitArm(operand, refs)
|
||||||
if _, isBound := bound[a.key]; isBound {
|
if _, isBound := bound[a.key]; isBound && !isRef(a.key) {
|
||||||
names = append(names, reader{a.name, a.path, fmt.Sprintf("%s operand %q", fn, operand)})
|
names = append(names, reader{a.name, a.path, fmt.Sprintf("%s operand %q", fn, operand)})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
for _, a := range splitArms(t.body, refs) {
|
for _, a := range splitArms(t.body, refs) {
|
||||||
if _, isBound := bound[a.key]; isBound {
|
if _, isBound := bound[a.key]; isBound && !isRef(a.key) {
|
||||||
names = append(names, reader{a.name, a.path, "token {" + a.name + "}"})
|
names = append(names, reader{a.name, a.path, "token {" + a.name + "}"})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,6 +136,9 @@ func inputValue(input string) (any, error) {
|
|||||||
// bindInline links an inline node's references against root and runs check over it, naming its
|
// bindInline links an inline node's references against root and runs check over it, naming its
|
||||||
// nodes from label.
|
// nodes from label.
|
||||||
func bindInline(n node, label string, root map[string]node, check func(nodeScope) error) error {
|
func bindInline(n node, label string, root map[string]node, check func(nodeScope) error) error {
|
||||||
|
if t, isTemplate := n.(*template); isTemplate && t.group != "" {
|
||||||
|
return fmt.Errorf("%s: group %q names nothing, since nothing can reference an inline template; drop it", label, t.group)
|
||||||
|
}
|
||||||
scope := inlineScope(n, label)
|
scope := inlineScope(n, label)
|
||||||
if err := linkNodeRefs(scope, root); err != nil {
|
if err := linkNodeRefs(scope, root); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -151,6 +154,7 @@ func linkNodeRefs(scope nodeScope, root map[string]node) error {
|
|||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
t.keyGroup("")
|
||||||
for _, name := range refTokens(t.format) {
|
for _, name := range refTokens(t.format) {
|
||||||
sigil, rest, err := refShape(name)
|
sigil, rest, err := refShape(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -62,7 +62,8 @@ type template struct {
|
|||||||
fromString bool // written as a JSON string rather than an object
|
fromString bool // written as a JSON string rather than an object
|
||||||
readsColumn *columnRead // set when the format is one reference alone reading a record's column
|
readsColumn *columnRead // set when the format is one reference alone reading a record's column
|
||||||
record bool // compiled at the top without a repeat, so its fields are record columns
|
record bool // compiled at the top without a repeat, so its fields are record columns
|
||||||
drawGroup string // the group its render reads reference paths in; "" keeps its caller's
|
group string // the group it draws in, as written; "" keeps its caller's
|
||||||
|
groupKey string // group keyed by its category once linked, what a render reads its reference paths under
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*template) isNode() {}
|
func (*template) isNode() {}
|
||||||
@@ -248,7 +249,10 @@ func compileTemplate(m map[string]any, pos position) (node, error) {
|
|||||||
if err := checkTokens(o.format, fields); err != nil {
|
if err := checkTokens(o.format, fields); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
t := &template{format: o.format, fields: fields, repeat: o.repeat, separator: o.separator, datatype: o.datatype, drawGroup: o.group, record: fieldPos == inColumn}
|
if err := checkNestedGroup(fields, o.group); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
t := &template{format: o.format, fields: fields, repeat: o.repeat, separator: o.separator, datatype: o.datatype, group: o.group, record: fieldPos == inColumn}
|
||||||
if err := t.compileFormat(); err != nil {
|
if err := t.compileFormat(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -280,7 +284,7 @@ func readOptions(m map[string]any, pos position) (templateOptions, error) {
|
|||||||
if o.datatype, err = datatypeOf(m, pos); err != nil {
|
if o.datatype, err = datatypeOf(m, pos); err != nil {
|
||||||
return o, err
|
return o, err
|
||||||
}
|
}
|
||||||
if o.group, err = groupOf(m); err != nil {
|
if o.group, err = groupOf(m, repeat); err != nil {
|
||||||
return o, err
|
return o, err
|
||||||
}
|
}
|
||||||
if sv, ok := m["separator"]; ok {
|
if sv, ok := m["separator"]; ok {
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ func checkPath(n node, tail []string, level string) error {
|
|||||||
switch {
|
switch {
|
||||||
case t.repeat > 1:
|
case t.repeat > 1:
|
||||||
return fmt.Errorf("the level %q carries a repeat, which a path reading one draw of it cannot apply", name)
|
return fmt.Errorf("the level %q carries a repeat, which a path reading one draw of it cannot apply", name)
|
||||||
case t.drawGroup != "":
|
case t.group != "":
|
||||||
return fmt.Errorf("the level %q carries a group, which a path reading into it cannot apply", name)
|
return fmt.Errorf("the level %q carries a group, which a path reading into it cannot apply", name)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ func refSegments(name string, folder []string) ([]string, error) {
|
|||||||
// error, never a random render-time one.
|
// error, never a random render-time one.
|
||||||
func linkRefs(root map[string]node) error {
|
func linkRefs(root map[string]node) error {
|
||||||
return eachTemplate(root, func(folder []string, path string, t *template) error {
|
return eachTemplate(root, func(folder []string, path string, t *template) error {
|
||||||
|
t.keyGroup(strings.Join(strings.Split(path, ".")[:len(folder)+1], "."))
|
||||||
return linkTemplateRefs(folder, path, t, root)
|
return linkTemplateRefs(folder, path, t, root)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ func render(s *session, n node, sc drawScope) string {
|
|||||||
if i > 0 {
|
if i > 0 {
|
||||||
b.WriteString(n.separator)
|
b.WriteString(n.separator)
|
||||||
}
|
}
|
||||||
b.WriteString(expandAnew(s, n, sc.group))
|
b.WriteString(expandAnew(s, n))
|
||||||
}
|
}
|
||||||
return b.String()
|
return b.String()
|
||||||
default:
|
default:
|
||||||
@@ -80,13 +80,13 @@ func render(s *session, n node, sc drawScope) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// expandAnew expands one repeat iteration of t as a render of its own, in group. Inlined into
|
// expandAnew expands one repeat iteration of t as a render of its own, in no group. Inlined into
|
||||||
// render's loop, its draw set would move to the heap.
|
// render's loop, its draw set would move to the heap.
|
||||||
//
|
//
|
||||||
//go:noinline
|
//go:noinline
|
||||||
func expandAnew(s *session, t *template, group string) string {
|
func expandAnew(s *session, t *template) string {
|
||||||
var set drawSet
|
var set drawSet
|
||||||
return expand(s, t, drawScope{set: &set, group: group})
|
return expand(s, t, drawScope{set: &set})
|
||||||
}
|
}
|
||||||
|
|
||||||
// pick selects one item. Uniform choices are O(1); weighted choices are an
|
// pick selects one item. Uniform choices are O(1); weighted choices are an
|
||||||
|
|||||||
Reference in New Issue
Block a user