Page generation to use title instead of key

This commit is contained in:
2026-05-22 13:21:08 +02:00
parent 835d1153f0
commit a17477cff1
4 changed files with 104 additions and 20 deletions
+23
View File
@@ -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="
+32
View File
@@ -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 := `<!-- sow-topdata-wiki:page=feat:bardic_music_inspire_competence -->
<!-- sow-topdata-wiki:managed:start hash="sha256:local" -->
<h1>Inspire Competence</h1>
<p>Generated page.</p>
<!-- sow-topdata-wiki:managed:end -->
`
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")
+33 -7
View File
@@ -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")
}
+16 -13
View File
@@ -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)