Files
fejkdata/family.go
T
lilleman d8c348d07b
Tests / vet + fmt + tests (pull_request) Successful in 1m6s
Tests / Gitea release from CHANGELOG.md (pull_request) Has been skipped
Treat a column's cells as alternatives in the family fence, probe a path before drawing, and record that two categories may name one TSV
2026-09-17 15:39:07 +02:00

322 lines
8.4 KiB
Go

package fejkdata
import (
"fmt"
"sort"
"strings"
)
// tablePin is one table's pinned row.
type tablePin struct {
t *table
row int
}
// pinned is the row the render pinned for t, if any.
func (d *draws) pinned(t *table) (int, bool) {
for _, p := range d.pins[:d.npins] {
if p.t == t {
return p.row, true
}
}
r, ok := d.more[t]
return r, ok
}
// mustRow is the row pinned for t, which the walk reaching a column pinned.
func (d *draws) mustRow(t *table) int {
r, ok := d.pinned(t)
if !ok {
panic(fmt.Sprintf("fejkdata: a column of %s is rendered with no row pinned", t.category))
}
return r
}
// pin pins row r of t, and the rows of t's ancestors it links to.
func (d *draws) pin(t *table, r int) {
for {
if _, done := d.pinned(t); done {
return
}
if d.npins < len(d.pins) {
d.pins[d.npins] = tablePin{t, r}
d.npins++
} else {
if d.more == nil {
d.more = map[*table]int{}
}
d.more[t] = r
}
if t.parentT == nil {
return
}
t, r = t.parentT, t.parentRow(r)
}
}
// each calls fn for every pinned row, in pin order, the spilled ones by table name.
func (d *draws) each(fn func(t *table, r int)) {
for _, p := range d.pins[:d.npins] {
fn(p.t, p.row)
}
spilled := make([]*table, 0, len(d.more))
for t := range d.more {
spilled = append(spilled, t)
}
sort.Slice(spilled, func(i, j int) bool { return spilled[i].category < spilled[j].category })
for _, t := range spilled {
fn(t, d.more[t])
}
}
// rowOf is the render's row of t: the one pinned, else one drawn inside the
// nearest pinned ancestor — its parent drawn inside that first where the ancestor
// is further up — or over the whole table, and pinned with its ancestors.
func (d *draws) rowOf(t *table) int {
if r, ok := d.pinned(t); ok {
return r
}
r := -1
for a := t.parentT; a != nil && r < 0; a = a.parentT {
if _, ok := d.pinned(a); !ok {
continue
}
if t.parentT != a {
d.rowOf(t.parentT)
}
pr, _ := d.pinned(t.parentT)
r = t.drawUnder(d.s, pr)
}
if r < 0 {
r = t.draw(d.s)
}
d.pin(t, r)
return r
}
// pinRow pins row r of t where it agrees with the rows pinned before it.
func (d *draws) pinRow(t *table, r int) error {
if pr, ok := d.pinned(t); ok && pr != r {
return fmt.Errorf("%s and %s are two rows of %s", t.selectorSpelling(pr), t.selectorSpelling(r), t.category)
}
for a := t.parentT; a != nil; a = a.parentT {
if pa, ok := d.pinned(a); ok && !t.under(r, a, pa) {
return fmt.Errorf("%s is not inside %s", t.selectorSpelling(r), a.selectorSpelling(pa))
}
}
d.pin(t, r)
return nil
}
// selectRow pins the row a selector names.
func (d *draws) selectRow(t *table, sel string) error {
r, err := t.find(sel, d)
if err != nil {
return err
}
return d.pinRow(t, r)
}
// inside keeps the rows of t that sit inside every pinned ancestor.
func (d *draws) inside(t *table, rows []int) []int {
for a := t.parentT; a != nil; a = a.parentT {
pa, ok := d.pinned(a)
if !ok {
continue
}
var kept []int
for _, r := range rows {
if t.under(r, a, pa) {
kept = append(kept, r)
}
}
rows = kept
}
return rows
}
// tableRead is what a reference path reads of a table family: the table its head
// names, the rows its selectors pin, the tables it draws — those it walks with no
// row pinned, and their unpinned ancestors — each selector's spelling, and whether
// it lands on a row rendered whole.
type tableRead struct {
head *table
pins draws
drawn map[*table]bool
sels []tableSel
whole bool
}
// tableSel is one selector on the way: the table it selects a row of, and the path
// as written up to and including it.
type tableSel struct {
t *table
spelling string
}
// tableReadOf replays a reference's selectors through the walk a render uses, so
// two paths pinning one row by different routes compare equal. checkPath proved
// each selector names a row.
func tableReadOf(head node, a arm, leaf node) *tableRead {
t, isTable := head.(*table)
if !isTable {
return nil
}
tr := &tableRead{head: t, drawn: map[*table]bool{}}
_, _ = walkPath(t, a.tail, pathWalk{pins: &tr.pins})
written := a.name[:len(a.name)-len(joinSegments(a.tail))]
cur := t
tr.draws(cur)
for i, seg := range a.tail {
switch d := cur.descendant(seg); {
case isSelector(seg):
tr.sels = append(tr.sels, tableSel{cur, written + joinSegments(a.tail[:i+1])})
case d != nil:
cur = d
tr.draws(cur)
}
}
_, tr.whole = leaf.(*row)
return tr
}
// draws marks t and its ancestors drawn, up to the first the read pins.
func (r *tableRead) draws(t *table) {
for ; t != nil; t = t.parentT {
if _, pinned := r.pins.pinned(t); pinned {
return
}
r.drawn[t] = true
}
}
// joinSegments spells segments as a path: a selector attaches to the name before it.
func joinSegments(segs []string) string {
var b strings.Builder
for _, s := range segs {
if b.Len() > 0 && !isSelector(s) {
b.WriteByte('.')
}
b.WriteString(s)
}
return b.String()
}
// family is the table a chain of parents ends at.
func (t *table) family() *table {
for t.parentT != nil {
t = t.parentT
}
return t
}
// selected is the selector in r on t, or on the nearest ancestor of t it selects.
func (r *tableRead) selected(t *table) (tableSel, bool) {
for ; t != nil; t = t.parentT {
for _, s := range r.sels {
if s.t == t {
return s, true
}
}
}
return tableSel{}, false
}
// checkFamilies refuses reads of one table family in one draw group that cannot
// read one consistent draw: a table rendered whole beside a path into the family,
// a table one read draws that another pins, and two reads pinning different rows.
// The reads come sorted by group and path, so which pair is reported does not vary.
func checkFamilies(reads []pathRead) error {
for i, r := range reads {
if r.tr == nil {
continue
}
for _, o := range reads[:i] {
if o.tr == nil || o.at.group != r.at.group || alternatives(o.at, r.at) || o.tr.head.family() != r.tr.head.family() {
continue
}
if err := checkFamilyPair(o, r); err != nil {
return err
}
}
}
return replayAlternatives(reads)
}
// replayAlternatives replays the reads of each draw group into one draws per cell
// alternative — the reads outside any cell, then the cell's own — so two cells of
// one column are never replayed together.
func replayAlternatives(reads []pathRead) error {
type alt struct {
group string
cell node
}
var alts []alt
seen := map[alt]bool{}
for _, r := range reads {
if a := (alt{r.at.group, r.at.cell}); r.tr != nil && !seen[a] {
seen[a] = true
alts = append(alts, a)
}
}
for _, a := range alts {
var d draws
for _, r := range reads {
if r.tr == nil || r.at.group != a.group || r.at.cell != nil && r.at.cell != a.cell {
continue
}
if err := r.tr.replay(&d); err != nil {
return fmt.Errorf("%s: %w; select the same rows in every path into the family, or draw them apart with a drawGroup", r.at.route.spelled(r.a.name), err)
}
}
}
return nil
}
// replay pins the read's rows into d, where they agree with the rows pinned before.
func (r *tableRead) replay(d *draws) error {
var err error
r.pins.each(func(t *table, row int) {
if err == nil {
err = d.pinRow(t, row)
}
})
return err
}
// checkFamilyPair refuses a table read whole beside a path into its family, and a
// table one read draws that the other pins, since which token renders first would
// then decide the row.
func checkFamilyPair(a, b pathRead) error {
for _, pair := range [][2]pathRead{{a, b}, {b, a}} {
x, y := pair[0], pair[1]
if len(x.a.tail) == 0 && len(y.a.tail) > 0 {
return overlapError(x.at.route, x.a.name, y)
}
if drawn := x.tr.drawnOf(&y.tr.pins); drawn != nil {
if s, ok := y.tr.selected(x.tr.head); ok && len(x.tr.sels) == 0 {
tail := x.a.tail
if s.t != x.tr.head {
tail = append([]string{x.tr.head.category}, tail...)
}
return fmt.Errorf("%s draws %s, which %s selects a row of; write {%s.%s}, or draw them apart with a drawGroup",
x.at.route.spelled(x.a.name), drawn.category, y.at.route.spelled(y.a.name), s.spelling, joinSegments(tail))
}
return fmt.Errorf("%s draws %s, which %s selects a row of; select that row in both, or draw them apart with a drawGroup",
x.at.route.spelled(x.a.name), drawn.category, y.at.route.spelled(y.a.name))
}
}
return nil
}
// drawnOf is a table the read draws that pins holds a row of, if any.
func (r *tableRead) drawnOf(pins *draws) *table {
var found *table
pins.each(func(t *table, _ int) {
if found == nil && r.drawn[t] {
found = t
}
})
return found
}