diff --git a/README.md b/README.md index cd4b0bf..169daf5 100644 --- a/README.md +++ b/README.md @@ -259,13 +259,23 @@ topdata: granted_on_level: -1 on_menu: 0 wiki: + source: topdata/wiki output_root: wiki pages_dir: pages state_file: state.json + page_index_file: page-index.json managed_namespaces: [classes, feat, itemtypes, races, skills, spells, meta] deploy_manifest: .wiki_deploy_manifest.json deploy_edit_summary: Auto-generated from native builder + namespaces_file: namespaces.yaml + data_file: data.yaml + page_paths_file: wiki.yaml + tables_file: tables.yaml visibility_file: visibility.yaml + templates_dir: templates + page_templates_dir: templates/pages + manual_sections_dir: manual-sections + manual_sections_file: manual-sections/default.yaml title_prefix_min_length: 3 status_listing_scope: all @@ -325,6 +335,15 @@ topdata: target_pattern: Guides/Alignment/{alignment} ``` +Generated wiki input and output paths are YAML-configurable. `source` points to +the wiki authoring root, `data_file`, `page_paths_file`, `tables_file`, +`visibility_file`, `page_templates_dir`, and `manual_sections_file` are resolved +under that source root, and `page_index_file`, `pages_dir`, and `state_file` are +resolved under the configured wiki output root. `templates_dir` and +`manual_sections_dir` remain as compatibility defaults for deriving +`page_templates_dir` and `manual_sections_file` when the newer explicit paths +are omitted. + `paths.source` and `paths.assets` must name real repository subtrees when they are configured. They may be omitted for repositories that do not own that class of source, but they must not resolve to the repository root (`.`). Output and diff --git a/internal/project/effective.go b/internal/project/effective.go index b73c7ac..139717c 100644 --- a/internal/project/effective.go +++ b/internal/project/effective.go @@ -41,8 +41,13 @@ const ( DefaultTopDataWikiNamespacesFile = "namespaces.yaml" DefaultTopDataWikiTablesFile = "tables.yaml" DefaultTopDataWikiVisibilityFile = "visibility.yaml" + DefaultTopDataWikiDataFile = "data.yaml" + DefaultTopDataWikiPagePathsFile = "wiki.yaml" DefaultTopDataWikiTemplatesDir = "templates" + DefaultTopDataWikiPageTemplatesDir = "templates/pages" DefaultTopDataWikiManualSectionsDir = "manual-sections" + DefaultTopDataWikiManualSectionsFile = "manual-sections/default.yaml" + DefaultTopDataWikiPageIndexFile = "page-index.json" DefaultTopDataWikiStaleDefault = "report" DefaultTopDataWikiStaleLiveCleanup = "archive" DefaultTopDataWikiMarkerFormat = "html_comments" @@ -258,8 +263,13 @@ func (p *Project) EffectiveConfig() EffectiveConfig { 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, TitlePrefixMinLength: defaultInt(p.Config.TopData.Wiki.TitlePrefixMinLength, DefaultTopDataWikiTitlePrefixMinLength), StatusPages: defaultBoolPointer(p.Config.TopData.Wiki.StatusPages, DefaultTopDataWikiStatusPages), @@ -551,8 +561,13 @@ func markMissingDefaults(provenance ConfigProvenance) { "topdata.wiki.namespaces_file": DefaultTopDataWikiNamespacesFile, "topdata.wiki.tables_file": DefaultTopDataWikiTablesFile, "topdata.wiki.visibility_file": DefaultTopDataWikiVisibilityFile, + "topdata.wiki.data_file": DefaultTopDataWikiDataFile, + "topdata.wiki.page_paths_file": DefaultTopDataWikiPagePathsFile, "topdata.wiki.templates_dir": DefaultTopDataWikiTemplatesDir, + "topdata.wiki.page_templates_dir": DefaultTopDataWikiPageTemplatesDir, "topdata.wiki.manual_sections_dir": DefaultTopDataWikiManualSectionsDir, + "topdata.wiki.manual_sections_file": DefaultTopDataWikiManualSectionsFile, + "topdata.wiki.page_index_file": DefaultTopDataWikiPageIndexFile, "topdata.wiki.title_prefix_min_length": "3", "topdata.wiki.status_pages": "true", "topdata.wiki.status_listing_scope": DefaultTopDataWikiStatusListingScope, @@ -618,6 +633,26 @@ func defaultStringSlice(value, fallback []string) []string { return slices.Clone(value) } +func defaultWikiPageTemplatesDir(cfg TopDataWikiConfig) string { + if strings.TrimSpace(cfg.PageTemplatesDir) != "" { + return strings.TrimSpace(cfg.PageTemplatesDir) + } + if strings.TrimSpace(cfg.TemplatesDir) != "" { + return filepath.ToSlash(filepath.Join(filepath.FromSlash(strings.TrimSpace(cfg.TemplatesDir)), "pages")) + } + return DefaultTopDataWikiPageTemplatesDir +} + +func defaultWikiManualSectionsFile(cfg TopDataWikiConfig) string { + if strings.TrimSpace(cfg.ManualSectionsFile) != "" { + return strings.TrimSpace(cfg.ManualSectionsFile) + } + if strings.TrimSpace(cfg.ManualSectionsDir) != "" { + return filepath.ToSlash(filepath.Join(filepath.FromSlash(strings.TrimSpace(cfg.ManualSectionsDir)), "default.yaml")) + } + return DefaultTopDataWikiManualSectionsFile +} + func defaultRequiredFields(configured map[string][]string) map[string][]string { if len(configured) > 0 { return cloneStringSliceMap(normalizeRequiredFields(configured)) diff --git a/internal/project/project.go b/internal/project/project.go index d45ce6b..0227ca5 100644 --- a/internal/project/project.go +++ b/internal/project/project.go @@ -277,8 +277,13 @@ type TopDataWikiConfig struct { NamespacesFile string `json:"namespaces_file" yaml:"namespaces_file"` TablesFile string `json:"tables_file" yaml:"tables_file"` VisibilityFile string `json:"visibility_file" yaml:"visibility_file"` + DataFile string `json:"data_file" yaml:"data_file"` + PagePathsFile string `json:"page_paths_file" yaml:"page_paths_file"` TemplatesDir string `json:"templates_dir" yaml:"templates_dir"` + PageTemplatesDir string `json:"page_templates_dir" yaml:"page_templates_dir"` ManualSectionsDir string `json:"manual_sections_dir" yaml:"manual_sections_dir"` + ManualSectionsFile string `json:"manual_sections_file" yaml:"manual_sections_file"` + PageIndexFile string `json:"page_index_file" yaml:"page_index_file"` AlignmentLinks TopDataWikiAlignmentLinks `json:"alignment_links" yaml:"alignment_links"` TitlePrefixMinLength int `json:"title_prefix_min_length" yaml:"title_prefix_min_length"` StatusPages *bool `json:"status_pages,omitempty" yaml:"status_pages,omitempty"` @@ -587,8 +592,17 @@ func (p *Project) ValidateLayout() error { "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, @@ -627,8 +641,13 @@ func (p *Project) ValidateLayout() error { 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, validateRelativePath("topdata.wiki.data_file", effective.TopData.Wiki.DataFile)...) + failures = append(failures, validateRelativePath("topdata.wiki.page_paths_file", effective.TopData.Wiki.PagePathsFile)...) failures = append(failures, validateTreeRootPath("topdata.wiki.templates_dir", effective.TopData.Wiki.TemplatesDir)...) + failures = append(failures, validateRelativePath("topdata.wiki.page_templates_dir", effective.TopData.Wiki.PageTemplatesDir)...) failures = append(failures, validateTreeRootPath("topdata.wiki.manual_sections_dir", effective.TopData.Wiki.ManualSectionsDir)...) + failures = append(failures, validateRelativePath("topdata.wiki.manual_sections_file", effective.TopData.Wiki.ManualSectionsFile)...) + failures = append(failures, validateRelativePath("topdata.wiki.page_index_file", effective.TopData.Wiki.PageIndexFile)...) failures = append(failures, validateRelativePath("autogen.cache.root", effective.Autogen.Cache.Root)...) if err := validateOutputFileName("outputs.module_archive", filepath.Base(effective.Outputs.ModuleArchive), ".mod"); err != nil { failures = append(failures, err) @@ -642,6 +661,9 @@ func (p *Project) ValidateLayout() error { if err := validateOutputFileName("topdata.wiki.deploy_manifest", filepath.Base(effective.TopData.Wiki.DeployManifest), ".json"); err != nil { failures = append(failures, err) } + if err := validateOutputFileName("topdata.wiki.page_index_file", filepath.Base(effective.TopData.Wiki.PageIndexFile), ".json"); err != nil { + failures = append(failures, err) + } switch effective.TopData.Wiki.Renderer { case "nodebb_tiptap_html": default: @@ -1218,6 +1240,26 @@ func (p *Project) TopDataWikiStatePath() string { return filepath.Join(p.TopDataWikiRootDir(), filepath.FromSlash(p.EffectiveConfig().TopData.Wiki.StateFile)) } +func (p *Project) TopDataWikiPageIndexPath() string { + return filepath.Join(p.TopDataWikiRootDir(), filepath.FromSlash(p.EffectiveConfig().TopData.Wiki.PageIndexFile)) +} + +func (p *Project) TopDataWikiDataPath() string { + return filepath.Join(p.TopDataWikiSourceDir(), filepath.FromSlash(p.EffectiveConfig().TopData.Wiki.DataFile)) +} + +func (p *Project) TopDataWikiPagePathsPath() string { + return filepath.Join(p.TopDataWikiSourceDir(), filepath.FromSlash(p.EffectiveConfig().TopData.Wiki.PagePathsFile)) +} + +func (p *Project) TopDataWikiPageTemplatesDir() string { + return filepath.Join(p.TopDataWikiSourceDir(), filepath.FromSlash(p.EffectiveConfig().TopData.Wiki.PageTemplatesDir)) +} + +func (p *Project) TopDataWikiManualSectionsPath() string { + return filepath.Join(p.TopDataWikiSourceDir(), filepath.FromSlash(p.EffectiveConfig().TopData.Wiki.ManualSectionsFile)) +} + func (p *Project) TopDataPackageHAKName() string { return p.EffectiveConfig().TopData.PackageHAK } diff --git a/internal/project/project_test.go b/internal/project/project_test.go index a1f2810..4e2e7b4 100644 --- a/internal/project/project_test.go +++ b/internal/project/project_test.go @@ -432,6 +432,12 @@ topdata: output_root: docs pages_dir: pages state_file: wiki-state.json + page_index_file: manifest/page-index.json + source: docs-src/wiki + data_file: providers/wiki-data.yaml + page_paths_file: paths/wiki-paths.yaml + page_templates_dir: page-fragments + manual_sections_file: sections/defaults.yaml autogen: cache: root: "{paths.cache}/autogen" @@ -460,6 +466,21 @@ autogen: if got, want := proj.TopDataWikiStatePath(), filepath.Join(root, "cache", "docs", "wiki-state.json"); got != want { t.Fatalf("expected wiki state path %s, got %s", want, got) } + if got, want := proj.TopDataWikiPageIndexPath(), filepath.Join(root, "cache", "docs", "manifest", "page-index.json"); got != want { + t.Fatalf("expected wiki page-index path %s, got %s", want, got) + } + if got, want := proj.TopDataWikiDataPath(), filepath.Join(root, "docs-src", "wiki", "providers", "wiki-data.yaml"); got != want { + t.Fatalf("expected wiki data path %s, got %s", want, got) + } + if got, want := proj.TopDataWikiPagePathsPath(), filepath.Join(root, "docs-src", "wiki", "paths", "wiki-paths.yaml"); got != want { + t.Fatalf("expected wiki page paths path %s, got %s", want, got) + } + if got, want := proj.TopDataWikiPageTemplatesDir(), filepath.Join(root, "docs-src", "wiki", "page-fragments"); got != want { + t.Fatalf("expected wiki page template path %s, got %s", want, got) + } + if got, want := proj.TopDataWikiManualSectionsPath(), filepath.Join(root, "docs-src", "wiki", "sections", "defaults.yaml"); got != want { + t.Fatalf("expected wiki manual sections path %s, got %s", want, got) + } if got, want := proj.AutogenCachePath("manifest.json"), filepath.Join(root, "cache", "autogen", "manifest.json"); got != want { t.Fatalf("expected autogen cache path %s, got %s", want, got) } @@ -1179,6 +1200,11 @@ func TestValidateLayoutRejectsInvalidTopDataWikiConfig(t *testing.T) { TablesFile: "../tables.yaml", VisibilityFile: "../visibility.yaml", TemplatesDir: ".", + DataFile: "../data.yaml", + PagePathsFile: "../wiki.yaml", + PageTemplatesDir: "../templates/pages", + ManualSectionsFile: "../manual/default.yaml", + PageIndexFile: "../page-index.json", ManualSectionsDir: "../manual", StalePages: TopDataWikiStalePagesConfig{ Default: "delete", @@ -1202,6 +1228,11 @@ func TestValidateLayoutRejectsInvalidTopDataWikiConfig(t *testing.T) { "topdata.wiki.tables_file", "topdata.wiki.visibility_file", "topdata.wiki.templates_dir", + "topdata.wiki.data_file", + "topdata.wiki.page_paths_file", + "topdata.wiki.page_templates_dir", + "topdata.wiki.manual_sections_file", + "topdata.wiki.page_index_file", "topdata.wiki.manual_sections_dir", "topdata.wiki.stale_pages.default", "topdata.wiki.stale_pages.live_cleanup", diff --git a/internal/topdata/native.go b/internal/topdata/native.go index 054bd75..b1726af 100644 --- a/internal/topdata/native.go +++ b/internal/topdata/native.go @@ -210,7 +210,7 @@ func BuildNativeWithOptions(p *project.Project, opts NativeBuildOptions, progres } var prebuiltWiki *wikiResult if opts.BuildWiki { - if newestWikiSource, _, err := newestRelevantWikiSource(p.TopDataSourceDir()); err == nil && !newestWikiSource.IsZero() { + if newestWikiSource, _, err := newestRelevantWikiSource(p); err == nil && !newestWikiSource.IsZero() { statePath := wikiOutputStatePath(p) outputDir := wikiOutputPagesDir(p) if stateInfo, err := os.Stat(statePath); err == nil && !newestWikiSource.After(stateInfo.ModTime()) { diff --git a/internal/topdata/wiki_deploy.go b/internal/topdata/wiki_deploy.go index 57e01fa..23b8762 100644 --- a/internal/topdata/wiki_deploy.go +++ b/internal/topdata/wiki_deploy.go @@ -34,6 +34,7 @@ const ( type DeployWikiOptions struct { SourceDir string + PageIndexPath string Endpoint string Token string Version string @@ -119,9 +120,17 @@ func DeployWikiWithOptions(p *project.Project, opts DeployWikiOptions, progress if progress == nil { progress = func(string) {} } - if opts.SourceDir == "" { + sourceDirWasDefaulted := opts.SourceDir == "" + if sourceDirWasDefaulted { opts.SourceDir = wikiOutputPagesDir(p) } + if opts.PageIndexPath == "" { + if sourceDirWasDefaulted { + opts.PageIndexPath = p.TopDataWikiPageIndexPath() + } else { + opts.PageIndexPath = filepath.Join(filepath.Dir(opts.SourceDir), p.EffectiveConfig().TopData.Wiki.PageIndexFile) + } + } if _, err := os.Stat(opts.SourceDir); err != nil { return deployResult{}, fmt.Errorf("wiki source directory not found: %w", err) } @@ -172,7 +181,7 @@ func DeployWikiWithOptions(p *project.Project, opts DeployWikiOptions, progress } progress(fmt.Sprintf("Collecting local wiki pages from %s", opts.SourceDir)) - pages, err := collectLocalPages(opts.SourceDir, namespaces) + pages, err := collectLocalPages(opts.SourceDir, opts.PageIndexPath, namespaces) if err != nil { return deployResult{}, err } @@ -377,12 +386,12 @@ func categoryIDsFromNamespaceEnv(declarations []wikiNamespaceDeclaration) (map[s return categories, nil } -func collectLocalPages(sourceDir string, namespaces []string) (map[string]wikiDeployPage, error) { +func collectLocalPages(sourceDir, pageIndexPath string, namespaces []string) (map[string]wikiDeployPage, error) { namespaceSet := map[string]struct{}{} for _, namespace := range namespaces { namespaceSet[namespace] = struct{}{} } - pageIndex, err := loadDeployPageIndex(sourceDir) + pageIndex, err := loadDeployPageIndex(sourceDir, pageIndexPath) if err != nil { return nil, err } @@ -447,8 +456,11 @@ func collectLocalPages(sourceDir string, namespaces []string) (map[string]wikiDe return pages, nil } -func loadDeployPageIndex(sourceDir string) (map[string]wikiPageIndexEntry, error) { - path := filepath.Join(filepath.Dir(sourceDir), "page-index.json") +func loadDeployPageIndex(sourceDir, pageIndexPath string) (map[string]wikiPageIndexEntry, error) { + path := strings.TrimSpace(pageIndexPath) + if path == "" { + path = filepath.Join(filepath.Dir(sourceDir), "page-index.json") + } raw, err := os.ReadFile(path) if err != nil { if os.IsNotExist(err) { diff --git a/internal/topdata/wiki_deploy_test.go b/internal/topdata/wiki_deploy_test.go index 15b784f..ed85b88 100644 --- a/internal/topdata/wiki_deploy_test.go +++ b/internal/topdata/wiki_deploy_test.go @@ -305,7 +305,7 @@ func TestCollectLocalPagesReadsGeneratedPageIDFromMarkerWhenPathIsTitleBased(t * t.Fatalf("write source page: %v", err) } - pages, err := collectLocalPages(sourceDir, []string{"feat"}) + pages, err := collectLocalPages(sourceDir, "", []string{"feat"}) if err != nil { t.Fatalf("collectLocalPages failed: %v", err) } @@ -349,7 +349,7 @@ func TestCollectLocalPagesReadsCanonicalPublicPathFromPageIndex(t *testing.T) { t.Fatalf("write page index: %v", err) } - pages, err := collectLocalPages(sourceDir, []string{"spells"}) + pages, err := collectLocalPages(sourceDir, "", []string{"spells"}) if err != nil { t.Fatalf("collectLocalPages failed: %v", err) } @@ -358,6 +358,44 @@ func TestCollectLocalPagesReadsCanonicalPublicPathFromPageIndex(t *testing.T) { } } +func TestCollectLocalPagesUsesConfiguredPageIndexPath(t *testing.T) { + root := t.TempDir() + sourceDir := filepath.Join(root, "pages") + if err := os.MkdirAll(filepath.Join(sourceDir, "spells"), 0755); err != nil { + t.Fatalf("create source dir: %v", err) + } + generated := ` + +
Changed
`+"\n") - after, err := computeWikiSourceDigest(root) + after, err := computeWikiSourceDigest(proj) if err != nil { t.Fatalf("compute changed digest: %v", err) } @@ -2013,6 +2022,80 @@ func TestWikiSourceDigestIncludesTemplates(t *testing.T) { } } +func TestBuildWikiHonorsConfiguredWikiPaths(t *testing.T) { + root := t.TempDir() + mkdirAll(t, filepath.Join(root, "topdata", "data", "skills")) + mkdirAll(t, filepath.Join(root, "docs", "wiki", "page-fragments")) + mkdirAll(t, filepath.Join(root, "docs", "wiki", "config")) + mkdirAll(t, filepath.Join(root, "docs", "wiki", "sections")) + writeFile(t, filepath.Join(root, "topdata", "base_dialog.json"), "{}\n") + writeFile(t, filepath.Join(root, "topdata", "data", "skills", "base.json"), `{ + "columns": ["Label", "Name"], + "rows": [ + { + "id": 0, + "key": "skills:athletics", + "Label": "Athletics", + "Name": {"tlk": {"key": "skills:athletics.name", "text": "Athletics"}} + } + ] +}`+"\n") + writeFile(t, filepath.Join(root, "topdata", "data", "skills", "lock.json"), `{"skills:athletics":0}`+"\n") + writeFile(t, filepath.Join(root, "docs", "wiki", "page-fragments", "skills.html"), `