package topdata import ( "fmt" "html" "strings" ) func (ctx *wikiContext) renderWikiFormatter(spec string, page wikiTemplatePage) (string, error) { name := strings.Fields(spec) if len(name) == 0 { return "", fmt.Errorf("render wiki template for %s: empty formatter", page.PageID) } switch name[0] { case "Description": return renderWikiTemplatePlainHTML(ctx.resolveRowDescription(page.Category, page.Row)), nil case "FactsTable": return ctx.renderFactsHTML(page.Category, page.Key, page.Row), nil case "Prerequisites": return renderWikiFactTableHTML([]wikiHTMLFact{{Label: "Prerequisites", Value: ctx.renderFeatPrerequisites(page.Row)}}), nil case "SkillList": if page.Category != "classes" { return "", nil } return ctx.renderClassSkillListHTML(page.Row), nil case "ClassFeatureTable": if page.Category != "classes" { return "", nil } return ctx.renderClassFeatureTableHTML(page.Row), nil case "FeatProgression": if page.Category != "classes" { return "", nil } return ctx.renderClassFeatProgressionHTML(page.Row), nil case "SpellTable", "SavesProgression", "BABProgression": return "", nil case "RaceFeatList": return renderWikiFactTableHTML([]wikiHTMLFact{{Label: "Racial Feats", Value: ctx.renderRaceFeatList(page.Row)}}), nil case "StatusCallout": return buildWikiStatusBlock(page.Status, page.Category), nil case "RaceNameForms": return ctx.renderRaceNameFormsHTML(page.Row), nil case "UserBottomFallback": return "", nil default: return "", fmt.Errorf("render wiki template for %s: unknown formatter %q", page.PageID, name[0]) } } type wikiHTMLFact struct { Label string Value string } func (ctx *wikiContext) renderFactsHTML(category, key string, row map[string]any) string { facts := []wikiHTMLFact{} add := func(label, value string) { if strings.TrimSpace(value) != "" { facts = append(facts, wikiHTMLFact{Label: label, Value: value}) } } switch category { case "feat": for _, fact := range ctx.renderFeatFactsHTML(row) { add(fact.Label, fact.Value) } case "skills": add("Key Ability", stringValue(row, "KeyAbility")) add("Untrained", yesNoValue(stringValue(row, "Untrained"))) add("Armor Check Penalty", yesNoValue(stringValue(row, "ArmorCheckPenalty"))) case "spells": add("Constant", stringValue(row, "Constant")) case "baseitems": add("Item Class", stringValue(row, "ItemClass")) add("Weapon Type", stringValue(row, "WeaponType")) add("Required Feats", ctx.renderReferenceList(ctx.wikiReqFeats(row))) add("Base Item Stats", ctx.resolveTextValue(fieldValue(row, "BaseItemStatRef"), nil)) case "classes": add("Hit Die", "d"+stringValue(row, "HitDie")) add("Skill Points", stringValue(row, "SkillPointBase")) add("Primary Ability", stringValue(row, "PrimaryAbil")) case "racialtypes": add("Ability Adjustments", formatAbilityAdjustments(row)) add("Favored Class", ctx.renderReference(fieldValue(row, "Favored"), ctx.classIDToKey, ctx.resolveClassName)) add("Favored Enemy", ctx.renderReference(fieldValue(row, "FavoredEnemyFeat"), ctx.featIDToKey, ctx.resolveFeatName)) add("Racial Feats", ctx.renderRaceFeatList(row)) } _ = key return renderWikiFactTableHTML(facts) } func (ctx *wikiContext) renderFeatFactsHTML(row map[string]any) []wikiHTMLFact { return []wikiHTMLFact{ {Label: "Prerequisites", Value: ctx.renderFeatPrerequisites(row)}, {Label: "Minimum Attack Bonus", Value: stringValue(row, "MINATTACKBONUS")}, {Label: "Minimum Spell Level", Value: stringValue(row, "MINSPELLLVL")}, {Label: "Minimum Fortitude Save", Value: stringValue(row, "MinFortSave")}, } } func renderWikiFactTableHTML(facts []wikiHTMLFact) string { rows := []string{} for _, fact := range facts { if strings.TrimSpace(fact.Value) == "" { continue } rows = append(rows, ``+html.EscapeString(fact.Label)+``+renderMultilineInlineHTML(fact.Value)+``) } if len(rows) == 0 { return "" } return `` + strings.Join(rows, "\n") + `
` } func renderMultilineInlineHTML(text string) string { lines := []string{} for _, line := range strings.Split(strings.ReplaceAll(strings.TrimSpace(text), "\r\n", "\n"), "\n") { line = strings.TrimSpace(line) if line != "" { lines = append(lines, renderInlineHTML(line)) } } return strings.Join(lines, "
") } func renderWikiListHTML(class string, items []string) string { rendered := []string{} for _, item := range items { if strings.TrimSpace(item) != "" { rendered = append(rendered, "
  • "+renderInlineHTML(item)+"
  • ") } } if len(rendered) == 0 { return "" } classAttr := "" if class != "" { classAttr = ` class="` + html.EscapeString(class) + `"` } return "" + strings.Join(rendered, "\n") + "" } func (ctx *wikiContext) renderClassSkillList(row map[string]any) string { table := ctx.tableForValue(fieldValue(row, "SkillsTable"), ctx.classSkillTables) if table == nil { return "" } skills := []string{} for _, skillRow := range table.Rows { if stringValue(skillRow, "ClassSkill") != "1" { continue } name := ctx.renderReference(fieldValue(skillRow, "SkillIndex"), ctx.skillIDToKey, ctx.resolveSkillName) if name != "" { skills = append(skills, name) } } if len(skills) == 0 { return "" } return "==== Class Skills ====\n\n * " + strings.Join(skills, ", ") } func (ctx *wikiContext) renderClassSkillListHTML(row map[string]any) string { table := ctx.tableForValue(fieldValue(row, "SkillsTable"), ctx.classSkillTables) if table == nil { return "" } skills := []string{} for _, skillRow := range table.Rows { if stringValue(skillRow, "ClassSkill") != "1" { continue } name := ctx.renderReference(fieldValue(skillRow, "SkillIndex"), ctx.skillIDToKey, ctx.resolveSkillName) if name != "" { skills = append(skills, name) } } if len(skills) == 0 { return "" } return `

    Class Skills

    ` + renderWikiListHTML("wiki-list wiki-class-skills", skills) } func (ctx *wikiContext) renderClassFeatProgression(row map[string]any) string { table := ctx.tableForValue(fieldValue(row, "FeatsTable"), ctx.classFeatTables) if table == nil { return "" } lines := []string{} byLevel := map[string][]string{} selectable := []string{} for _, featRow := range table.Rows { name := ctx.renderReference(fieldValue(featRow, "FeatIndex"), ctx.featIDToKey, ctx.resolveFeatName) if name == "" { name = ctx.renderReference(fieldValue(featRow, "FeatIndex"), nil, func(string) string { return "" }) } if name == "" { continue } level := stringValue(featRow, "GrantedOnLevel") if level == "" || strings.HasPrefix(level, "-") { selectable = append(selectable, name) continue } byLevel[level] = append(byLevel[level], name) } if len(byLevel) > 0 { lines = append(lines, "==== Granted Feats ====", "") for _, level := range sortedKeys(byLevel) { lines = append(lines, " * Level "+level+": "+strings.Join(byLevel[level], ", ")) } lines = append(lines, "") } if len(selectable) > 0 { lines = append(lines, "==== Selectable Feats ====", "", " * "+strings.Join(selectable, ", "), "") } return strings.TrimSpace(strings.Join(lines, "\n")) } func (ctx *wikiContext) renderClassFeatProgressionHTML(row map[string]any) string { table := ctx.tableForValue(fieldValue(row, "FeatsTable"), ctx.classFeatTables) if table == nil { return "" } byLevel := map[string][]string{} selectable := []string{} for _, featRow := range table.Rows { name := ctx.renderReference(fieldValue(featRow, "FeatIndex"), ctx.featIDToKey, ctx.resolveFeatName) if name == "" { name = ctx.renderReference(fieldValue(featRow, "FeatIndex"), nil, func(string) string { return "" }) } if name == "" { continue } level := stringValue(featRow, "GrantedOnLevel") if level == "" || strings.HasPrefix(level, "-") { selectable = append(selectable, name) continue } byLevel[level] = append(byLevel[level], name) } parts := []string{} if len(byLevel) > 0 { items := []string{} for _, level := range sortedKeys(byLevel) { items = append(items, "Level "+level+": "+strings.Join(byLevel[level], ", ")) } parts = append(parts, `

    Granted Feats

    `+renderWikiListHTML("wiki-list wiki-class-granted-feats", items)) } if len(selectable) > 0 { parts = append(parts, `

    Selectable Feats

    `+renderWikiListHTML("wiki-list wiki-class-selectable-feats", []string{strings.Join(selectable, ", ")})) } return strings.Join(parts, "\n") } func (ctx *wikiContext) renderClassFeatureTable(row map[string]any) string { table := ctx.tableForValue(fieldValue(row, "BonusFeatsTable"), ctx.classBonusTables) if table == nil || len(table.Rows) == 0 { return "" } return "==== Bonus Feats ====\n\n * Bonus feat table: " + table.OutputStem } func (ctx *wikiContext) renderClassFeatureTableHTML(row map[string]any) string { table := ctx.tableForValue(fieldValue(row, "BonusFeatsTable"), ctx.classBonusTables) if table == nil || len(table.Rows) == 0 { return "" } return `

    Bonus Feats

    ` + renderWikiListHTML("wiki-list wiki-class-bonus-feats", []string{"Bonus feat table: " + table.OutputStem}) } func (ctx *wikiContext) renderRaceNameFormsHTML(row map[string]any) string { facts := []wikiHTMLFact{} if plural := ctx.resolveTextValue(fieldValue(row, "NamePlural"), nil); plural != "" { facts = append(facts, wikiHTMLFact{Label: "Plural", Value: plural}) } if converted := ctx.resolveTextValue(fieldValue(row, "ConverName"), nil); converted != "" { facts = append(facts, wikiHTMLFact{Label: "Converted Name", Value: converted}) } if lower := ctx.resolveTextValue(fieldValue(row, "ConverNameLower"), nil); lower != "" { facts = append(facts, wikiHTMLFact{Label: "Lower Name", Value: lower}) } return renderWikiFactTableHTML(facts) }