Phase 2 wiki template refactor
This commit is contained in:
@@ -39,6 +39,7 @@ const (
|
|||||||
DefaultTopDataWikiRenderer = "nodebb_tiptap_html"
|
DefaultTopDataWikiRenderer = "nodebb_tiptap_html"
|
||||||
DefaultTopDataWikiLinkStrategy = "preserve_westgate_wiki_links"
|
DefaultTopDataWikiLinkStrategy = "preserve_westgate_wiki_links"
|
||||||
DefaultTopDataWikiNamespacesFile = "namespaces.yaml"
|
DefaultTopDataWikiNamespacesFile = "namespaces.yaml"
|
||||||
|
DefaultTopDataWikiTablesFile = "tables.yaml"
|
||||||
DefaultTopDataWikiTemplatesDir = "templates"
|
DefaultTopDataWikiTemplatesDir = "templates"
|
||||||
DefaultTopDataWikiManualSectionsDir = "manual-sections"
|
DefaultTopDataWikiManualSectionsDir = "manual-sections"
|
||||||
DefaultTopDataWikiStaleDefault = "report"
|
DefaultTopDataWikiStaleDefault = "report"
|
||||||
@@ -244,6 +245,7 @@ func (p *Project) EffectiveConfig() EffectiveConfig {
|
|||||||
Renderer: defaultString(p.Config.TopData.Wiki.Renderer, DefaultTopDataWikiRenderer),
|
Renderer: defaultString(p.Config.TopData.Wiki.Renderer, DefaultTopDataWikiRenderer),
|
||||||
LinkStrategy: defaultString(p.Config.TopData.Wiki.LinkStrategy, DefaultTopDataWikiLinkStrategy),
|
LinkStrategy: defaultString(p.Config.TopData.Wiki.LinkStrategy, DefaultTopDataWikiLinkStrategy),
|
||||||
NamespacesFile: defaultString(p.Config.TopData.Wiki.NamespacesFile, DefaultTopDataWikiNamespacesFile),
|
NamespacesFile: defaultString(p.Config.TopData.Wiki.NamespacesFile, DefaultTopDataWikiNamespacesFile),
|
||||||
|
TablesFile: defaultString(p.Config.TopData.Wiki.TablesFile, DefaultTopDataWikiTablesFile),
|
||||||
TemplatesDir: defaultString(p.Config.TopData.Wiki.TemplatesDir, DefaultTopDataWikiTemplatesDir),
|
TemplatesDir: defaultString(p.Config.TopData.Wiki.TemplatesDir, DefaultTopDataWikiTemplatesDir),
|
||||||
ManualSectionsDir: defaultString(p.Config.TopData.Wiki.ManualSectionsDir, DefaultTopDataWikiManualSectionsDir),
|
ManualSectionsDir: defaultString(p.Config.TopData.Wiki.ManualSectionsDir, DefaultTopDataWikiManualSectionsDir),
|
||||||
TitlePrefixMinLength: defaultInt(p.Config.TopData.Wiki.TitlePrefixMinLength, DefaultTopDataWikiTitlePrefixMinLength),
|
TitlePrefixMinLength: defaultInt(p.Config.TopData.Wiki.TitlePrefixMinLength, DefaultTopDataWikiTitlePrefixMinLength),
|
||||||
@@ -479,6 +481,7 @@ func markMissingDefaults(provenance ConfigProvenance) {
|
|||||||
"topdata.wiki.renderer": DefaultTopDataWikiRenderer,
|
"topdata.wiki.renderer": DefaultTopDataWikiRenderer,
|
||||||
"topdata.wiki.link_strategy": DefaultTopDataWikiLinkStrategy,
|
"topdata.wiki.link_strategy": DefaultTopDataWikiLinkStrategy,
|
||||||
"topdata.wiki.namespaces_file": DefaultTopDataWikiNamespacesFile,
|
"topdata.wiki.namespaces_file": DefaultTopDataWikiNamespacesFile,
|
||||||
|
"topdata.wiki.tables_file": DefaultTopDataWikiTablesFile,
|
||||||
"topdata.wiki.templates_dir": DefaultTopDataWikiTemplatesDir,
|
"topdata.wiki.templates_dir": DefaultTopDataWikiTemplatesDir,
|
||||||
"topdata.wiki.manual_sections_dir": DefaultTopDataWikiManualSectionsDir,
|
"topdata.wiki.manual_sections_dir": DefaultTopDataWikiManualSectionsDir,
|
||||||
"topdata.wiki.title_prefix_min_length": "3",
|
"topdata.wiki.title_prefix_min_length": "3",
|
||||||
|
|||||||
@@ -228,6 +228,7 @@ type TopDataWikiConfig struct {
|
|||||||
Renderer string `json:"renderer" yaml:"renderer"`
|
Renderer string `json:"renderer" yaml:"renderer"`
|
||||||
LinkStrategy string `json:"link_strategy" yaml:"link_strategy"`
|
LinkStrategy string `json:"link_strategy" yaml:"link_strategy"`
|
||||||
NamespacesFile string `json:"namespaces_file" yaml:"namespaces_file"`
|
NamespacesFile string `json:"namespaces_file" yaml:"namespaces_file"`
|
||||||
|
TablesFile string `json:"tables_file" yaml:"tables_file"`
|
||||||
TemplatesDir string `json:"templates_dir" yaml:"templates_dir"`
|
TemplatesDir string `json:"templates_dir" yaml:"templates_dir"`
|
||||||
ManualSectionsDir string `json:"manual_sections_dir" yaml:"manual_sections_dir"`
|
ManualSectionsDir string `json:"manual_sections_dir" yaml:"manual_sections_dir"`
|
||||||
TitlePrefixMinLength int `json:"title_prefix_min_length" yaml:"title_prefix_min_length"`
|
TitlePrefixMinLength int `json:"title_prefix_min_length" yaml:"title_prefix_min_length"`
|
||||||
@@ -514,6 +515,7 @@ func (p *Project) ValidateLayout() error {
|
|||||||
"topdata.wiki.state_file": p.Config.TopData.Wiki.StateFile,
|
"topdata.wiki.state_file": p.Config.TopData.Wiki.StateFile,
|
||||||
"topdata.wiki.deploy_manifest": p.Config.TopData.Wiki.DeployManifest,
|
"topdata.wiki.deploy_manifest": p.Config.TopData.Wiki.DeployManifest,
|
||||||
"topdata.wiki.deploy_edit_summary": p.Config.TopData.Wiki.DeployEditSummary,
|
"topdata.wiki.deploy_edit_summary": p.Config.TopData.Wiki.DeployEditSummary,
|
||||||
|
"topdata.wiki.tables_file": p.Config.TopData.Wiki.TablesFile,
|
||||||
"extract.layout": p.Config.Extract.Layout,
|
"extract.layout": p.Config.Extract.Layout,
|
||||||
"extract.hak_discovery": p.Config.Extract.HAKDiscovery,
|
"extract.hak_discovery": p.Config.Extract.HAKDiscovery,
|
||||||
"autogen.cache.root": p.Config.Autogen.Cache.Root,
|
"autogen.cache.root": p.Config.Autogen.Cache.Root,
|
||||||
@@ -546,6 +548,7 @@ func (p *Project) ValidateLayout() error {
|
|||||||
failures = append(failures, validateRelativePath("topdata.wiki.deploy_manifest", effective.TopData.Wiki.DeployManifest)...)
|
failures = append(failures, validateRelativePath("topdata.wiki.deploy_manifest", effective.TopData.Wiki.DeployManifest)...)
|
||||||
failures = append(failures, validateTreeRootPath("topdata.wiki.source", effective.TopData.Wiki.Source)...)
|
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.namespaces_file", effective.TopData.Wiki.NamespacesFile)...)
|
||||||
|
failures = append(failures, validateRelativePath("topdata.wiki.tables_file", effective.TopData.Wiki.TablesFile)...)
|
||||||
failures = append(failures, validateTreeRootPath("topdata.wiki.templates_dir", effective.TopData.Wiki.TemplatesDir)...)
|
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, validateTreeRootPath("topdata.wiki.manual_sections_dir", effective.TopData.Wiki.ManualSectionsDir)...)
|
||||||
failures = append(failures, validateRelativePath("autogen.cache.root", effective.Autogen.Cache.Root)...)
|
failures = append(failures, validateRelativePath("autogen.cache.root", effective.Autogen.Cache.Root)...)
|
||||||
|
|||||||
@@ -262,6 +262,7 @@ topdata:
|
|||||||
renderer: nodebb_tiptap_html
|
renderer: nodebb_tiptap_html
|
||||||
link_strategy: preserve_westgate_wiki_links
|
link_strategy: preserve_westgate_wiki_links
|
||||||
namespaces_file: namespaces.yaml
|
namespaces_file: namespaces.yaml
|
||||||
|
tables_file: custom-tables.yaml
|
||||||
templates_dir: templates
|
templates_dir: templates
|
||||||
manual_sections_dir: manual-sections
|
manual_sections_dir: manual-sections
|
||||||
deploy_manifest: wiki-manifest.json
|
deploy_manifest: wiki-manifest.json
|
||||||
@@ -319,6 +320,9 @@ autogen:
|
|||||||
if got, want := effective.TopData.Wiki.NamespacesFile, "namespaces.yaml"; got != want {
|
if got, want := effective.TopData.Wiki.NamespacesFile, "namespaces.yaml"; got != want {
|
||||||
t.Fatalf("expected wiki namespaces file %q, got %q", want, got)
|
t.Fatalf("expected wiki namespaces file %q, got %q", want, got)
|
||||||
}
|
}
|
||||||
|
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.TemplatesDir, "templates"; got != want {
|
if got, want := effective.TopData.Wiki.TemplatesDir, "templates"; got != want {
|
||||||
t.Fatalf("expected wiki templates dir %q, got %q", want, got)
|
t.Fatalf("expected wiki templates dir %q, got %q", want, got)
|
||||||
}
|
}
|
||||||
@@ -758,6 +762,7 @@ func TestValidateLayoutRejectsInvalidTopDataWikiConfig(t *testing.T) {
|
|||||||
Renderer: "dokuwiki",
|
Renderer: "dokuwiki",
|
||||||
LinkStrategy: "rewrite_links",
|
LinkStrategy: "rewrite_links",
|
||||||
NamespacesFile: "../namespaces.yaml",
|
NamespacesFile: "../namespaces.yaml",
|
||||||
|
TablesFile: "../tables.yaml",
|
||||||
TemplatesDir: ".",
|
TemplatesDir: ".",
|
||||||
ManualSectionsDir: "../manual",
|
ManualSectionsDir: "../manual",
|
||||||
StalePages: TopDataWikiStalePagesConfig{
|
StalePages: TopDataWikiStalePagesConfig{
|
||||||
@@ -778,6 +783,7 @@ func TestValidateLayoutRejectsInvalidTopDataWikiConfig(t *testing.T) {
|
|||||||
"topdata.wiki.renderer",
|
"topdata.wiki.renderer",
|
||||||
"topdata.wiki.link_strategy",
|
"topdata.wiki.link_strategy",
|
||||||
"topdata.wiki.namespaces_file",
|
"topdata.wiki.namespaces_file",
|
||||||
|
"topdata.wiki.tables_file",
|
||||||
"topdata.wiki.templates_dir",
|
"topdata.wiki.templates_dir",
|
||||||
"topdata.wiki.manual_sections_dir",
|
"topdata.wiki.manual_sections_dir",
|
||||||
"topdata.wiki.stale_pages.default",
|
"topdata.wiki.stale_pages.default",
|
||||||
|
|||||||
@@ -105,11 +105,14 @@ type wikiContext struct {
|
|||||||
classFeatTables map[string]wikiTable
|
classFeatTables map[string]wikiTable
|
||||||
classSkillTables map[string]wikiTable
|
classSkillTables map[string]wikiTable
|
||||||
classBonusTables map[string]wikiTable
|
classBonusTables map[string]wikiTable
|
||||||
|
classSaveTables map[string]wikiTable
|
||||||
raceFeatTables map[string]wikiTable
|
raceFeatTables map[string]wikiTable
|
||||||
statuses map[string]map[string]string
|
statuses map[string]map[string]string
|
||||||
implementedFeats map[string]struct{}
|
implementedFeats map[string]struct{}
|
||||||
manualSections []wikiManualSection
|
manualSections []wikiManualSection
|
||||||
templateDir string
|
templateDir string
|
||||||
|
wikiTablesPath string
|
||||||
|
wikiTableDefinitions map[string]wikiTableDefinition
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildWiki(p *project.Project, nativeResult BuildResult, force bool, progress func(string)) (wikiResult, error) {
|
func buildWiki(p *project.Project, nativeResult BuildResult, force bool, progress func(string)) (wikiResult, error) {
|
||||||
@@ -143,6 +146,12 @@ func buildWiki(p *project.Project, nativeResult BuildResult, force bool, progres
|
|||||||
manualSections := loadWikiManualSections(p)
|
manualSections := loadWikiManualSections(p)
|
||||||
ctx.manualSections = manualSections
|
ctx.manualSections = manualSections
|
||||||
ctx.templateDir = filepath.Join(p.TopDataWikiSourceDir(), filepath.FromSlash(p.EffectiveConfig().TopData.Wiki.TemplatesDir), "pages")
|
ctx.templateDir = filepath.Join(p.TopDataWikiSourceDir(), filepath.FromSlash(p.EffectiveConfig().TopData.Wiki.TemplatesDir), "pages")
|
||||||
|
ctx.wikiTablesPath = filepath.Join(p.TopDataWikiSourceDir(), filepath.FromSlash(p.EffectiveConfig().TopData.Wiki.TablesFile))
|
||||||
|
tableDefinitions, err := loadWikiTableDefinitions(ctx.wikiTablesPath)
|
||||||
|
if err != nil {
|
||||||
|
return wikiResult{}, err
|
||||||
|
}
|
||||||
|
ctx.wikiTableDefinitions = tableDefinitions
|
||||||
|
|
||||||
if err := os.RemoveAll(outputDir); err != nil {
|
if err := os.RemoveAll(outputDir); err != nil {
|
||||||
return wikiResult{}, fmt.Errorf("clean wiki output: %w", err)
|
return wikiResult{}, fmt.Errorf("clean wiki output: %w", err)
|
||||||
@@ -535,6 +544,10 @@ func loadWikiContext(dataDir, sourceDir string) (*wikiContext, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
classSaveTables, err := loadWikiTables(filepath.Join(dataDir, "classes", "savthr"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
ctx := &wikiContext{
|
ctx := &wikiContext{
|
||||||
dialog: dialog,
|
dialog: dialog,
|
||||||
@@ -544,6 +557,7 @@ func loadWikiContext(dataDir, sourceDir string) (*wikiContext, error) {
|
|||||||
classFeatTables: classFeatTables,
|
classFeatTables: classFeatTables,
|
||||||
classSkillTables: classSkillTables,
|
classSkillTables: classSkillTables,
|
||||||
classBonusTables: classBonusTables,
|
classBonusTables: classBonusTables,
|
||||||
|
classSaveTables: classSaveTables,
|
||||||
}
|
}
|
||||||
if ok {
|
if ok {
|
||||||
ctx.featRows, ctx.featIDToKey = rowsByKeyAndID(featDataset.Rows)
|
ctx.featRows, ctx.featIDToKey = rowsByKeyAndID(featDataset.Rows)
|
||||||
@@ -672,6 +686,9 @@ func loadWikiTables(root string) (map[string]wikiTable, error) {
|
|||||||
Rows: rows,
|
Rows: rows,
|
||||||
}
|
}
|
||||||
tables[table.OutputStem] = table
|
tables[table.OutputStem] = table
|
||||||
|
if table.Key != "" {
|
||||||
|
tables[table.Key] = table
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -206,6 +206,176 @@ func TestWikiTemplateMissingFieldFailsUnlessDefaultProvided(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestWikiTemplateRendersConfiguredClassProgressionTable(t *testing.T) {
|
||||||
|
ctx := testWikiTableContext(t, `
|
||||||
|
tables:
|
||||||
|
ClassProgression:
|
||||||
|
source: class_progression
|
||||||
|
class: "wiki-table wiki-class-progression"
|
||||||
|
rows:
|
||||||
|
levels: 1-4
|
||||||
|
when: "{{Level >= 1 && Level <= 4}}"
|
||||||
|
columns:
|
||||||
|
- key: level
|
||||||
|
label: Lvl
|
||||||
|
value: "{{ordinal(Level)}}"
|
||||||
|
- key: bab
|
||||||
|
label: BAB
|
||||||
|
value: "{{BAB}}"
|
||||||
|
- key: saves
|
||||||
|
label: Saves
|
||||||
|
children:
|
||||||
|
- key: fort
|
||||||
|
label: Fort
|
||||||
|
value: "{{FortSave}}"
|
||||||
|
- key: reflex
|
||||||
|
label: Ref
|
||||||
|
value: "{{RefSave}}"
|
||||||
|
- key: will
|
||||||
|
label: Will
|
||||||
|
value: "{{WillSave}}"
|
||||||
|
- key: feats
|
||||||
|
label: Feats
|
||||||
|
value: "{{join(GrantedFeats, \", \")}}"
|
||||||
|
empty: ""
|
||||||
|
allow_wiki_markup: true
|
||||||
|
- key: hp_range
|
||||||
|
label: HP range
|
||||||
|
value: "{{range(HPMin, HPMax)}}"
|
||||||
|
- key: notes
|
||||||
|
label: Notes
|
||||||
|
value: "{{if(Level == 4 && BonusFeat != \"0\", link(\"feat:weapon_specialization\", \"Weapon Specialization\") + \" first available.\", Notes)}}"
|
||||||
|
empty: ""
|
||||||
|
allow_wiki_markup: true
|
||||||
|
`)
|
||||||
|
page := wikiTemplatePage{
|
||||||
|
PageID: "classes:fighter",
|
||||||
|
Category: "classes",
|
||||||
|
Key: "classes:fighter",
|
||||||
|
Title: "Fighter",
|
||||||
|
Row: map[string]any{
|
||||||
|
"AttackBonusTable": "cls_atk_1",
|
||||||
|
"BonusFeatsTable": map[string]any{"table": "classes/bonusfeats:fighter"},
|
||||||
|
"FeatsTable": map[string]any{"table": "classes/feats:fighter"},
|
||||||
|
"HitDie": 10,
|
||||||
|
"SavingThrowTable": map[string]any{"table": "classes/saves:fortitude"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := ctx.renderWikiTemplateString("classes.html", `<h1>{{title}}</h1>
|
||||||
|
{{table:ClassProgression}}`, page)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("render table template: %v", err)
|
||||||
|
}
|
||||||
|
for _, expected := range []string{
|
||||||
|
`<table class="wiki-table wiki-class-progression">`,
|
||||||
|
`<th scope="col" rowspan="2">Lvl</th>`,
|
||||||
|
`<th scope="colgroup" colspan="3">Saves</th>`,
|
||||||
|
`<td>1st</td>`,
|
||||||
|
`<td>+1</td>`,
|
||||||
|
`<td>2</td>`,
|
||||||
|
`<td>[[feat:literate|Literate]], [[feat:weapon:proficiency:simple|Simple Weapon Proficiency]]</td>`,
|
||||||
|
`<td>1-10</td>`,
|
||||||
|
`<td>[[feat:weapon:specialization|Weapon Specialization]] first available.</td>`,
|
||||||
|
} {
|
||||||
|
if !strings.Contains(got, expected) {
|
||||||
|
t.Fatalf("expected %q in rendered table:\n%s", expected, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWikiTableMissingFieldReportsTableAndColumn(t *testing.T) {
|
||||||
|
ctx := testWikiTableContext(t, `
|
||||||
|
tables:
|
||||||
|
Broken:
|
||||||
|
source: class_progression
|
||||||
|
rows:
|
||||||
|
levels: 1
|
||||||
|
columns:
|
||||||
|
- key: missing
|
||||||
|
label: Missing
|
||||||
|
value: "{{NoSuchField}}"
|
||||||
|
`)
|
||||||
|
page := wikiTemplatePage{
|
||||||
|
PageID: "classes:fighter",
|
||||||
|
Category: "classes",
|
||||||
|
Key: "classes:fighter",
|
||||||
|
Row: map[string]any{
|
||||||
|
"AttackBonusTable": "cls_atk_1",
|
||||||
|
"HitDie": 10,
|
||||||
|
"SavingThrowTable": map[string]any{"table": "classes/saves:fortitude"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := ctx.renderWikiTemplateString("classes.html", `{{table:Broken}}`, page)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected missing field error")
|
||||||
|
}
|
||||||
|
for _, expected := range []string{`classes:fighter`, `table Broken`, `column missing`, `NoSuchField`} {
|
||||||
|
if !strings.Contains(err.Error(), expected) {
|
||||||
|
t.Fatalf("expected error to contain %q, got %v", expected, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWikiTableRejectsImpossibleNestedHeader(t *testing.T) {
|
||||||
|
_, err := parseWikiTableDefinitions([]byte(`
|
||||||
|
tables:
|
||||||
|
Broken:
|
||||||
|
source: class_progression
|
||||||
|
rows:
|
||||||
|
levels: 1
|
||||||
|
columns:
|
||||||
|
- key: bad
|
||||||
|
label: Bad
|
||||||
|
value: "{{Level}}"
|
||||||
|
children:
|
||||||
|
- key: child
|
||||||
|
label: Child
|
||||||
|
value: "{{Level}}"
|
||||||
|
`), "test-tables.yaml")
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "cannot have both value and children") {
|
||||||
|
t.Fatalf("expected impossible nested header error, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testWikiTableContext(t *testing.T, yamlSource string) *wikiContext {
|
||||||
|
t.Helper()
|
||||||
|
ctx := &wikiContext{
|
||||||
|
featRows: map[string]map[string]any{},
|
||||||
|
featIDToKey: map[int]string{},
|
||||||
|
classFeatTables: map[string]wikiTable{},
|
||||||
|
classBonusTables: map[string]wikiTable{},
|
||||||
|
classSaveTables: map[string]wikiTable{},
|
||||||
|
wikiTablesPath: "test-tables.yaml",
|
||||||
|
}
|
||||||
|
ctx.featRows["feat:literate"] = map[string]any{"FEAT": map[string]any{"tlk": map[string]any{"text": "Literate"}}}
|
||||||
|
ctx.featRows["feat:weapon_proficiency_simple"] = map[string]any{"FEAT": map[string]any{"tlk": map[string]any{"text": "Simple Weapon Proficiency"}}}
|
||||||
|
ctx.featRows["feat:weapon_specialization"] = map[string]any{"FEAT": map[string]any{"tlk": map[string]any{"text": "Weapon Specialization"}}}
|
||||||
|
ctx.classFeatTables["cls_feat_fight"] = wikiTable{OutputStem: "cls_feat_fight", Rows: []map[string]any{
|
||||||
|
{"FeatIndex": map[string]any{"id": "feat:literate"}, "GrantedOnLevel": "1", "List": "3"},
|
||||||
|
{"FeatIndex": map[string]any{"id": "feat:weapon_proficiency_simple"}, "GrantedOnLevel": "1", "List": "3"},
|
||||||
|
}}
|
||||||
|
ctx.classFeatTables["classes/feats:fighter"] = ctx.classFeatTables["cls_feat_fight"]
|
||||||
|
ctx.classBonusTables["cls_bfeat_fight"] = wikiTable{OutputStem: "cls_bfeat_fight", Rows: []map[string]any{
|
||||||
|
{"id": 1, "Bonus": "1"}, {"id": 2, "Bonus": "1"}, {"id": 3, "Bonus": "0"}, {"id": 4, "Bonus": "1"},
|
||||||
|
}}
|
||||||
|
ctx.classBonusTables["classes/bonusfeats:fighter"] = ctx.classBonusTables["cls_bfeat_fight"]
|
||||||
|
ctx.classSaveTables["cls_savthr_fort"] = wikiTable{OutputStem: "cls_savthr_fort", Rows: []map[string]any{
|
||||||
|
{"Level": "1", "FortSave": "2", "RefSave": "0", "WillSave": "0"},
|
||||||
|
{"Level": "2", "FortSave": "3", "RefSave": "0", "WillSave": "0"},
|
||||||
|
{"Level": "3", "FortSave": "3", "RefSave": "1", "WillSave": "1"},
|
||||||
|
{"Level": "4", "FortSave": "4", "RefSave": "1", "WillSave": "1"},
|
||||||
|
}}
|
||||||
|
ctx.classSaveTables["classes/saves:fortitude"] = ctx.classSaveTables["cls_savthr_fort"]
|
||||||
|
tables, err := parseWikiTableDefinitions([]byte(yamlSource), ctx.wikiTablesPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parse table definitions: %v", err)
|
||||||
|
}
|
||||||
|
ctx.wikiTableDefinitions = tables
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
|
||||||
func TestWikiManagedHashIgnoresPreservedSectionBody(t *testing.T) {
|
func TestWikiManagedHashIgnoresPreservedSectionBody(t *testing.T) {
|
||||||
first := strings.Join([]string{
|
first := strings.Join([]string{
|
||||||
`<!-- sow-topdata-wiki:page=classes:acolyte -->`,
|
`<!-- sow-topdata-wiki:page=classes:acolyte -->`,
|
||||||
|
|||||||
@@ -0,0 +1,957 @@
|
|||||||
|
package topdata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"html"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type wikiTableDefinitionsDocument struct {
|
||||||
|
Tables map[string]wikiTableDefinition `json:"tables" yaml:"tables"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type wikiTableDefinition struct {
|
||||||
|
Source string `json:"source" yaml:"source"`
|
||||||
|
Class string `json:"class" yaml:"class"`
|
||||||
|
When string `json:"when" yaml:"when"`
|
||||||
|
Rows wikiTableRowsConfig `json:"rows" yaml:"rows"`
|
||||||
|
Columns []wikiTableColumn `json:"columns" yaml:"columns"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type wikiTableRowsConfig struct {
|
||||||
|
Levels string `json:"levels" yaml:"levels"`
|
||||||
|
When string `json:"when" yaml:"when"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type wikiTableColumn struct {
|
||||||
|
Key string `json:"key" yaml:"key"`
|
||||||
|
Label string `json:"label" yaml:"label"`
|
||||||
|
Value string `json:"value" yaml:"value"`
|
||||||
|
Empty string `json:"empty" yaml:"empty"`
|
||||||
|
Class string `json:"class" yaml:"class"`
|
||||||
|
HeaderClass string `json:"header_class" yaml:"header_class"`
|
||||||
|
AllowWikiMarkup bool `json:"allow_wiki_markup" yaml:"allow_wiki_markup"`
|
||||||
|
When string `json:"when" yaml:"when"`
|
||||||
|
Children []wikiTableColumn `json:"children" yaml:"children"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type wikiTableRenderContext struct {
|
||||||
|
Page wikiTemplatePage
|
||||||
|
TableName string
|
||||||
|
Path string
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadWikiTableDefinitions(path string) (map[string]wikiTableDefinition, error) {
|
||||||
|
raw, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return map[string]wikiTableDefinition{}, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("read wiki table definitions %s: %w", path, err)
|
||||||
|
}
|
||||||
|
return parseWikiTableDefinitions(raw, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseWikiTableDefinitions(raw []byte, path string) (map[string]wikiTableDefinition, error) {
|
||||||
|
var doc wikiTableDefinitionsDocument
|
||||||
|
if err := yaml.Unmarshal(raw, &doc); err != nil {
|
||||||
|
return nil, fmt.Errorf("parse wiki table definitions %s: %w", path, err)
|
||||||
|
}
|
||||||
|
if len(doc.Tables) == 0 {
|
||||||
|
return map[string]wikiTableDefinition{}, nil
|
||||||
|
}
|
||||||
|
for name, def := range doc.Tables {
|
||||||
|
if strings.TrimSpace(def.Source) == "" {
|
||||||
|
return nil, fmt.Errorf("wiki table definitions %s: table %s missing source", path, name)
|
||||||
|
}
|
||||||
|
if len(def.Columns) == 0 {
|
||||||
|
return nil, fmt.Errorf("wiki table definitions %s: table %s missing columns", path, name)
|
||||||
|
}
|
||||||
|
for _, col := range def.Columns {
|
||||||
|
if err := validateWikiTableColumn(path, name, col); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return doc.Tables, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateWikiTableColumn(path, tableName string, col wikiTableColumn) error {
|
||||||
|
if strings.TrimSpace(col.Key) == "" {
|
||||||
|
return fmt.Errorf("wiki table definitions %s: table %s has column without key", path, tableName)
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(col.Value) != "" && len(col.Children) > 0 {
|
||||||
|
return fmt.Errorf("wiki table definitions %s: table %s column %s cannot have both value and children", path, tableName, col.Key)
|
||||||
|
}
|
||||||
|
for _, child := range col.Children {
|
||||||
|
if err := validateWikiTableColumn(path, tableName, child); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *wikiContext) renderConfiguredWikiTable(templatePath, tableName string, page wikiTemplatePage) (string, error) {
|
||||||
|
def, ok := ctx.wikiTableDefinitions[tableName]
|
||||||
|
if !ok {
|
||||||
|
return "", fmt.Errorf("render wiki template %s for %s: missing table definition %q in %s", templatePath, page.PageID, tableName, ctx.wikiTablesPath)
|
||||||
|
}
|
||||||
|
renderCtx := wikiTableRenderContext{Page: page, TableName: tableName, Path: ctx.wikiTablesPath}
|
||||||
|
if strings.TrimSpace(def.When) != "" {
|
||||||
|
value, err := ctx.evalWikiTableTemplate(def.When, page.Row, renderCtx)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if !truthyWikiTableValue(value) {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rows, err := ctx.wikiTableSourceRows(def, page)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("render wiki table %s for %s: %w", tableName, page.PageID, err)
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(def.Rows.When) != "" {
|
||||||
|
filtered := rows[:0]
|
||||||
|
for _, row := range rows {
|
||||||
|
value, err := ctx.evalWikiTableTemplate(def.Rows.When, row, renderCtx)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("render wiki table %s for %s row condition %q: %w", tableName, page.PageID, def.Rows.When, err)
|
||||||
|
}
|
||||||
|
if truthyWikiTableValue(value) {
|
||||||
|
filtered = append(filtered, row)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rows = filtered
|
||||||
|
}
|
||||||
|
if len(rows) == 0 {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
columns := visibleWikiTableColumns(def.Columns, map[string]any{}, ctx, renderCtx)
|
||||||
|
if len(columns) == 0 {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
headerRows, leaves := buildWikiTableHeaders(columns)
|
||||||
|
if len(leaves) == 0 {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
var out strings.Builder
|
||||||
|
class := strings.TrimSpace(def.Class)
|
||||||
|
if class != "" {
|
||||||
|
out.WriteString(`<table class="` + html.EscapeString(class) + `">`)
|
||||||
|
} else {
|
||||||
|
out.WriteString("<table>")
|
||||||
|
}
|
||||||
|
out.WriteString("\n <thead>\n")
|
||||||
|
for _, headerRow := range headerRows {
|
||||||
|
out.WriteString(" <tr>")
|
||||||
|
for _, cell := range headerRow {
|
||||||
|
out.WriteString(cell)
|
||||||
|
}
|
||||||
|
out.WriteString("</tr>\n")
|
||||||
|
}
|
||||||
|
out.WriteString(" </thead>\n <tbody>\n")
|
||||||
|
for _, row := range rows {
|
||||||
|
out.WriteString(" <tr>")
|
||||||
|
for _, col := range leaves {
|
||||||
|
cell, err := ctx.renderWikiTableCell(col, row, renderCtx)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
out.WriteString(cell)
|
||||||
|
}
|
||||||
|
out.WriteString("</tr>\n")
|
||||||
|
}
|
||||||
|
out.WriteString(" </tbody>\n</table>")
|
||||||
|
return out.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func visibleWikiTableColumns(columns []wikiTableColumn, row map[string]any, ctx *wikiContext, renderCtx wikiTableRenderContext) []wikiTableColumn {
|
||||||
|
out := []wikiTableColumn{}
|
||||||
|
for _, col := range columns {
|
||||||
|
if strings.TrimSpace(col.When) != "" {
|
||||||
|
value, err := ctx.evalWikiTableTemplate(col.When, row, renderCtx)
|
||||||
|
if err != nil || !truthyWikiTableValue(value) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(col.Children) > 0 {
|
||||||
|
col.Children = visibleWikiTableColumns(col.Children, row, ctx, renderCtx)
|
||||||
|
if len(col.Children) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out = append(out, col)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
type wikiTableHeaderCell struct {
|
||||||
|
HTML string
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildWikiTableHeaders(columns []wikiTableColumn) ([][]string, []wikiTableColumn) {
|
||||||
|
hasGroups := false
|
||||||
|
for _, col := range columns {
|
||||||
|
if len(col.Children) > 0 {
|
||||||
|
hasGroups = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
first := []string{}
|
||||||
|
second := []string{}
|
||||||
|
leaves := []wikiTableColumn{}
|
||||||
|
for _, col := range columns {
|
||||||
|
label := html.EscapeString(defaultStringValue(col.Label, col.Key))
|
||||||
|
headerClass := attrClass(col.HeaderClass)
|
||||||
|
if len(col.Children) == 0 {
|
||||||
|
leaves = append(leaves, col)
|
||||||
|
rowspan := ""
|
||||||
|
if hasGroups {
|
||||||
|
rowspan = ` rowspan="2"`
|
||||||
|
}
|
||||||
|
first = append(first, `<th scope="col"`+rowspan+headerClass+`>`+label+`</th>`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
childLeaves := leafWikiTableColumns(col.Children)
|
||||||
|
first = append(first, `<th scope="colgroup" colspan="`+strconv.Itoa(len(childLeaves))+`"`+headerClass+`>`+label+`</th>`)
|
||||||
|
for _, child := range col.Children {
|
||||||
|
childLabel := html.EscapeString(defaultStringValue(child.Label, child.Key))
|
||||||
|
second = append(second, `<th scope="col"`+attrClass(child.HeaderClass)+`>`+childLabel+`</th>`)
|
||||||
|
}
|
||||||
|
leaves = append(leaves, childLeaves...)
|
||||||
|
}
|
||||||
|
if hasGroups {
|
||||||
|
return [][]string{first, second}, leaves
|
||||||
|
}
|
||||||
|
return [][]string{first}, leaves
|
||||||
|
}
|
||||||
|
|
||||||
|
func leafWikiTableColumns(columns []wikiTableColumn) []wikiTableColumn {
|
||||||
|
out := []wikiTableColumn{}
|
||||||
|
for _, col := range columns {
|
||||||
|
if len(col.Children) == 0 {
|
||||||
|
out = append(out, col)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, leafWikiTableColumns(col.Children)...)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *wikiContext) renderWikiTableCell(col wikiTableColumn, row map[string]any, renderCtx wikiTableRenderContext) (string, error) {
|
||||||
|
value, err := ctx.evalWikiTableTemplate(col.Value, row, renderCtx)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("render wiki table %s for %s column %s expression %q: %w", renderCtx.TableName, renderCtx.Page.PageID, col.Key, col.Value, err)
|
||||||
|
}
|
||||||
|
text := stringifyWikiTableValue(value)
|
||||||
|
if strings.TrimSpace(text) == "" {
|
||||||
|
text = col.Empty
|
||||||
|
}
|
||||||
|
if !col.AllowWikiMarkup {
|
||||||
|
text = html.EscapeString(text)
|
||||||
|
}
|
||||||
|
return `<td` + attrClass(col.Class) + `>` + text + `</td>`, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func attrClass(class string) string {
|
||||||
|
class = strings.TrimSpace(class)
|
||||||
|
if class == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return ` class="` + html.EscapeString(class) + `"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultStringValue(value, fallback string) string {
|
||||||
|
if strings.TrimSpace(value) != "" {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *wikiContext) wikiTableSourceRows(def wikiTableDefinition, page wikiTemplatePage) ([]map[string]any, error) {
|
||||||
|
switch strings.TrimSpace(def.Source) {
|
||||||
|
case "class_progression":
|
||||||
|
if page.Category != "classes" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return ctx.classProgressionRows(def, page.Row)
|
||||||
|
case "class_feats":
|
||||||
|
if page.Category != "classes" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return ctx.classFeatRows(page.Row), nil
|
||||||
|
case "spell_progression":
|
||||||
|
if page.Category != "classes" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if !hasSpellProgression(page.Row) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return []map[string]any{}, nil
|
||||||
|
case "class_summary":
|
||||||
|
if page.Category != "classes" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return ctx.classSummaryRows(page.Row), nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unknown source %q", def.Source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *wikiContext) classProgressionRows(def wikiTableDefinition, classRow map[string]any) ([]map[string]any, error) {
|
||||||
|
start, end, err := parseLevelRange(def.Rows.Levels)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
saveRows := map[string]map[string]any{}
|
||||||
|
if table := ctx.tableForValue(fieldValue(classRow, "SavingThrowTable"), ctx.classSaveTables); table != nil {
|
||||||
|
for _, row := range table.Rows {
|
||||||
|
level := stringValue(row, "Level")
|
||||||
|
if level != "" {
|
||||||
|
saveRows[level] = row
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
granted := map[int][]string{}
|
||||||
|
if table := ctx.tableForValue(fieldValue(classRow, "FeatsTable"), ctx.classFeatTables); table != nil {
|
||||||
|
for _, featRow := range table.Rows {
|
||||||
|
level, err := strconv.Atoi(stringValue(featRow, "GrantedOnLevel"))
|
||||||
|
if err != nil || level < start || level > end {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name := ctx.renderReference(fieldValue(featRow, "FeatIndex"), ctx.featIDToKey, ctx.resolveFeatName)
|
||||||
|
if name != "" {
|
||||||
|
granted[level] = append(granted[level], name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bonusByLevel := map[int]string{}
|
||||||
|
if table := ctx.tableForValue(fieldValue(classRow, "BonusFeatsTable"), ctx.classBonusTables); table != nil {
|
||||||
|
for _, bonusRow := range table.Rows {
|
||||||
|
level, err := asInt(fieldValue(bonusRow, "id"))
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
bonusByLevel[level] = stringValue(bonusRow, "Bonus")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hitDie, _ := asInt(fieldValue(classRow, "HitDie"))
|
||||||
|
out := []map[string]any{}
|
||||||
|
for level := start; level <= end; level++ {
|
||||||
|
row := cloneRowMap(classRow)
|
||||||
|
row["Level"] = level
|
||||||
|
row["BAB"] = formatSignedInt(classBABAtLevel(stringValue(classRow, "AttackBonusTable"), level))
|
||||||
|
if saveRow := saveRows[strconv.Itoa(level)]; saveRow != nil {
|
||||||
|
for key, value := range saveRow {
|
||||||
|
row[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
row["GrantedFeats"] = granted[level]
|
||||||
|
row["BonusFeat"] = bonusByLevel[level]
|
||||||
|
row["HPMin"] = level
|
||||||
|
if hitDie > 0 {
|
||||||
|
row["HPMax"] = level * hitDie
|
||||||
|
}
|
||||||
|
row["Notes"] = ""
|
||||||
|
row["HasSpellProgression"] = hasSpellProgression(classRow)
|
||||||
|
out = append(out, row)
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseLevelRange(raw string) (int, int, error) {
|
||||||
|
raw = strings.TrimSpace(raw)
|
||||||
|
if raw == "" {
|
||||||
|
return 1, 20, nil
|
||||||
|
}
|
||||||
|
if !strings.Contains(raw, "-") {
|
||||||
|
level, err := strconv.Atoi(raw)
|
||||||
|
if err != nil || level < 1 {
|
||||||
|
return 0, 0, fmt.Errorf("bad row range %q", raw)
|
||||||
|
}
|
||||||
|
return level, level, nil
|
||||||
|
}
|
||||||
|
left, right, _ := strings.Cut(raw, "-")
|
||||||
|
start, err1 := strconv.Atoi(strings.TrimSpace(left))
|
||||||
|
end, err2 := strconv.Atoi(strings.TrimSpace(right))
|
||||||
|
if err1 != nil || err2 != nil || start < 1 || end < start {
|
||||||
|
return 0, 0, fmt.Errorf("bad row range %q", raw)
|
||||||
|
}
|
||||||
|
return start, end, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func classBABAtLevel(table string, level int) int {
|
||||||
|
switch strings.ToLower(strings.TrimSpace(table)) {
|
||||||
|
case "cls_atk_1":
|
||||||
|
return level
|
||||||
|
case "cls_atk_2":
|
||||||
|
return (level * 3) / 4
|
||||||
|
case "cls_atk_3":
|
||||||
|
return level / 2
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatSignedInt(value int) string {
|
||||||
|
if value > 0 {
|
||||||
|
return "+" + strconv.Itoa(value)
|
||||||
|
}
|
||||||
|
return strconv.Itoa(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *wikiContext) classFeatRows(classRow map[string]any) []map[string]any {
|
||||||
|
table := ctx.tableForValue(fieldValue(classRow, "FeatsTable"), ctx.classFeatTables)
|
||||||
|
if table == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := []map[string]any{}
|
||||||
|
for _, source := range table.Rows {
|
||||||
|
row := cloneRowMap(source)
|
||||||
|
key := resolveReferenceKey(fieldValue(source, "FeatIndex"), ctx.featIDToKey)
|
||||||
|
if key == "" {
|
||||||
|
key = resolveReferenceKey(fieldValue(source, "FeatIndex"), nil)
|
||||||
|
}
|
||||||
|
row["FeatKey"] = key
|
||||||
|
row["FeatName"] = ctx.resolveFeatName(key)
|
||||||
|
out = append(out, row)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *wikiContext) classSummaryRows(classRow map[string]any) []map[string]any {
|
||||||
|
return []map[string]any{
|
||||||
|
{"Label": "Hit Die", "Value": "d" + stringValue(classRow, "HitDie")},
|
||||||
|
{"Label": "Skill Points", "Value": stringValue(classRow, "SkillPointBase")},
|
||||||
|
{"Label": "Primary Ability", "Value": stringValue(classRow, "PrimaryAbil")},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasSpellProgression(row map[string]any) bool {
|
||||||
|
return stringValue(row, "SpellGainTable") != "" || stringValue(row, "SpellKnownTable") != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *wikiContext) evalWikiTableTemplate(template string, row map[string]any, renderCtx wikiTableRenderContext) (any, error) {
|
||||||
|
template = strings.TrimSpace(template)
|
||||||
|
if strings.HasPrefix(template, "{{") && strings.HasSuffix(template, "}}") {
|
||||||
|
expr := strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(template, "{{"), "}}"))
|
||||||
|
return evalWikiTableExpression(expr, row, ctx, renderCtx)
|
||||||
|
}
|
||||||
|
if strings.Contains(template, "{{") {
|
||||||
|
return nil, fmt.Errorf("mixed literal/expression templates are not supported")
|
||||||
|
}
|
||||||
|
return template, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func evalWikiTableExpression(expr string, row map[string]any, ctx *wikiContext, renderCtx wikiTableRenderContext) (any, error) {
|
||||||
|
parser := wikiExprParser{tokens: tokenizeWikiExpr(expr), row: row, ctx: ctx, renderCtx: renderCtx}
|
||||||
|
value, err := parser.parseComparison()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if parser.peek().kind != wikiExprEOF {
|
||||||
|
return nil, fmt.Errorf("unexpected token %q", parser.peek().text)
|
||||||
|
}
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type wikiExprTokenKind int
|
||||||
|
|
||||||
|
const (
|
||||||
|
wikiExprEOF wikiExprTokenKind = iota
|
||||||
|
wikiExprIdent
|
||||||
|
wikiExprString
|
||||||
|
wikiExprNumber
|
||||||
|
wikiExprLParen
|
||||||
|
wikiExprRParen
|
||||||
|
wikiExprComma
|
||||||
|
wikiExprPlus
|
||||||
|
wikiExprEq
|
||||||
|
wikiExprNE
|
||||||
|
wikiExprGT
|
||||||
|
wikiExprGE
|
||||||
|
wikiExprLT
|
||||||
|
wikiExprLE
|
||||||
|
wikiExprAnd
|
||||||
|
wikiExprOr
|
||||||
|
)
|
||||||
|
|
||||||
|
type wikiExprToken struct {
|
||||||
|
kind wikiExprTokenKind
|
||||||
|
text string
|
||||||
|
}
|
||||||
|
|
||||||
|
func tokenizeWikiExpr(expr string) []wikiExprToken {
|
||||||
|
tokens := []wikiExprToken{}
|
||||||
|
for i := 0; i < len(expr); {
|
||||||
|
r := rune(expr[i])
|
||||||
|
if unicode.IsSpace(r) {
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch expr[i] {
|
||||||
|
case '(':
|
||||||
|
tokens = append(tokens, wikiExprToken{kind: wikiExprLParen, text: "("})
|
||||||
|
i++
|
||||||
|
case ')':
|
||||||
|
tokens = append(tokens, wikiExprToken{kind: wikiExprRParen, text: ")"})
|
||||||
|
i++
|
||||||
|
case ',':
|
||||||
|
tokens = append(tokens, wikiExprToken{kind: wikiExprComma, text: ","})
|
||||||
|
i++
|
||||||
|
case '+':
|
||||||
|
tokens = append(tokens, wikiExprToken{kind: wikiExprPlus, text: "+"})
|
||||||
|
i++
|
||||||
|
case '>':
|
||||||
|
if i+1 < len(expr) && expr[i+1] == '=' {
|
||||||
|
tokens = append(tokens, wikiExprToken{kind: wikiExprGE, text: ">="})
|
||||||
|
i += 2
|
||||||
|
} else {
|
||||||
|
tokens = append(tokens, wikiExprToken{kind: wikiExprGT, text: ">"})
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
case '<':
|
||||||
|
if i+1 < len(expr) && expr[i+1] == '=' {
|
||||||
|
tokens = append(tokens, wikiExprToken{kind: wikiExprLE, text: "<="})
|
||||||
|
i += 2
|
||||||
|
} else {
|
||||||
|
tokens = append(tokens, wikiExprToken{kind: wikiExprLT, text: "<"})
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
case '!':
|
||||||
|
if i+1 < len(expr) && expr[i+1] == '=' {
|
||||||
|
tokens = append(tokens, wikiExprToken{kind: wikiExprNE, text: "!="})
|
||||||
|
i += 2
|
||||||
|
} else {
|
||||||
|
tokens = append(tokens, wikiExprToken{kind: wikiExprEOF, text: expr[i:]})
|
||||||
|
return tokens
|
||||||
|
}
|
||||||
|
case '&':
|
||||||
|
if i+1 < len(expr) && expr[i+1] == '&' {
|
||||||
|
tokens = append(tokens, wikiExprToken{kind: wikiExprAnd, text: "&&"})
|
||||||
|
i += 2
|
||||||
|
} else {
|
||||||
|
tokens = append(tokens, wikiExprToken{kind: wikiExprEOF, text: expr[i:]})
|
||||||
|
return tokens
|
||||||
|
}
|
||||||
|
case '|':
|
||||||
|
if i+1 < len(expr) && expr[i+1] == '|' {
|
||||||
|
tokens = append(tokens, wikiExprToken{kind: wikiExprOr, text: "||"})
|
||||||
|
i += 2
|
||||||
|
} else {
|
||||||
|
tokens = append(tokens, wikiExprToken{kind: wikiExprEOF, text: expr[i:]})
|
||||||
|
return tokens
|
||||||
|
}
|
||||||
|
case '=':
|
||||||
|
if i+1 < len(expr) && expr[i+1] == '=' {
|
||||||
|
tokens = append(tokens, wikiExprToken{kind: wikiExprEq, text: "=="})
|
||||||
|
i += 2
|
||||||
|
} else {
|
||||||
|
tokens = append(tokens, wikiExprToken{kind: wikiExprEOF, text: expr[i:]})
|
||||||
|
return tokens
|
||||||
|
}
|
||||||
|
case '"':
|
||||||
|
j := i + 1
|
||||||
|
var b strings.Builder
|
||||||
|
for ; j < len(expr); j++ {
|
||||||
|
if expr[j] == '\\' && j+1 < len(expr) {
|
||||||
|
j++
|
||||||
|
b.WriteByte(expr[j])
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if expr[j] == '"' {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
b.WriteByte(expr[j])
|
||||||
|
}
|
||||||
|
if j >= len(expr) {
|
||||||
|
tokens = append(tokens, wikiExprToken{kind: wikiExprEOF, text: expr[i:]})
|
||||||
|
return tokens
|
||||||
|
}
|
||||||
|
tokens = append(tokens, wikiExprToken{kind: wikiExprString, text: b.String()})
|
||||||
|
i = j + 1
|
||||||
|
default:
|
||||||
|
if unicode.IsDigit(r) || expr[i] == '-' {
|
||||||
|
j := i + 1
|
||||||
|
for j < len(expr) && unicode.IsDigit(rune(expr[j])) {
|
||||||
|
j++
|
||||||
|
}
|
||||||
|
tokens = append(tokens, wikiExprToken{kind: wikiExprNumber, text: expr[i:j]})
|
||||||
|
i = j
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if unicode.IsLetter(r) || expr[i] == '_' {
|
||||||
|
j := i + 1
|
||||||
|
for j < len(expr) {
|
||||||
|
rr := rune(expr[j])
|
||||||
|
if !unicode.IsLetter(rr) && !unicode.IsDigit(rr) && expr[j] != '_' && expr[j] != '.' {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
j++
|
||||||
|
}
|
||||||
|
tokens = append(tokens, wikiExprToken{kind: wikiExprIdent, text: expr[i:j]})
|
||||||
|
i = j
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
tokens = append(tokens, wikiExprToken{kind: wikiExprEOF, text: expr[i:]})
|
||||||
|
return tokens
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tokens = append(tokens, wikiExprToken{kind: wikiExprEOF})
|
||||||
|
return tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
type wikiExprParser struct {
|
||||||
|
tokens []wikiExprToken
|
||||||
|
pos int
|
||||||
|
row map[string]any
|
||||||
|
ctx *wikiContext
|
||||||
|
renderCtx wikiTableRenderContext
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *wikiExprParser) peek() wikiExprToken {
|
||||||
|
if p.pos >= len(p.tokens) {
|
||||||
|
return wikiExprToken{kind: wikiExprEOF}
|
||||||
|
}
|
||||||
|
return p.tokens[p.pos]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *wikiExprParser) take(kind wikiExprTokenKind) (wikiExprToken, bool) {
|
||||||
|
token := p.peek()
|
||||||
|
if token.kind != kind {
|
||||||
|
return token, false
|
||||||
|
}
|
||||||
|
p.pos++
|
||||||
|
return token, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *wikiExprParser) parseComparison() (any, error) {
|
||||||
|
return p.parseOr()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *wikiExprParser) parseOr() (any, error) {
|
||||||
|
left, err := p.parseAnd()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for p.peek().kind == wikiExprOr {
|
||||||
|
p.pos++
|
||||||
|
right, err := p.parseAnd()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
left = truthyWikiTableValue(left) || truthyWikiTableValue(right)
|
||||||
|
}
|
||||||
|
return left, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *wikiExprParser) parseAnd() (any, error) {
|
||||||
|
left, err := p.parseRelational()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for p.peek().kind == wikiExprAnd {
|
||||||
|
p.pos++
|
||||||
|
right, err := p.parseRelational()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
left = truthyWikiTableValue(left) && truthyWikiTableValue(right)
|
||||||
|
}
|
||||||
|
return left, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *wikiExprParser) parseRelational() (any, error) {
|
||||||
|
left, err := p.parseConcat()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
switch p.peek().kind {
|
||||||
|
case wikiExprEq, wikiExprNE, wikiExprGT, wikiExprGE, wikiExprLT, wikiExprLE:
|
||||||
|
op := p.peek().kind
|
||||||
|
p.pos++
|
||||||
|
right, err := p.parseConcat()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
switch op {
|
||||||
|
case wikiExprEq:
|
||||||
|
return stringifyWikiTableValue(left) == stringifyWikiTableValue(right), nil
|
||||||
|
case wikiExprNE:
|
||||||
|
return stringifyWikiTableValue(left) != stringifyWikiTableValue(right), nil
|
||||||
|
case wikiExprGT:
|
||||||
|
return numericWikiTableValue(left) > numericWikiTableValue(right), nil
|
||||||
|
case wikiExprGE:
|
||||||
|
return numericWikiTableValue(left) >= numericWikiTableValue(right), nil
|
||||||
|
case wikiExprLT:
|
||||||
|
return numericWikiTableValue(left) < numericWikiTableValue(right), nil
|
||||||
|
case wikiExprLE:
|
||||||
|
return numericWikiTableValue(left) <= numericWikiTableValue(right), nil
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
default:
|
||||||
|
return left, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *wikiExprParser) parseConcat() (any, error) {
|
||||||
|
left, err := p.parsePrimary()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for p.peek().kind == wikiExprPlus {
|
||||||
|
p.pos++
|
||||||
|
right, err := p.parsePrimary()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
left = stringifyWikiTableValue(left) + stringifyWikiTableValue(right)
|
||||||
|
}
|
||||||
|
return left, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *wikiExprParser) parsePrimary() (any, error) {
|
||||||
|
token := p.peek()
|
||||||
|
switch token.kind {
|
||||||
|
case wikiExprString:
|
||||||
|
p.pos++
|
||||||
|
return token.text, nil
|
||||||
|
case wikiExprNumber:
|
||||||
|
p.pos++
|
||||||
|
value, err := strconv.Atoi(token.text)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid number %q", token.text)
|
||||||
|
}
|
||||||
|
return value, nil
|
||||||
|
case wikiExprIdent:
|
||||||
|
p.pos++
|
||||||
|
if p.peek().kind == wikiExprLParen {
|
||||||
|
p.pos++
|
||||||
|
args := []any{}
|
||||||
|
if p.peek().kind != wikiExprRParen {
|
||||||
|
for {
|
||||||
|
arg, err := p.parseComparison()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
args = append(args, arg)
|
||||||
|
if _, ok := p.take(wikiExprComma); !ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, ok := p.take(wikiExprRParen); !ok {
|
||||||
|
return nil, fmt.Errorf("missing closing parenthesis for %s", token.text)
|
||||||
|
}
|
||||||
|
return p.callHelper(token.text, args)
|
||||||
|
}
|
||||||
|
return p.resolveField(token.text)
|
||||||
|
case wikiExprLParen:
|
||||||
|
p.pos++
|
||||||
|
value, err := p.parseComparison()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, ok := p.take(wikiExprRParen); !ok {
|
||||||
|
return nil, fmt.Errorf("missing closing parenthesis")
|
||||||
|
}
|
||||||
|
return value, nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unexpected token %q", token.text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *wikiExprParser) resolveField(name string) (any, error) {
|
||||||
|
name = strings.TrimPrefix(name, "row.")
|
||||||
|
if name == "HasSpellProgression" {
|
||||||
|
return hasSpellProgression(p.renderCtx.Page.Row), 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
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("missing field %q", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *wikiExprParser) callHelper(name string, args []any) (any, error) {
|
||||||
|
switch name {
|
||||||
|
case "ordinal":
|
||||||
|
if len(args) != 1 {
|
||||||
|
return nil, fmt.Errorf("ordinal expects 1 argument")
|
||||||
|
}
|
||||||
|
return ordinalWikiTableValue(numericWikiTableValue(args[0])), nil
|
||||||
|
case "range":
|
||||||
|
if len(args) != 2 {
|
||||||
|
return nil, fmt.Errorf("range expects 2 arguments")
|
||||||
|
}
|
||||||
|
left := stringifyWikiTableValue(args[0])
|
||||||
|
right := stringifyWikiTableValue(args[1])
|
||||||
|
if left == "" || right == "" {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
return left + "-" + right, nil
|
||||||
|
case "link":
|
||||||
|
if len(args) == 1 {
|
||||||
|
key := stringifyWikiTableValue(args[0])
|
||||||
|
if pageID := wikiPageIDForKey(key); pageID != "" {
|
||||||
|
key = pageID
|
||||||
|
}
|
||||||
|
return "[[" + key + "]]", nil
|
||||||
|
}
|
||||||
|
if len(args) == 2 {
|
||||||
|
key := stringifyWikiTableValue(args[0])
|
||||||
|
label := stringifyWikiTableValue(args[1])
|
||||||
|
if key == "" {
|
||||||
|
return label, nil
|
||||||
|
}
|
||||||
|
if pageID := wikiPageIDForKey(key); pageID != "" {
|
||||||
|
key = pageID
|
||||||
|
}
|
||||||
|
return "[[" + key + "|" + label + "]]", nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("link expects 1 or 2 arguments")
|
||||||
|
case "if":
|
||||||
|
if len(args) != 3 {
|
||||||
|
return nil, fmt.Errorf("if expects 3 arguments")
|
||||||
|
}
|
||||||
|
if truthyWikiTableValue(args[0]) {
|
||||||
|
return args[1], nil
|
||||||
|
}
|
||||||
|
return args[2], nil
|
||||||
|
case "join":
|
||||||
|
if len(args) != 2 {
|
||||||
|
return nil, fmt.Errorf("join expects 2 arguments")
|
||||||
|
}
|
||||||
|
sep := stringifyWikiTableValue(args[1])
|
||||||
|
switch typed := args[0].(type) {
|
||||||
|
case []string:
|
||||||
|
return strings.Join(typed, sep), nil
|
||||||
|
case []any:
|
||||||
|
parts := make([]string, 0, len(typed))
|
||||||
|
for _, value := range typed {
|
||||||
|
if text := stringifyWikiTableValue(value); text != "" {
|
||||||
|
parts = append(parts, text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Join(parts, sep), nil
|
||||||
|
default:
|
||||||
|
return stringifyWikiTableValue(args[0]), nil
|
||||||
|
}
|
||||||
|
case "nonzero":
|
||||||
|
if len(args) != 1 {
|
||||||
|
return nil, fmt.Errorf("nonzero expects 1 argument")
|
||||||
|
}
|
||||||
|
if numericWikiTableValue(args[0]) == 0 {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
return args[0], nil
|
||||||
|
case "eq":
|
||||||
|
if len(args) != 2 {
|
||||||
|
return nil, fmt.Errorf("eq expects 2 arguments")
|
||||||
|
}
|
||||||
|
return stringifyWikiTableValue(args[0]) == stringifyWikiTableValue(args[1]), nil
|
||||||
|
case "field":
|
||||||
|
if len(args) != 1 {
|
||||||
|
return nil, fmt.Errorf("field expects 1 argument")
|
||||||
|
}
|
||||||
|
return p.resolveField(stringifyWikiTableValue(args[0]))
|
||||||
|
case "default":
|
||||||
|
if len(args) != 2 {
|
||||||
|
return nil, fmt.Errorf("default expects 2 arguments")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(stringifyWikiTableValue(args[0])) == "" {
|
||||||
|
return args[1], nil
|
||||||
|
}
|
||||||
|
return args[0], nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unknown helper %q", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ordinalWikiTableValue(value int) string {
|
||||||
|
suffix := "th"
|
||||||
|
if value%100 < 11 || value%100 > 13 {
|
||||||
|
switch value % 10 {
|
||||||
|
case 1:
|
||||||
|
suffix = "st"
|
||||||
|
case 2:
|
||||||
|
suffix = "nd"
|
||||||
|
case 3:
|
||||||
|
suffix = "rd"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strconv.Itoa(value) + suffix
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringifyWikiTableValue(value any) string {
|
||||||
|
switch typed := value.(type) {
|
||||||
|
case nil:
|
||||||
|
return ""
|
||||||
|
case string:
|
||||||
|
if typed == nullValue {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return typed
|
||||||
|
case bool:
|
||||||
|
if typed {
|
||||||
|
return "true"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
case int:
|
||||||
|
return strconv.Itoa(typed)
|
||||||
|
case float64:
|
||||||
|
return strconv.Itoa(int(typed))
|
||||||
|
case []string:
|
||||||
|
return strings.Join(typed, ", ")
|
||||||
|
case []any:
|
||||||
|
parts := make([]string, 0, len(typed))
|
||||||
|
for _, value := range typed {
|
||||||
|
if text := stringifyWikiTableValue(value); text != "" {
|
||||||
|
parts = append(parts, text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Join(parts, ", ")
|
||||||
|
default:
|
||||||
|
return fmt.Sprint(typed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func numericWikiTableValue(value any) int {
|
||||||
|
switch typed := value.(type) {
|
||||||
|
case int:
|
||||||
|
return typed
|
||||||
|
case float64:
|
||||||
|
return int(typed)
|
||||||
|
case string:
|
||||||
|
parsed, _ := strconv.Atoi(strings.TrimSpace(typed))
|
||||||
|
return parsed
|
||||||
|
default:
|
||||||
|
parsed, _ := strconv.Atoi(strings.TrimSpace(stringifyWikiTableValue(value)))
|
||||||
|
return parsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func truthyWikiTableValue(value any) bool {
|
||||||
|
switch typed := value.(type) {
|
||||||
|
case bool:
|
||||||
|
return typed
|
||||||
|
case nil:
|
||||||
|
return false
|
||||||
|
case string:
|
||||||
|
trimmed := strings.TrimSpace(strings.ToLower(typed))
|
||||||
|
return trimmed != "" && trimmed != "0" && trimmed != "false" && trimmed != nullValue
|
||||||
|
case int:
|
||||||
|
return typed != 0
|
||||||
|
case float64:
|
||||||
|
return typed != 0
|
||||||
|
default:
|
||||||
|
return strings.TrimSpace(stringifyWikiTableValue(value)) != ""
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -131,6 +131,10 @@ func (ctx *wikiContext) renderWikiTemplateToken(path, token string, page wikiTem
|
|||||||
rendered, err := ctx.renderWikiFormatter(strings.TrimSpace(strings.TrimPrefix(token, "format:")), page)
|
rendered, err := ctx.renderWikiFormatter(strings.TrimSpace(strings.TrimPrefix(token, "format:")), page)
|
||||||
return rendered, "", err
|
return rendered, "", err
|
||||||
}
|
}
|
||||||
|
if strings.HasPrefix(token, "table:") {
|
||||||
|
rendered, err := ctx.renderConfiguredWikiTable(path, strings.TrimSpace(strings.TrimPrefix(token, "table:")), page)
|
||||||
|
return rendered, "", err
|
||||||
|
}
|
||||||
if strings.HasPrefix(token, "field:") {
|
if strings.HasPrefix(token, "field:") {
|
||||||
rendered, err := ctx.renderExplicitWikiField(path, strings.TrimSpace(strings.TrimPrefix(token, "field:")), page)
|
rendered, err := ctx.renderExplicitWikiField(path, strings.TrimSpace(strings.TrimPrefix(token, "field:")), page)
|
||||||
return rendered, "", err
|
return rendered, "", err
|
||||||
|
|||||||
Reference in New Issue
Block a user