From a17477cff1fe536593e82b5c7b9638465bd09605 Mon Sep 17 00:00:00 2001 From: vickydotbat Date: Fri, 22 May 2026 13:21:08 +0200 Subject: [PATCH] Page generation to use title instead of key --- internal/topdata/wiki_deploy.go | 23 ++++++++++++++++ internal/topdata/wiki_deploy_test.go | 32 ++++++++++++++++++++++ internal/topdata/wiki_native.go | 40 +++++++++++++++++++++++----- internal/topdata/wiki_native_test.go | 29 +++++++++++--------- 4 files changed, 104 insertions(+), 20 deletions(-) diff --git a/internal/topdata/wiki_deploy.go b/internal/topdata/wiki_deploy.go index be92ebc..f78aad8 100644 --- a/internal/topdata/wiki_deploy.go +++ b/internal/topdata/wiki_deploy.go @@ -330,6 +330,13 @@ func collectLocalPages(sourceDir string, namespaces []string) (map[string]wikiDe return err } content := string(raw) + if markerPageID := extractWikiPageID(content); markerPageID != "" { + pageID = markerPageID + namespace = strings.SplitN(pageID, ":", 2)[0] + if _, ok := namespaceSet[namespace]; !ok { + return nil + } + } pages[pageID] = wikiDeployPage{ PageID: pageID, Title: extractPageTitle(content, pageID), @@ -924,6 +931,22 @@ func renderArchivedWikiPage(pageID, title string) string { }, "\n")) } +func extractWikiPageID(content string) string { + const pageMarker = "sow-topdata-wiki:page=" + start := strings.Index(content, pageMarker) + if start < 0 { + return "" + } + value := content[start+len(pageMarker):] + if end := strings.Index(value, "-->"); end >= 0 { + value = value[:end] + } + if fields := strings.Fields(value); len(fields) > 0 { + return strings.Trim(strings.TrimSpace(fields[0]), `"`) + } + return "" +} + func extractWikiPagePublicPath(content string) string { const pageMarker = "sow-topdata-wiki:page=" const pathField = "public_path=" diff --git a/internal/topdata/wiki_deploy_test.go b/internal/topdata/wiki_deploy_test.go index 1801cae..bb7c0d9 100644 --- a/internal/topdata/wiki_deploy_test.go +++ b/internal/topdata/wiki_deploy_test.go @@ -76,6 +76,38 @@ func TestDeployWikiDryRunDoesNotWriteRemoteOrManifest(t *testing.T) { } } +func TestCollectLocalPagesReadsGeneratedPageIDFromMarkerWhenPathIsTitleBased(t *testing.T) { + root := t.TempDir() + sourceDir := filepath.Join(root, "pages") + if err := os.MkdirAll(filepath.Join(sourceDir, "feat"), 0755); err != nil { + t.Fatalf("create source dir: %v", err) + } + generated := ` + +

Inspire Competence

+

Generated page.

+ +` + if err := os.WriteFile(filepath.Join(sourceDir, "feat", "Inspire_Competence.html"), []byte(generated), 0644); err != nil { + t.Fatalf("write source page: %v", err) + } + + pages, err := collectLocalPages(sourceDir, []string{"feat"}) + if err != nil { + t.Fatalf("collectLocalPages failed: %v", err) + } + page, ok := pages["feat:bardic_music_inspire_competence"] + if !ok { + t.Fatalf("expected marker page ID to be used as deploy identity, got keys %#v", sortedKeysFromMap(pages)) + } + if page.PageID != "feat:bardic_music_inspire_competence" || page.Namespace != "feat" || page.Title != "Inspire Competence" { + t.Fatalf("unexpected collected page metadata: %#v", page) + } + if _, ok := pages["feat:Inspire_Competence"]; ok { + t.Fatalf("expected title-based path not to become generated page identity") + } +} + func TestDeployWikiDryRunReadoptsMissingMappedPost(t *testing.T) { root := t.TempDir() sourceDir := filepath.Join(root, "pages") diff --git a/internal/topdata/wiki_native.go b/internal/topdata/wiki_native.go index bc82054..1eaca57 100644 --- a/internal/topdata/wiki_native.go +++ b/internal/topdata/wiki_native.go @@ -189,9 +189,15 @@ func buildWiki(p *project.Project, nativeResult BuildResult, force bool, progres pageTitles := map[string]string{} pageStatuses := map[string]string{} pageIndex := wikiPageIndex{Version: wikiGeneratorVersion} + outputPaths := map[string]string{} writePage := func(pageID, content, title, status string) error { - path := filepath.Join(outputDir, wikiPageIDToRelPath(pageID)) + relPath := wikiPageOutputRelPath(pageID, title) + if previous := outputPaths[relPath]; previous != "" { + return fmt.Errorf("duplicate wiki page output path %q for %s and %s", filepath.ToSlash(relPath), previous, pageID) + } + outputPaths[relPath] = pageID + path := filepath.Join(outputDir, relPath) if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { return err } @@ -366,15 +372,18 @@ func indexUnregisteredWikiPages(rootDir, outputDir, stalePolicy string, index *w if err != nil { return err } - pageID := strings.TrimSuffix(filepath.ToSlash(relToPages), filepath.Ext(relToPages)) - pageID = strings.ReplaceAll(pageID, "/", ":") - if _, ok := seen[pageID]; ok { - return nil - } raw, err := os.ReadFile(path) if err != nil { return err } + pageID := extractWikiPageID(string(raw)) + if pageID == "" { + pageID = strings.TrimSuffix(filepath.ToSlash(relToPages), filepath.Ext(relToPages)) + pageID = strings.ReplaceAll(pageID, "/", ":") + } + if _, ok := seen[pageID]; ok { + return nil + } namespace := strings.SplitN(pageID, ":", 2)[0] relToRoot, err := filepath.Rel(rootDir, path) if err != nil { @@ -1669,7 +1678,7 @@ func wikiPageIDForKey(key string) string { if namespace == "" { return "" } - value = strings.NewReplacer("/", ":", "\\", ":").Replace(value) + value = strings.NewReplacer("/", "_", "\\", "_").Replace(value) return namespace + ":" + value } @@ -1711,6 +1720,23 @@ func wikiSlugifyTitle(value string) string { return slugifyWikiText(value, "topic") } +func wikiPageOutputRelPath(pageID, title string) string { + namespace := strings.TrimSpace(strings.SplitN(pageID, ":", 2)[0]) + if namespace == "" { + namespace = "page" + } + parts := []string{namespace} + for _, part := range strings.Split(title, " :: ") { + parts = append(parts, part) + } + path := canonicalWikiPath(parts...) + if path == "" { + path = wikiPageIDToRelPath(pageID) + return path + } + return filepath.FromSlash(path + ".html") +} + func wikiPageIDToRelPath(pageID string) string { return filepath.FromSlash(strings.ReplaceAll(pageID, ":", "/") + ".html") } diff --git a/internal/topdata/wiki_native_test.go b/internal/topdata/wiki_native_test.go index fbd833f..cbe4dba 100644 --- a/internal/topdata/wiki_native_test.go +++ b/internal/topdata/wiki_native_test.go @@ -44,7 +44,7 @@ func TestBuildNativeGeneratesAndSkipsWikiPages(t *testing.T) { if result.WikiPages != generatedHTMLCount { t.Fatalf("expected wiki page count to match generated HTML files, got result=%d files=%d", result.WikiPages, generatedHTMLCount) } - pagePath := filepath.Join(root, ".cache", "wiki", "pages", "skills", "athletics.html") + pagePath := filepath.Join(root, ".cache", "wiki", "pages", "skills", "Athletics.html") got, err := os.ReadFile(pagePath) if err != nil { t.Fatalf("read generated page: %v", err) @@ -85,7 +85,7 @@ func TestBuildNativeGeneratesAndSkipsWikiPages(t *testing.T) { t.Fatalf("expected page-index entries to match generated HTML files, got index=%d files=%d", len(pageIndex.Pages), generatedHTMLCount) } indexText := string(indexRaw) - if !strings.Contains(indexText, `"page_id": "skills:athletics"`) || !strings.Contains(indexText, `"public_path": "Skills/Athletics"`) || strings.Contains(indexText, `"public_slug"`) || !strings.Contains(indexText, `"output_path": "pages/skills/athletics.html"`) || !strings.Contains(indexText, `"edit_policy": "preserve_manual_sections"`) { + if !strings.Contains(indexText, `"page_id": "skills:athletics"`) || !strings.Contains(indexText, `"public_path": "Skills/Athletics"`) || strings.Contains(indexText, `"public_slug"`) || !strings.Contains(indexText, `"output_path": "pages/skills/Athletics.html"`) || !strings.Contains(indexText, `"edit_policy": "preserve_manual_sections"`) { t.Fatalf("expected deterministic page-index metadata, got:\n%s", indexText) } if !strings.Contains(indexText, `"page_id": "meta:wikistatus"`) || !strings.Contains(indexText, `"output_path": "pages/meta/wikistatus.html"`) || !strings.Contains(indexText, `"edit_policy": "generated_only"`) { @@ -160,7 +160,7 @@ func TestBuildWikiRegeneratesMissingCachedPages(t *testing.T) { if result.Status != wikiGeneratedStatus { t.Fatalf("expected missing cached pages to regenerate, got wiki status %q", result.Status) } - if _, err := os.Stat(filepath.Join(pagesDir, "skills", "athletics.html")); err != nil { + if _, err := os.Stat(filepath.Join(pagesDir, "skills", "Athletics.html")); err != nil { t.Fatalf("expected regenerated skill page: %v", err) } @@ -934,14 +934,14 @@ datasets: } } for _, path := range []string{ - filepath.Join(root, ".cache", "wiki", "pages", "feat", "visible.html"), - filepath.Join(root, ".cache", "wiki", "pages", "feat", "class_only.html"), + filepath.Join(root, ".cache", "wiki", "pages", "feat", "Visible_Feat.html"), + filepath.Join(root, ".cache", "wiki", "pages", "feat", "Class_Feat.html"), } { if _, err := os.Stat(path); err != nil { t.Fatalf("expected visible wiki page %s: %v", path, err) } } - classPage, err := os.ReadFile(filepath.Join(root, ".cache", "wiki", "pages", "classes", "fighter.html")) + classPage, err := os.ReadFile(filepath.Join(root, ".cache", "wiki", "pages", "classes", "Fighter.html")) if err != nil { t.Fatalf("read class page: %v", err) } @@ -1321,20 +1321,23 @@ func TestWikiSourceDigestIncludesTemplates(t *testing.T) { func TestWikiPageIDForKeyNormalizesSlashSeparatorsOnly(t *testing.T) { got := wikiPageIDForKey("feat:special/attacks_bull_rush") - if want := "feat:special:attacks_bull_rush"; got != want { + if want := "feat:special_attacks_bull_rush"; got != want { t.Fatalf("expected normalized page ID %q, got %q", want, got) } if got := wikiPageIDForKey("feat:bardic_music_inspire_competence"); got != "feat:bardic_music_inspire_competence" { t.Fatalf("expected underscore to remain in page ID leaf, got %q", got) } + if got := wikiPageIDForKey("feat:bardic_music/inspire_competence"); got != "feat:bardic_music_inspire_competence" { + t.Fatalf("expected slash-authored key to stay in one page ID leaf, got %q", got) + } } -func TestWikiPageIDToRelPathKeepsUnderscoreInFileLeaf(t *testing.T) { - if got, want := wikiPageIDToRelPath("feat:bardic_music_inspire_competence"), filepath.FromSlash("feat/bardic_music_inspire_competence.html"); got != want { +func TestWikiPageOutputRelPathUsesCanonicalTitleLeaf(t *testing.T) { + if got, want := wikiPageOutputRelPath("feat:bardic_music_inspire_competence", "Inspire Competence"), filepath.FromSlash("feat/Inspire_Competence.html"); got != want { t.Fatalf("expected underscore page ID to stay in one file leaf, got %q want %q", got, want) } - if got := wikiPageIDToRelPath("feat:bardic:music:inspire:competence"); got == filepath.FromSlash("feat/bardic_music_inspire_competence.html") { - t.Fatalf("expected explicit hierarchy path to remain distinct from underscore leaf, got %q", got) + if got, want := wikiPageOutputRelPath(wikiPageIDForKey("feat:bardic_music/inspire_competence"), "Inspire Competence"), filepath.FromSlash("feat/Inspire_Competence.html"); got != want { + t.Fatalf("expected slash-authored key to stay in one file leaf, got %q want %q", got, want) } } @@ -1531,7 +1534,7 @@ func TestBuildNativeRegeneratesWikiWhenTLKTextChanges(t *testing.T) { if _, err := BuildNative(proj, nil); err != nil { t.Fatalf("BuildNative after text change failed: %v", err) } - pagePath := filepath.Join(root, ".cache", "wiki", "pages", "skills", "athletics.html") + pagePath := filepath.Join(root, ".cache", "wiki", "pages", "skills", "Athletics.html") got, err := os.ReadFile(pagePath) if err != nil { t.Fatalf("read regenerated page: %v", err) @@ -1573,7 +1576,7 @@ func TestBuildPackageIgnoresWikiSourcesForFreshness(t *testing.T) { t.Fatalf("BuildNative failed: %v", err) } - pagePath := filepath.Join(root, ".cache", "wiki", "pages", "skills", "athletics.html") + pagePath := filepath.Join(root, ".cache", "wiki", "pages", "skills", "Athletics.html") newTime := time.Now().Add(2 * time.Second) if err := os.Chtimes(pagePath, newTime, newTime); err != nil { t.Fatalf("touch wiki page: %v", err)