237 lines
6.5 KiB
Go
237 lines
6.5 KiB
Go
package fejkdata
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/larvit/fejkdata/internal/grammar"
|
|
"github.com/larvit/fejkdata/internal/rows"
|
|
)
|
|
|
|
// table is a category whose rows come from a TSV beside it: the header names the
|
|
// columns, each row is one draw, and the format renders the drawn row. A column is a
|
|
// cell of the row a path pinned; a cell carrying tokens compiles to a string node.
|
|
type table struct {
|
|
rows *rowsTable
|
|
formatTemplate *template // fields are the column nodes
|
|
rowNode *tableRow
|
|
cellTemplates map[int]*template
|
|
}
|
|
|
|
type (
|
|
rowsTable = rows.Table[*table]
|
|
pinSet = rows.Pins[*table]
|
|
)
|
|
|
|
func (*table) isNode() {}
|
|
|
|
// tableColumn is one column of a table, rendered as the cell of the row the render pinned.
|
|
type tableColumn struct {
|
|
t *table
|
|
i int
|
|
}
|
|
|
|
func (*tableColumn) isNode() {}
|
|
|
|
func (c *tableColumn) name() string { return c.t.rows.Header()[c.i] }
|
|
|
|
// tableRow is the row of a table the render pinned, rendered by the table's format.
|
|
type tableRow struct{ t *table }
|
|
|
|
func (*tableRow) isNode() {}
|
|
|
|
// cellTemplate is what a cell renders: its compiled template where it carries tokens,
|
|
// else nil for its text.
|
|
func (t *table) cellTemplate(row, col int) *template {
|
|
return t.cellTemplates[row*len(t.rows.Header())+col]
|
|
}
|
|
|
|
// tableOptions are the keys a table object takes; every other key is refused.
|
|
var tableOptions = []string{"format", "key", "name", "parent", "rows", "weight"}
|
|
|
|
func isTableOption(name string) bool {
|
|
for _, o := range tableOptions {
|
|
if o == name {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func compileTable(m map[string]any, dir []string, name string, readRows func(file string) (string, error)) (*table, error) {
|
|
o, err := readTableOptions(m)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
data, err := readRows(o.rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
t := &table{}
|
|
if t.rows, err = rows.Parse(t, name, categoryPath(dir, name), o.rows, data, o.columns); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := t.checkCells(); err != nil {
|
|
return nil, fmt.Errorf("%s: %w", o.rows, err)
|
|
}
|
|
if err := t.compileRowFormat(o.format); err != nil {
|
|
return nil, err
|
|
}
|
|
return t, nil
|
|
}
|
|
|
|
type tableOptionValues struct {
|
|
format, rows string
|
|
columns rows.Columns
|
|
}
|
|
|
|
func readTableOptions(m map[string]any) (tableOptionValues, error) {
|
|
var o tableOptionValues
|
|
for k := range m {
|
|
if !isTableOption(k) {
|
|
return o, fmt.Errorf("a table takes %s; %q is none of them", strings.Join(tableOptions, ", "), k)
|
|
}
|
|
}
|
|
for k, into := range map[string]*string{"format": &o.format, "key": &o.columns.Key, "name": &o.columns.Name, "parent": &o.columns.Parent, "rows": &o.rows, "weight": &o.columns.Weight} {
|
|
v, ok := m[k]
|
|
if !ok {
|
|
continue
|
|
}
|
|
s, isString := v.(string)
|
|
if !isString {
|
|
return o, fmt.Errorf("%s must be a string, got %T", k, v)
|
|
}
|
|
if k != "format" && s == "" {
|
|
return o, fmt.Errorf("%s names a column, so it cannot be empty; drop it", k)
|
|
}
|
|
*into = s
|
|
}
|
|
if _, ok := m["format"]; !ok {
|
|
return o, fmt.Errorf("template object missing string \"format\"")
|
|
}
|
|
if !strings.HasSuffix(o.rows, ".tsv") || strings.Contains(o.rows, "/") {
|
|
return o, fmt.Errorf("rows names a .tsv file beside the category, not %q", o.rows)
|
|
}
|
|
if o.columns.Key != "" && o.columns.Key == o.columns.Name {
|
|
return o, fmt.Errorf("name names the key column %q, which a selector already reads; drop it", o.columns.Name)
|
|
}
|
|
return o, nil
|
|
}
|
|
|
|
// checkCells compiles every cell carrying a token.
|
|
func (t *table) checkCells() error {
|
|
header := t.rows.Header()
|
|
for row := 0; row < t.rows.Len(); row++ {
|
|
for col := range header {
|
|
cell := t.rows.Cell(row, col)
|
|
if strings.IndexByte(cell, '{') < 0 && strings.IndexByte(cell, '}') < 0 {
|
|
continue
|
|
}
|
|
n, err := compileCell(cell)
|
|
if err != nil {
|
|
return fmt.Errorf("line %d, %s: %w", row+2, header[col], err)
|
|
}
|
|
if t.cellTemplates == nil {
|
|
t.cellTemplates = map[int]*template{}
|
|
}
|
|
t.cellTemplates[row*len(header)+col] = n
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// compileCell compiles a cell carrying a token. A cell sits in no name scope, so it binds no
|
|
// name and reads only references.
|
|
func compileCell(cell string) (*template, error) {
|
|
n, err := compileString(cell)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := refuseTableBinding(n.tokens); err != nil {
|
|
return nil, err
|
|
}
|
|
if len(n.unbound) > 0 {
|
|
return nil, n.unbound[0].err
|
|
}
|
|
return n, nil
|
|
}
|
|
|
|
// compileRowFormat compiles the format a row renders through, over the columns as its
|
|
// fields.
|
|
func (t *table) compileRowFormat(format string) error {
|
|
toks, err := grammar.ParseFormat(format)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := refuseTableBinding(toks); err != nil {
|
|
return err
|
|
}
|
|
for _, tok := range toks {
|
|
if tok.Kind != grammar.NameRead {
|
|
continue
|
|
}
|
|
for _, name := range tok.Arms {
|
|
a := splitArm(name, nil)
|
|
if _, ok := t.rows.Column(a.head); !ok && !grammar.IsRef(a.head) && a.head != "" {
|
|
return fmt.Errorf("format names no column %q of %s; the columns are %v", a.head, t.rows.File(), t.rows.Header())
|
|
}
|
|
}
|
|
}
|
|
fields := make(map[string]node, len(t.rows.Header()))
|
|
for i, name := range t.rows.Header() {
|
|
fields[name] = &tableColumn{t, i}
|
|
}
|
|
unbound, err := checkTokens(toks, fields)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(unbound) > 0 {
|
|
return unbound[0].err
|
|
}
|
|
t.formatTemplate = &template{format: format, tokens: toks, fields: fields, repeat: 1, isRecord: true}
|
|
t.rowNode = &tableRow{t}
|
|
return nil
|
|
}
|
|
|
|
// refuseTableBinding refuses a table's format or cell binding a name.
|
|
func refuseTableBinding(toks []grammar.Token) error {
|
|
for _, tok := range toks {
|
|
if tok.Kind == grammar.NameBind {
|
|
return fmt.Errorf("token {%s}: a table binds no name; bind it in a template reading the table", tok.Body)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// linkTables links each table to the parent beside it.
|
|
func linkTables(sites []categorySite) error {
|
|
for _, s := range sites {
|
|
t, isTable := s.n.(*table)
|
|
if !isTable || t.rows.LinkColumn() < 0 {
|
|
continue
|
|
}
|
|
if err := t.linkParent(s.in.children); err != nil {
|
|
return fmt.Errorf("%s: %w", s.path, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// linkParent links t to the table among siblings that its link column names.
|
|
func (t *table) linkParent(siblings map[string]node) error {
|
|
name := t.rows.Options().Parent
|
|
p, ok := siblings[name].(*table)
|
|
switch {
|
|
case siblings[name] == nil:
|
|
return fmt.Errorf("parent %q names no table beside it", name)
|
|
case !ok:
|
|
return fmt.Errorf("parent %q is not a table; a link column reads a table's key", name)
|
|
}
|
|
return t.rows.Link(p.rows, func(name string) *rowsTable {
|
|
if q, ok := siblings[name].(*table); ok {
|
|
return q.rows
|
|
}
|
|
return nil
|
|
})
|
|
}
|