One reference draw per render in both views: a repeat iteration draws anew, a group option draws apart, and one fence refuses what a render's draws cannot answer for
Tests / vet + fmt + tests (pull_request) Successful in 1m6s

This commit is contained in:
2026-09-15 22:55:42 +02:00
parent 4b31482a53
commit 246f99a319
10 changed files with 400 additions and 167 deletions
+6 -74
View File
@@ -4,7 +4,6 @@ import (
"encoding/json"
"errors"
"fmt"
"sort"
"strings"
"unicode"
"unicode/utf8"
@@ -23,7 +22,7 @@ type Column struct {
// Record is one record rendered from a template: every direct field is a column,
// listed in name order. Each column is its own expansion, so a sibling field is
// local to it, while a reference that reads a path is drawn once for the whole
// record.
// record, per group.
type Record struct {
columns []Column
}
@@ -212,7 +211,7 @@ func recordOf(n node) (*template, []Column, error) {
if !t.record {
return nil, nil, fmt.Errorf("carries repeat %d, which composes its format into one string; a record projects columns instead — drop the repeat and render the record again for more rows", t.repeat)
}
if err := checkColumnRefs(t, names); err != nil {
if err := checkColumnDraws(t, names); err != nil {
return nil, nil, err
}
columns := make([]Column, len(names))
@@ -223,80 +222,13 @@ func recordOf(n node) (*template, []Column, error) {
return t, columns, nil
}
// checkColumnRefs rejects the reference reads a record's shared draw cannot answer
// for: one column rendering a level another reads a path into, and a column
// reading the record back through its own path.
func checkColumnRefs(t *template, columns []string) error {
reads, err := columnRefs(t, columns)
if err != nil {
return err
}
sort.Slice(reads, func(i, j int) bool {
if reads[i].a.path != reads[j].a.path {
return reads[i].a.path < reads[j].a.path
}
return reads[i].column < reads[j].column
})
for i, level := range reads {
for _, into := range reads[i+1:] {
if strings.HasPrefix(into.a.path, level.a.path+".") {
return fmt.Errorf("column %q renders {%s}, a level column %q reads a path into with {%s}; name the fields you want instead",
level.column, level.a.name, into.column, into.a.name)
}
}
}
return nil
}
// columnRef is one held reference read, and the column whose render reaches it.
type columnRef struct {
column string
a arm
}
// columnRefs lists every reference that reads a path, anywhere a column renders,
// following the same edges expand does. A reference landing back on the record
// itself is reported rather than collected, whether it reads a path or the record
// whole: either way the column describes a draw other than its neighbours'.
func columnRefs(t *template, columns []string) ([]columnRef, error) {
var out []columnRef
var err error
for _, name := range columns {
seen := map[node]bool{}
var walk func(n node)
walk = func(n node) {
if n == nil || seen[n] || err != nil {
return
}
seen[n] = true
if tm, ok := n.(*template); ok {
for _, ref := range refTokens(tm.format) {
a := splitArm(ref, tm.refs)
if tm.fields[a.key] == node(t) {
err = fmt.Errorf("column %q 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", name, a.name)
return
}
if len(a.tail) > 0 {
out = append(out, columnRef{name, a})
}
}
}
for _, e := range renderEdges(n) {
walk(e.to)
}
}
walk(t.fields[name])
}
return out, err
}
// renderRecord draws each column once, in the name order recordOf fixed, over one
// reference scope shared across them.
// renderRecord draws each column once, in the name order recordOf fixed, as one render.
func renderRecord(s *session, t *template, columns []Column) *Record {
scope := &draws{variant: map[string]node{}, value: map[string]draw{}}
set := newDrawSet()
sc := drawScope{set: &set}.in(t)
r := &Record{columns: append([]Column(nil), columns...)}
for i := range r.columns {
column := renderLeaf(s, t.fields[r.columns[i].Name], scope)
column := renderLeaf(s, t.fields[r.columns[i].Name], sc)
r.columns[i].Value, r.columns[i].Null = column.text, column.null
}
return r