From 68d2461704cc011c5212234da8672d406d390abb Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Wed, 16 Sep 2026 08:55:55 +0200 Subject: [PATCH] Name the draw option drawGroup, refuse one whose reads all sit behind a nested draw group, and record why the two fences stay apart --- README.md | 49 ++++++++++++++---------- draw.go | 106 ++++++++++++++++++++++++++++++++------------------- graph.go | 2 +- inline.go | 6 +-- node.go | 20 +++++----- path.go | 4 +- reference.go | 2 +- 7 files changed, 111 insertions(+), 78 deletions(-) diff --git a/README.md b/README.md index 3147a1c..5d7c637 100644 --- a/README.md +++ b/README.md @@ -127,8 +127,8 @@ renders nothing by `Fake`, and is compiled only so the tree's fences still run. The columns are the point, and their facts stay together: a record is one render, so columns that read a path into one category — `{/currency.code}` and `{/currency.symbol}` — read one draw of it ([References](#references)), and a -[group](#group) draws a column apart. That one draw is also why the columns of one -record may not overlap — `{/cat.a}`, or a bare `{/cat}` rendering it, beside +[draw group](#draw-group) draws a column apart. That one draw is also why the columns of one +record may not overlap — `{/cat.a}`, or a bare `{/cat}`, beside `{/cat.a.b}` is refused, naming the fields to write instead, as [One draw, one spelling](#one-draw-one-spelling) refuses that pair inside a single format. A column may not reference the record it belongs to by any spelling: `{/users.first}` @@ -341,7 +341,7 @@ different datatypes. ### Options and fields -`format`, `weight`, `repeat`, `separator`, `datatype` and `group` are the only options; +`format`, `weight`, `repeat`, `separator`, `datatype` and `drawGroup` are the only options; **any other key is a field** (see [Decisions](#decisions)). An object that does nothing a string can't — only a `format` — is rejected naming the string, as is a one-item choice naming its item. @@ -443,7 +443,7 @@ 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 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 -a render of its own, in no group, so it draws anew, and a [group](#group) holds a +a render of its own, in no group, so it draws anew, and a [draw group](#draw-group) holds a draw apart. A bare reference names no field and makes its own picks each time — `{/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 @@ -451,27 +451,28 @@ 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, mutually or through a chain. -### Group +### Draw group -A template may carry `group` to hold its reference draws apart: every reference path -it renders, however deep short of a `repeat`, reads the draw of that group, and the -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. +A template may carry `drawGroup` to hold its reference draws apart: every reference path +it renders, however deep short of a `repeat` or a nested `drawGroup`, reads the draw of +that group, and the templates of one category naming one group in one render read one +draw. A 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 { "format": "{payer} pays {payee}; signed {signature}", - "payer": { "format": "{/sv_SE.person.femalefirst} {/sv_SE.person.last}", "group": "payer" }, + "payer": { "format": "{/sv_SE.person.femalefirst} {/sv_SE.person.last}", "drawGroup": "payer" }, "payee": "{/sv_SE.person.femalefirst} {/sv_SE.person.last}", - "signature": { "format": "{/sv_SE.person.last}", "group": "payer" } } + "signature": { "format": "{/sv_SE.person.last}", "drawGroup": "payer" } } ``` 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, each naming nothing: a -`group` of `""` (the default); one naming the group its template already draws in; 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`. +`drawGroup` of `""` (the default); one naming the draw group its template already draws +in; one on a template that renders no reference path short of a `repeat` or a nested +`drawGroup`, on a `repeat` itself — each iteration renders in no draw group — or on an +inline template's root, which nothing references. So is a path reading into a level that +carries a `drawGroup`. ### Correlated fields @@ -505,9 +506,9 @@ The sub-fields stay addressable — `Fake("address.place.locality")` renders, an A name any token reads as a path (`{p.first}`) or as an operand (`{calc(net * 2)}`, `{uppercase(w)}`) is drawn **once per expansion**, a reference path (`{/cat.p.first}`) -**once per render** in its [group](#group), and every other route to either — a bare +**once per render** in its [draw group](#draw-group), and every other route to either — a bare `{p}`, a second bare `{w}`, `{/cat.net}`, a nested template rendering `{/cat.p.last}` -beside `{p.first}`, a bare `{/cat}` rendering what `{/cat.p.first}` reads, at any +beside `{p.first}`, a bare `{/cat}` beside `{/cat.p.first}`, at any depth — is a load error naming the spelling to use. A name nothing reads that way is drawn each time: `{word} {word}` differs. An expansion is one render of one format, so each nested template draws its own names again; a render is one `Fake` or one record, @@ -552,7 +553,7 @@ tokens add cost in proportion to the output. ## Decisions - **Options and fields share one namespace.** `format`, `weight`, `repeat`, - `separator`, `datatype` and `group` are reserved; every other key is a field. Nesting fields under a + `separator`, `datatype` and `drawGroup` are reserved; every other key is a field. Nesting fields under a key, or prefixing options, would tax every template to guard against a misspelt option. - **`{a|b}` stays beside nested choices.** `[[…], […]]` picks the same way, but @@ -673,16 +674,22 @@ tokens add cost in proportion to the output. value's facts agree across its fields, nested templates and columns alike — `{/currency.code}` in one field and `{/currency.symbol}` in another name one currency, whichever view renders them. A `repeat` iteration is a render of its - own, since repeating asks for another entity, and a [group](#group) names further + own, since repeating asks for another entity, and a [draw group](#draw-group) names further entities within one render, so a payer and a payee are two groups over one `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 `first` in the column next to it. -- **A group name is local to its category.** A category's groups are its own +- **A draw 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. +- **The expansion hold and the render's draws are two fences.** One proves a sibling + path or an operand is reached only by its readers within an expansion, the other + does the same for reference paths across a render and its draw groups. They pin + different things — an operand pins the value its own render produced and stops at a + reference, a path pins every level it passes through — so one walk would carry both + rules and both scopes anyway, and tell them apart at every step. - **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 choice, errors. A tail may pass through a choice whose variants carry different diff --git a/draw.go b/draw.go index 39b2f73..272d91b 100644 --- a/draw.go +++ b/draw.go @@ -6,13 +6,13 @@ import ( "strings" ) -// drawSet is one render's reference draws: the unnamed group's, and each named group's. +// drawSet is one render's reference draws: the unnamed draw group's, and each named one's. type drawSet struct { unnamed draws named map[string]*draws } -// drawScope is where a render reads its reference paths: a draw set, in the group of the +// drawScope is where a render reads its reference paths: a draw set, in the draw group of the // template rendering. type drawScope struct { set *drawSet @@ -31,15 +31,15 @@ func renderOnce(s *session, n node) string { return render(s, n, drawScope{set: &set}) } -// in is the scope t renders in: its group where it names one, else its caller's. +// in is the scope t renders in: its draw group where it names one, else its caller's. func (sc drawScope) in(t *template) drawScope { - if t.groupKey != "" { - sc.group = t.groupKey + if t.drawGroupKey != "" { + sc.group = t.drawGroupKey } return sc } -// draws is the set's draws for sc's group. +// draws is the set's draws for sc's draw group. func (sc drawScope) draws() *draws { if sc.group == "" { return &sc.set.unnamed @@ -55,36 +55,36 @@ func (sc drawScope) draws() *draws { return d } -// groupOf reads a template's "group" (default ""), which a repeat cannot carry: each iteration -// renders in no group. -func groupOf(m map[string]any, repeat int) (string, error) { - v, ok := m["group"] +// drawGroupOf reads a template's "drawGroup" (default ""), which a repeat cannot carry: each +// iteration renders in no draw group. +func drawGroupOf(m map[string]any, repeat int) (string, error) { + v, ok := m["drawGroup"] if !ok { return "", nil } name, ok := v.(string) switch { case !ok: - return "", fmt.Errorf("group must be a string, got %T", v) + return "", fmt.Errorf("drawGroup must be a string, got %T", v) case name == "": - return "", fmt.Errorf(`group "" is the default, so it has no effect; drop it`) + return "", fmt.Errorf(`drawGroup "" 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 "", fmt.Errorf("drawGroup %q on a repeat names nothing, since each iteration is a render of its own; drop it", name) } return name, nil } -// keyGroup keys t's group by the category t sits in, "" for an inline template, so a group name is -// local to its category. -func (t *template) keyGroup(category string) { - if t.group != "" { - t.groupKey = category + "/" + t.group +// keyDrawGroup keys t's draw group by the category t sits in, "" for an inline template, so a name +// is local to its category. +func (t *template) keyDrawGroup(category string) { + if t.drawGroup != "" { + t.drawGroupKey = category + "/" + t.drawGroup } } -// 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 { +// checkNestedDrawGroup refuses a template beneath one drawing in group that names group again, +// short of a repeat or another draw group. +func checkNestedDrawGroup(fields map[string]node, group string) error { if group == "" { return nil } @@ -92,9 +92,9 @@ func checkNestedGroup(fields map[string]node, group string) 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): + case isTemplate && t.drawGroup == group: + return fmt.Errorf("%q names drawGroup %q, the draw group this template draws in already; drop it", path, group) + case isTemplate && (t.drawGroup != "" || t.repeat > 1): return nil } for _, c := range contained(n) { @@ -115,14 +115,15 @@ func checkNestedGroup(fields map[string]node, group string) error { // drawCheck fences each template of a scope as a render of its own, remembering which nodes read a // reference path. type drawCheck struct { - reads map[node]bool + reads map[node]bool + splits map[node]bool } -// 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) +// checkDrawGroup refuses a draw group that splits nothing: one whose render reads every reference +// path inside a repeat or a nested draw group, which draw apart from it whatever it names. +func (c *drawCheck) checkDrawGroup(path string, n node) error { + if t, ok := n.(*template); ok && t.drawGroup != "" && !c.splitsDraws(t) { + return fmt.Errorf("%s: drawGroup %q splits nothing, since nothing it renders reads a reference path outside a repeat or a nested drawGroup; drop it", path, t.drawGroup) } return nil } @@ -134,7 +135,7 @@ func (c *drawCheck) checkDraws(path string, n node) error { } w := newDrawWalk(nil) for _, e := range renderEdges(t) { - w.edge(t, e, drawAt{group: t.groupKey, route: drawRoute{e.reached(), e.label}}) + w.edge(t, e, drawAt{group: t.drawGroupKey, route: drawRoute{e.reached(), e.label}}) } if err := w.check(); err != nil { return fmt.Errorf("%s: %w", path, err) @@ -142,8 +143,8 @@ func (c *drawCheck) checkDraws(path string, n node) error { return nil } -// readsPath reports whether rendering n reads a reference path, short of a repeat, which renders over -// draws of its own. +// 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 { if r, done := c.reads[n]; done { return r @@ -162,11 +163,36 @@ func (c *drawCheck) readsPath(n node) bool { return r } +// splitsDraws reports whether rendering n reads a reference path that n's own draw group answers +// for: one outside a repeat and outside a nested draw group, which hold their own draws. +func (c *drawCheck) splitsDraws(n node) bool { + if r, done := c.splits[n]; done { + return r + } + r := false + for _, e := range renderEdges(n) { + if a, _, isRef := refRead(n, e.label); (isRef && len(a.tail) > 0) || (!repeats(e.to) && !grouped(e.to) && c.splitsDraws(e.to)) { + r = true + break + } + } + if c.splits == nil { + c.splits = map[node]bool{} + } + c.splits[n] = r + return r +} + func repeats(n node) bool { t, isTemplate := n.(*template) return isTemplate && t.repeat > 1 } +func grouped(n node) bool { + t, isTemplate := n.(*template) + return isTemplate && t.drawGroup != "" +} + // 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) { @@ -182,13 +208,13 @@ func refRead(n node, label string) (arm, node, bool) { func checkColumnDraws(t *template, columns []string) error { w := newDrawWalk(t) for _, name := range columns { - w.walk(t.fields[name], drawAt{group: t.groupKey, route: drawRoute{spelling: fmt.Sprintf("column %q", name)}}) + w.walk(t.fields[name], drawAt{group: t.drawGroupKey, route: drawRoute{spelling: fmt.Sprintf("column %q", name)}}) } return w.check() } -// drawWalk gathers what one render reads by reference and what it draws afresh, each by group, for -// check to compare. record is set for a record's columns, which may not read the record back. +// drawWalk gathers what one render reads by reference and what it draws afresh, each by draw group, +// for check to compare. record is set for a record's columns, which may not read the record back. type drawWalk struct { record *template reads []pathRead @@ -198,7 +224,7 @@ type drawWalk struct { err error } -// drawAt is where a walk stands: the group it draws in, how the render's root reached it, the +// drawAt is where a walk stands: the draw 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 @@ -251,8 +277,8 @@ func (w *drawWalk) walk(n node, at drawAt) { if repeats(n) { return } - if t, isTemplate := n.(*template); isTemplate && t.groupKey != "" { - at.group = t.groupKey + if t, isTemplate := n.(*template); isTemplate && t.drawGroupKey != "" { + at.group = t.drawGroupKey } for _, e := range renderEdges(n) { w.edge(n, e, at) @@ -331,7 +357,7 @@ func (w *drawWalk) checkFreshDraws() error { } func overlapError(route drawRoute, ref string, into pathRead) error { - 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)) + 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 drawGroup", 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 diff --git a/graph.go b/graph.go index 9e51a59..8c96e90 100644 --- a/graph.go +++ b/graph.go @@ -197,7 +197,7 @@ func checkRenders(s nodeScope) error { return err } fence := &drawCheck{} - if err := s(fence.checkGroup); err != nil { + if err := s(fence.checkDrawGroup); err != nil { return err } if err := s(fence.checkDraws); err != nil { diff --git a/inline.go b/inline.go index 98207d7..eb8e735 100644 --- a/inline.go +++ b/inline.go @@ -136,8 +136,8 @@ func inputValue(input string) (any, error) { // bindInline links an inline node's references against root and runs check over it, naming its // nodes from label. 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) + if t, isTemplate := n.(*template); isTemplate && t.drawGroup != "" { + return fmt.Errorf("%s: drawGroup %q names nothing, since nothing can reference an inline template; drop it", label, t.drawGroup) } scope := inlineScope(n, label) if err := linkNodeRefs(scope, root); err != nil { @@ -154,7 +154,7 @@ func linkNodeRefs(scope nodeScope, root map[string]node) error { if !ok { return nil } - t.keyGroup("") + t.keyDrawGroup("") for _, name := range refTokens(t.format) { sigil, rest, err := refShape(name) if err != nil { diff --git a/node.go b/node.go index 4d02360..b5c9cb0 100644 --- a/node.go +++ b/node.go @@ -58,12 +58,12 @@ type template struct { bound map[string]string // held is every name drawn once per expansion: the bound levels above, plus the // siblings a {calc()} reads. nil when the format holds nothing (see expand). - held map[string]bool - 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 - record bool // compiled at the top without a repeat, so its fields are record columns - 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 + held map[string]bool + 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 + record bool // compiled at the top without a repeat, so its fields are record columns + drawGroup string // the draw group it draws in, as written; "" keeps its caller's + drawGroupKey string // its draw group keyed by its category once linked: what a render reads its reference paths under } func (*template) isNode() {} @@ -249,10 +249,10 @@ func compileTemplate(m map[string]any, pos position) (node, error) { if err := checkTokens(o.format, fields); err != nil { return nil, err } - if err := checkNestedGroup(fields, o.group); err != nil { + if err := checkNestedDrawGroup(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} + t := &template{format: o.format, fields: fields, repeat: o.repeat, separator: o.separator, datatype: o.datatype, drawGroup: o.group, record: fieldPos == inColumn} if err := t.compileFormat(); err != nil { return nil, err } @@ -284,7 +284,7 @@ func readOptions(m map[string]any, pos position) (templateOptions, error) { if o.datatype, err = datatypeOf(m, pos); err != nil { return o, err } - if o.group, err = groupOf(m, repeat); err != nil { + if o.group, err = drawGroupOf(m, repeat); err != nil { return o, err } if sv, ok := m["separator"]; ok { @@ -419,7 +419,7 @@ func checkPathNames(path string) error { // field. These names can never be fields. func isOption(name string) bool { switch name { - case "datatype", "format", "group", "repeat", "separator", "weight": + case "datatype", "drawGroup", "format", "repeat", "separator", "weight": return true } return false diff --git a/path.go b/path.go index 247560a..a48b80e 100644 --- a/path.go +++ b/path.go @@ -105,8 +105,8 @@ func checkPath(n node, tail []string, level string) error { switch { case t.repeat > 1: return fmt.Errorf("the level %q carries a repeat, which a path reading one draw of it cannot apply", name) - case t.group != "": - return fmt.Errorf("the level %q carries a group, which a path reading into it cannot apply", name) + case t.drawGroup != "": + return fmt.Errorf("the level %q carries a drawGroup, which a path reading into it cannot apply", name) } return nil }, diff --git a/reference.go b/reference.go index 319e0f5..31ce49f 100644 --- a/reference.go +++ b/reference.go @@ -72,7 +72,7 @@ func refSegments(name string, folder []string) ([]string, error) { // error, never a random render-time one. func linkRefs(root map[string]node) error { return eachTemplate(root, func(folder []string, path string, t *template) error { - t.keyGroup(strings.Join(strings.Split(path, ".")[:len(folder)+1], ".")) + t.keyDrawGroup(strings.Join(strings.Split(path, ".")[:len(folder)+1], ".")) return linkTemplateRefs(folder, path, t, root) }) }