diff --git a/format.go b/format.go index c1459c1..4013b37 100644 --- a/format.go +++ b/format.go @@ -435,7 +435,7 @@ func pathArm(name, head, writtenHead string, segs []string) arm { return arm{spelling: name, head: head, writtenHead: writtenHead, tail: segs, levels: levels, path: head + "." + strings.Join(segs, ".")} } -// checkSegments rejects an unfinished path: "{a.}" and "{a..b}" each have a +// checkSegments rejects an unfinished path: "{a.}" and "{a...b}" each have a // segment naming nothing. A field really named "" would otherwise make them // resolve, so a typo would read as a path that worked. func checkSegments(a arm) error { diff --git a/path.go b/path.go index bcb8747..399ec84 100644 --- a/path.go +++ b/path.go @@ -6,8 +6,8 @@ import ( ) // splitPath splits a dotted path into segments, a selector — [key or name] after a -// table's name — becoming a segment of its own, brackets kept. A dot inside a -// selector is part of the key or name. +// table's name — becoming a segment of its own, brackets kept, and so does each "..". A +// dot inside a selector is part of the key or name. func splitPath(path string) ([]string, error) { segs := make([]string, 0, strings.Count(path, ".")+2*strings.Count(path, "[")+1) start, open := 0, -1 @@ -24,11 +24,13 @@ func splitPath(path string) ([]string, error) { return nil, err } segs = append(segs, path[start:i+1]) - start, open = i+2, -1 - i++ + open = -1 + segs, i = stepUpAt(segs, path, i+1) + start = i + 1 case '.': if open < 0 { segs = append(segs, path[start:i]) + segs, i = stepUpAt(segs, path, i) start = i + 1 } } @@ -36,12 +38,21 @@ func splitPath(path string) ([]string, error) { if open >= 0 { return nil, fmt.Errorf(`%q opens a selector with "[" and never closes it with "]"`, path) } - if start <= len(path) { + if start < len(path) || start == len(path) && (len(segs) == 0 || segs[len(segs)-1] != "..") { segs = append(segs, path[start:]) } return segs, nil } +// stepUpAt appends a ".." segment where the dot at i is the first of two, returning the +// index of the last dot it consumed. +func stepUpAt(segs []string, path string, i int) ([]string, int) { + if i+1 < len(path) && path[i+1] == '.' { + return append(segs, ".."), i + 1 + } + return segs, i +} + // checkOpen refuses a "[" at i that opens no selector: one inside a selector, or one // following no name. func checkOpen(path string, i, start, open int) error { @@ -139,8 +150,14 @@ func nameSegments(segs []string) []string { // stepInto is what seg names under n: a folder's entry or a template's field. func stepInto(n node, seg string) (node, error) { - if isSelector(seg) { + switch { + case isSelector(seg): return nil, fmt.Errorf("%s is not a table, so it has no row to select", selectorOf(seg)) + case seg == "..": + if c, ok := n.(*tableColumn); ok { + return nil, fmt.Errorf(`".." steps up from a table's row, and %q is a column`, c.t.header[c.i]) + } + return nil, fmt.Errorf(`".." steps up from a table's row to the row it links to, and what it follows is not a table`) } switch n := n.(type) { case *folder: @@ -169,6 +186,7 @@ type tableRoute struct { next node rest []string descends bool + up bool } // route is how tail passes t. descended means the previous route stepped into t @@ -180,6 +198,12 @@ func (t *table) route(tail []string, descended bool) (tableRoute, error) { if err != nil { return tableRoute{}, err } + if len(tail) > 0 && tail[0] == ".." { + if err := t.stepUp(tail[1:]); err != nil { + return tableRoute{}, err + } + return tableRoute{sel: sel, draw: sel == "", next: t.parentT, rest: tail[2:], descends: true, up: true}, nil + } column, child, err := t.step(tail) if err != nil { return tableRoute{}, err @@ -200,6 +224,20 @@ func (t *table) route(tail []string, descended bool) (tableRoute, error) { return r, nil } +// stepUp proves a ".." after a row of t can step up: t has a parent, rest names it, and no +// selector follows, since a row selected below the parent row could lie outside it. +func (t *table) stepUp(rest []string) error { + switch { + case t.parentT == nil: + return fmt.Errorf(`%s has no parent table for ".." to step up to`, t.segment) + case len(rest) == 0 || rest[0] != t.parentT.segment: + return fmt.Errorf(`".." steps up from %s to its parent table, so name that next: ..%s`, t.segment, t.parentT.segment) + case hasSelector(rest): + return fmt.Errorf(`a path selects its rows before a "..", since a row selected after it could lie outside the row it steps up to; start a path at the table to select from it`) + } + return nil +} + // pathStep is one step of a compiled path, taken at the node the steps before it // reached, a choice there resolved first; at indexes the tail where it is taken, and // only a table's step sits past the tail's end. A step holds no node, so a @@ -220,6 +258,7 @@ const ( stepRow // land on the table's row node stepColumn stepChild + stepParent // up to the table's parent, at the row its own row links to ) // pathCheck proves a path resolves whichever way the draws go: every variant of a @@ -297,7 +336,10 @@ func routeSteps(steps []pathStep, pins *pinSet, t *table, r tableRoute, at int) } switch next := r.next.(type) { case *table: - if next != t { + switch { + case r.up: + steps = append(steps, pathStep{kind: stepParent, at: at}) + case next != t: steps = append(steps, pathStep{kind: stepChild, at: at, name: next.segment}) } case *tableRow: @@ -380,8 +422,10 @@ func probePath(n node, tail []string, steps []pathStep) ([]pathStep, error) { // drawSteps draws the rows and variants a path's steps read from n, pinning the rows in pins; // pins is nil for a sibling path, which never crosses a table, since a table is only a category. // For a memoized read, memo keeps the variant drawn at each of levels, so paths sharing a prefix -// share it. -func drawSteps(s *generatorState, n node, steps []pathStep, pins *pinSet, memo *drawMemo, levels []string) node { +// share it. It returns the leaf and the pins its row is in: a step down after a step up draws +// afresh, into pins of its own. +func drawSteps(s *generatorState, n node, steps []pathStep, pins *pinSet, memo *drawMemo, levels []string) (node, *pinSet) { + var climbed *table for _, st := range steps { if c, ok := n.(*choice); ok { n = drawVariant(s, c, memo, levels, st.at) @@ -397,9 +441,15 @@ func drawSteps(s *generatorState, n node, steps []pathStep, pins *pinSet, memo * if !ok { panic(internalError("step %d: a table's step of kind %d on %T", st.at, st.kind, n)) } + switch { + case st.kind == stepParent: + climbed = t.parentT + case st.kind == stepChild && climbed != nil: + pins, climbed = pins.above(climbed), nil + } n = t.drawStep(s, st, pins) } - return n + return n, pins } func (t *table) drawStep(s *generatorState, st pathStep, pins *pinSet) node { @@ -418,6 +468,8 @@ func (t *table) drawStep(s *generatorState, st pathStep, pins *pinSet) node { return t.formatTemplate.fields[st.name] case stepChild: return t.descendant(st.name) + case stepParent: + return t.parentT } panic(internalError("drawStep has no case for step kind %d", st.kind)) } @@ -487,11 +539,12 @@ func compilePath(n node, tail []string) pathCheck { return w } -// joinSegments spells segments as a path: a selector attaches to the name before it. +// joinSegments spells segments as a path: a selector attaches to the name before it, and +// so do a ".." and the name after it. func joinSegments(segs []string) string { var b strings.Builder - for _, s := range segs { - if b.Len() > 0 && !isSelector(s) { + for i, s := range segs { + if b.Len() > 0 && !isSelector(s) && s != ".." && segs[i-1] != ".." { b.WriteByte('.') } b.WriteString(s) diff --git a/pick.go b/pick.go index 95ff8e7..7fbe12e 100644 --- a/pick.go +++ b/pick.go @@ -130,9 +130,9 @@ func readName(s *generatorState, sc renderScope, a arm) readValue { if r, done := p.memo.value[a.path]; done { return r } - leaf := p.draw(s, a.named.head, a.steps, a.levels, a.path) + leaf, pins := p.draw(s, a.named.head, a.steps, a.levels, a.path) sc, mark := sc.enter(leaf, &p.memo) - r := p.renderAt(s, leaf, a.path, sc) + r := p.renderAt(s, leaf, pins, a.path, sc) sc.draws.popFrames(mark) return r } @@ -162,7 +162,8 @@ func readUnder(s *generatorState, t *template, sc renderScope, a arm) readValue for i, l := range a.levels { levels[i] = underKey(sc.pickKey, l) } - return p.renderAt(s, p.draw(s, t.head(a.head), a.steps, levels, key), key, sc) + leaf, pins := p.draw(s, t.head(a.head), a.steps, levels, key) + return p.renderAt(s, leaf, pins, key, sc) } // underKey is the key of path under the pick key prefix. It never returns prefix itself, which a @@ -174,18 +175,19 @@ func underKey(prefix, path string) string { return prefix + "." + path } -// draw draws the path key names under p, its variant at key kept too. -func (p *namedPick) draw(s *generatorState, head node, steps []pathStep, levels []string, key string) node { - leaf := drawSteps(s, head, steps, &p.pins, &p.memo, levels) +// draw draws the path key names under p, its variant at key kept too, returning the leaf and the +// pins its row is in. +func (p *namedPick) draw(s *generatorState, head node, steps []pathStep, levels []string, key string) (node, *pinSet) { + leaf, pins := drawSteps(s, head, steps, &p.pins, &p.memo, levels) if c, isChoice := leaf.(*choice); isChoice { leaf = p.memo.variantOf(s, c, key) } - return leaf + return leaf, pins } -// renderAt renders leaf, drawn at key, as part of p, and keeps what it rendered. -func (p *namedPick) renderAt(s *generatorState, leaf node, key string, sc renderScope) readValue { - sc = sc.at(leaf, &p.pins) +// renderAt renders leaf, drawn at key into pins, as part of p, and keeps what it rendered. +func (p *namedPick) renderAt(s *generatorState, leaf node, pins *pinSet, key string, sc renderScope) readValue { + sc = sc.at(leaf, pins) sc.pick, sc.pickKey = p, key r := renderLeaf(s, leaf, sc) if p.memo.value == nil { diff --git a/pins.go b/pins.go index c866373..c7f4fd3 100644 --- a/pins.go +++ b/pins.go @@ -154,6 +154,17 @@ func (p *pinSet) inside(t *table, rows []int) []int { return rows } +// above is a pin set holding the rows p pins of t and its ancestors, and none below them. +func (p *pinSet) above(t *table) *pinSet { + q := new(pinSet) + for ; t != nil; t = t.parentT { + if r, ok := p.pinned(t); ok { + q.add(t, r) + } + } + return q +} + // clone is a copy of p that pins apart from it; a plain copy shares the spill map. func (p pinSet) clone() pinSet { p.spill = maps.Clone(p.spill) diff --git a/reads.go b/reads.go index e8bf049..0c050be 100644 --- a/reads.go +++ b/reads.go @@ -144,7 +144,7 @@ func pinBelow(reads []pathRead, tr *tableRead) []pathRead { kept := reads[:0:0] for _, r := range reads { pins := r.branches.pins.clone() - if err := tr.replay(&pins); err != nil { + if err := tr.replayBelow(&pins); err != nil { continue } if r.tr != nil && r.clash == nil { @@ -230,6 +230,7 @@ func (r readRoute) spelled(ref string) string { type tableRead struct { headTable *table pins pinSet + leafPins *pinSet // the rows the leaf renders in: pins, short of the rows below a step up drawn map[*table]bool sels []tableSel landsRow bool @@ -250,9 +251,17 @@ func tableReadOf(head node, a arm, leaf node) *tableRead { return nil } r := &tableRead{headTable: t} + r.leafPins = &r.pins _, r.landsRow = leaf.(*tableRow) + climbed := false for _, st := range a.steps { + if climbed && st.kind != stepChild && st.kind != stepParent { + continue + } switch st.kind { + case stepParent: + t, climbed = t.parentT, true + r.leafPins = r.leafPins.above(t) case stepSelect: r.pins.pin(t, st.row) r.sels = append(r.sels, tableSel{t, joinSegments(append([]string{a.writtenHead}, a.tail[:st.at+1]...))}) @@ -294,9 +303,14 @@ func (r *tableRead) drawnOf(pins *pinSet) *table { } // replay pins the read's rows into d, where they agree with the rows pinned before. -func (r *tableRead) replay(d *pinSet) error { +func (r *tableRead) replay(d *pinSet) error { return replayPins(&r.pins, d) } + +// replayBelow pins into d the rows the read's leaf renders in. +func (r *tableRead) replayBelow(d *pinSet) error { return replayPins(r.leafPins, d) } + +func replayPins(from, d *pinSet) error { var err error - r.pins.each(func(t *table, row int) { + from.each(func(t *table, row int) { if err == nil { err = d.pinRow(t, row) } diff --git a/render.go b/render.go index d08dc04..889617c 100644 --- a/render.go +++ b/render.go @@ -48,7 +48,12 @@ func descend(s *generatorState, root node, segments []string, sc renderScope) (n if err != nil { return nil, err } - return drawSteps(s, root, steps, &sc.groupDraws().pins, nil, nil), nil + group := sc.groupDraws() + n, pins := drawSteps(s, root, steps, &group.pins, nil, nil) + // A caller's path is its render's first draw, so the rows a step down after a step up drew + // are all the render pins. + group.pins = *pins + return n, nil } // renderOnce renders n as one render, over draws of its own. @@ -209,10 +214,10 @@ func readMemo(s *generatorState, t *template, memo *drawMemo, pins *pinSet, sc r if r, done := memo.value[a.path]; done { return r } - leaf := drawSteps(s, t.head(a.head), a.steps, pins, memo, a.levels) + leaf, leafPins := drawSteps(s, t.head(a.head), a.steps, pins, memo, a.levels) mark := -1 if pins != nil { - sc, mark = sc.at(leaf, pins).enter(leaf, memo) + sc, mark = sc.at(leaf, leafPins).enter(leaf, memo) } r := renderLeaf(s, leaf, sc) if mark >= 0 {