228 lines
6.4 KiB
Go
228 lines
6.4 KiB
Go
package topdata
|
|
|
|
import (
|
|
"fmt"
|
|
"html"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
type wikiTemplatePage struct {
|
|
PageID string
|
|
Category string
|
|
Key string
|
|
Title string
|
|
Status string
|
|
Row map[string]any
|
|
ManualSections []wikiManualSection
|
|
}
|
|
|
|
func (ctx *wikiContext) renderWikiPageTemplate(category string, page wikiTemplatePage) (string, error) {
|
|
name := wikiTemplateName(category)
|
|
source, path := ctx.loadWikiTemplateSource(name)
|
|
if source == "" && name != "default.html" {
|
|
source, path = ctx.loadWikiTemplateSource("default.html")
|
|
}
|
|
if source == "" {
|
|
source = builtinWikiTemplate(category)
|
|
path = "builtin:" + name
|
|
}
|
|
return ctx.renderWikiTemplateString(path, source, page)
|
|
}
|
|
|
|
func wikiTemplateName(category string) string {
|
|
switch category {
|
|
case "classes", "feat", "skills", "spells", "racialtypes", "baseitems":
|
|
return category + ".html"
|
|
default:
|
|
return "default.html"
|
|
}
|
|
}
|
|
|
|
func (ctx *wikiContext) loadWikiTemplateSource(name string) (string, string) {
|
|
if strings.TrimSpace(ctx.templateDir) == "" {
|
|
return "", ""
|
|
}
|
|
path := filepath.Join(ctx.templateDir, filepath.FromSlash(name))
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return "", path
|
|
}
|
|
return string(raw), path
|
|
}
|
|
|
|
func builtinWikiTemplate(category string) string {
|
|
switch category {
|
|
case "classes":
|
|
return `<h1>{{title}}</h1>
|
|
{{UserTopSection}}
|
|
{{format:StatusCallout}}
|
|
{{format:Description}}
|
|
{{format:FactsTable}}
|
|
{{format:SkillList}}
|
|
{{format:FeatProgression}}
|
|
{{format:UserBottomFallback}}`
|
|
case "racialtypes":
|
|
return `<h1>{{title}}</h1>
|
|
{{UserTopSection}}
|
|
{{format:StatusCallout}}
|
|
{{format:Description}}
|
|
{{format:FactsTable}}
|
|
{{format:RaceNameForms}}
|
|
{{format:UserBottomFallback}}`
|
|
default:
|
|
return `<h1>{{title}}</h1>
|
|
{{UserTopSection}}
|
|
{{format:StatusCallout}}
|
|
{{format:Description}}
|
|
{{format:FactsTable}}
|
|
{{format:UserBottomFallback}}`
|
|
}
|
|
}
|
|
|
|
func (ctx *wikiContext) renderWikiTemplateString(path, source string, page wikiTemplatePage) (string, error) {
|
|
var out strings.Builder
|
|
offset := 0
|
|
seenSections := map[string]struct{}{}
|
|
for {
|
|
start := strings.Index(source[offset:], "{{")
|
|
if start < 0 {
|
|
out.WriteString(source[offset:])
|
|
break
|
|
}
|
|
start += offset
|
|
end := strings.Index(source[start+2:], "}}")
|
|
if end < 0 {
|
|
return "", fmt.Errorf("render wiki template %s for %s: unclosed placeholder", path, page.PageID)
|
|
}
|
|
end += start + 2
|
|
out.WriteString(source[offset:start])
|
|
token := strings.TrimSpace(source[start+2 : end])
|
|
rendered, sectionID, err := ctx.renderWikiTemplateToken(path, token, page)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if sectionID != "" {
|
|
if _, exists := seenSections[sectionID]; exists {
|
|
return "", fmt.Errorf("render wiki template %s for %s: duplicate preserved section %q", path, page.PageID, sectionID)
|
|
}
|
|
seenSections[sectionID] = struct{}{}
|
|
}
|
|
out.WriteString(rendered)
|
|
offset = end + 2
|
|
}
|
|
return strings.TrimSpace(out.String()), nil
|
|
}
|
|
|
|
func (ctx *wikiContext) renderWikiTemplateToken(path, token string, page wikiTemplatePage) (string, string, error) {
|
|
switch token {
|
|
case "title":
|
|
return html.EscapeString(page.Title), "", nil
|
|
case "page_id":
|
|
return html.EscapeString(page.PageID), "", nil
|
|
case "status":
|
|
return html.EscapeString(page.Status), "", nil
|
|
}
|
|
if section, ok := wikiManualSectionForAlias(page.ManualSections, token); ok {
|
|
return renderWikiManualSection(section), section.ID, nil
|
|
}
|
|
if strings.HasPrefix(token, "format:") {
|
|
rendered, err := ctx.renderWikiFormatter(strings.TrimSpace(strings.TrimPrefix(token, "format:")), page)
|
|
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:") {
|
|
rendered, err := ctx.renderExplicitWikiField(path, strings.TrimSpace(strings.TrimPrefix(token, "field:")), page)
|
|
return rendered, "", err
|
|
}
|
|
rendered, err := ctx.renderRequiredWikiField(path, token, page)
|
|
return rendered, "", err
|
|
}
|
|
|
|
func wikiManualSectionForAlias(sections []wikiManualSection, alias string) (wikiManualSection, bool) {
|
|
for _, section := range sections {
|
|
if section.Alias == alias {
|
|
return section, true
|
|
}
|
|
}
|
|
switch alias {
|
|
case "UserTopSection":
|
|
for _, section := range sections {
|
|
if section.ID == "user_top" {
|
|
return section, true
|
|
}
|
|
}
|
|
case "UserBottomSection":
|
|
for _, section := range sections {
|
|
if section.ID == "notes" {
|
|
return section, true
|
|
}
|
|
}
|
|
}
|
|
return wikiManualSection{}, false
|
|
}
|
|
|
|
func renderWikiManualSection(section wikiManualSection) string {
|
|
return strings.Join([]string{
|
|
"<!-- sow-topdata-wiki:manual:start id=\"" + html.EscapeString(section.ID) + "\" -->",
|
|
strings.TrimSpace(section.InitialHTML),
|
|
"<!-- sow-topdata-wiki:manual:end id=\"" + html.EscapeString(section.ID) + "\" -->",
|
|
}, "\n")
|
|
}
|
|
|
|
func (ctx *wikiContext) renderExplicitWikiField(path, spec string, page wikiTemplatePage) (string, error) {
|
|
fieldSpec, defaultValue, hasDefault := strings.Cut(spec, "|default=")
|
|
fieldSpec = strings.TrimSpace(fieldSpec)
|
|
value, ok := ctx.resolveWikiTemplateField(fieldSpec, page)
|
|
if !ok {
|
|
if hasDefault {
|
|
return html.EscapeString(defaultValue), nil
|
|
}
|
|
return "", fmt.Errorf("render wiki template %s for %s: missing field %q", path, page.PageID, fieldSpec)
|
|
}
|
|
return html.EscapeString(value), nil
|
|
}
|
|
|
|
func (ctx *wikiContext) renderRequiredWikiField(path, fieldSpec string, page wikiTemplatePage) (string, error) {
|
|
value, ok := ctx.resolveWikiTemplateField(fieldSpec, page)
|
|
if !ok {
|
|
return "", fmt.Errorf("render wiki template %s for %s: missing field %q", path, page.PageID, fieldSpec)
|
|
}
|
|
return html.EscapeString(value), nil
|
|
}
|
|
|
|
func (ctx *wikiContext) resolveWikiTemplateField(fieldSpec string, page wikiTemplatePage) (string, bool) {
|
|
fieldSpec = strings.TrimSpace(fieldSpec)
|
|
if strings.HasSuffix(fieldSpec, ".text") {
|
|
base := strings.TrimSuffix(fieldSpec, ".text")
|
|
value, ok := lookupField(page.Row, base)
|
|
if !ok {
|
|
return "", false
|
|
}
|
|
text := ctx.resolveTextValue(value, nil)
|
|
return text, text != ""
|
|
}
|
|
value, ok := lookupField(page.Row, fieldSpec)
|
|
if !ok || value == nil {
|
|
return "", false
|
|
}
|
|
switch typed := value.(type) {
|
|
case string:
|
|
if typed == "" || typed == nullValue {
|
|
return "", false
|
|
}
|
|
return typed, true
|
|
case int:
|
|
return fmt.Sprintf("%d", typed), true
|
|
case float64:
|
|
return fmt.Sprintf("%d", int(typed)), true
|
|
default:
|
|
text := ctx.resolveTextValue(typed, nil)
|
|
return text, text != ""
|
|
}
|
|
}
|