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
Tests / vet + fmt + tests (pull_request) Successful in 1m6s
This commit is contained in:
@@ -124,11 +124,11 @@ string and a backslash as an escape.
|
|||||||
A record written only to emit columns still needs a `format` — the grammar's one
|
A record written only to emit columns still needs a `format` — the grammar's one
|
||||||
required key — so `"format": ""` carries the fields with an inert format: it
|
required key — so `"format": ""` carries the fields with an inert format: it
|
||||||
renders nothing by `Fake`, and is compiled only so the tree's fences still run.
|
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: two columns that read a
|
The columns are the point, and their facts stay together: a record is one render, so
|
||||||
path into one category — `{/currency.code}` and `{/currency.symbol}` — share one
|
columns that read a path into one category — `{/currency.code}` and
|
||||||
draw of it, so the record is internally consistent. A bare `{/currency}` names no
|
`{/currency.symbol}` — read one draw of it ([References](#references)), and a
|
||||||
field, so it keeps drawing on its own. That one draw is also why two
|
[group](#group) draws a column apart. That one draw is also why the columns of one
|
||||||
columns may not read overlapping reference *paths* — `{/cat.a}` beside
|
record may not overlap — `{/cat.a}`, or a bare `{/cat}` rendering it, beside
|
||||||
`{/cat.a.b}` is refused, naming the fields to write instead, as
|
`{/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
|
[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}`
|
format. A column may not reference the record it belongs to by any spelling: `{/users.first}`
|
||||||
@@ -341,8 +341,8 @@ different datatypes.
|
|||||||
|
|
||||||
### Options and fields
|
### Options and fields
|
||||||
|
|
||||||
`format`, `weight`, `repeat`, `separator` and `datatype` are the only options; **any
|
`format`, `weight`, `repeat`, `separator`, `datatype` and `group` are the only options;
|
||||||
other key is a field** (see [Decisions](#decisions)). An object that does nothing a
|
**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
|
string can't — only a `format` — is rejected naming the string, as is a one-item
|
||||||
choice naming its item.
|
choice naming its item.
|
||||||
|
|
||||||
@@ -438,15 +438,36 @@ without naming `sv_SE`:
|
|||||||
"Hej, {/en_US.person}!"
|
"Hej, {/en_US.person}!"
|
||||||
```
|
```
|
||||||
|
|
||||||
Renders e.g. `Hej, Pat Smith!`. A reference into a category is held like a
|
Renders e.g. `Hej, Pat Smith!`. A reference path into a category is held like a
|
||||||
[correlated](#correlated-fields) path — `{.person.femalefirst} {.person.last}` name
|
[correlated](#correlated-fields) path, but for the whole render — one `Fake`, or one
|
||||||
one person, `{lowercase(.person.femalefirst)}` reads that same draw, and
|
record — rather than one format: `{.person.femalefirst} {.person.last}` name one
|
||||||
`{.person.femalefirst}` beside `{/sv_SE.person.last}` in `sv_SE` is one person too — while a bare
|
person, as do the same two references in sibling fields or a nested template, and
|
||||||
`{/misc.uuid} {/misc.uuid}` is two draws. Rejected at `New`: a path that is
|
`{lowercase(.person.femalefirst)}` reads that same draw. Each `repeat` iteration is
|
||||||
|
a render of its own, so it draws anew, and a [group](#group) holds a draw apart. A
|
||||||
|
bare reference names no field and draws each time: `{/misc.uuid} {/misc.uuid}` is
|
||||||
|
two draws. Rejected at `New`: a path that is
|
||||||
unknown, names a folder, has no folder above, or reads a field not every variant
|
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,
|
of a choice carries, and a reference that leads back to its own value, directly,
|
||||||
mutually or through a chain.
|
mutually or through a chain.
|
||||||
|
|
||||||
|
### Group
|
||||||
|
|
||||||
|
A template may carry `group` to hold its reference draws apart: every reference path
|
||||||
|
it renders, however deep, reads the draw of that group, and the templates naming one
|
||||||
|
group in a render read one draw. A nested `group` names another.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "format": "{payer} pays {payee}; signed {signature}",
|
||||||
|
"payer": { "format": "{/sv_SE.person.femalefirst} {/sv_SE.person.last}", "group": "payer" },
|
||||||
|
"payee": "{/sv_SE.person.femalefirst} {/sv_SE.person.last}",
|
||||||
|
"signature": { "format": "{/sv_SE.person.last}", "group": "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: a `group` of `""` (the
|
||||||
|
default), one on a template that renders no reference path, and a path reading into a
|
||||||
|
level that carries one.
|
||||||
|
|
||||||
### Correlated fields
|
### Correlated fields
|
||||||
|
|
||||||
`{name.field}` reads a path into a sibling, which holds the sibling to one draw
|
`{name.field}` reads a path into a sibling, which holds the sibling to one draw
|
||||||
@@ -478,12 +499,14 @@ The sub-fields stay addressable — `Fake("address.place.locality")` renders, an
|
|||||||
### One draw, one spelling
|
### One draw, one spelling
|
||||||
|
|
||||||
A name any token reads as a path (`{p.first}`) or as an operand (`{calc(net * 2)}`,
|
A name any token reads as a path (`{p.first}`) or as an operand (`{calc(net * 2)}`,
|
||||||
`{uppercase(w)}`) is drawn **once per expansion**, and every other route to it —
|
`{uppercase(w)}`) is drawn **once per expansion**, a reference path (`{/cat.p.first}`)
|
||||||
a bare `{p}`, a second bare `{w}`, `{/cat.net}`, a nested template rendering
|
**once per render** in its [group](#group), and every other route to either — a bare
|
||||||
`{/cat.p.last}`, at any depth — is a load error naming the spelling to use. A
|
`{p}`, a second bare `{w}`, `{/cat.net}`, a nested template rendering `{/cat.p.last}`
|
||||||
name nothing reads that way is drawn each time: `{word} {word}` differs. An
|
beside `{p.first}`, a bare `{/cat}` rendering what `{/cat.p.first}` reads, at any
|
||||||
expansion is one render of one format, so each `repeat` iteration and each nested
|
depth — is a load error naming the spelling to use. A name nothing reads that way is
|
||||||
template draws again.
|
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,
|
||||||
|
and each `repeat` iteration is an expansion and a render of its own.
|
||||||
|
|
||||||
```text
|
```text
|
||||||
token {p} renders a level that {p.first} reads a path into; name the fields you want instead
|
token {p} renders a level that {p.first} reads a path into; name the fields you want instead
|
||||||
@@ -524,7 +547,7 @@ tokens add cost in proportion to the output.
|
|||||||
## Decisions
|
## Decisions
|
||||||
|
|
||||||
- **Options and fields share one namespace.** `format`, `weight`, `repeat`,
|
- **Options and fields share one namespace.** `format`, `weight`, `repeat`,
|
||||||
`separator` and `datatype` are reserved; every other key is a field. Nesting fields under a
|
`separator`, `datatype` and `group` are reserved; every other key is a field. Nesting fields under a
|
||||||
key, or prefixing options, would tax every template to guard against a
|
key, or prefixing options, would tax every template to guard against a
|
||||||
misspelt option.
|
misspelt option.
|
||||||
- **`{a|b}` stays beside nested choices.** `[[…], […]]` picks the same way, but
|
- **`{a|b}` stays beside nested choices.** `[[…], […]]` picks the same way, but
|
||||||
@@ -571,10 +594,10 @@ tokens add cost in proportion to the output.
|
|||||||
to have would make `--seed 42` machine-dependent. Data still lives in `data/`
|
to have would make `--seed 42` machine-dependent. Data still lives in `data/`
|
||||||
as JSON; `--data-path` layers over it.
|
as JSON; `--data-path` layers over it.
|
||||||
- **A bare reference draws each time; a reference path is held.** `{/p} {/p}`
|
- **A bare reference draws each time; a reference path is held.** `{/p} {/p}`
|
||||||
is two draws, as `{word} {word}` is, while `{/p.first}` beside a nested template
|
is two draws, as `{word} {word}` is, while every `{/p.first}` in one render reads
|
||||||
rendering `{/p.first}` is a load error: a bare token is by contract an
|
one draw, and a bare `{/p}` rendering it beside them is a load error: a bare token
|
||||||
independent draw, a path pins its level, and any route into a pinned level from
|
is by contract an independent draw, a path pins its level, and a fresh draw of a
|
||||||
another expansion could show another row.
|
pinned level could show another row.
|
||||||
- **Reference sigils follow the filesystem.** `/` is the root, `.` this file's
|
- **Reference sigils follow the filesystem.** `/` is the root, `.` this file's
|
||||||
folder, `..` the folder above — what those spellings already mean to anyone who
|
folder, `..` the folder above — what those spellings already mean to anyone who
|
||||||
has typed a path. A locale's files reach each other without naming the locale,
|
has typed a path. A locale's files reach each other without naming the locale,
|
||||||
@@ -640,24 +663,16 @@ tokens add cost in proportion to the output.
|
|||||||
see a caller's types, so the first `FakeStruct` for a type compiles its tags and the
|
see a caller's types, so the first `FakeStruct` for a type compiles its tags and the
|
||||||
answer, error included, is kept per type: a test's first call is its load, and no
|
answer, error included, is kept per type: a test's first call is its load, and no
|
||||||
`NewStruct` handle is needed, as the cache already compiles once.
|
`NewStruct` handle is needed, as the cache already compiles once.
|
||||||
- **A record shares one reference draw per category.** Two columns that reference
|
- **A render shares one reference draw per category, per group.** Every reference
|
||||||
one category — `{/currency.code}` beside `{/currency.symbol}` — read one draw of
|
path into a category in one `Fake`, or one record, reads one draw of it, so a
|
||||||
it, so a record's facts agree the way a template's [correlated
|
value's facts agree across its fields, nested templates and columns alike —
|
||||||
fields](#correlated-fields) do. The draw is one per record, so it spans a
|
`{/currency.code}` in one field and `{/currency.symbol}` in another name one
|
||||||
column's `repeat` and nested templates too (one record is one coherent unit);
|
currency, whichever view renders them. A `repeat` iteration is a render of its
|
||||||
a bare reference — `{/currency}`, no field — stays an independent draw every
|
own, since repeating asks for another entity, and a [group](#group) names further
|
||||||
time, the rule a format string already follows. Only references share: a sibling
|
entities within one render, so a payer and a payee are two groups over one
|
||||||
field is local to its own column, so a `first` column does not silently bind to
|
`person` rather than two copies of it. Only references share: a sibling field is
|
||||||
a `first` in the column next to it.
|
local to its own expansion, so a `first` column does not silently bind to a
|
||||||
|
`first` in the column next to it.
|
||||||
The string view of that same template does not share. `Fake` renders each
|
|
||||||
sibling field as its own expansion, so a `{/currency.code}` field beside a
|
|
||||||
`{/currency.symbol}` field is two draws and may render `EUR $`; writing both
|
|
||||||
references in one `format` holds them together, as
|
|
||||||
[One draw, one spelling](#one-draw-one-spelling) says. The scope is what makes a
|
|
||||||
row coherent when the columns *are* the output, and there the caller cannot fall
|
|
||||||
back on one format string. Widening it to every render would change what `Fake`
|
|
||||||
has emitted since the start, for a correlation a single format already reaches.
|
|
||||||
- **A record's column set is fixed before the first draw.** Only a category-level
|
- **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
|
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
|
choice, errors. A tail may pass through a choice whose variants carry different
|
||||||
@@ -737,6 +752,7 @@ struct.go structs: FakeStruct, fake tags, and a field's Go type as its col
|
|||||||
inline.go inline templates: Template, NewTemplate, FakeTemplate, IsTemplate, and their compile and link
|
inline.go inline templates: Template, NewTemplate, FakeTemplate, IsTemplate, and their compile and link
|
||||||
template.go the {token} grammar: scanning, tokens, operands, validation, compiling a format
|
template.go the {token} grammar: scanning, tokens, operands, validation, compiling a format
|
||||||
hold.go the hold: one draw per expansion for paths and operands, and its fences
|
hold.go the hold: one draw per expansion for paths and operands, and its fences
|
||||||
|
draw.go one reference draw per render and group: draw sets, the group option, and its fence
|
||||||
reference.go reference sigils, and binding references across the tree
|
reference.go reference sigils, and binding references across the tree
|
||||||
graph.go the render graph: edges, cycles, the repeat bound, tree walks
|
graph.go the render graph: edges, cycles, the repeat bound, tree walks
|
||||||
builtins.go the {name()} function registry and its implementations
|
builtins.go the {name()} function registry and its implementations
|
||||||
|
|||||||
@@ -0,0 +1,270 @@
|
|||||||
|
package fejkdata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// drawSet is one render's reference draws: the unnamed group's, and each named group'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
|
||||||
|
// template rendering.
|
||||||
|
type drawScope struct {
|
||||||
|
set *drawSet
|
||||||
|
group string
|
||||||
|
}
|
||||||
|
|
||||||
|
// newDrawSet makes the unnamed group's maps where the set is declared, keeping them on that frame's
|
||||||
|
// stack for a render that reads through them; a zero drawSet makes them on its first read instead.
|
||||||
|
func newDrawSet() drawSet {
|
||||||
|
return drawSet{unnamed: draws{variant: map[string]node{}, value: map[string]draw{}}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// renderOnce renders n as one render, over draws of its own.
|
||||||
|
func renderOnce(s *session, n node) string {
|
||||||
|
var set drawSet
|
||||||
|
return render(s, n, drawScope{set: &set})
|
||||||
|
}
|
||||||
|
|
||||||
|
// in is the scope t renders in: the group it names, else its caller's.
|
||||||
|
func (sc drawScope) in(t *template) drawScope {
|
||||||
|
if t.drawGroup != "" {
|
||||||
|
sc.group = t.drawGroup
|
||||||
|
}
|
||||||
|
return sc
|
||||||
|
}
|
||||||
|
|
||||||
|
// draws is the set's draws for sc's group.
|
||||||
|
func (sc drawScope) draws() *draws {
|
||||||
|
if sc.group == "" {
|
||||||
|
return &sc.set.unnamed
|
||||||
|
}
|
||||||
|
d, drew := sc.set.named[sc.group]
|
||||||
|
if !drew {
|
||||||
|
if sc.set.named == nil {
|
||||||
|
sc.set.named = map[string]*draws{}
|
||||||
|
}
|
||||||
|
d = &draws{variant: map[string]node{}, value: map[string]draw{}}
|
||||||
|
sc.set.named[strings.Clone(sc.group)] = d // a key from sc would leak sc, and with it every render's draw set, to the heap
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
// groupOf reads a template's "group" (default "").
|
||||||
|
func groupOf(m map[string]any) (string, error) {
|
||||||
|
v, ok := m["group"]
|
||||||
|
if !ok {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
name, ok := v.(string)
|
||||||
|
switch {
|
||||||
|
case !ok:
|
||||||
|
return "", fmt.Errorf("group must be a string, got %T", v)
|
||||||
|
case name == "":
|
||||||
|
return "", fmt.Errorf(`group "" is the default, so it has no effect; drop it`)
|
||||||
|
}
|
||||||
|
return name, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *drawCheck) check(path string, n node) error {
|
||||||
|
t, ok := n.(*template)
|
||||||
|
switch {
|
||||||
|
case !ok:
|
||||||
|
return nil
|
||||||
|
case !c.readsPath(t) && t.drawGroup != "":
|
||||||
|
return fmt.Errorf("%s: group %q splits nothing, since nothing it renders reads a reference path; drop it", path, t.drawGroup)
|
||||||
|
case !c.readsPath(t):
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
w := newDrawWalk(nil)
|
||||||
|
for _, e := range renderEdges(t) {
|
||||||
|
w.edge(t, e, t.drawGroup, drawRoute{e.reached(), e.label}, "", false)
|
||||||
|
}
|
||||||
|
if err := w.check(); err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", path, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// readsPath reports whether rendering n reads a reference path, however deep.
|
||||||
|
func (c *drawCheck) readsPath(n node) bool {
|
||||||
|
if r, done := c.reads[n]; done {
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
r := false
|
||||||
|
for _, e := range renderEdges(n) {
|
||||||
|
if a, _, isRef := refRead(n, e.label); (isRef && len(a.tail) > 0) || c.readsPath(e.to) {
|
||||||
|
r = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if c.reads == nil {
|
||||||
|
c.reads = map[node]bool{}
|
||||||
|
}
|
||||||
|
c.reads[n] = r
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
t, isTemplate := n.(*template)
|
||||||
|
if !isTemplate {
|
||||||
|
return arm{}, nil, false
|
||||||
|
}
|
||||||
|
a := splitArm(label, t.refs)
|
||||||
|
return a, t.fields[a.key], isRef(a.key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkColumnDraws fences a record's columns as one render.
|
||||||
|
func checkColumnDraws(t *template, columns []string) error {
|
||||||
|
w := newDrawWalk(t)
|
||||||
|
for _, name := range columns {
|
||||||
|
w.walk(t.fields[name], t.drawGroup, drawRoute{spelling: fmt.Sprintf("column %q", name)}, "", false)
|
||||||
|
}
|
||||||
|
return w.check()
|
||||||
|
}
|
||||||
|
|
||||||
|
// drawWalk gathers what one render reads through its draws 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.
|
||||||
|
type drawWalk struct {
|
||||||
|
record *template
|
||||||
|
reads []pathRead
|
||||||
|
read map[drawKey]bool
|
||||||
|
fresh []freshDraw
|
||||||
|
seen map[drawVisit]bool
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
// drawRoute is how a render reaches a draw: as its author spells it, and the root edge's label.
|
||||||
|
type drawRoute struct{ spelling, label string }
|
||||||
|
|
||||||
|
type pathRead struct {
|
||||||
|
group string
|
||||||
|
route drawRoute
|
||||||
|
a arm
|
||||||
|
target node
|
||||||
|
}
|
||||||
|
|
||||||
|
type freshDraw struct {
|
||||||
|
group string
|
||||||
|
route drawRoute
|
||||||
|
via string
|
||||||
|
n node
|
||||||
|
}
|
||||||
|
|
||||||
|
type drawVisit struct {
|
||||||
|
n node
|
||||||
|
group string
|
||||||
|
held bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type drawKey struct {
|
||||||
|
group string
|
||||||
|
key any
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDrawWalk(record *template) *drawWalk {
|
||||||
|
return &drawWalk{record: record, read: map[drawKey]bool{}, seen: map[drawVisit]bool{}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// walk follows what rendering n renders. held says n renders inside a reference path's draw, so
|
||||||
|
// what it draws belongs to that draw; a repeat renders over draws of its own, so the walk stops there.
|
||||||
|
func (w *drawWalk) walk(n node, group string, route drawRoute, via string, held bool) {
|
||||||
|
v := drawVisit{n, group, held}
|
||||||
|
if w.seen[v] || w.err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.seen[v] = true
|
||||||
|
if !held {
|
||||||
|
w.fresh = append(w.fresh, freshDraw{group, route, via, n})
|
||||||
|
}
|
||||||
|
t, isTemplate := n.(*template)
|
||||||
|
if isTemplate && t.repeat > 1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if isTemplate && t.drawGroup != "" {
|
||||||
|
group = t.drawGroup
|
||||||
|
}
|
||||||
|
for _, e := range renderEdges(n) {
|
||||||
|
w.edge(n, e, group, route, via, held)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *drawWalk) edge(from node, e renderEdge, group string, route drawRoute, via string, held bool) {
|
||||||
|
a, target, reads := refRead(from, e.label)
|
||||||
|
switch {
|
||||||
|
case !reads:
|
||||||
|
w.walk(e.to, group, route, via, held)
|
||||||
|
case w.record != nil && target == node(w.record):
|
||||||
|
w.err = fmt.Errorf("%s 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", route.spelling, a.name)
|
||||||
|
case len(a.tail) > 0:
|
||||||
|
if k := (drawKey{group, a.path}); !w.read[k] {
|
||||||
|
w.read[k] = true
|
||||||
|
w.reads = append(w.reads, pathRead{group, route, a, target})
|
||||||
|
}
|
||||||
|
w.walk(e.to, group, route, a.name, true)
|
||||||
|
default:
|
||||||
|
w.walk(e.to, group, route, a.name, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check refuses what one draw per reference path cannot answer for: a path read into a level
|
||||||
|
// another read renders, and a node drawn afresh beside a path whose draw holds it.
|
||||||
|
func (w *drawWalk) check() error {
|
||||||
|
if w.err != nil {
|
||||||
|
return w.err
|
||||||
|
}
|
||||||
|
sort.SliceStable(w.reads, func(i, j int) bool {
|
||||||
|
if w.reads[i].group != w.reads[j].group {
|
||||||
|
return w.reads[i].group < w.reads[j].group
|
||||||
|
}
|
||||||
|
return w.reads[i].a.path < w.reads[j].a.path
|
||||||
|
})
|
||||||
|
pins := map[drawKey]pathRead{}
|
||||||
|
for i, level := range w.reads {
|
||||||
|
for _, into := range w.reads[i+1:] {
|
||||||
|
if into.group == level.group && strings.HasPrefix(into.a.path, level.a.path+".") {
|
||||||
|
return overlap(level.route, level.a.name, into)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
held := map[node]bool{}
|
||||||
|
coverPath(level.target, level.a.tail, held)
|
||||||
|
for n := range held {
|
||||||
|
if _, pinned := pins[drawKey{level.group, n}]; !pinned {
|
||||||
|
pins[drawKey{level.group, n}] = level
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, f := range w.fresh {
|
||||||
|
if r, pinned := pins[drawKey{f.group, f.n}]; pinned {
|
||||||
|
return overlap(f.route, f.via, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func overlap(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.route.spelled(into.a.name))
|
||||||
|
}
|
||||||
|
|
||||||
|
// spelled names the route, and the reference it reaches a draw by where its root edge is not
|
||||||
|
// that reference.
|
||||||
|
func (r drawRoute) spelled(ref string) string {
|
||||||
|
if ref == "" || ref == r.label {
|
||||||
|
return r.spelling
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s with {%s}", r.spelling, ref)
|
||||||
|
}
|
||||||
@@ -196,6 +196,9 @@ func checkRenders(s nodeScope) error {
|
|||||||
if err := s(heldCheck); err != nil {
|
if err := s(heldCheck); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := s((&drawCheck{}).check); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return s((&valueProof{}).checkDatatype)
|
return s((&valueProof{}).checkDatatype)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ func heldCheck(path string, n node) error {
|
|||||||
}
|
}
|
||||||
readers := boundReaders(t.format, t.bound, t.refs)
|
readers := boundReaders(t.format, t.bound, t.refs)
|
||||||
for _, head := range heldHeads(t) {
|
for _, head := range heldHeads(t) {
|
||||||
|
if _, isPath := t.bound[head]; isPath && isRef(head) {
|
||||||
|
continue // held for the whole render, which drawCheck fences
|
||||||
|
}
|
||||||
if err := checkHeadHeld(t, head, readers); err != nil {
|
if err := checkHeadHeld(t, head, readers); err != nil {
|
||||||
return fmt.Errorf("%s: %w", path, err)
|
return fmt.Errorf("%s: %w", path, err)
|
||||||
}
|
}
|
||||||
@@ -252,9 +255,10 @@ func checkNoRepeatedRead(format string, c formatOps, refs map[string]refBinding)
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// draws is what an expansion has already drawn for its held names: the variant each
|
// draws is what has already been drawn for held names: the variant each was drawn as,
|
||||||
// was drawn as, so every path under it reads one row, and the draw each read made, by
|
// so every path under it reads one row, and the draw each read made, by its one
|
||||||
// its one spelling, so the same read written twice reads one draw.
|
// spelling, so the same read written twice reads one draw. An expansion keeps one for
|
||||||
|
// its sibling names, and a render one per group for its reference paths.
|
||||||
type draws struct {
|
type draws struct {
|
||||||
variant map[string]node
|
variant map[string]node
|
||||||
value map[string]draw
|
value map[string]draw
|
||||||
@@ -273,14 +277,14 @@ type draw struct {
|
|||||||
// gives one value, and a shown operand is the operand computed. Every other name is
|
// gives one value, and a shown operand is the operand computed. Every other name is
|
||||||
// drawn afresh, so {word} {word} still draws twice. checkTokens, checkPath and
|
// drawn afresh, so {word} {word} still draws twice. checkTokens, checkPath and
|
||||||
// linkRefs prove every step, so the walk cannot fail.
|
// linkRefs prove every step, so the walk cannot fail.
|
||||||
func readField(s *session, t *template, held, refScope *draws, a arm) draw {
|
func readField(s *session, t *template, held *draws, sc drawScope, a arm) draw {
|
||||||
if !t.held[a.key] {
|
if !t.held[a.key] {
|
||||||
if len(a.tail) > 0 {
|
if len(a.tail) > 0 {
|
||||||
panic(fmt.Sprintf("fejkdata: %q reads a path into %q, which the expansion does not hold", a.name, a.key))
|
panic(fmt.Sprintf("fejkdata: %q reads a path into %q, which the expansion does not hold", a.name, a.key))
|
||||||
}
|
}
|
||||||
return draw{text: render(s, t.fields[a.key], refScope)}
|
return draw{text: render(s, t.fields[a.key], sc)}
|
||||||
}
|
}
|
||||||
d := readScope(held, refScope, a)
|
d := readScope(held, sc, a)
|
||||||
if r, done := d.value[a.path]; done {
|
if r, done := d.value[a.path]; done {
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
@@ -296,37 +300,42 @@ func readField(s *session, t *template, held, refScope *draws, a arm) draw {
|
|||||||
n, drew := d.variant[key]
|
n, drew := d.variant[key]
|
||||||
if !drew {
|
if !drew {
|
||||||
n = drawn(s, c)
|
n = drawn(s, c)
|
||||||
|
if d.variant == nil {
|
||||||
|
d.variant = map[string]node{}
|
||||||
|
}
|
||||||
d.variant[key] = n
|
d.variant[key] = n
|
||||||
}
|
}
|
||||||
return []node{n}, nil
|
return []node{n}, nil
|
||||||
},
|
},
|
||||||
leaf: func(n node) error { r = renderLeaf(s, n, refScope); return nil },
|
leaf: func(n node) error { r = renderLeaf(s, n, sc); return nil },
|
||||||
})
|
})
|
||||||
|
if d.value == nil {
|
||||||
|
d.value = map[string]draw{}
|
||||||
|
}
|
||||||
d.value[a.path] = r
|
d.value[a.path] = r
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
// readScope is the draws a held read keeps its draw in: for a reference that reads a path,
|
// readScope is the draws a held read keeps its draw in: for a reference that reads a path,
|
||||||
// refScope, so its draw outlives this expansion; for a sibling, or a reference read whole, held.
|
// the render's draws for its group, so its draw spans the render; for a sibling, or a
|
||||||
func readScope(held, refScope *draws, a arm) *draws {
|
// reference read whole, held.
|
||||||
if isRef(a.key) && refScope != nil && len(a.tail) > 0 {
|
func readScope(held *draws, sc drawScope, a arm) *draws {
|
||||||
return refScope
|
if isRef(a.key) && len(a.tail) > 0 {
|
||||||
|
return sc.draws()
|
||||||
}
|
}
|
||||||
return held
|
return held
|
||||||
}
|
}
|
||||||
|
|
||||||
// renderLeaf draws and renders what a read lands on: null on a null item, or on a column of one
|
// renderLeaf draws and renders what a read lands on: null on a null item, or on a column of one
|
||||||
// reference alone whose read drew null.
|
// reference alone whose read drew null.
|
||||||
func renderLeaf(s *session, n node, scope *draws) draw {
|
func renderLeaf(s *session, n node, sc drawScope) draw {
|
||||||
n = drawn(s, n)
|
n = drawn(s, n)
|
||||||
if _, isNull := n.(*null); isNull {
|
if _, isNull := n.(*null); isNull {
|
||||||
return draw{null: true}
|
return draw{null: true}
|
||||||
}
|
}
|
||||||
r := draw{text: render(s, n, scope)}
|
r := draw{text: render(s, n, sc)}
|
||||||
if t, _ := n.(*template); t != nil && t.readsColumn != nil {
|
if t, _ := n.(*template); t != nil && t.readsColumn != nil {
|
||||||
if d := readScope(nil, scope, t.readsColumn.a); d != nil {
|
r.null = sc.in(t).draws().value[t.readsColumn.a.path].null
|
||||||
r.null = d.value[t.readsColumn.a.path].null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ type Template struct {
|
|||||||
func (t *Template) Fake() string {
|
func (t *Template) Fake() string {
|
||||||
t.g.mu.Lock()
|
t.g.mu.Lock()
|
||||||
defer t.g.mu.Unlock()
|
defer t.g.mu.Unlock()
|
||||||
return render(t.g.rand, t.n, nil)
|
return renderOnce(t.g.rand, t.n)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTemplate compiles an inline template — a format string or a JSON value — and
|
// NewTemplate compiles an inline template — a format string or a JSON value — and
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ type template struct {
|
|||||||
fromString bool // written as a JSON string rather than an object
|
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
|
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
|
record bool // compiled at the top without a repeat, so its fields are record columns
|
||||||
|
drawGroup string // the group its render reads reference paths in; "" keeps its caller's
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*template) isNode() {}
|
func (*template) isNode() {}
|
||||||
@@ -241,13 +242,13 @@ func compileTemplate(m map[string]any, pos position) (node, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if len(fields) == 0 && o.repeat == 1 && !o.weighted && o.datatype == DataTypeString {
|
if len(fields) == 0 && o.repeat == 1 && !o.weighted && o.datatype == DataTypeString && o.group == "" {
|
||||||
return nil, fmt.Errorf("an object holding only a format is a string; write %q", o.format)
|
return nil, fmt.Errorf("an object holding only a format is a string; write %q", o.format)
|
||||||
}
|
}
|
||||||
if err := checkTokens(o.format, fields); err != nil {
|
if err := checkTokens(o.format, fields); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
t := &template{format: o.format, fields: fields, repeat: o.repeat, separator: o.separator, datatype: o.datatype, 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 {
|
if err := t.compileFormat(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -258,6 +259,7 @@ func compileTemplate(m map[string]any, pos position) (node, error) {
|
|||||||
type templateOptions struct {
|
type templateOptions struct {
|
||||||
datatype DataType
|
datatype DataType
|
||||||
format string
|
format string
|
||||||
|
group string
|
||||||
repeat int
|
repeat int
|
||||||
separator string
|
separator string
|
||||||
weighted bool
|
weighted bool
|
||||||
@@ -278,6 +280,9 @@ func readOptions(m map[string]any, pos position) (templateOptions, error) {
|
|||||||
if o.datatype, err = datatypeOf(m, pos); err != nil {
|
if o.datatype, err = datatypeOf(m, pos); err != nil {
|
||||||
return o, err
|
return o, err
|
||||||
}
|
}
|
||||||
|
if o.group, err = groupOf(m); err != nil {
|
||||||
|
return o, err
|
||||||
|
}
|
||||||
if sv, ok := m["separator"]; ok {
|
if sv, ok := m["separator"]; ok {
|
||||||
if o.separator, ok = sv.(string); !ok {
|
if o.separator, ok = sv.(string); !ok {
|
||||||
return o, fmt.Errorf("separator must be a string, got %T", sv)
|
return o, fmt.Errorf("separator must be a string, got %T", sv)
|
||||||
@@ -410,7 +415,7 @@ func checkPathNames(path string) error {
|
|||||||
// field. These names can never be fields.
|
// field. These names can never be fields.
|
||||||
func isOption(name string) bool {
|
func isOption(name string) bool {
|
||||||
switch name {
|
switch name {
|
||||||
case "datatype", "format", "repeat", "separator", "weight":
|
case "datatype", "format", "group", "repeat", "separator", "weight":
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -89,9 +89,9 @@ func unreachableInChoice(c *choice, want string) error {
|
|||||||
|
|
||||||
// checkPath proves a dotted tail resolves whichever way the draws go — a choice
|
// checkPath proves a dotted tail resolves whichever way the draws go — a choice
|
||||||
// must carry the rest of the path in the set every variant shares — and that no
|
// must carry the rest of the path in the set every variant shares — and that no
|
||||||
// level a path reads carries a repeat, which one draw of it could not apply. So a
|
// level a path reads carries a repeat or a group, which one draw of it could not
|
||||||
// path that validates here resolves on every render, and a typo is a New-time
|
// apply. So a path that validates here resolves on every render, and a typo is a
|
||||||
// error.
|
// New-time error.
|
||||||
func checkPath(n node, tail []string, level string) error {
|
func checkPath(n node, tail []string, level string) error {
|
||||||
return walkPath(n, tail, pathWalk{
|
return walkPath(n, tail, pathWalk{
|
||||||
choice: func(c *choice, rest []string) ([]node, error) {
|
choice: func(c *choice, rest []string) ([]node, error) {
|
||||||
@@ -101,8 +101,12 @@ func checkPath(n node, tail []string, level string) error {
|
|||||||
return c.items, nil
|
return c.items, nil
|
||||||
},
|
},
|
||||||
level: func(t *template, rest []string) error {
|
level: func(t *template, rest []string) error {
|
||||||
if t.repeat > 1 {
|
name := join(level, strings.Join(tail[:len(tail)-len(rest)], "."))
|
||||||
return fmt.Errorf("the level %q carries a repeat, which a path reading one draw of it cannot apply", join(level, strings.Join(tail[:len(tail)-len(rest)], ".")))
|
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.drawGroup != "":
|
||||||
|
return fmt.Errorf("the level %q carries a group, which a path reading into it cannot apply", name)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
|
||||||
"strings"
|
"strings"
|
||||||
"unicode"
|
"unicode"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
@@ -23,7 +22,7 @@ type Column struct {
|
|||||||
// Record is one record rendered from a template: every direct field is a column,
|
// 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
|
// 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
|
// local to it, while a reference that reads a path is drawn once for the whole
|
||||||
// record.
|
// record, per group.
|
||||||
type Record struct {
|
type Record struct {
|
||||||
columns []Column
|
columns []Column
|
||||||
}
|
}
|
||||||
@@ -212,7 +211,7 @@ func recordOf(n node) (*template, []Column, error) {
|
|||||||
if !t.record {
|
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)
|
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
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
columns := make([]Column, len(names))
|
columns := make([]Column, len(names))
|
||||||
@@ -223,80 +222,13 @@ func recordOf(n node) (*template, []Column, error) {
|
|||||||
return t, columns, nil
|
return t, columns, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkColumnRefs rejects the reference reads a record's shared draw cannot answer
|
// renderRecord draws each column once, in the name order recordOf fixed, as one render.
|
||||||
// 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.
|
|
||||||
func renderRecord(s *session, t *template, columns []Column) *Record {
|
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...)}
|
r := &Record{columns: append([]Column(nil), columns...)}
|
||||||
for i := range r.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
|
r.columns[i].Value, r.columns[i].Null = column.text, column.null
|
||||||
}
|
}
|
||||||
return r
|
return r
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ func (f *Generator) Fake(path string) (string, error) {
|
|||||||
if _, ok := n.(*group); ok {
|
if _, ok := n.(*group); ok {
|
||||||
return "", fmt.Errorf("fejkdata: %s names a folder, not a value", path)
|
return "", fmt.Errorf("fejkdata: %s names a folder, not a value", path)
|
||||||
}
|
}
|
||||||
return render(f.rand, n, nil), nil
|
return renderOnce(f.rand, n), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// descend walks named fields to the node a path names. It is the one render-side
|
// descend walks named fields to the node a path names. It is the one render-side
|
||||||
@@ -50,20 +50,21 @@ func descend(s *session, root node, segments []string) (node, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// render evaluates a compiled node to a string. compile validates every node up
|
// render evaluates a compiled node to a string. compile validates every node up
|
||||||
// front, so rendering a compiled tree cannot fail. refScope carries the draws a
|
// front, so rendering a compiled tree cannot fail. sc holds the reference draws the
|
||||||
// reference shares beyond its own expansion; nil keeps every reference local.
|
// render shares; each repeat iteration renders over draws of its own.
|
||||||
func render(s *session, n node, refScope *draws) string {
|
func render(s *session, n node, sc drawScope) string {
|
||||||
switch n := n.(type) {
|
switch n := n.(type) {
|
||||||
case *choice:
|
case *choice:
|
||||||
return render(s, pick(s, n), refScope)
|
return render(s, pick(s, n), sc)
|
||||||
case *null:
|
case *null:
|
||||||
return ""
|
return ""
|
||||||
case *template:
|
case *template:
|
||||||
|
sc = sc.in(n)
|
||||||
if n.repeat == 1 {
|
if n.repeat == 1 {
|
||||||
if n.fixed {
|
if n.fixed {
|
||||||
return n.lit
|
return n.lit
|
||||||
}
|
}
|
||||||
return expand(s, n, refScope)
|
return expand(s, n, sc)
|
||||||
}
|
}
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
b.Grow(n.repeat * (n.grow + len(n.separator)))
|
b.Grow(n.repeat * (n.grow + len(n.separator)))
|
||||||
@@ -71,7 +72,7 @@ func render(s *session, n node, refScope *draws) string {
|
|||||||
if i > 0 {
|
if i > 0 {
|
||||||
b.WriteString(n.separator)
|
b.WriteString(n.separator)
|
||||||
}
|
}
|
||||||
b.WriteString(expand(s, n, refScope))
|
b.WriteString(expandAnew(s, n, sc.group))
|
||||||
}
|
}
|
||||||
return b.String()
|
return b.String()
|
||||||
default:
|
default:
|
||||||
@@ -79,6 +80,15 @@ func render(s *session, n node, refScope *draws) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// expandAnew expands one repeat iteration of t as a render of its own, in group. Inlined into
|
||||||
|
// render's loop, its draw set would move to the heap.
|
||||||
|
//
|
||||||
|
//go:noinline
|
||||||
|
func expandAnew(s *session, t *template, group string) string {
|
||||||
|
var set drawSet
|
||||||
|
return expand(s, t, drawScope{set: &set, group: group})
|
||||||
|
}
|
||||||
|
|
||||||
// pick selects one item. Uniform choices are O(1); weighted choices are an
|
// pick selects one item. Uniform choices are O(1); weighted choices are an
|
||||||
// O(log n) search over precomputed cumulative weights. compile guarantees a
|
// O(log n) search over precomputed cumulative weights. compile guarantees a
|
||||||
// non-empty choice and a finite positive total, so the index is always in range.
|
// non-empty choice and a finite positive total, so the index is always in range.
|
||||||
@@ -93,12 +103,12 @@ func pick(r rng, c *choice) node {
|
|||||||
|
|
||||||
// expand renders a template's compiled ops. compile validated every token, so this
|
// expand renders a template's compiled ops. compile validated every token, so this
|
||||||
// cannot fail.
|
// cannot fail.
|
||||||
func expand(s *session, t *template, refScope *draws) string {
|
func expand(s *session, t *template, sc drawScope) string {
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
b.Grow(t.grow)
|
b.Grow(t.grow)
|
||||||
// One draw per held name, for this expansion only: a nested template and each
|
// One draw per held name, for this expansion only: a nested template and each
|
||||||
// repeat iteration get their own, since each is its own expansion. A reference
|
// repeat iteration get their own, since each is its own expansion. A reference
|
||||||
// reads the caller's scope instead, whenever one was supplied.
|
// path reads the render's draws in sc instead.
|
||||||
var held *draws
|
var held *draws
|
||||||
if len(t.held) > 0 {
|
if len(t.held) > 0 {
|
||||||
held = &draws{
|
held = &draws{
|
||||||
@@ -112,7 +122,7 @@ func expand(s *session, t *template, refScope *draws) string {
|
|||||||
case 'l':
|
case 'l':
|
||||||
b.WriteString(o.lit)
|
b.WriteString(o.lit)
|
||||||
case 'f':
|
case 'f':
|
||||||
b.WriteString(readField(s, t, held, refScope, o.arms[s.IntN(len(o.arms))]).text)
|
b.WriteString(readField(s, t, held, sc, o.arms[s.IntN(len(o.arms))]).text)
|
||||||
case 'b':
|
case 'b':
|
||||||
// Read before the call, so the value a calc computes is the value the
|
// Read before the call, so the value a calc computes is the value the
|
||||||
// format showed. calcVars fixed the order op.operands holds.
|
// format showed. calcVars fixed the order op.operands holds.
|
||||||
@@ -120,7 +130,7 @@ func expand(s *session, t *template, refScope *draws) string {
|
|||||||
if len(o.operands) > 0 {
|
if len(o.operands) > 0 {
|
||||||
operands = make([]string, len(o.operands))
|
operands = make([]string, len(o.operands))
|
||||||
for j, a := range o.operands {
|
for j, a := range o.operands {
|
||||||
operands[j] = readField(s, t, held, refScope, a).text
|
operands[j] = readField(s, t, held, sc, a).text
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
b.WriteString(o.call(s, b.String(), operands)) // b.String() is the output so far
|
b.WriteString(o.call(s, b.String(), operands)) // b.String() is the output so far
|
||||||
|
|||||||
@@ -2,22 +2,6 @@
|
|||||||
|
|
||||||
## Before v0.1.0
|
## Before v0.1.0
|
||||||
|
|
||||||
The record API lands first, so the data update can use it.
|
|
||||||
|
|
||||||
### Record API
|
|
||||||
|
|
||||||
- Draw groups — references into one category share one draw per render (one
|
|
||||||
record, or one `Fake`) in both views; each `repeat` iteration draws anew, and a
|
|
||||||
bare reference draws each time. An option naming a draw group splits a render
|
|
||||||
into several entities. Replaces the Decision "A record shares one reference draw
|
|
||||||
per category". Each expectation becomes a test:
|
|
||||||
- `first`, `last` and `email` reading `person` → one person
|
|
||||||
- `from_first`/`from_last` grouped `from`, `to_first`/`to_last` grouped `to` → two people
|
|
||||||
- `host`, plus `guests` repeated 3 times → four people
|
|
||||||
- `code` and `symbol` sibling fields reading `currency`, as `{code} {symbol}` → a matching pair
|
|
||||||
- `{a} & {b}`, each reading `person` → one person, or two when `a` and `b` name different groups
|
|
||||||
- two bare `{/sv_SE.word}` → two words
|
|
||||||
|
|
||||||
### Data
|
### Data
|
||||||
|
|
||||||
- Major data update. Shipped categories render as records with their building
|
- Major data update. Shipped categories render as records with their building
|
||||||
|
|||||||
Reference in New Issue
Block a user