Add configurable topdata wiki visibility policy (#4)
## 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>
This commit is contained in:
@@ -243,6 +243,7 @@ topdata:
|
||||
managed_namespaces: [classes, feat, itemtypes, races, skills, spells, meta]
|
||||
deploy_manifest: .wiki_deploy_manifest.json
|
||||
deploy_edit_summary: Auto-generated from native builder
|
||||
visibility_file: visibility.yaml
|
||||
title_prefix_min_length: 3
|
||||
status_listing_scope: all
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ const (
|
||||
DefaultTopDataWikiLinkStrategy = "preserve_westgate_wiki_links"
|
||||
DefaultTopDataWikiNamespacesFile = "namespaces.yaml"
|
||||
DefaultTopDataWikiTablesFile = "tables.yaml"
|
||||
DefaultTopDataWikiVisibilityFile = "visibility.yaml"
|
||||
DefaultTopDataWikiTemplatesDir = "templates"
|
||||
DefaultTopDataWikiManualSectionsDir = "manual-sections"
|
||||
DefaultTopDataWikiStaleDefault = "report"
|
||||
@@ -247,6 +248,7 @@ func (p *Project) EffectiveConfig() EffectiveConfig {
|
||||
LinkStrategy: defaultString(p.Config.TopData.Wiki.LinkStrategy, DefaultTopDataWikiLinkStrategy),
|
||||
NamespacesFile: defaultString(p.Config.TopData.Wiki.NamespacesFile, DefaultTopDataWikiNamespacesFile),
|
||||
TablesFile: defaultString(p.Config.TopData.Wiki.TablesFile, DefaultTopDataWikiTablesFile),
|
||||
VisibilityFile: defaultString(p.Config.TopData.Wiki.VisibilityFile, DefaultTopDataWikiVisibilityFile),
|
||||
TemplatesDir: defaultString(p.Config.TopData.Wiki.TemplatesDir, DefaultTopDataWikiTemplatesDir),
|
||||
ManualSectionsDir: defaultString(p.Config.TopData.Wiki.ManualSectionsDir, DefaultTopDataWikiManualSectionsDir),
|
||||
TitlePrefixMinLength: defaultInt(p.Config.TopData.Wiki.TitlePrefixMinLength, DefaultTopDataWikiTitlePrefixMinLength),
|
||||
@@ -484,6 +486,7 @@ func markMissingDefaults(provenance ConfigProvenance) {
|
||||
"topdata.wiki.link_strategy": DefaultTopDataWikiLinkStrategy,
|
||||
"topdata.wiki.namespaces_file": DefaultTopDataWikiNamespacesFile,
|
||||
"topdata.wiki.tables_file": DefaultTopDataWikiTablesFile,
|
||||
"topdata.wiki.visibility_file": DefaultTopDataWikiVisibilityFile,
|
||||
"topdata.wiki.templates_dir": DefaultTopDataWikiTemplatesDir,
|
||||
"topdata.wiki.manual_sections_dir": DefaultTopDataWikiManualSectionsDir,
|
||||
"topdata.wiki.title_prefix_min_length": "3",
|
||||
|
||||
@@ -229,6 +229,7 @@ type TopDataWikiConfig struct {
|
||||
LinkStrategy string `json:"link_strategy" yaml:"link_strategy"`
|
||||
NamespacesFile string `json:"namespaces_file" yaml:"namespaces_file"`
|
||||
TablesFile string `json:"tables_file" yaml:"tables_file"`
|
||||
VisibilityFile string `json:"visibility_file" yaml:"visibility_file"`
|
||||
TemplatesDir string `json:"templates_dir" yaml:"templates_dir"`
|
||||
ManualSectionsDir string `json:"manual_sections_dir" yaml:"manual_sections_dir"`
|
||||
TitlePrefixMinLength int `json:"title_prefix_min_length" yaml:"title_prefix_min_length"`
|
||||
@@ -534,6 +535,7 @@ func (p *Project) ValidateLayout() error {
|
||||
"topdata.wiki.deploy_manifest": p.Config.TopData.Wiki.DeployManifest,
|
||||
"topdata.wiki.deploy_edit_summary": p.Config.TopData.Wiki.DeployEditSummary,
|
||||
"topdata.wiki.tables_file": p.Config.TopData.Wiki.TablesFile,
|
||||
"topdata.wiki.visibility_file": p.Config.TopData.Wiki.VisibilityFile,
|
||||
"extract.layout": p.Config.Extract.Layout,
|
||||
"extract.hak_discovery": p.Config.Extract.HAKDiscovery,
|
||||
"autogen.cache.root": p.Config.Autogen.Cache.Root,
|
||||
@@ -567,6 +569,7 @@ func (p *Project) ValidateLayout() error {
|
||||
failures = append(failures, validateTreeRootPath("topdata.wiki.source", effective.TopData.Wiki.Source)...)
|
||||
failures = append(failures, validateRelativePath("topdata.wiki.namespaces_file", effective.TopData.Wiki.NamespacesFile)...)
|
||||
failures = append(failures, validateRelativePath("topdata.wiki.tables_file", effective.TopData.Wiki.TablesFile)...)
|
||||
failures = append(failures, validateRelativePath("topdata.wiki.visibility_file", effective.TopData.Wiki.VisibilityFile)...)
|
||||
failures = append(failures, validateTreeRootPath("topdata.wiki.templates_dir", effective.TopData.Wiki.TemplatesDir)...)
|
||||
failures = append(failures, validateTreeRootPath("topdata.wiki.manual_sections_dir", effective.TopData.Wiki.ManualSectionsDir)...)
|
||||
failures = append(failures, validateRelativePath("autogen.cache.root", effective.Autogen.Cache.Root)...)
|
||||
|
||||
@@ -273,6 +273,7 @@ topdata:
|
||||
link_strategy: preserve_westgate_wiki_links
|
||||
namespaces_file: namespaces.yaml
|
||||
tables_file: custom-tables.yaml
|
||||
visibility_file: visibility.yaml
|
||||
templates_dir: templates
|
||||
manual_sections_dir: manual-sections
|
||||
deploy_manifest: wiki-manifest.json
|
||||
@@ -347,6 +348,9 @@ autogen:
|
||||
if got, want := effective.TopData.Wiki.TablesFile, "custom-tables.yaml"; got != want {
|
||||
t.Fatalf("expected wiki tables file %q, got %q", want, got)
|
||||
}
|
||||
if got, want := effective.TopData.Wiki.VisibilityFile, "visibility.yaml"; got != want {
|
||||
t.Fatalf("expected wiki visibility file %q, got %q", want, got)
|
||||
}
|
||||
if got, want := effective.TopData.Wiki.TemplatesDir, "templates"; got != want {
|
||||
t.Fatalf("expected wiki templates dir %q, got %q", want, got)
|
||||
}
|
||||
@@ -876,6 +880,7 @@ func TestValidateLayoutRejectsInvalidTopDataWikiConfig(t *testing.T) {
|
||||
StatusListingScope: "global",
|
||||
NamespacesFile: "../namespaces.yaml",
|
||||
TablesFile: "../tables.yaml",
|
||||
VisibilityFile: "../visibility.yaml",
|
||||
TemplatesDir: ".",
|
||||
ManualSectionsDir: "../manual",
|
||||
StalePages: TopDataWikiStalePagesConfig{
|
||||
@@ -898,6 +903,7 @@ func TestValidateLayoutRejectsInvalidTopDataWikiConfig(t *testing.T) {
|
||||
"topdata.wiki.status_listing_scope",
|
||||
"topdata.wiki.namespaces_file",
|
||||
"topdata.wiki.tables_file",
|
||||
"topdata.wiki.visibility_file",
|
||||
"topdata.wiki.templates_dir",
|
||||
"topdata.wiki.manual_sections_dir",
|
||||
"topdata.wiki.stale_pages.default",
|
||||
|
||||
@@ -113,6 +113,8 @@ type wikiContext struct {
|
||||
templateDir string
|
||||
wikiTablesPath string
|
||||
wikiTableDefinitions map[string]wikiTableDefinition
|
||||
visibilityPolicy *wikiVisibilityDefinitions
|
||||
visibleWikiKeys map[string]bool
|
||||
}
|
||||
|
||||
func buildWiki(p *project.Project, nativeResult BuildResult, force bool, progress func(string)) (wikiResult, error) {
|
||||
@@ -152,6 +154,10 @@ func buildWiki(p *project.Project, nativeResult BuildResult, force bool, progres
|
||||
return wikiResult{}, err
|
||||
}
|
||||
ctx.wikiTableDefinitions = tableDefinitions
|
||||
visibilityPath := filepath.Join(p.TopDataWikiSourceDir(), filepath.FromSlash(p.EffectiveConfig().TopData.Wiki.VisibilityFile))
|
||||
if err := ctx.loadWikiVisibility(visibilityPath); err != nil {
|
||||
return wikiResult{}, err
|
||||
}
|
||||
|
||||
if err := os.RemoveAll(outputDir); err != nil {
|
||||
return wikiResult{}, fmt.Errorf("clean wiki output: %w", err)
|
||||
@@ -746,6 +752,9 @@ func (ctx *wikiContext) shouldGeneratePage(category, key string, row map[string]
|
||||
if row == nil {
|
||||
return false
|
||||
}
|
||||
if ctx.visibilityPolicy != nil {
|
||||
return ctx.canReferenceWikiKey(key)
|
||||
}
|
||||
override := ctx.wikiGenerateOverride(row)
|
||||
if override == "0" {
|
||||
return false
|
||||
@@ -790,6 +799,21 @@ func (ctx *wikiContext) shouldGeneratePage(category, key string, row map[string]
|
||||
}
|
||||
}
|
||||
|
||||
func (ctx *wikiContext) wikiRowCanRender(category string, row map[string]any) bool {
|
||||
if row == nil {
|
||||
return false
|
||||
}
|
||||
name := ctx.resolveRowName(category, row)
|
||||
description := ctx.resolveRowDescription(category, row)
|
||||
switch category {
|
||||
case "classes", "spells":
|
||||
if isToolLike(name, stringField(row, "Label"), stringField(row, "Constant")) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return name != "" && description != ""
|
||||
}
|
||||
|
||||
func (ctx *wikiContext) renderPage(category, key string, row map[string]any, title string) (string, error) {
|
||||
manualSections := ctx.manualSections
|
||||
if len(manualSections) == 0 {
|
||||
@@ -1000,7 +1024,7 @@ func (ctx *wikiContext) renderRaceFeatList(row map[string]any) string {
|
||||
|
||||
func (ctx *wikiContext) renderReference(value any, idToKey map[int]string, nameResolver func(string) string) string {
|
||||
key := resolveReferenceKey(value, idToKey)
|
||||
if key == "" {
|
||||
if key == "" || !ctx.canReferenceWikiKey(key) {
|
||||
return ""
|
||||
}
|
||||
name := nameResolver(key)
|
||||
|
||||
@@ -401,6 +401,298 @@ tables:
|
||||
}
|
||||
}
|
||||
|
||||
func TestWikiVisibilityPolicyRejectsNullLikeFeatsAndHiddenReferences(t *testing.T) {
|
||||
root := testProjectRoot(t)
|
||||
for _, dir := range []string{
|
||||
filepath.Join(root, "topdata", "data", "classes", "core"),
|
||||
filepath.Join(root, "topdata", "data", "classes", "feats"),
|
||||
filepath.Join(root, "topdata", "data", "classes", "skills"),
|
||||
filepath.Join(root, "topdata", "data", "feat"),
|
||||
filepath.Join(root, "topdata", "data", "skills"),
|
||||
filepath.Join(root, "topdata", "wiki"),
|
||||
filepath.Join(root, "topdata", "wiki", "templates", "pages"),
|
||||
} {
|
||||
mkdirAll(t, dir)
|
||||
}
|
||||
writeFile(t, filepath.Join(root, "topdata", "base_dialog.json"), "{}\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "wiki", "templates", "pages", "classes.html"), `<h1>{{title}}</h1>
|
||||
{{format:SkillList}}
|
||||
{{table:FeatProgression}}`+"\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "wiki", "tables.yaml"), `
|
||||
tables:
|
||||
FeatProgression:
|
||||
source: class_feats
|
||||
columns:
|
||||
- key: feat
|
||||
label: Feat
|
||||
value: "{{link(FeatKey, FeatName)}}"
|
||||
allow_wiki_markup: true
|
||||
`+"\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "wiki", "visibility.yaml"), `
|
||||
reference_sets:
|
||||
class_feats:
|
||||
target_dataset: feat
|
||||
sources:
|
||||
- dataset: classes
|
||||
table_field: FeatsTable
|
||||
table_kind: class_feats
|
||||
row_ref_field: FeatIndex
|
||||
datasets:
|
||||
classes:
|
||||
eligibility:
|
||||
when: "{{all_present(Name, Description)}}"
|
||||
include:
|
||||
- when: "{{PlayerClass == 1}}"
|
||||
feat:
|
||||
eligibility:
|
||||
when: "{{all_present(FEAT, DESCRIPTION)}}"
|
||||
include:
|
||||
- when: "{{ALLCLASSESCANUSE == 1}}"
|
||||
- reference_set: class_feats
|
||||
exclude:
|
||||
- when: "{{PreReqEpic == 1}}"
|
||||
skills:
|
||||
eligibility:
|
||||
when: "{{all_present(Name, Description)}}"
|
||||
include:
|
||||
- when: "{{true}}"
|
||||
exclude:
|
||||
- when: "{{HideFromLevelUp == 1}}"
|
||||
`+"\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "skills", "base.json"), `{
|
||||
"output": "skills.2da",
|
||||
"columns": ["Label", "Name", "Description", "HideFromLevelUp"],
|
||||
"rows": [
|
||||
{"id": 0, "key": "skills:athletics", "Label": "Athletics", "Name": {"tlk": {"text": "Athletics"}}, "Description": {"tlk": {"text": "Visible skill."}}, "HideFromLevelUp": "0"},
|
||||
{"id": 1, "key": "skills:secret", "Label": "Secret", "Name": {"tlk": {"text": "Secret"}}, "Description": {"tlk": {"text": "Hidden skill."}}, "HideFromLevelUp": "1"}
|
||||
]
|
||||
}`+"\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "skills", "lock.json"), `{"skills:athletics":0,"skills:secret":1}`+"\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "feat", "base.json"), `{
|
||||
"output": "feat.2da",
|
||||
"columns": ["LABEL", "FEAT", "DESCRIPTION", "ALLCLASSESCANUSE", "PreReqEpic"],
|
||||
"rows": [
|
||||
{"id": 0, "key": "feat:visible", "LABEL": "Visible", "FEAT": {"tlk": {"text": "Visible Feat"}}, "DESCRIPTION": {"tlk": {"text": "Visible feat description."}}, "ALLCLASSESCANUSE": "1", "PreReqEpic": "0"},
|
||||
{"id": 1, "key": "feat:epic", "LABEL": "Epic", "FEAT": {"tlk": {"text": "Epic Feat"}}, "DESCRIPTION": {"tlk": {"text": "Epic feat description."}}, "ALLCLASSESCANUSE": "1", "PreReqEpic": "1"},
|
||||
{"id": 2, "key": "feat:nullfeat", "LABEL": "Null", "FEAT": "****", "DESCRIPTION": {"tlk": {"text": "Null feat description."}}, "ALLCLASSESCANUSE": "1", "PreReqEpic": "0"},
|
||||
{"id": 3, "key": "feat:class_only", "LABEL": "ClassOnly", "FEAT": {"tlk": {"text": "Class Feat"}}, "DESCRIPTION": {"tlk": {"text": "Class feat description."}}, "ALLCLASSESCANUSE": "0", "PreReqEpic": "0"}
|
||||
]
|
||||
}`+"\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "feat", "lock.json"), `{"feat:visible":0,"feat:epic":1,"feat:nullfeat":2,"feat:class_only":3}`+"\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "classes", "core", "base.json"), `{
|
||||
"output": "classes.2da",
|
||||
"columns": ["Label", "Name", "Description", "PlayerClass", "SkillsTable", "FeatsTable"],
|
||||
"rows": [
|
||||
{"id": 0, "key": "classes:fighter", "Label": "Fighter", "Name": {"tlk": {"text": "Fighter"}}, "Description": {"tlk": {"text": "Fighter text."}}, "PlayerClass": "1", "SkillsTable": {"table": "classes/skills:fighter"}, "FeatsTable": {"table": "classes/feats:fighter"}}
|
||||
]
|
||||
}`+"\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "classes", "core", "lock.json"), `{"classes:fighter":0}`+"\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "classes", "skills", "fighter.json"), `{
|
||||
"key": "classes/skills:fighter",
|
||||
"output": "cls_skill_fight.2da",
|
||||
"columns": ["SkillIndex", "ClassSkill"],
|
||||
"rows": [
|
||||
{"SkillIndex": {"id": "skills:athletics"}, "ClassSkill": "1"},
|
||||
{"SkillIndex": {"id": "skills:secret"}, "ClassSkill": "1"}
|
||||
]
|
||||
}`+"\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "classes", "feats", "fighter.json"), `{
|
||||
"key": "classes/feats:fighter",
|
||||
"output": "cls_feat_fight.2da",
|
||||
"columns": ["FeatIndex", "GrantedOnLevel", "List"],
|
||||
"rows": [
|
||||
{"FeatIndex": {"id": "feat:class_only"}, "GrantedOnLevel": "1", "List": "3"},
|
||||
{"FeatIndex": {"id": "feat:epic"}, "GrantedOnLevel": "1", "List": "3"},
|
||||
{"FeatIndex": {"id": "feat:nullfeat"}, "GrantedOnLevel": "1", "List": "3"}
|
||||
]
|
||||
}`+"\n")
|
||||
|
||||
proj := testProject(root)
|
||||
proj.Config.TopData.ReferenceBuilder = ""
|
||||
proj.Config.TopData.Wiki.VisibilityFile = "visibility.yaml"
|
||||
|
||||
if _, err := buildWiki(proj, BuildResult{}, true, nil); err != nil {
|
||||
t.Fatalf("buildWiki failed: %v", err)
|
||||
}
|
||||
for _, path := range []string{
|
||||
filepath.Join(root, ".cache", "wiki", "pages", "skills", "secret.html"),
|
||||
filepath.Join(root, ".cache", "wiki", "pages", "feat", "epic.html"),
|
||||
filepath.Join(root, ".cache", "wiki", "pages", "feat", "nullfeat.html"),
|
||||
} {
|
||||
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
||||
t.Fatalf("expected hidden or ineligible wiki page %s to be omitted, got %v", path, err)
|
||||
}
|
||||
}
|
||||
for _, path := range []string{
|
||||
filepath.Join(root, ".cache", "wiki", "pages", "feat", "visible.html"),
|
||||
filepath.Join(root, ".cache", "wiki", "pages", "feat", "class", "only.html"),
|
||||
} {
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
t.Fatalf("expected visible wiki page %s: %v", path, err)
|
||||
}
|
||||
}
|
||||
classPage, err := os.ReadFile(filepath.Join(root, ".cache", "wiki", "pages", "classes", "fighter.html"))
|
||||
if err != nil {
|
||||
t.Fatalf("read class page: %v", err)
|
||||
}
|
||||
classText := string(classPage)
|
||||
for _, hidden := range []string{"Secret", "Epic Feat", "Null"} {
|
||||
if strings.Contains(classText, hidden) {
|
||||
t.Fatalf("expected hidden %q reference to be omitted from class page:\n%s", hidden, classText)
|
||||
}
|
||||
}
|
||||
for _, visible := range []string{"[[skills:athletics|Athletics]]", "[[feat:class:only|Class Feat]]"} {
|
||||
if !strings.Contains(classText, visible) {
|
||||
t.Fatalf("expected visible %q reference in class page:\n%s", visible, classText)
|
||||
}
|
||||
}
|
||||
indexRaw, err := os.ReadFile(filepath.Join(root, ".cache", "wiki", "page-index.json"))
|
||||
if err != nil {
|
||||
t.Fatalf("read page index: %v", err)
|
||||
}
|
||||
if strings.Contains(string(indexRaw), `"page_id": "skills:secret"`) ||
|
||||
strings.Contains(string(indexRaw), `"page_id": "feat:epic"`) ||
|
||||
strings.Contains(string(indexRaw), `"page_id": "feat:nullfeat"`) {
|
||||
t.Fatalf("expected hidden and ineligible pages out of page index:\n%s", string(indexRaw))
|
||||
}
|
||||
}
|
||||
|
||||
func TestWikiVisibilityPredicatesTreatNullLikeFieldsAsAbsent(t *testing.T) {
|
||||
policy, err := parseWikiVisibilityDefinitions([]byte(`
|
||||
datasets:
|
||||
feat:
|
||||
eligibility:
|
||||
when: "{{all_present(FEAT, DESCRIPTION)}}"
|
||||
include:
|
||||
- when: "{{true}}"
|
||||
`), "visibility.yaml")
|
||||
if err != nil {
|
||||
t.Fatalf("parse visibility policy: %v", err)
|
||||
}
|
||||
ctx := &wikiContext{}
|
||||
for name, row := range map[string]map[string]any{
|
||||
"missing": {"DESCRIPTION": "Description"},
|
||||
"null": {"FEAT": nil, "DESCRIPTION": "Description"},
|
||||
"nwn-null": {"FEAT": "****", "DESCRIPTION": "Description"},
|
||||
"empty": {"FEAT": "", "DESCRIPTION": "Description"},
|
||||
"present": {"FEAT": "Feat", "DESCRIPTION": "Description"},
|
||||
"missing-desc": {"FEAT": "Feat"},
|
||||
} {
|
||||
got, err := ctx.evalWikiVisibilityPredicate(policy.Datasets["feat"].Eligibility.When, row)
|
||||
if err != nil {
|
||||
t.Fatalf("%s predicate failed: %v", name, err)
|
||||
}
|
||||
if want := name == "present"; got != want {
|
||||
t.Fatalf("%s expected eligibility %v, got %v", name, want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWikiVisibilityPolicyRejectsInvalidDatasetAndReferenceSet(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
yaml string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "unknown dataset",
|
||||
yaml: `
|
||||
datasets:
|
||||
traps:
|
||||
include:
|
||||
- when: "{{true}}"
|
||||
`,
|
||||
want: `unknown dataset "traps"`,
|
||||
},
|
||||
{
|
||||
name: "unknown reference set",
|
||||
yaml: `
|
||||
datasets:
|
||||
feat:
|
||||
include:
|
||||
- reference_set: missing
|
||||
`,
|
||||
want: `unknown reference_set "missing"`,
|
||||
},
|
||||
{
|
||||
name: "bad table kind",
|
||||
yaml: `
|
||||
reference_sets:
|
||||
feats:
|
||||
target_dataset: feat
|
||||
sources:
|
||||
- dataset: classes
|
||||
table_field: FeatsTable
|
||||
table_kind: unsupported
|
||||
row_ref_field: FeatIndex
|
||||
datasets:
|
||||
feat:
|
||||
include:
|
||||
- reference_set: feats
|
||||
`,
|
||||
want: `unsupported table_kind`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := parseWikiVisibilityDefinitions([]byte(tt.yaml), "visibility.yaml")
|
||||
if err == nil || !strings.Contains(err.Error(), tt.want) {
|
||||
t.Fatalf("expected %q error, got %v", tt.want, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWikiVisibilityMetadataOverrideCannotBypassEligibility(t *testing.T) {
|
||||
policy, err := parseWikiVisibilityDefinitions([]byte(`
|
||||
datasets:
|
||||
feat:
|
||||
eligibility:
|
||||
when: "{{present(FEAT)}}"
|
||||
include:
|
||||
- when: "{{ALLCLASSESCANUSE == 1}}"
|
||||
exclude:
|
||||
- when: "{{PreReqEpic == 1}}"
|
||||
`), "visibility.yaml")
|
||||
if err != nil {
|
||||
t.Fatalf("parse visibility policy: %v", err)
|
||||
}
|
||||
ctx := &wikiContext{}
|
||||
definition := policy.Datasets["feat"]
|
||||
for name, tt := range map[string]struct {
|
||||
row map[string]any
|
||||
want bool
|
||||
}{
|
||||
"exclude overridden": {
|
||||
row: map[string]any{
|
||||
"FEAT": "Visible",
|
||||
"DESCRIPTION": "Description",
|
||||
"PreReqEpic": "1",
|
||||
"ALLCLASSESCANUSE": "1",
|
||||
"meta": map[string]any{"wiki": map[string]any{"generate": "1"}},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
"eligibility preserved": {
|
||||
row: map[string]any{
|
||||
"FEAT": "****",
|
||||
"DESCRIPTION": "Description",
|
||||
"ALLCLASSESCANUSE": "1",
|
||||
"meta": map[string]any{"wiki": map[string]any{"generate": "1"}},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
} {
|
||||
got, err := ctx.evaluateWikiVisibility("feat", "feat:"+name, tt.row, definition, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("%s visibility evaluation failed: %v", name, err)
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Fatalf("%s expected visibility %v, got %v", name, tt.want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testWikiTableContext(t *testing.T, yamlSource string) *wikiContext {
|
||||
t.Helper()
|
||||
ctx := &wikiContext{
|
||||
|
||||
@@ -612,6 +612,7 @@ type wikiExprParser struct {
|
||||
row map[string]any
|
||||
ctx *wikiContext
|
||||
renderCtx wikiTableRenderContext
|
||||
allowMissingFields bool
|
||||
}
|
||||
|
||||
func (p *wikiExprParser) peek() wikiExprToken {
|
||||
@@ -771,12 +772,21 @@ func (p *wikiExprParser) resolveField(name string) (any, error) {
|
||||
if name == "HasSpellProgression" {
|
||||
return hasSpellProgression(p.renderCtx.Page.Row), nil
|
||||
}
|
||||
switch name {
|
||||
case "true":
|
||||
return true, nil
|
||||
case "false":
|
||||
return false, nil
|
||||
}
|
||||
if value, ok := lookupField(p.row, name); ok {
|
||||
return value, nil
|
||||
}
|
||||
if value, ok := lookupField(p.renderCtx.Page.Row, name); ok {
|
||||
return value, nil
|
||||
}
|
||||
if p.allowMissingFields {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("missing field %q", name)
|
||||
}
|
||||
|
||||
@@ -800,6 +810,9 @@ func (p *wikiExprParser) callHelper(name string, args []any) (any, error) {
|
||||
case "link":
|
||||
if len(args) == 1 {
|
||||
key := stringifyWikiTableValue(args[0])
|
||||
if p.ctx != nil && !p.ctx.canReferenceWikiKey(key) {
|
||||
return "", nil
|
||||
}
|
||||
if pageID := wikiPageIDForKey(key); pageID != "" {
|
||||
key = pageID
|
||||
}
|
||||
@@ -811,6 +824,9 @@ func (p *wikiExprParser) callHelper(name string, args []any) (any, error) {
|
||||
if key == "" {
|
||||
return label, nil
|
||||
}
|
||||
if p.ctx != nil && !p.ctx.canReferenceWikiKey(key) {
|
||||
return "", nil
|
||||
}
|
||||
if pageID := wikiPageIDForKey(key); pageID != "" {
|
||||
key = pageID
|
||||
}
|
||||
@@ -870,11 +886,46 @@ func (p *wikiExprParser) callHelper(name string, args []any) (any, error) {
|
||||
return args[1], nil
|
||||
}
|
||||
return args[0], nil
|
||||
case "present":
|
||||
if len(args) != 1 {
|
||||
return nil, fmt.Errorf("present expects 1 argument")
|
||||
}
|
||||
return presentWikiValue(args[0]), nil
|
||||
case "all_present":
|
||||
if len(args) == 0 {
|
||||
return nil, fmt.Errorf("all_present expects at least 1 argument")
|
||||
}
|
||||
for _, value := range args {
|
||||
if !presentWikiValue(value) {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
case "any_present":
|
||||
if len(args) == 0 {
|
||||
return nil, fmt.Errorf("any_present expects at least 1 argument")
|
||||
}
|
||||
for _, value := range args {
|
||||
if presentWikiValue(value) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown helper %q", name)
|
||||
}
|
||||
}
|
||||
|
||||
func presentWikiValue(value any) bool {
|
||||
if isNullLike(value) {
|
||||
return false
|
||||
}
|
||||
if text, ok := value.(string); ok {
|
||||
return strings.TrimSpace(text) != ""
|
||||
}
|
||||
return value != nil
|
||||
}
|
||||
|
||||
func ordinalWikiTableValue(value int) string {
|
||||
suffix := "th"
|
||||
if value%100 < 11 || value%100 > 13 {
|
||||
|
||||
@@ -0,0 +1,393 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user