## Summary - add YAML-backed generated wiki visibility policies for topdata datasets - centralize page and reference eligibility through a visibility index - cover eligibility helpers, metadata precedence, derived feat sets, and link suppression with toolkit tests ## Verification - go test ./... - module ./validate-topdata.sh with the feature toolkit binary - module ./build-wiki.sh --force with the feature toolkit binary Companion module branch: codex-wiki-visibility. Reviewed-on: https://gitea.westgate.pw/ShadowsOverWestgate/sow-tools/pulls/4 Co-authored-by: vickydotbat <vickydotbat@tutamail.com> Co-committed-by: vickydotbat <vickydotbat@tutamail.com>
394 lines
13 KiB
Go
394 lines
13 KiB
Go
package topdata
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
type wikiVisibilityDefinitions struct {
|
|
ReferenceSets map[string]wikiVisibilityReferenceSet `json:"reference_sets" yaml:"reference_sets"`
|
|
Datasets map[string]wikiVisibilityDataset `json:"datasets" yaml:"datasets"`
|
|
}
|
|
|
|
type wikiVisibilityDataset struct {
|
|
Eligibility wikiVisibilityPredicate `json:"eligibility" yaml:"eligibility"`
|
|
Include []wikiVisibilityRule `json:"include" yaml:"include"`
|
|
Exclude []wikiVisibilityRule `json:"exclude" yaml:"exclude"`
|
|
}
|
|
|
|
type wikiVisibilityPredicate struct {
|
|
When string `json:"when" yaml:"when"`
|
|
}
|
|
|
|
type wikiVisibilityRule struct {
|
|
When string `json:"when" yaml:"when"`
|
|
ReferenceSet string `json:"reference_set" yaml:"reference_set"`
|
|
}
|
|
|
|
type wikiVisibilityReferenceSet struct {
|
|
TargetDataset string `json:"target_dataset" yaml:"target_dataset"`
|
|
Sources []wikiVisibilityReferenceSource `json:"sources" yaml:"sources"`
|
|
}
|
|
|
|
type wikiVisibilityReferenceSource struct {
|
|
Dataset string `json:"dataset" yaml:"dataset"`
|
|
TableField string `json:"table_field" yaml:"table_field"`
|
|
TableKind string `json:"table_kind" yaml:"table_kind"`
|
|
RowRefField string `json:"row_ref_field" yaml:"row_ref_field"`
|
|
ExpandMasterFeat bool `json:"expand_masterfeat" yaml:"expand_masterfeat"`
|
|
}
|
|
|
|
func loadWikiVisibilityDefinitions(path string) (*wikiVisibilityDefinitions, error) {
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil, nil
|
|
}
|
|
return nil, fmt.Errorf("read wiki visibility definitions %s: %w", path, err)
|
|
}
|
|
policy, err := parseWikiVisibilityDefinitions(raw, path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &policy, nil
|
|
}
|
|
|
|
func parseWikiVisibilityDefinitions(raw []byte, path string) (wikiVisibilityDefinitions, error) {
|
|
var policy wikiVisibilityDefinitions
|
|
if err := yaml.Unmarshal(raw, &policy); err != nil {
|
|
return wikiVisibilityDefinitions{}, fmt.Errorf("parse wiki visibility definitions %s: %w", path, err)
|
|
}
|
|
if policy.ReferenceSets == nil {
|
|
policy.ReferenceSets = map[string]wikiVisibilityReferenceSet{}
|
|
}
|
|
if policy.Datasets == nil {
|
|
policy.Datasets = map[string]wikiVisibilityDataset{}
|
|
}
|
|
for dataset, definition := range policy.Datasets {
|
|
if !isWikiVisibilityDataset(dataset) {
|
|
return wikiVisibilityDefinitions{}, fmt.Errorf("wiki visibility definitions %s: unknown dataset %q", path, dataset)
|
|
}
|
|
if strings.TrimSpace(definition.Eligibility.When) != "" {
|
|
if err := validateWikiVisibilityExpression(definition.Eligibility.When); err != nil {
|
|
return wikiVisibilityDefinitions{}, fmt.Errorf("wiki visibility definitions %s: dataset %s eligibility: %w", path, dataset, err)
|
|
}
|
|
}
|
|
for _, rules := range [][]wikiVisibilityRule{definition.Include, definition.Exclude} {
|
|
for _, rule := range rules {
|
|
if strings.TrimSpace(rule.When) == "" && strings.TrimSpace(rule.ReferenceSet) == "" {
|
|
return wikiVisibilityDefinitions{}, fmt.Errorf("wiki visibility definitions %s: dataset %s rule requires when or reference_set", path, dataset)
|
|
}
|
|
if strings.TrimSpace(rule.When) != "" {
|
|
if err := validateWikiVisibilityExpression(rule.When); err != nil {
|
|
return wikiVisibilityDefinitions{}, fmt.Errorf("wiki visibility definitions %s: dataset %s rule: %w", path, dataset, err)
|
|
}
|
|
}
|
|
if set := strings.TrimSpace(rule.ReferenceSet); set != "" {
|
|
referenceSet, ok := policy.ReferenceSets[set]
|
|
if !ok {
|
|
return wikiVisibilityDefinitions{}, fmt.Errorf("wiki visibility definitions %s: dataset %s references unknown reference_set %q", path, dataset, set)
|
|
}
|
|
if referenceSet.TargetDataset != dataset {
|
|
return wikiVisibilityDefinitions{}, fmt.Errorf("wiki visibility definitions %s: dataset %s cannot use reference_set %q targeting %s", path, dataset, set, referenceSet.TargetDataset)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for name, referenceSet := range policy.ReferenceSets {
|
|
if !isWikiVisibilityDataset(referenceSet.TargetDataset) {
|
|
return wikiVisibilityDefinitions{}, fmt.Errorf("wiki visibility definitions %s: reference_set %s has unknown target_dataset %q", path, name, referenceSet.TargetDataset)
|
|
}
|
|
if len(referenceSet.Sources) == 0 {
|
|
return wikiVisibilityDefinitions{}, fmt.Errorf("wiki visibility definitions %s: reference_set %s requires sources", path, name)
|
|
}
|
|
for _, source := range referenceSet.Sources {
|
|
if !isWikiVisibilityDataset(source.Dataset) {
|
|
return wikiVisibilityDefinitions{}, fmt.Errorf("wiki visibility definitions %s: reference_set %s has unknown source dataset %q", path, name, source.Dataset)
|
|
}
|
|
if strings.TrimSpace(source.TableField) == "" || strings.TrimSpace(source.RowRefField) == "" {
|
|
return wikiVisibilityDefinitions{}, fmt.Errorf("wiki visibility definitions %s: reference_set %s source %s requires table_field and row_ref_field", path, name, source.Dataset)
|
|
}
|
|
switch source.TableKind {
|
|
case "class_feats", "class_bonus_feats", "race_feats":
|
|
default:
|
|
return wikiVisibilityDefinitions{}, fmt.Errorf("wiki visibility definitions %s: reference_set %s source %s has unsupported table_kind %q", path, name, source.Dataset, source.TableKind)
|
|
}
|
|
}
|
|
}
|
|
return policy, nil
|
|
}
|
|
|
|
func isWikiVisibilityDataset(name string) bool {
|
|
switch name {
|
|
case "baseitems", "classes", "feat", "racialtypes", "skills", "spells":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func validateWikiVisibilityExpression(expression string) error {
|
|
parser := wikiExprParser{
|
|
tokens: tokenizeWikiExpr(wikiExpressionBody(expression)),
|
|
row: map[string]any{},
|
|
allowMissingFields: true,
|
|
}
|
|
if _, err := parser.parseComparison(); err != nil {
|
|
return err
|
|
}
|
|
if parser.peek().kind != wikiExprEOF {
|
|
return fmt.Errorf("unexpected token %q", parser.peek().text)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func wikiExpressionBody(expression string) string {
|
|
expression = strings.TrimSpace(expression)
|
|
if strings.HasPrefix(expression, "{{") && strings.HasSuffix(expression, "}}") {
|
|
return strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(expression, "{{"), "}}"))
|
|
}
|
|
return expression
|
|
}
|
|
|
|
func (ctx *wikiContext) loadWikiVisibility(path string) error {
|
|
policy, err := loadWikiVisibilityDefinitions(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ctx.visibilityPolicy = policy
|
|
if policy == nil {
|
|
ctx.visibleWikiKeys = nil
|
|
return nil
|
|
}
|
|
ctx.visibleWikiKeys, err = ctx.buildWikiVisibilityIndex(policy)
|
|
return err
|
|
}
|
|
|
|
func (ctx *wikiContext) buildWikiVisibilityIndex(policy *wikiVisibilityDefinitions) (map[string]bool, error) {
|
|
visible := map[string]bool{}
|
|
for _, dataset := range []string{"classes", "racialtypes", "skills", "spells", "baseitems", "feat"} {
|
|
definition, ok := policy.Datasets[dataset]
|
|
if !ok {
|
|
continue
|
|
}
|
|
rows := ctx.wikiRowsForDataset(dataset)
|
|
for _, key := range sortedKeys(rows) {
|
|
row := rows[key]
|
|
include, err := ctx.evaluateWikiVisibilityWithoutSets(dataset, key, row, definition)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
visible[key] = include
|
|
}
|
|
}
|
|
referenceSets := ctx.collectWikiVisibilityReferenceSets(policy, visible)
|
|
for _, dataset := range []string{"classes", "racialtypes", "skills", "spells", "baseitems", "feat"} {
|
|
definition, ok := policy.Datasets[dataset]
|
|
if !ok {
|
|
continue
|
|
}
|
|
rows := ctx.wikiRowsForDataset(dataset)
|
|
for _, key := range sortedKeys(rows) {
|
|
include, err := ctx.evaluateWikiVisibility(dataset, key, rows[key], definition, referenceSets)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
visible[key] = include
|
|
}
|
|
}
|
|
return visible, nil
|
|
}
|
|
|
|
func (ctx *wikiContext) evaluateWikiVisibilityWithoutSets(dataset, key string, row map[string]any, definition wikiVisibilityDataset) (bool, error) {
|
|
return ctx.evaluateWikiVisibility(dataset, key, row, definition, map[string]map[string]struct{}{})
|
|
}
|
|
|
|
func (ctx *wikiContext) evaluateWikiVisibility(dataset, key string, row map[string]any, definition wikiVisibilityDataset, referenceSets map[string]map[string]struct{}) (bool, error) {
|
|
if row == nil || !ctx.wikiRowCanRender(dataset, row) {
|
|
return false, nil
|
|
}
|
|
if expression := strings.TrimSpace(definition.Eligibility.When); expression != "" {
|
|
eligible, err := ctx.evalWikiVisibilityPredicate(expression, row)
|
|
if err != nil {
|
|
return false, fmt.Errorf("evaluate wiki visibility dataset %s eligibility for %s: %w", dataset, key, err)
|
|
}
|
|
if !eligible {
|
|
return false, nil
|
|
}
|
|
}
|
|
included := len(definition.Include) == 0
|
|
for _, rule := range definition.Include {
|
|
match, err := ctx.matchWikiVisibilityRule(rule, key, row, referenceSets)
|
|
if err != nil {
|
|
return false, fmt.Errorf("evaluate wiki visibility dataset %s include for %s: %w", dataset, key, err)
|
|
}
|
|
included = included || match
|
|
}
|
|
excluded := false
|
|
for _, rule := range definition.Exclude {
|
|
match, err := ctx.matchWikiVisibilityRule(rule, key, row, referenceSets)
|
|
if err != nil {
|
|
return false, fmt.Errorf("evaluate wiki visibility dataset %s exclude for %s: %w", dataset, key, err)
|
|
}
|
|
excluded = excluded || match
|
|
}
|
|
switch ctx.wikiGenerateOverride(row) {
|
|
case "0":
|
|
return false, nil
|
|
case "1":
|
|
return true, nil
|
|
default:
|
|
return included && !excluded, nil
|
|
}
|
|
}
|
|
|
|
func (ctx *wikiContext) matchWikiVisibilityRule(rule wikiVisibilityRule, key string, row map[string]any, referenceSets map[string]map[string]struct{}) (bool, error) {
|
|
match := false
|
|
if expression := strings.TrimSpace(rule.When); expression != "" {
|
|
value, err := ctx.evalWikiVisibilityPredicate(expression, row)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
match = match || value
|
|
}
|
|
if set := strings.TrimSpace(rule.ReferenceSet); set != "" {
|
|
_, ok := referenceSets[set][key]
|
|
match = match || ok
|
|
}
|
|
return match, nil
|
|
}
|
|
|
|
func (ctx *wikiContext) evalWikiVisibilityPredicate(expression string, row map[string]any) (bool, error) {
|
|
parser := wikiExprParser{
|
|
tokens: tokenizeWikiExpr(wikiExpressionBody(expression)),
|
|
row: row,
|
|
ctx: ctx,
|
|
allowMissingFields: true,
|
|
}
|
|
value, err := parser.parseComparison()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if parser.peek().kind != wikiExprEOF {
|
|
return false, fmt.Errorf("unexpected token %q", parser.peek().text)
|
|
}
|
|
return truthyWikiTableValue(value), nil
|
|
}
|
|
|
|
func (ctx *wikiContext) collectWikiVisibilityReferenceSets(policy *wikiVisibilityDefinitions, visible map[string]bool) map[string]map[string]struct{} {
|
|
out := map[string]map[string]struct{}{}
|
|
names := make([]string, 0, len(policy.ReferenceSets))
|
|
for name := range policy.ReferenceSets {
|
|
names = append(names, name)
|
|
}
|
|
sort.Strings(names)
|
|
for _, name := range names {
|
|
keys := map[string]struct{}{}
|
|
for _, source := range policy.ReferenceSets[name].Sources {
|
|
for key, row := range ctx.wikiRowsForDataset(source.Dataset) {
|
|
if !visible[key] {
|
|
continue
|
|
}
|
|
table := ctx.tableForValue(fieldValue(row, source.TableField), ctx.wikiVisibilityTables(source.TableKind))
|
|
if table == nil {
|
|
continue
|
|
}
|
|
for _, tableRow := range table.Rows {
|
|
refKey := resolveReferenceKey(fieldValue(tableRow, source.RowRefField), ctx.wikiIDMapForDataset(policy.ReferenceSets[name].TargetDataset))
|
|
if refKey == "" {
|
|
refKey = resolveReferenceKey(fieldValue(tableRow, source.RowRefField), nil)
|
|
}
|
|
if refKey == "" {
|
|
continue
|
|
}
|
|
if source.ExpandMasterFeat && strings.HasPrefix(refKey, "masterfeats:") {
|
|
for _, featKey := range ctx.masterFeatGroupKeys(refKey) {
|
|
keys[featKey] = struct{}{}
|
|
}
|
|
continue
|
|
}
|
|
keys[refKey] = struct{}{}
|
|
}
|
|
}
|
|
}
|
|
out[name] = keys
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (ctx *wikiContext) wikiRowsForDataset(dataset string) map[string]map[string]any {
|
|
switch dataset {
|
|
case "baseitems":
|
|
return ctx.baseitemRows
|
|
case "classes":
|
|
return ctx.classRows
|
|
case "feat":
|
|
return ctx.featRows
|
|
case "racialtypes":
|
|
return ctx.raceRows
|
|
case "skills":
|
|
return ctx.skillRows
|
|
case "spells":
|
|
return ctx.spellRows
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func (ctx *wikiContext) wikiIDMapForDataset(dataset string) map[int]string {
|
|
switch dataset {
|
|
case "classes":
|
|
return ctx.classIDToKey
|
|
case "feat":
|
|
return ctx.featIDToKey
|
|
case "racialtypes":
|
|
return ctx.raceIDToKey
|
|
case "skills":
|
|
return ctx.skillIDToKey
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func (ctx *wikiContext) wikiVisibilityTables(kind string) map[string]wikiTable {
|
|
switch kind {
|
|
case "class_feats":
|
|
return ctx.classFeatTables
|
|
case "class_bonus_feats":
|
|
return ctx.classBonusTables
|
|
case "race_feats":
|
|
return ctx.raceFeatTables
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func (ctx *wikiContext) masterFeatGroupKeys(group string) []string {
|
|
keys := []string{}
|
|
for key, row := range ctx.featRows {
|
|
if masterfeatGroupKey(fieldValue(row, "MASTERFEAT"), ctx.masterfeatRows) == group {
|
|
keys = append(keys, key)
|
|
}
|
|
}
|
|
sort.Strings(keys)
|
|
return keys
|
|
}
|
|
|
|
func (ctx *wikiContext) canReferenceWikiKey(key string) bool {
|
|
if key == "" || ctx.visibilityPolicy == nil {
|
|
return true
|
|
}
|
|
visible, configured := ctx.visibleWikiKeys[key]
|
|
if configured {
|
|
return visible
|
|
}
|
|
dataset := strings.SplitN(key, ":", 2)[0]
|
|
_, configured = ctx.visibilityPolicy.Datasets[dataset]
|
|
return !configured
|
|
}
|