Move how the template language is written into internal/grammar

This commit is contained in:
2026-10-05 12:11:36 +02:00
committed by Lillem4n
parent 41292f9d89
commit c2f2e86735
21 changed files with 903 additions and 853 deletions
+11 -166
View File
@@ -5,156 +5,14 @@ import (
"strings"
"github.com/larvit/fejkdata/internal/drawstate"
"github.com/larvit/fejkdata/internal/grammar"
)
// 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, 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
for i := 0; i < len(path); i++ {
switch path[i] {
case '[':
if err := checkOpen(path, i, start, open); err != nil {
return nil, err
}
segs = append(segs, path[start:i])
start, open = i, i
case ']':
if err := checkClose(path, i, open); err != nil {
return nil, err
}
segs = append(segs, path[start:i+1])
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
}
}
}
if open >= 0 {
return nil, fmt.Errorf(`%q opens a selector with "[" and never closes it with "]"`, 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 {
switch {
case open >= 0:
return fmt.Errorf(`%q holds a "[" inside a selector, which no key or name may`, path)
case i == 0:
return fmt.Errorf(`%q starts with "["; a path starts with a name, and a selector follows a table's name`, path)
case i == start:
return fmt.Errorf(`%q: a selector follows its table's name; write %s`, path, path[:i-1]+path[i:])
}
return nil
}
// checkClose refuses a "]" at i that closes no selector, closes an empty one, or is
// followed by anything but a dot or the end.
func checkClose(path string, i, open int) error {
switch {
case open < 0:
return fmt.Errorf(`%q holds a "]" that closes no "["`, path)
case i == open+1:
return fmt.Errorf("%q holds an empty selector; a selector names a row by key or name", path)
case i+1 < len(path) && path[i+1] != '.':
return fmt.Errorf(`%q: a "]" ends its selector, so a dot or the end must follow it`, path)
}
return nil
}
// indexOutside is the first c in s outside a [selector], or -1.
func indexOutside(s string, c byte) int {
depth := 0
for i := 0; i < len(s); i++ {
switch {
case s[i] == '[':
depth++
case s[i] == ']' && depth > 0:
depth--
case s[i] == c && depth == 0:
return i
}
}
return -1
}
// cutOutside cuts s around the first sep outside a [selector].
func cutOutside(s, sep string) (before, after string, found bool) {
depth := 0
for i := 0; i < len(s); i++ {
switch {
case s[i] == '[':
depth++
case s[i] == ']' && depth > 0:
depth--
case depth == 0 && strings.HasPrefix(s[i:], sep):
return s[:i], s[i+len(sep):], true
}
}
return s, "", false
}
func splitOutside(s string, c byte) []string {
var parts []string
for {
i := indexOutside(s, c)
if i < 0 {
return append(parts, s)
}
parts, s = append(parts, s[:i]), s[i+1:]
}
}
func isSelector(seg string) bool { return strings.HasPrefix(seg, "[") }
func hasSelector(segs []string) bool {
for _, s := range segs {
if isSelector(s) {
return true
}
}
return false
}
func selectorOf(seg string) string { return seg[1 : len(seg)-1] }
// nameSegments is the segments of a path that are names, its selectors and ".." left out.
func nameSegments(segs []string) []string {
out := segs[:0:0]
for _, s := range segs {
if !isSelector(s) && s != ".." {
out = append(out, s)
}
}
return out
}
// stepInto is what seg names under n: a folder's entry or a template's field.
func stepInto(n node, seg string) (node, error) {
switch {
case isSelector(seg):
return nil, fmt.Errorf("%s is not a table, so it has no row to select", selectorOf(seg))
case grammar.IsSelector(seg):
return nil, fmt.Errorf("%s is not a table, so it has no row to select", grammar.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; put the ".." right after the row`, c.t.header[c.i])
@@ -215,7 +73,7 @@ func (t *table) route(tail []string, descended bool) (tableRoute, error) {
}
// A selector further down pins this table by ancestry, so the walk draws only
// where none follows; drawing first could pick a row the selector is not inside.
r := tableRoute{sel: sel, draw: sel == "" && (descended || len(tail) > 0) && !hasSelector(tail)}
r := tableRoute{sel: sel, draw: sel == "" && (descended || len(tail) > 0) && !grammar.HasSelector(tail)}
switch {
case len(tail) == 0 && sel == "" && !descended:
r.next = t
@@ -239,8 +97,8 @@ func (t *table) stepUp(rest []string) error {
return fmt.Errorf("path has an empty 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.climbTo(rest))
case hasSelector(rest):
return fmt.Errorf(`no selector after "..": a row selected there could lie outside the row stepped up to; select from the table's path from the data root: %s`, joinSegments(append([]string{t.parentT.path}, rest[1:]...)))
case grammar.HasSelector(rest):
return fmt.Errorf(`no selector after "..": a row selected there could lie outside the row stepped up to; select from the table's path from the data root: %s`, grammar.JoinSegments(append([]string{t.parentT.path}, rest[1:]...)))
}
return nil
}
@@ -314,7 +172,7 @@ func (w *pathCheck) walk(n node, tail []string) (node, error) {
n, tail, descended = r.next, r.rest, r.descends
continue
case *template:
if isSelector(tail[0]) {
if grammar.IsSelector(tail[0]) {
break
}
if err := w.enter(x, tail); err != nil {
@@ -490,11 +348,11 @@ func drawVariant(s *drawstate.State, c *choice, memo *drawMemo, levels []string,
}
func (t *table) selector(tail []string) (sel string, rest []string, err error) {
if len(tail) == 0 || !isSelector(tail[0]) {
if len(tail) == 0 || !grammar.IsSelector(tail[0]) {
return "", tail, nil
}
sel, rest = selectorOf(tail[0]), tail[1:]
if len(rest) > 0 && isSelector(rest[0]) {
sel, rest = grammar.SelectorOf(tail[0]), tail[1:]
if len(rest) > 0 && grammar.IsSelector(rest[0]) {
return "", nil, fmt.Errorf("%s[%s] is selected twice; one selector names its row", t.segment, sel)
}
return sel, rest, nil
@@ -542,20 +400,7 @@ func checkPathResolves(n node, tail []string, level string) error {
func compilePath(n node, tail []string) pathCheck {
w := pathCheck{tail: tail}
if _, err := w.run(n); err != nil {
panic(internalError("%s: %v; the path should have been proved before it was compiled", joinSegments(tail), err))
panic(internalError("%s: %v; the path should have been proved before it was compiled", grammar.JoinSegments(tail), err))
}
return w
}
// 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 i, s := range segs {
if b.Len() > 0 && !isSelector(s) && s != ".." && segs[i-1] != ".." {
b.WriteByte('.')
}
b.WriteString(s)
}
return b.String()
}