diff --git a/internal/topdata/wiki_deploy.go b/internal/topdata/wiki_deploy.go index b8cd6f5..eb39298 100644 --- a/internal/topdata/wiki_deploy.go +++ b/internal/topdata/wiki_deploy.go @@ -22,8 +22,10 @@ import ( const ( defaultDeployManifestName = ".wiki_deploy_manifest.json" defaultEditSummary = "Auto-generated from native builder" - managedStartMarker = "" - managedEndMarker = "" + managedStartMarker = "[//]: # (sow-topdata-wiki:managed:start)" + managedEndMarker = "[//]: # (sow-topdata-wiki:managed:end)" + legacyManagedStartMarker = "" + legacyManagedEndMarker = "" ) var defaultManagedNamespaces = []string{"classes", "feat", "itemtypes", "races", "skills", "spells", "meta"} @@ -177,14 +179,15 @@ func collectLocalPages(sourceDir string, namespaces []string) (map[string]wikiDe if err != nil { return err } - if d.IsDir() || filepath.Ext(path) != ".html" { + ext := filepath.Ext(path) + if d.IsDir() || (ext != ".md" && ext != ".html") { return nil } rel, err := filepath.Rel(sourceDir, path) if err != nil { return err } - pageID := strings.TrimSuffix(filepath.ToSlash(rel), ".html") + pageID := strings.TrimSuffix(filepath.ToSlash(rel), ext) pageID = strings.ReplaceAll(pageID, "/", ":") namespace := strings.SplitN(pageID, ":", 2)[0] if _, ok := namespaceSet[namespace]; !ok { @@ -197,7 +200,7 @@ func collectLocalPages(sourceDir string, namespaces []string) (map[string]wikiDe content := string(raw) pages[pageID] = wikiDeployPage{ PageID: pageID, - Title: extractHTMLTitle(content, pageID), + Title: extractPageTitle(content, pageID), Namespace: namespace, Content: content, Hash: computeManagedHash(content), @@ -310,12 +313,11 @@ func computeManagedHash(content string) string { } func extractManagedRegion(content string) (string, bool) { - start := strings.Index(content, managedStartMarker) - end := strings.Index(content, managedEndMarker) + startMarker, _, start, end := managedRegionBounds(content) if start < 0 || end < 0 || end < start { return "", false } - start += len(managedStartMarker) + start += len(startMarker) return strings.TrimSpace(content[start:end]), true } @@ -324,13 +326,47 @@ func mergeManagedContent(existing, generated string) string { if !ok { return generated } - start := strings.Index(existing, managedStartMarker) - end := strings.Index(existing, managedEndMarker) + _, endMarker, start, end := managedRegionBounds(existing) if start < 0 || end < 0 || end < start { return generated } - end += len(managedEndMarker) - return existing[:start+len(managedStartMarker)] + "\n" + generatedRegion + "\n" + existing[end:] + end += len(endMarker) + return existing[:start] + managedStartMarker + "\n" + generatedRegion + "\n" + managedEndMarker + existing[end:] +} + +func managedRegionBounds(content string) (string, string, int, int) { + for _, markers := range [][2]string{ + {managedStartMarker, managedEndMarker}, + {legacyManagedStartMarker, legacyManagedEndMarker}, + } { + start := strings.Index(content, markers[0]) + end := strings.Index(content, markers[1]) + if start >= 0 && end >= 0 { + return markers[0], markers[1], start, end + } + } + return "", "", -1, -1 +} + +func extractPageTitle(content, fallback string) string { + if title := extractMarkdownTitle(content); title != "" { + return title + } + return extractHTMLTitle(content, fallback) +} + +func extractMarkdownTitle(content string) string { + for _, line := range strings.Split(content, "\n") { + trimmed := strings.TrimSpace(line) + if !strings.HasPrefix(trimmed, "# ") { + continue + } + title := strings.TrimSpace(strings.TrimPrefix(trimmed, "# ")) + if title != "" { + return title + } + } + return "" } func extractHTMLTitle(content, fallback string) string { diff --git a/internal/topdata/wiki_deploy_test.go b/internal/topdata/wiki_deploy_test.go index 973dbb8..e214563 100644 --- a/internal/topdata/wiki_deploy_test.go +++ b/internal/topdata/wiki_deploy_test.go @@ -17,8 +17,8 @@ func TestDeployWikiDryRunDoesNotWriteRemoteOrManifest(t *testing.T) { if err := os.MkdirAll(filepath.Join(sourceDir, "skills"), 0755); err != nil { t.Fatalf("create source dir: %v", err) } - generated := "\n

Athletics

\n

Generated athletics page

\n\n" - if err := os.WriteFile(filepath.Join(sourceDir, "skills", "athletics.html"), []byte(generated), 0644); err != nil { + generated := "[//]: # (sow-topdata-wiki:managed:start)\n# Athletics\n\nGenerated athletics page\n[//]: # (sow-topdata-wiki:managed:end)\n" + if err := os.WriteFile(filepath.Join(sourceDir, "skills", "athletics.md"), []byte(generated), 0644); err != nil { t.Fatalf("write source page: %v", err) } old := "\n

Athletics

\n

Existing athletics page

\n\n" @@ -81,8 +81,8 @@ func TestDeployWikiCreatesNodeBBTopicAndWritesManifest(t *testing.T) { if err := os.MkdirAll(filepath.Join(sourceDir, "skills"), 0755); err != nil { t.Fatalf("create source dir: %v", err) } - generated := "\n

Athletics

\n

Generated athletics page

\n\n" - if err := os.WriteFile(filepath.Join(sourceDir, "skills", "athletics.html"), []byte(generated), 0644); err != nil { + generated := "[//]: # (sow-topdata-wiki:managed:start)\n# Athletics\n\nGenerated athletics page\n[//]: # (sow-topdata-wiki:managed:end)\n" + if err := os.WriteFile(filepath.Join(sourceDir, "skills", "athletics.md"), []byte(generated), 0644); err != nil { t.Fatalf("write source page: %v", err) } @@ -147,8 +147,8 @@ func TestDeployWikiCreatesNodeBBTopicWithFallbackForShortTitle(t *testing.T) { if err := os.MkdirAll(filepath.Join(sourceDir, "spells"), 0755); err != nil { t.Fatalf("create source dir: %v", err) } - generated := "\n

Aid

\n

Generated spell page

\n\n" - if err := os.WriteFile(filepath.Join(sourceDir, "spells", "aid.html"), []byte(generated), 0644); err != nil { + generated := "[//]: # (sow-topdata-wiki:managed:start)\n# Aid\n\nGenerated spell page\n[//]: # (sow-topdata-wiki:managed:end)\n" + if err := os.WriteFile(filepath.Join(sourceDir, "spells", "aid.md"), []byte(generated), 0644); err != nil { t.Fatalf("write source page: %v", err) } diff --git a/internal/topdata/wiki_native.go b/internal/topdata/wiki_native.go index 27b2cf9..379321a 100644 --- a/internal/topdata/wiki_native.go +++ b/internal/topdata/wiki_native.go @@ -5,7 +5,6 @@ import ( "embed" "encoding/json" "fmt" - "html" "io/fs" "os" "path/filepath" @@ -25,7 +24,7 @@ const ( legacyWikiRootDirName = ".wiki" wikiPagesDirName = "pages" wikiStateFileName = "state.json" - wikiGeneratorVersion = "nodebb-html-v1" + wikiGeneratorVersion = "nodebb-markdown-v1" wikiGeneratedStatus = "generated" wikiSkippedStatus = "skipped" ) @@ -126,7 +125,7 @@ func buildWiki(p *project.Project, nativeResult BuildResult, force bool, progres if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { return err } - content = renderNodeBBManagedHTML(pageID, content) + content = renderNodeBBManagedMarkdown(pageID, content) if err := os.WriteFile(path, []byte(content), 0o644); err != nil { return err } @@ -1240,24 +1239,24 @@ func wikiStatusNoun(category string) string { func generateStatusPages(outputDir string, pageStatuses map[string]string, pageTitles map[string]string) error { summaries := buildWikiNamespaceSummaries(pageStatuses) - if err := writeWikiFile(filepath.Join(outputDir, "meta", "wikistatus.html"), renderWikiStatusReportPage(summaries)); err != nil { + if err := writeWikiFile(filepath.Join(outputDir, "meta", "wikistatus.md"), renderWikiStatusReportPage(summaries)); err != nil { return err } for _, namespace := range wikiManagedNamespaces { summary := summaries[namespace] - if err := writeWikiFile(filepath.Join(outputDir, "meta", "wikistatus", namespace+".html"), renderWikiNamespaceFragment(namespace, summary)); err != nil { + if err := writeWikiFile(filepath.Join(outputDir, "meta", "wikistatus", namespace+".md"), renderWikiNamespaceFragment(namespace, summary)); err != nil { return err } for _, status := range []string{wikiStatusNew, wikiStatusModified, wikiStatusVanilla} { pageIDs := filterWikiPageIDs(pageStatuses, namespace, status) - if err := writeWikiFile(filepath.Join(outputDir, "meta", "wikistatus", namespace, status+".html"), renderWikiStatusListingPage(namespaceTitle(namespace)+" "+wikiStatusLabels[status]+" Pages", pageIDs, pageTitles)); err != nil { + if err := writeWikiFile(filepath.Join(outputDir, "meta", "wikistatus", namespace, status+".md"), renderWikiStatusListingPage(namespaceTitle(namespace)+" "+wikiStatusLabels[status]+" Pages", pageIDs, pageTitles)); err != nil { return err } } } for _, status := range []string{wikiStatusNew, wikiStatusModified, wikiStatusVanilla} { pageIDs := filterWikiPageIDs(pageStatuses, "", status) - if err := writeWikiFile(filepath.Join(outputDir, "meta", "wikistatus", status+".html"), renderWikiStatusListingPage(wikiStatusLabels[status]+" Pages", pageIDs, pageTitles)); err != nil { + if err := writeWikiFile(filepath.Join(outputDir, "meta", "wikistatus", status+".md"), renderWikiStatusListingPage(wikiStatusLabels[status]+" Pages", pageIDs, pageTitles)); err != nil { return err } } @@ -1268,7 +1267,7 @@ func writeWikiFile(path, content string) error { if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { return err } - content = renderNodeBBManagedHTML(wikiRelPathToPageID(path), content) + content = renderNodeBBManagedMarkdown(wikiRelPathToPageID(path), content) return os.WriteFile(path, []byte(ensureTrailingNewline(content)), 0o644) } @@ -1397,11 +1396,12 @@ func wikiPageIDForKey(key string) string { } func wikiPageIDToRelPath(pageID string) string { - return filepath.FromSlash(strings.ReplaceAll(pageID, ":", "/") + ".html") + return filepath.FromSlash(strings.ReplaceAll(pageID, ":", "/") + ".md") } func wikiRelPathToPageID(path string) string { path = filepath.ToSlash(path) + path = strings.TrimSuffix(path, ".md") path = strings.TrimSuffix(path, ".html") if index := strings.LastIndex(path, "/pages/"); index >= 0 { path = path[index+len("/pages/"):] @@ -1583,18 +1583,18 @@ func toDokuWiki(text string) string { return strings.Join(out, "\n") } -func renderNodeBBManagedHTML(pageID, dokuText string) string { +func renderNodeBBManagedMarkdown(pageID, dokuText string) string { generated, notes := splitDokuWikiNotes(dokuText) - body := dokuWikiToNodeBBHTML(generated) + body := dokuWikiToNodeBBMarkdown(generated) parts := []string{ - "", - "", + "[//]: # (sow-topdata-wiki:page=" + pageID + ")", + "[//]: # (sow-topdata-wiki:managed:start)", body, - "", + "[//]: # (sow-topdata-wiki:managed:end)", } - notesHTML := dokuWikiToNodeBBHTML("===== Notes =====\n" + notes) - if strings.TrimSpace(notesHTML) != "" { - parts = append(parts, notesHTML) + notesMarkdown := dokuWikiToNodeBBMarkdown("===== Notes =====\n" + notes) + if strings.TrimSpace(notesMarkdown) != "" { + parts = append(parts, notesMarkdown) } return ensureTrailingNewline(strings.Join(parts, "\n")) } @@ -1608,72 +1608,59 @@ func splitDokuWikiNotes(text string) (string, string) { return before, after } -func dokuWikiToNodeBBHTML(text string) string { +func dokuWikiToNodeBBMarkdown(text string) string { text = strings.ReplaceAll(strings.TrimSpace(text), "\r\n", "\n") if text == "" { return "" } var out []string - var paragraph []string - inList := false inNotice := false - flushParagraph := func() { - if len(paragraph) == 0 { - return - } - out = append(out, "

"+strings.Join(paragraph, "
")+"

") - paragraph = nil - } - closeList := func() { - if inList { - out = append(out, "") - inList = false - } - } closeNotice := func() { if inNotice { - out = append(out, "") inNotice = false + if len(out) > 0 && strings.TrimSpace(out[len(out)-1]) != "" { + out = append(out, "") + } } } + appendBlank := func() { + if len(out) == 0 || strings.TrimSpace(out[len(out)-1]) == "" { + return + } + out = append(out, "") + } for _, line := range strings.Split(text, "\n") { trimmed := strings.TrimSpace(line) if trimmed == "" { - flushParagraph() - closeList() + appendBlank() continue } if strings.HasPrefix(trimmed, "`) + appendBlank() inNotice = true continue } if trimmed == "" { - flushParagraph() - closeList() closeNotice() continue } if heading, level, ok := parseDokuHeading(trimmed); ok { - flushParagraph() - closeList() closeNotice() - out = append(out, fmt.Sprintf("%s", level, renderInlineNodeBBHTML(heading), level)) + appendBlank() + out = append(out, strings.Repeat("#", level)+" "+renderInlineNodeBBMarkdown(heading)) + out = append(out, "") continue } if strings.HasPrefix(trimmed, " * ") || strings.HasPrefix(trimmed, "* ") || strings.HasPrefix(trimmed, "- ") { - flushParagraph() - if !inList { - out = append(out, "