diff --git a/README.md b/README.md index 169daf5..18f2fdc 100644 --- a/README.md +++ b/README.md @@ -326,13 +326,17 @@ the toolkit keeps its legacy default injections for compatibility. `topdata.wiki.alignment_links.target_pattern` controls whether formatted alignment names become internal wiki links. The pattern may use `{alignment}` for the title-shaped alignment segment with spaces converted to underscores, and -`{label}` for the display label: +`{label}` for the display label. `alignment_links.link_format` defaults to +`wiki_markup`, which emits `[[target|label]]`. Use `html_anchor` when the +configured target should be emitted as a root URL anchor instead of being +resolved as namespace-relative wiki markup: ```yaml topdata: wiki: alignment_links: - target_pattern: Guides/Alignment/{alignment} + target_pattern: /wiki/Guides/Alignment/{alignment} + link_format: html_anchor ``` Generated wiki input and output paths are YAML-configurable. `source` points to diff --git a/internal/project/effective.go b/internal/project/effective.go index 139717c..98350d3 100644 --- a/internal/project/effective.go +++ b/internal/project/effective.go @@ -55,6 +55,7 @@ const ( DefaultTopDataWikiTitlePrefixMinLength = 3 DefaultTopDataWikiStatusPages = true DefaultTopDataWikiStatusListingScope = "all" + DefaultTopDataWikiAlignmentLinkFormat = "wiki_markup" DefaultWikiDeployManifest = ".wiki_deploy_manifest.json" DefaultWikiDeployEditSummary = "Auto-generated from native builder" DefaultTopDataPackageHAK = "sow_top.hak" @@ -251,26 +252,29 @@ func (p *Project) EffectiveConfig() EffectiveConfig { RowGeneration: cloneTopDataRowGeneration(p.Config.TopData.RowGeneration), ClassFeatInjections: cloneTopDataClassFeatInjections(p.Config.TopData.ClassFeatInjections), Wiki: TopDataWikiConfig{ - OutputRoot: defaultString(p.Config.TopData.Wiki.OutputRoot, DefaultTopDataWikiOutputRoot), - PagesDir: defaultString(p.Config.TopData.Wiki.PagesDir, DefaultTopDataWikiPagesDir), - StateFile: defaultString(p.Config.TopData.Wiki.StateFile, DefaultTopDataWikiStateFile), - ManagedNamespaces: defaultStringSlice(p.Config.TopData.Wiki.ManagedNamespaces, []string{"classes", "feat", "itemtypes", "races", "skills", "spells", "meta"}), - DeployManifest: defaultString(p.Config.TopData.Wiki.DeployManifest, DefaultWikiDeployManifest), - DeployEditSummary: defaultString(p.Config.TopData.Wiki.DeployEditSummary, DefaultWikiDeployEditSummary), - Source: defaultString(p.Config.TopData.Wiki.Source, DefaultTopDataWikiSource), - Renderer: defaultString(p.Config.TopData.Wiki.Renderer, DefaultTopDataWikiRenderer), - 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), - DataFile: defaultString(p.Config.TopData.Wiki.DataFile, DefaultTopDataWikiDataFile), - PagePathsFile: defaultString(p.Config.TopData.Wiki.PagePathsFile, DefaultTopDataWikiPagePathsFile), - TemplatesDir: defaultString(p.Config.TopData.Wiki.TemplatesDir, DefaultTopDataWikiTemplatesDir), - PageTemplatesDir: defaultWikiPageTemplatesDir(p.Config.TopData.Wiki), - ManualSectionsDir: defaultString(p.Config.TopData.Wiki.ManualSectionsDir, DefaultTopDataWikiManualSectionsDir), - ManualSectionsFile: defaultWikiManualSectionsFile(p.Config.TopData.Wiki), - PageIndexFile: defaultString(p.Config.TopData.Wiki.PageIndexFile, DefaultTopDataWikiPageIndexFile), - AlignmentLinks: p.Config.TopData.Wiki.AlignmentLinks, + OutputRoot: defaultString(p.Config.TopData.Wiki.OutputRoot, DefaultTopDataWikiOutputRoot), + PagesDir: defaultString(p.Config.TopData.Wiki.PagesDir, DefaultTopDataWikiPagesDir), + StateFile: defaultString(p.Config.TopData.Wiki.StateFile, DefaultTopDataWikiStateFile), + ManagedNamespaces: defaultStringSlice(p.Config.TopData.Wiki.ManagedNamespaces, []string{"classes", "feat", "itemtypes", "races", "skills", "spells", "meta"}), + DeployManifest: defaultString(p.Config.TopData.Wiki.DeployManifest, DefaultWikiDeployManifest), + DeployEditSummary: defaultString(p.Config.TopData.Wiki.DeployEditSummary, DefaultWikiDeployEditSummary), + Source: defaultString(p.Config.TopData.Wiki.Source, DefaultTopDataWikiSource), + Renderer: defaultString(p.Config.TopData.Wiki.Renderer, DefaultTopDataWikiRenderer), + 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), + DataFile: defaultString(p.Config.TopData.Wiki.DataFile, DefaultTopDataWikiDataFile), + PagePathsFile: defaultString(p.Config.TopData.Wiki.PagePathsFile, DefaultTopDataWikiPagePathsFile), + TemplatesDir: defaultString(p.Config.TopData.Wiki.TemplatesDir, DefaultTopDataWikiTemplatesDir), + PageTemplatesDir: defaultWikiPageTemplatesDir(p.Config.TopData.Wiki), + ManualSectionsDir: defaultString(p.Config.TopData.Wiki.ManualSectionsDir, DefaultTopDataWikiManualSectionsDir), + ManualSectionsFile: defaultWikiManualSectionsFile(p.Config.TopData.Wiki), + PageIndexFile: defaultString(p.Config.TopData.Wiki.PageIndexFile, DefaultTopDataWikiPageIndexFile), + AlignmentLinks: TopDataWikiAlignmentLinks{ + TargetPattern: strings.TrimSpace(p.Config.TopData.Wiki.AlignmentLinks.TargetPattern), + LinkFormat: defaultString(p.Config.TopData.Wiki.AlignmentLinks.LinkFormat, DefaultTopDataWikiAlignmentLinkFormat), + }, TitlePrefixMinLength: defaultInt(p.Config.TopData.Wiki.TitlePrefixMinLength, DefaultTopDataWikiTitlePrefixMinLength), StatusPages: defaultBoolPointer(p.Config.TopData.Wiki.StatusPages, DefaultTopDataWikiStatusPages), StatusListingScope: defaultString(p.Config.TopData.Wiki.StatusListingScope, DefaultTopDataWikiStatusListingScope), @@ -571,6 +575,7 @@ func markMissingDefaults(provenance ConfigProvenance) { "topdata.wiki.title_prefix_min_length": "3", "topdata.wiki.status_pages": "true", "topdata.wiki.status_listing_scope": DefaultTopDataWikiStatusListingScope, + "topdata.wiki.alignment_links.link_format": DefaultTopDataWikiAlignmentLinkFormat, "topdata.wiki.stale_pages.default": DefaultTopDataWikiStaleDefault, "topdata.wiki.stale_pages.live_cleanup": DefaultTopDataWikiStaleLiveCleanup, "topdata.wiki.managed_region.marker_format": DefaultTopDataWikiMarkerFormat, diff --git a/internal/project/project.go b/internal/project/project.go index 0227ca5..16f01fe 100644 --- a/internal/project/project.go +++ b/internal/project/project.go @@ -294,6 +294,7 @@ type TopDataWikiConfig struct { type TopDataWikiAlignmentLinks struct { TargetPattern string `json:"target_pattern" yaml:"target_pattern"` + LinkFormat string `json:"link_format" yaml:"link_format"` } type TopDataWikiStalePagesConfig struct { @@ -567,50 +568,51 @@ func (p *Project) ValidateLayout() error { failures = append(failures, errors.New("at least one of paths.source, paths.assets, or topdata.source is required")) } for field, value := range map[string]string{ - "paths.source": p.Config.Paths.Source, - "paths.assets": p.Config.Paths.Assets, - "paths.build": p.Config.Paths.Build, - "paths.cache": p.Config.Paths.Cache, - "paths.tools": p.Config.Paths.Tools, - "outputs.module_archive": p.Config.Outputs.ModuleArchive, - "outputs.hak_manifest": p.Config.Outputs.HAKManifest, - "outputs.hak_archive": p.Config.Outputs.HAKArchive, - "generated_assets.topdata_2da": "", - "validation.profile": p.Config.Validation.Profile, - "scripts.source_dir": p.Config.Scripts.SourceDir, - "scripts.cache": p.Config.Scripts.Cache, - "scripts.compiler.path": p.Config.Scripts.Compiler.Path, - "scripts.compiler.env.path": p.Config.Scripts.Compiler.Env.Path, - "topdata.source": p.Config.TopData.Source, - "topdata.build": p.Config.TopData.Build, - "topdata.reference_builder": p.Config.TopData.ReferenceBuilder, - "topdata.assets": p.Config.TopData.Assets, - "topdata.compiled_2da_dir": p.Config.TopData.Compiled2DADir, - "topdata.compiled_tlk": p.Config.TopData.CompiledTLK, - "topdata.wiki.output_root": p.Config.TopData.Wiki.OutputRoot, - "topdata.wiki.pages_dir": p.Config.TopData.Wiki.PagesDir, - "topdata.wiki.state_file": p.Config.TopData.Wiki.StateFile, - "topdata.wiki.deploy_manifest": p.Config.TopData.Wiki.DeployManifest, - "topdata.wiki.deploy_edit_summary": p.Config.TopData.Wiki.DeployEditSummary, - "topdata.wiki.source": p.Config.TopData.Wiki.Source, - "topdata.wiki.namespaces_file": p.Config.TopData.Wiki.NamespacesFile, - "topdata.wiki.tables_file": p.Config.TopData.Wiki.TablesFile, - "topdata.wiki.visibility_file": p.Config.TopData.Wiki.VisibilityFile, - "topdata.wiki.data_file": p.Config.TopData.Wiki.DataFile, - "topdata.wiki.page_paths_file": p.Config.TopData.Wiki.PagePathsFile, - "topdata.wiki.templates_dir": p.Config.TopData.Wiki.TemplatesDir, - "topdata.wiki.page_templates_dir": p.Config.TopData.Wiki.PageTemplatesDir, - "topdata.wiki.manual_sections_dir": p.Config.TopData.Wiki.ManualSectionsDir, - "topdata.wiki.manual_sections_file": p.Config.TopData.Wiki.ManualSectionsFile, - "topdata.wiki.page_index_file": p.Config.TopData.Wiki.PageIndexFile, - "extract.layout": p.Config.Extract.Layout, - "extract.hak_discovery": p.Config.Extract.HAKDiscovery, - "autogen.cache.root": p.Config.Autogen.Cache.Root, - "autogen.cache.max_age": p.Config.Autogen.Cache.MaxAge, - "autogen.cache.refresh_env": p.Config.Autogen.Cache.RefreshEnv, - "autogen.release_source.provider": p.Config.Autogen.ReleaseSource.Provider, - "autogen.release_source.server_url_env": p.Config.Autogen.ReleaseSource.ServerURLEnv, - "autogen.release_source.repo_env": p.Config.Autogen.ReleaseSource.RepoEnv, + "paths.source": p.Config.Paths.Source, + "paths.assets": p.Config.Paths.Assets, + "paths.build": p.Config.Paths.Build, + "paths.cache": p.Config.Paths.Cache, + "paths.tools": p.Config.Paths.Tools, + "outputs.module_archive": p.Config.Outputs.ModuleArchive, + "outputs.hak_manifest": p.Config.Outputs.HAKManifest, + "outputs.hak_archive": p.Config.Outputs.HAKArchive, + "generated_assets.topdata_2da": "", + "validation.profile": p.Config.Validation.Profile, + "scripts.source_dir": p.Config.Scripts.SourceDir, + "scripts.cache": p.Config.Scripts.Cache, + "scripts.compiler.path": p.Config.Scripts.Compiler.Path, + "scripts.compiler.env.path": p.Config.Scripts.Compiler.Env.Path, + "topdata.source": p.Config.TopData.Source, + "topdata.build": p.Config.TopData.Build, + "topdata.reference_builder": p.Config.TopData.ReferenceBuilder, + "topdata.assets": p.Config.TopData.Assets, + "topdata.compiled_2da_dir": p.Config.TopData.Compiled2DADir, + "topdata.compiled_tlk": p.Config.TopData.CompiledTLK, + "topdata.wiki.output_root": p.Config.TopData.Wiki.OutputRoot, + "topdata.wiki.pages_dir": p.Config.TopData.Wiki.PagesDir, + "topdata.wiki.state_file": p.Config.TopData.Wiki.StateFile, + "topdata.wiki.deploy_manifest": p.Config.TopData.Wiki.DeployManifest, + "topdata.wiki.deploy_edit_summary": p.Config.TopData.Wiki.DeployEditSummary, + "topdata.wiki.source": p.Config.TopData.Wiki.Source, + "topdata.wiki.namespaces_file": p.Config.TopData.Wiki.NamespacesFile, + "topdata.wiki.tables_file": p.Config.TopData.Wiki.TablesFile, + "topdata.wiki.visibility_file": p.Config.TopData.Wiki.VisibilityFile, + "topdata.wiki.data_file": p.Config.TopData.Wiki.DataFile, + "topdata.wiki.page_paths_file": p.Config.TopData.Wiki.PagePathsFile, + "topdata.wiki.templates_dir": p.Config.TopData.Wiki.TemplatesDir, + "topdata.wiki.page_templates_dir": p.Config.TopData.Wiki.PageTemplatesDir, + "topdata.wiki.manual_sections_dir": p.Config.TopData.Wiki.ManualSectionsDir, + "topdata.wiki.manual_sections_file": p.Config.TopData.Wiki.ManualSectionsFile, + "topdata.wiki.page_index_file": p.Config.TopData.Wiki.PageIndexFile, + "topdata.wiki.alignment_links.link_format": p.Config.TopData.Wiki.AlignmentLinks.LinkFormat, + "extract.layout": p.Config.Extract.Layout, + "extract.hak_discovery": p.Config.Extract.HAKDiscovery, + "autogen.cache.root": p.Config.Autogen.Cache.Root, + "autogen.cache.max_age": p.Config.Autogen.Cache.MaxAge, + "autogen.cache.refresh_env": p.Config.Autogen.Cache.RefreshEnv, + "autogen.release_source.provider": p.Config.Autogen.ReleaseSource.Provider, + "autogen.release_source.server_url_env": p.Config.Autogen.ReleaseSource.ServerURLEnv, + "autogen.release_source.repo_env": p.Config.Autogen.ReleaseSource.RepoEnv, } { if strings.Contains(value, "\x00") { failures = append(failures, fmt.Errorf("%s must not contain NUL bytes", field)) @@ -674,6 +676,11 @@ func (p *Project) ValidateLayout() error { default: failures = append(failures, fmt.Errorf("topdata.wiki.link_strategy %q is not supported", effective.TopData.Wiki.LinkStrategy)) } + switch effective.TopData.Wiki.AlignmentLinks.LinkFormat { + case "wiki_markup", "html_anchor": + default: + failures = append(failures, fmt.Errorf("topdata.wiki.alignment_links.link_format %q is not supported", effective.TopData.Wiki.AlignmentLinks.LinkFormat)) + } if effective.TopData.Wiki.TitlePrefixMinLength < 1 { failures = append(failures, fmt.Errorf("topdata.wiki.title_prefix_min_length must be at least 1")) } diff --git a/internal/project/project_test.go b/internal/project/project_test.go index 4e2e7b4..649a64f 100644 --- a/internal/project/project_test.go +++ b/internal/project/project_test.go @@ -1206,6 +1206,9 @@ func TestValidateLayoutRejectsInvalidTopDataWikiConfig(t *testing.T) { ManualSectionsFile: "../manual/default.yaml", PageIndexFile: "../page-index.json", ManualSectionsDir: "../manual", + AlignmentLinks: TopDataWikiAlignmentLinks{ + LinkFormat: "raw_url", + }, StalePages: TopDataWikiStalePagesConfig{ Default: "delete", LiveCleanup: "delete", @@ -1234,6 +1237,7 @@ func TestValidateLayoutRejectsInvalidTopDataWikiConfig(t *testing.T) { "topdata.wiki.manual_sections_file", "topdata.wiki.page_index_file", "topdata.wiki.manual_sections_dir", + "topdata.wiki.alignment_links.link_format", "topdata.wiki.stale_pages.default", "topdata.wiki.stale_pages.live_cleanup", } { diff --git a/internal/topdata/wiki_native.go b/internal/topdata/wiki_native.go index 5f2ccaa..e3681cd 100644 --- a/internal/topdata/wiki_native.go +++ b/internal/topdata/wiki_native.go @@ -121,6 +121,7 @@ type wikiContext struct { namespaceTitles map[string]string namespacePaths map[string]string alignmentLinkPattern string + alignmentLinkFormat string } func buildWiki(p *project.Project, nativeResult BuildResult, force bool, progress func(string)) (wikiResult, error) { @@ -155,6 +156,7 @@ func buildWiki(p *project.Project, nativeResult BuildResult, force bool, progres ctx.manualSections = manualSections ctx.templateDir = p.TopDataWikiPageTemplatesDir() ctx.alignmentLinkPattern = p.EffectiveConfig().TopData.Wiki.AlignmentLinks.TargetPattern + ctx.alignmentLinkFormat = p.EffectiveConfig().TopData.Wiki.AlignmentLinks.LinkFormat ctx.wikiTablesPath = filepath.Join(p.TopDataWikiSourceDir(), filepath.FromSlash(p.EffectiveConfig().TopData.Wiki.TablesFile)) tableDefinitions, err := loadWikiTableDefinitions(ctx.wikiTablesPath) if err != nil { diff --git a/internal/topdata/wiki_native_test.go b/internal/topdata/wiki_native_test.go index 9b473cc..b67edc6 100644 --- a/internal/topdata/wiki_native_test.go +++ b/internal/topdata/wiki_native_test.go @@ -1312,6 +1312,31 @@ func TestWikiTemplateFormatsPreferredAlignmentsWithConfiguredLinks(t *testing.T) } } +func TestWikiTemplateFormatsPreferredAlignmentsWithConfiguredRootAnchors(t *testing.T) { + ctx := &wikiContext{ + alignmentLinkPattern: "/wiki/Guides/Alignment/{alignment}", + alignmentLinkFormat: "html_anchor", + } + page := wikiTemplatePage{ + PageID: "racialtypes:tiefling", + Category: "racialtypes", + Key: "racialtypes:tiefling", + Title: "Tiefling", + Row: map[string]any{ + "PreferredAlignments": map[string]any{"list": []any{"le"}}, + }, + } + + got, err := ctx.renderWikiTemplateString("racialtypes.html", `{{alignments(PreferredAlignments)}}`, page) + if err != nil { + t.Fatalf("render template: %v", err) + } + want := `Lawful Evil` + if got != want { + t.Fatalf("expected %q, got %q", want, got) + } +} + func TestWikiTemplateDescriptionFiltersExtractParagraphsAndSections(t *testing.T) { ctx := &wikiContext{} page := wikiTemplatePage{ diff --git a/internal/topdata/wiki_template_expr.go b/internal/topdata/wiki_template_expr.go index 3e05f1e..e3f632d 100644 --- a/internal/topdata/wiki_template_expr.go +++ b/internal/topdata/wiki_template_expr.go @@ -367,7 +367,7 @@ func joinWikiTemplateValue(value any, sep string) string { } } -func (ctx *wikiContext) formatWikiTemplateAlignments(value any) (string, error) { +func (ctx *wikiContext) formatWikiTemplateAlignments(value any) (any, error) { values, err := wikiTemplateAlignmentValues(value) if err != nil { return "", err @@ -389,13 +389,21 @@ func (ctx *wikiContext) formatWikiTemplateAlignments(value any) (string, error) if len(labels) == 0 { return "Any", nil } - return strings.Join(labels, ", "), nil + rendered := strings.Join(labels, ", ") + if ctx != nil && strings.TrimSpace(ctx.alignmentLinkFormat) == "html_anchor" { + return wikiTemplateHTML(rendered), nil + } + return rendered, nil } func (ctx *wikiContext) renderWikiTemplateAlignmentLabel(label string) string { pattern := "" + format := "wiki_markup" if ctx != nil { pattern = strings.TrimSpace(ctx.alignmentLinkPattern) + if strings.TrimSpace(ctx.alignmentLinkFormat) != "" { + format = strings.TrimSpace(ctx.alignmentLinkFormat) + } } if pattern == "" { return label @@ -405,6 +413,9 @@ func (ctx *wikiContext) renderWikiTemplateAlignmentLabel(label string) string { if strings.TrimSpace(target) == "" { return label } + if format == "html_anchor" { + return `` + html.EscapeString(label) + `` + } return "[[" + target + "|" + label + "]]" }