Goodbye legacy DokuWiki translators...
This commit is contained in:
@@ -403,7 +403,10 @@ configuration.
|
|||||||
## Wiki And Changelog Utilities
|
## Wiki And Changelog Utilities
|
||||||
|
|
||||||
`build-topdata --wiki` or `build-wiki` renders wiki pages from the current
|
`build-topdata --wiki` or `build-wiki` renders wiki pages from the current
|
||||||
native topdata state. `deploy-wiki` publishes those pages to NodeBB. It reads
|
native topdata state as Tiptap-compatible NodeBB HTML. Page templates are HTML
|
||||||
|
fragments; they do not need to include a visible title heading because deploy
|
||||||
|
uses generated `page-index.json` metadata for page titles and public paths.
|
||||||
|
`deploy-wiki` publishes those pages to NodeBB. It reads
|
||||||
`NODEBB_API_ENDPOINT`, `NODEBB_API_TOKEN`, `GITHUB_REF_NAME`, and
|
`NODEBB_API_ENDPOINT`, `NODEBB_API_TOKEN`, `GITHUB_REF_NAME`, and
|
||||||
`NODEBB_WIKI_CATEGORIES` when equivalent flags are omitted. Category mappings
|
`NODEBB_WIKI_CATEGORIES` when equivalent flags are omitted. Category mappings
|
||||||
use `namespace=cid`, for example:
|
use `namespace=cid`, for example:
|
||||||
|
|||||||
@@ -421,13 +421,18 @@ func collectLocalPages(sourceDir string, namespaces []string) (map[string]wikiDe
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
indexEntry := pageIndex[pageID]
|
||||||
publicPath := extractWikiPagePublicPath(content)
|
publicPath := extractWikiPagePublicPath(content)
|
||||||
if publicPath == "" {
|
if publicPath == "" {
|
||||||
publicPath = pageIndex[pageID].PublicPath
|
publicPath = indexEntry.PublicPath
|
||||||
|
}
|
||||||
|
title := strings.TrimSpace(indexEntry.Title)
|
||||||
|
if title == "" {
|
||||||
|
title = extractPageTitle(content, pageID)
|
||||||
}
|
}
|
||||||
pages[pageID] = wikiDeployPage{
|
pages[pageID] = wikiDeployPage{
|
||||||
PageID: pageID,
|
PageID: pageID,
|
||||||
Title: extractPageTitle(content, pageID),
|
Title: title,
|
||||||
PublicPath: publicPath,
|
PublicPath: publicPath,
|
||||||
Namespace: namespace,
|
Namespace: namespace,
|
||||||
Content: content,
|
Content: content,
|
||||||
@@ -1278,15 +1283,19 @@ func extractMarkdownTitle(content string) string {
|
|||||||
|
|
||||||
func extractHTMLTitle(content, fallback string) string {
|
func extractHTMLTitle(content, fallback string) string {
|
||||||
lower := strings.ToLower(content)
|
lower := strings.ToLower(content)
|
||||||
start := strings.Index(lower, "<h1>")
|
for _, tag := range []string{"h1", "h2"} {
|
||||||
end := strings.Index(lower, "</h1>")
|
open := "<" + tag + ">"
|
||||||
|
close := "</" + tag + ">"
|
||||||
|
start := strings.Index(lower, open)
|
||||||
|
end := strings.Index(lower, close)
|
||||||
if start >= 0 && end > start {
|
if start >= 0 && end > start {
|
||||||
title := strings.TrimSpace(content[start+len("<h1>") : end])
|
title := strings.TrimSpace(content[start+len(open) : end])
|
||||||
title = strings.ReplaceAll(title, "\n", " ")
|
title = strings.ReplaceAll(title, "\n", " ")
|
||||||
if title != "" {
|
if title != "" {
|
||||||
return html.UnescapeString(stripSimpleTags(title))
|
return html.UnescapeString(stripSimpleTags(title))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
parts := strings.Split(fallback, ":")
|
parts := strings.Split(fallback, ":")
|
||||||
return namespaceTitle(parts[len(parts)-1])
|
return namespaceTitle(parts[len(parts)-1])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -327,6 +327,48 @@ func TestCollectLocalPagesReadsCanonicalPublicPathFromPageIndex(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCollectLocalPagesReadsTitleFromPageIndexForHeadinglessHTML(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 := `<!-- sow-topdata-wiki:page=spells:acid_fog -->
|
||||||
|
<!-- sow-topdata-wiki:managed:start hash="sha256:local" -->
|
||||||
|
<p>Generated acid fog page.</p>
|
||||||
|
<table><tbody><tr><th>School</th><td>Conjuration</td></tr></tbody></table>
|
||||||
|
<!-- sow-topdata-wiki:managed:end -->
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(filepath.Join(sourceDir, "spells", "Acid_Fog.html"), []byte(generated), 0644); err != nil {
|
||||||
|
t.Fatalf("write source page: %v", err)
|
||||||
|
}
|
||||||
|
if err := saveWikiPageIndex(filepath.Join(root, "page-index.json"), wikiPageIndex{
|
||||||
|
Version: wikiGeneratorVersion,
|
||||||
|
Pages: []wikiPageIndexEntry{
|
||||||
|
{
|
||||||
|
PageID: "spells:acid_fog",
|
||||||
|
Title: "Acid Fog",
|
||||||
|
PublicPath: "Spells/Acid_Fog",
|
||||||
|
OutputPath: "pages/spells/Acid_Fog.html",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("write page index: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pages, err := collectLocalPages(sourceDir, []string{"spells"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("collectLocalPages failed: %v", err)
|
||||||
|
}
|
||||||
|
page := pages["spells:acid_fog"]
|
||||||
|
if page.Title != "Acid Fog" {
|
||||||
|
t.Fatalf("expected page-index title for headingless HTML, got %#v", page)
|
||||||
|
}
|
||||||
|
if page.PublicPath != "Spells/Acid_Fog" {
|
||||||
|
t.Fatalf("expected page-index public path for headingless HTML, got %#v", page)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestExtractWikiPagePublicPathAcceptsCanonicalSlashPath(t *testing.T) {
|
func TestExtractWikiPagePublicPathAcceptsCanonicalSlashPath(t *testing.T) {
|
||||||
content := `<!-- sow-topdata-wiki:page=spells:pdk:fear public_path="Magic/Fear" -->
|
content := `<!-- sow-topdata-wiki:page=spells:pdk:fear public_path="Magic/Fear" -->
|
||||||
<!-- sow-topdata-wiki:managed:start hash="sha256:local" -->
|
<!-- sow-topdata-wiki:managed:start hash="sha256:local" -->
|
||||||
@@ -997,6 +1039,154 @@ func TestDeployWikiRenamesManagedTopicWhenGeneratedTitleChanges(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDeployWikiDoesNotRenameHeadinglessPageToPageIDFallback(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 := `<!-- sow-topdata-wiki:page=spells:acid_fog -->
|
||||||
|
<!-- sow-topdata-wiki:managed:start hash="sha256:local" -->
|
||||||
|
<p>Generated acid fog page.</p>
|
||||||
|
<table><tbody><tr><th>School</th><td>Conjuration</td></tr></tbody></table>
|
||||||
|
<!-- sow-topdata-wiki:managed:end -->
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(filepath.Join(sourceDir, "spells", "Acid_Fog.html"), []byte(generated), 0644); err != nil {
|
||||||
|
t.Fatalf("write source page: %v", err)
|
||||||
|
}
|
||||||
|
if err := saveWikiPageIndex(filepath.Join(root, "page-index.json"), wikiPageIndex{
|
||||||
|
Version: wikiGeneratorVersion,
|
||||||
|
Pages: []wikiPageIndexEntry{
|
||||||
|
{PageID: "spells:acid_fog", Title: "Acid Fog", PublicPath: "Spells/Acid_Fog", OutputPath: "pages/spells/Acid_Fog.html"},
|
||||||
|
},
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("write page index: %v", err)
|
||||||
|
}
|
||||||
|
manifestPath := filepath.Join(root, "deploy-manifest.json")
|
||||||
|
if err := saveDeployManifest(manifestPath, wikiDeployManifest{
|
||||||
|
Version: "nodebb-v1",
|
||||||
|
Pages: map[string]wikiDeployManifestPage{
|
||||||
|
"spells:acid_fog": {
|
||||||
|
Hash: computeManagedHash(generated),
|
||||||
|
Title: "Acid Fog",
|
||||||
|
TopicTitle: "Acid Fog",
|
||||||
|
Namespace: "spells",
|
||||||
|
TID: 7,
|
||||||
|
PID: 42,
|
||||||
|
CID: 5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("write deploy manifest: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
t.Fatalf("headingless unchanged page must not call NodeBB, got %s %s", r.Method, r.URL.String())
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
result, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
||||||
|
SourceDir: sourceDir,
|
||||||
|
Endpoint: server.URL,
|
||||||
|
Token: "nodebb-token",
|
||||||
|
ManifestPath: manifestPath,
|
||||||
|
}, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("DeployWikiWithOptions headingless skip failed: %v", err)
|
||||||
|
}
|
||||||
|
if result.Renamed != 0 || result.Updated != 0 || result.Skipped != 1 {
|
||||||
|
t.Fatalf("expected headingless page to skip without page-id fallback rename, got %#v", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeployWikiRenamesBrokenHeadinglessFallbackTitleBackToPageIndexTitle(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 := `<!-- sow-topdata-wiki:page=spells:acid_fog -->
|
||||||
|
<!-- sow-topdata-wiki:managed:start hash="sha256:local" -->
|
||||||
|
<p>Generated acid fog page.</p>
|
||||||
|
<table><tbody><tr><th>School</th><td>Conjuration</td></tr></tbody></table>
|
||||||
|
<!-- sow-topdata-wiki:managed:end -->
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(filepath.Join(sourceDir, "spells", "Acid_Fog.html"), []byte(generated), 0644); err != nil {
|
||||||
|
t.Fatalf("write source page: %v", err)
|
||||||
|
}
|
||||||
|
if err := saveWikiPageIndex(filepath.Join(root, "page-index.json"), wikiPageIndex{
|
||||||
|
Version: wikiGeneratorVersion,
|
||||||
|
Pages: []wikiPageIndexEntry{
|
||||||
|
{PageID: "spells:acid_fog", Title: "Acid Fog", PublicPath: "Spells/Acid_Fog", OutputPath: "pages/spells/Acid_Fog.html"},
|
||||||
|
},
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("write page index: %v", err)
|
||||||
|
}
|
||||||
|
manifestPath := filepath.Join(root, "deploy-manifest.json")
|
||||||
|
if err := saveDeployManifest(manifestPath, wikiDeployManifest{
|
||||||
|
Version: "nodebb-v1",
|
||||||
|
Pages: map[string]wikiDeployManifestPage{
|
||||||
|
"spells:acid_fog": {
|
||||||
|
Hash: computeManagedHash(generated),
|
||||||
|
Title: "Acid_fog",
|
||||||
|
TopicTitle: "Acid_fog",
|
||||||
|
Namespace: "spells",
|
||||||
|
TID: 7,
|
||||||
|
PID: 42,
|
||||||
|
CID: 5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("write deploy manifest: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
renameCalls := 0
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch {
|
||||||
|
case r.Method == http.MethodGet && r.URL.Path == "/api/v3/topics/7":
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||||
|
"response": map[string]any{"tid": 7, "title": "Acid_fog", "mainPid": 42},
|
||||||
|
})
|
||||||
|
case r.Method == http.MethodPut && r.URL.Path == "/api/v3/plugins/westgate-wiki/page/move":
|
||||||
|
renameCalls++
|
||||||
|
var req struct {
|
||||||
|
TID int `json:"tid"`
|
||||||
|
CID int `json:"cid"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
t.Fatalf("decode rename request: %v", err)
|
||||||
|
}
|
||||||
|
if req.TID != 7 || req.CID != 5 || req.Title != "Acid Fog" {
|
||||||
|
t.Fatalf("unexpected rename request: %#v", req)
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{"response": map[string]any{"tid": 7}})
|
||||||
|
default:
|
||||||
|
t.Fatalf("unexpected request %s %s", r.Method, r.URL.String())
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
result, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
||||||
|
SourceDir: sourceDir,
|
||||||
|
Endpoint: server.URL,
|
||||||
|
Token: "nodebb-token",
|
||||||
|
ManifestPath: manifestPath,
|
||||||
|
}, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("DeployWikiWithOptions headingless title repair failed: %v", err)
|
||||||
|
}
|
||||||
|
if result.Renamed != 1 || result.Updated != 0 || result.Skipped != 1 || renameCalls != 1 {
|
||||||
|
t.Fatalf("expected one title repair rename and skipped body update, result=%#v renameCalls=%d", result, renameCalls)
|
||||||
|
}
|
||||||
|
entry := loadDeployManifest(manifestPath).Pages["spells:acid_fog"]
|
||||||
|
if entry.Title != "Acid Fog" || entry.TopicTitle != "Acid Fog" {
|
||||||
|
t.Fatalf("expected repaired manifest title from page-index, got %#v", entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestDeployWikiAdoptsExistingNodeBBPageWhenManifestIsMissingWithoutCreate(t *testing.T) {
|
func TestDeployWikiAdoptsExistingNodeBBPageWhenManifestIsMissingWithoutCreate(t *testing.T) {
|
||||||
root := t.TempDir()
|
root := t.TempDir()
|
||||||
sourceDir := filepath.Join(root, "pages")
|
sourceDir := filepath.Join(root, "pages")
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package topdata
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"html"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -12,34 +13,34 @@ func (ctx *wikiContext) renderWikiFormatter(spec string, page wikiTemplatePage)
|
|||||||
}
|
}
|
||||||
switch name[0] {
|
switch name[0] {
|
||||||
case "Description":
|
case "Description":
|
||||||
return dokuWikiToNodeBBHTML(toDokuWiki(ctx.resolveRowDescription(page.Category, page.Row))), nil
|
return renderWikiTemplatePlainHTML(ctx.resolveRowDescription(page.Category, page.Row)), nil
|
||||||
case "FactsTable":
|
case "FactsTable":
|
||||||
return dokuWikiToNodeBBHTML(ctx.renderFacts(page.Category, page.Key, page.Row)), nil
|
return ctx.renderFactsHTML(page.Category, page.Key, page.Row), nil
|
||||||
case "Prerequisites":
|
case "Prerequisites":
|
||||||
return dokuWikiToNodeBBHTML(formatFact("Prerequisites", ctx.renderFeatPrerequisites(page.Row))), nil
|
return renderWikiFactTableHTML([]wikiHTMLFact{{Label: "Prerequisites", Value: ctx.renderFeatPrerequisites(page.Row)}}), nil
|
||||||
case "SkillList":
|
case "SkillList":
|
||||||
if page.Category != "classes" {
|
if page.Category != "classes" {
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
return dokuWikiToNodeBBHTML(ctx.renderClassSkillList(page.Row)), nil
|
return ctx.renderClassSkillListHTML(page.Row), nil
|
||||||
case "ClassFeatureTable":
|
case "ClassFeatureTable":
|
||||||
if page.Category != "classes" {
|
if page.Category != "classes" {
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
return dokuWikiToNodeBBHTML(ctx.renderClassFeatureTable(page.Row)), nil
|
return ctx.renderClassFeatureTableHTML(page.Row), nil
|
||||||
case "FeatProgression":
|
case "FeatProgression":
|
||||||
if page.Category != "classes" {
|
if page.Category != "classes" {
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
return dokuWikiToNodeBBHTML(ctx.renderClassFeatProgression(page.Row)), nil
|
return ctx.renderClassFeatProgressionHTML(page.Row), nil
|
||||||
case "SpellTable", "SavesProgression", "BABProgression":
|
case "SpellTable", "SavesProgression", "BABProgression":
|
||||||
return "", nil
|
return "", nil
|
||||||
case "RaceFeatList":
|
case "RaceFeatList":
|
||||||
return dokuWikiToNodeBBHTML(formatFact("Racial Feats", ctx.renderRaceFeatList(page.Row))), nil
|
return renderWikiFactTableHTML([]wikiHTMLFact{{Label: "Racial Feats", Value: ctx.renderRaceFeatList(page.Row)}}), nil
|
||||||
case "StatusCallout":
|
case "StatusCallout":
|
||||||
return buildWikiStatusBlock(page.Status, page.Category), nil
|
return buildWikiStatusBlock(page.Status, page.Category), nil
|
||||||
case "RaceNameForms":
|
case "RaceNameForms":
|
||||||
return dokuWikiToNodeBBHTML(ctx.renderRaceNameForms(page.Row)), nil
|
return ctx.renderRaceNameFormsHTML(page.Row), nil
|
||||||
case "UserBottomFallback":
|
case "UserBottomFallback":
|
||||||
return "", nil
|
return "", nil
|
||||||
default:
|
default:
|
||||||
@@ -47,6 +48,99 @@ func (ctx *wikiContext) renderWikiFormatter(spec string, page wikiTemplatePage)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type wikiHTMLFact struct {
|
||||||
|
Label string
|
||||||
|
Value string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *wikiContext) renderFactsHTML(category, key string, row map[string]any) string {
|
||||||
|
facts := []wikiHTMLFact{}
|
||||||
|
add := func(label, value string) {
|
||||||
|
if strings.TrimSpace(value) != "" {
|
||||||
|
facts = append(facts, wikiHTMLFact{Label: label, Value: value})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch category {
|
||||||
|
case "feat":
|
||||||
|
for _, fact := range ctx.renderFeatFactsHTML(row) {
|
||||||
|
add(fact.Label, fact.Value)
|
||||||
|
}
|
||||||
|
case "skills":
|
||||||
|
add("Key Ability", stringValue(row, "KeyAbility"))
|
||||||
|
add("Untrained", yesNoValue(stringValue(row, "Untrained")))
|
||||||
|
add("Armor Check Penalty", yesNoValue(stringValue(row, "ArmorCheckPenalty")))
|
||||||
|
case "spells":
|
||||||
|
add("Constant", stringValue(row, "Constant"))
|
||||||
|
case "baseitems":
|
||||||
|
add("Item Class", stringValue(row, "ItemClass"))
|
||||||
|
add("Weapon Type", stringValue(row, "WeaponType"))
|
||||||
|
add("Required Feats", ctx.renderReferenceList(ctx.wikiReqFeats(row)))
|
||||||
|
add("Base Item Stats", ctx.resolveTextValue(fieldValue(row, "BaseItemStatRef"), nil))
|
||||||
|
case "classes":
|
||||||
|
add("Hit Die", "d"+stringValue(row, "HitDie"))
|
||||||
|
add("Skill Points", stringValue(row, "SkillPointBase"))
|
||||||
|
add("Primary Ability", stringValue(row, "PrimaryAbil"))
|
||||||
|
case "racialtypes":
|
||||||
|
add("Ability Adjustments", formatAbilityAdjustments(row))
|
||||||
|
add("Favored Class", ctx.renderReference(fieldValue(row, "Favored"), ctx.classIDToKey, ctx.resolveClassName))
|
||||||
|
add("Favored Enemy", ctx.renderReference(fieldValue(row, "FavoredEnemyFeat"), ctx.featIDToKey, ctx.resolveFeatName))
|
||||||
|
add("Racial Feats", ctx.renderRaceFeatList(row))
|
||||||
|
}
|
||||||
|
_ = key
|
||||||
|
return renderWikiFactTableHTML(facts)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *wikiContext) renderFeatFactsHTML(row map[string]any) []wikiHTMLFact {
|
||||||
|
return []wikiHTMLFact{
|
||||||
|
{Label: "Prerequisites", Value: ctx.renderFeatPrerequisites(row)},
|
||||||
|
{Label: "Minimum Attack Bonus", Value: stringValue(row, "MINATTACKBONUS")},
|
||||||
|
{Label: "Minimum Spell Level", Value: stringValue(row, "MINSPELLLVL")},
|
||||||
|
{Label: "Minimum Fortitude Save", Value: stringValue(row, "MinFortSave")},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderWikiFactTableHTML(facts []wikiHTMLFact) string {
|
||||||
|
rows := []string{}
|
||||||
|
for _, fact := range facts {
|
||||||
|
if strings.TrimSpace(fact.Value) == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rows = append(rows, `<tr><th scope="row">`+html.EscapeString(fact.Label)+`</th><td>`+renderMultilineInlineHTML(fact.Value)+`</td></tr>`)
|
||||||
|
}
|
||||||
|
if len(rows) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return `<table class="wiki-facts"><tbody>` + strings.Join(rows, "\n") + `</tbody></table>`
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderMultilineInlineHTML(text string) string {
|
||||||
|
lines := []string{}
|
||||||
|
for _, line := range strings.Split(strings.ReplaceAll(strings.TrimSpace(text), "\r\n", "\n"), "\n") {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line != "" {
|
||||||
|
lines = append(lines, renderInlineHTML(line))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Join(lines, "<br>")
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderWikiListHTML(class string, items []string) string {
|
||||||
|
rendered := []string{}
|
||||||
|
for _, item := range items {
|
||||||
|
if strings.TrimSpace(item) != "" {
|
||||||
|
rendered = append(rendered, "<li>"+renderInlineHTML(item)+"</li>")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(rendered) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
classAttr := ""
|
||||||
|
if class != "" {
|
||||||
|
classAttr = ` class="` + html.EscapeString(class) + `"`
|
||||||
|
}
|
||||||
|
return "<ul" + classAttr + ">" + strings.Join(rendered, "\n") + "</ul>"
|
||||||
|
}
|
||||||
|
|
||||||
func (ctx *wikiContext) renderClassSkillList(row map[string]any) string {
|
func (ctx *wikiContext) renderClassSkillList(row map[string]any) string {
|
||||||
table := ctx.tableForValue(fieldValue(row, "SkillsTable"), ctx.classSkillTables)
|
table := ctx.tableForValue(fieldValue(row, "SkillsTable"), ctx.classSkillTables)
|
||||||
if table == nil {
|
if table == nil {
|
||||||
@@ -68,6 +162,27 @@ func (ctx *wikiContext) renderClassSkillList(row map[string]any) string {
|
|||||||
return "==== Class Skills ====\n\n * " + strings.Join(skills, ", ")
|
return "==== Class Skills ====\n\n * " + strings.Join(skills, ", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ctx *wikiContext) renderClassSkillListHTML(row map[string]any) string {
|
||||||
|
table := ctx.tableForValue(fieldValue(row, "SkillsTable"), ctx.classSkillTables)
|
||||||
|
if table == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
skills := []string{}
|
||||||
|
for _, skillRow := range table.Rows {
|
||||||
|
if stringValue(skillRow, "ClassSkill") != "1" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name := ctx.renderReference(fieldValue(skillRow, "SkillIndex"), ctx.skillIDToKey, ctx.resolveSkillName)
|
||||||
|
if name != "" {
|
||||||
|
skills = append(skills, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(skills) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return `<h4>Class Skills</h4>` + renderWikiListHTML("wiki-list wiki-class-skills", skills)
|
||||||
|
}
|
||||||
|
|
||||||
func (ctx *wikiContext) renderClassFeatProgression(row map[string]any) string {
|
func (ctx *wikiContext) renderClassFeatProgression(row map[string]any) string {
|
||||||
table := ctx.tableForValue(fieldValue(row, "FeatsTable"), ctx.classFeatTables)
|
table := ctx.tableForValue(fieldValue(row, "FeatsTable"), ctx.classFeatTables)
|
||||||
if table == nil {
|
if table == nil {
|
||||||
@@ -104,6 +219,42 @@ func (ctx *wikiContext) renderClassFeatProgression(row map[string]any) string {
|
|||||||
return strings.TrimSpace(strings.Join(lines, "\n"))
|
return strings.TrimSpace(strings.Join(lines, "\n"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ctx *wikiContext) renderClassFeatProgressionHTML(row map[string]any) string {
|
||||||
|
table := ctx.tableForValue(fieldValue(row, "FeatsTable"), ctx.classFeatTables)
|
||||||
|
if table == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
byLevel := map[string][]string{}
|
||||||
|
selectable := []string{}
|
||||||
|
for _, featRow := range table.Rows {
|
||||||
|
name := ctx.renderReference(fieldValue(featRow, "FeatIndex"), ctx.featIDToKey, ctx.resolveFeatName)
|
||||||
|
if name == "" {
|
||||||
|
name = ctx.renderReference(fieldValue(featRow, "FeatIndex"), nil, func(string) string { return "" })
|
||||||
|
}
|
||||||
|
if name == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
level := stringValue(featRow, "GrantedOnLevel")
|
||||||
|
if level == "" || strings.HasPrefix(level, "-") {
|
||||||
|
selectable = append(selectable, name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
byLevel[level] = append(byLevel[level], name)
|
||||||
|
}
|
||||||
|
parts := []string{}
|
||||||
|
if len(byLevel) > 0 {
|
||||||
|
items := []string{}
|
||||||
|
for _, level := range sortedKeys(byLevel) {
|
||||||
|
items = append(items, "Level "+level+": "+strings.Join(byLevel[level], ", "))
|
||||||
|
}
|
||||||
|
parts = append(parts, `<h4>Granted Feats</h4>`+renderWikiListHTML("wiki-list wiki-class-granted-feats", items))
|
||||||
|
}
|
||||||
|
if len(selectable) > 0 {
|
||||||
|
parts = append(parts, `<h4>Selectable Feats</h4>`+renderWikiListHTML("wiki-list wiki-class-selectable-feats", []string{strings.Join(selectable, ", ")}))
|
||||||
|
}
|
||||||
|
return strings.Join(parts, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
func (ctx *wikiContext) renderClassFeatureTable(row map[string]any) string {
|
func (ctx *wikiContext) renderClassFeatureTable(row map[string]any) string {
|
||||||
table := ctx.tableForValue(fieldValue(row, "BonusFeatsTable"), ctx.classBonusTables)
|
table := ctx.tableForValue(fieldValue(row, "BonusFeatsTable"), ctx.classBonusTables)
|
||||||
if table == nil || len(table.Rows) == 0 {
|
if table == nil || len(table.Rows) == 0 {
|
||||||
@@ -111,3 +262,25 @@ func (ctx *wikiContext) renderClassFeatureTable(row map[string]any) string {
|
|||||||
}
|
}
|
||||||
return "==== Bonus Feats ====\n\n * Bonus feat table: " + table.OutputStem
|
return "==== Bonus Feats ====\n\n * Bonus feat table: " + table.OutputStem
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ctx *wikiContext) renderClassFeatureTableHTML(row map[string]any) string {
|
||||||
|
table := ctx.tableForValue(fieldValue(row, "BonusFeatsTable"), ctx.classBonusTables)
|
||||||
|
if table == nil || len(table.Rows) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return `<h4>Bonus Feats</h4>` + renderWikiListHTML("wiki-list wiki-class-bonus-feats", []string{"Bonus feat table: " + table.OutputStem})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *wikiContext) renderRaceNameFormsHTML(row map[string]any) string {
|
||||||
|
facts := []wikiHTMLFact{}
|
||||||
|
if plural := ctx.resolveTextValue(fieldValue(row, "NamePlural"), nil); plural != "" {
|
||||||
|
facts = append(facts, wikiHTMLFact{Label: "Plural", Value: plural})
|
||||||
|
}
|
||||||
|
if converted := ctx.resolveTextValue(fieldValue(row, "ConverName"), nil); converted != "" {
|
||||||
|
facts = append(facts, wikiHTMLFact{Label: "Converted Name", Value: converted})
|
||||||
|
}
|
||||||
|
if lower := ctx.resolveTextValue(fieldValue(row, "ConverNameLower"), nil); lower != "" {
|
||||||
|
facts = append(facts, wikiHTMLFact{Label: "Lower Name", Value: lower})
|
||||||
|
}
|
||||||
|
return renderWikiFactTableHTML(facts)
|
||||||
|
}
|
||||||
|
|||||||
+19
-270
@@ -912,7 +912,7 @@ func (ctx *wikiContext) renderFacts(category, key string, row map[string]any) st
|
|||||||
lines = append(lines, formatFact("Item Class", stringValue(row, "ItemClass")))
|
lines = append(lines, formatFact("Item Class", stringValue(row, "ItemClass")))
|
||||||
lines = append(lines, formatFact("Weapon Type", stringValue(row, "WeaponType")))
|
lines = append(lines, formatFact("Weapon Type", stringValue(row, "WeaponType")))
|
||||||
lines = append(lines, formatFact("Required Feats", ctx.renderReferenceList(ctx.wikiReqFeats(row))))
|
lines = append(lines, formatFact("Required Feats", ctx.renderReferenceList(ctx.wikiReqFeats(row))))
|
||||||
lines = append(lines, formatFact("Base Item Stats", toInlineDokuWiki(ctx.resolveTextValue(fieldValue(row, "BaseItemStatRef"), nil))))
|
lines = append(lines, formatFact("Base Item Stats", ctx.resolveTextValue(fieldValue(row, "BaseItemStatRef"), nil)))
|
||||||
case "classes":
|
case "classes":
|
||||||
lines = append(lines, formatFact("Hit Die", "d"+stringValue(row, "HitDie")))
|
lines = append(lines, formatFact("Hit Die", "d"+stringValue(row, "HitDie")))
|
||||||
lines = append(lines, formatFact("Skill Points", stringValue(row, "SkillPointBase")))
|
lines = append(lines, formatFact("Skill Points", stringValue(row, "SkillPointBase")))
|
||||||
@@ -1607,14 +1607,14 @@ func wikiNamespacePublicTitle(ctx *wikiContext, namespace string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func renderWikiNamespaceFragment(namespace string, summary map[string]any, ctx *wikiContext) string {
|
func renderWikiNamespaceFragment(namespace string, summary map[string]any, ctx *wikiContext) string {
|
||||||
return renderStatusTable(wikiNamespacePublicTitle(ctx, namespace), summary, "")
|
return renderStatusTable(wikiNamespacePublicTitle(ctx, namespace)+" Namespace", summary, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderWikiStatusReportPage(summaries map[string]map[string]any, namespaces []string, ctx *wikiContext) string {
|
func renderWikiStatusReportPage(summaries map[string]map[string]any, namespaces []string, ctx *wikiContext) string {
|
||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
ctx = &wikiContext{}
|
ctx = &wikiContext{}
|
||||||
}
|
}
|
||||||
lines := []string{"====== Wiki Status ======", "", "This page is auto-generated from builder source provenance.", ""}
|
lines := []string{"<h2>Wiki Status</h2>", "<p>This page is auto-generated from builder source provenance.</p>"}
|
||||||
for _, namespace := range namespaces {
|
for _, namespace := range namespaces {
|
||||||
summary := summaries[namespace]
|
summary := summaries[namespace]
|
||||||
title := wikiNamespacePublicTitle(ctx, namespace)
|
title := wikiNamespacePublicTitle(ctx, namespace)
|
||||||
@@ -1627,35 +1627,38 @@ func renderWikiStatusListingPage(title string, pageIDs []string, pageTitles map[
|
|||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
ctx = &wikiContext{}
|
ctx = &wikiContext{}
|
||||||
}
|
}
|
||||||
lines := []string{"====== " + title + " ======", ""}
|
lines := []string{"<h2>" + html.EscapeString(title) + "</h2>"}
|
||||||
if len(pageIDs) == 0 {
|
if len(pageIDs) == 0 {
|
||||||
lines = append(lines, "No matching generated pages.")
|
lines = append(lines, "<p>No matching generated pages.</p>")
|
||||||
} else {
|
} else {
|
||||||
|
lines = append(lines, `<ul class="wiki-list wiki-status-list">`)
|
||||||
for _, pageID := range pageIDs {
|
for _, pageID := range pageIDs {
|
||||||
title := pageTitles[pageID]
|
title := pageTitles[pageID]
|
||||||
if title == "" {
|
if title == "" {
|
||||||
title = pageID
|
title = pageID
|
||||||
}
|
}
|
||||||
namespace := strings.SplitN(pageID, ":", 2)[0]
|
namespace := strings.SplitN(pageID, ":", 2)[0]
|
||||||
lines = append(lines, " * [["+ctx.publicWikiPath(namespace, title)+"|"+title+"]]")
|
lines = append(lines, "<li>"+renderInlineHTML("[["+ctx.publicWikiPath(namespace, title)+"|"+title+"]]")+"</li>")
|
||||||
}
|
}
|
||||||
|
lines = append(lines, "</ul>")
|
||||||
}
|
}
|
||||||
return strings.Join(lines, "\n")
|
return strings.Join(lines, "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderStatusTable(title string, summary map[string]any, namespaceLink string) string {
|
func renderStatusTable(title string, summary map[string]any, namespaceLink string) string {
|
||||||
rows := []string{
|
rows := []string{
|
||||||
"===== " + title + " =====",
|
"<h2>" + html.EscapeString(title) + "</h2>",
|
||||||
"",
|
`<table class="wiki-facts wiki-status-summary"><tbody>`,
|
||||||
fmt.Sprintf("**Namespace Status:** %s\\\\", wikiStatusLabels[summary["status"].(string)]),
|
"<tr><th>Namespace Status</th><td>" + html.EscapeString(wikiStatusLabels[summary["status"].(string)]) + "</td></tr>",
|
||||||
fmt.Sprintf("**Generated Pages:** %d\\\\", summary["total"].(int)),
|
fmt.Sprintf("<tr><th>Generated Pages</th><td>%d</td></tr>", summary["total"].(int)),
|
||||||
fmt.Sprintf("**New:** %d\\\\", summary["new"].(int)),
|
fmt.Sprintf("<tr><th>New</th><td>%d</td></tr>", summary["new"].(int)),
|
||||||
fmt.Sprintf("**Modified:** %d\\\\", summary["modified"].(int)),
|
fmt.Sprintf("<tr><th>Modified</th><td>%d</td></tr>", summary["modified"].(int)),
|
||||||
fmt.Sprintf("**Vanilla:** %d\\\\", summary["vanilla"].(int)),
|
fmt.Sprintf("<tr><th>Vanilla</th><td>%d</td></tr>", summary["vanilla"].(int)),
|
||||||
}
|
}
|
||||||
if namespaceLink != "" {
|
if namespaceLink != "" {
|
||||||
rows = append(rows, fmt.Sprintf("**Namespace:** %s", namespaceLink))
|
rows = append(rows, "<tr><th>Namespace</th><td>"+renderInlineHTML(namespaceLink)+"</td></tr>")
|
||||||
}
|
}
|
||||||
|
rows = append(rows, "</tbody></table>")
|
||||||
return strings.Join(rows, "\n")
|
return strings.Join(rows, "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1973,61 +1976,13 @@ func formatFact(label, value string) string {
|
|||||||
return "**" + label + ":** " + value + `\\`
|
return "**" + label + ":** " + value + `\\`
|
||||||
}
|
}
|
||||||
|
|
||||||
func toInlineDokuWiki(text string) string {
|
|
||||||
lines := strings.Split(strings.TrimSpace(text), "\n")
|
|
||||||
for index, line := range lines {
|
|
||||||
lines[index] = strings.TrimSpace(line)
|
|
||||||
}
|
|
||||||
return strings.Join(lines, `\\`)
|
|
||||||
}
|
|
||||||
|
|
||||||
func toDokuWiki(text string) string {
|
|
||||||
text = strings.ReplaceAll(strings.TrimSpace(text), "\r\n", "\n")
|
|
||||||
if text == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
lines := strings.Split(text, "\n")
|
|
||||||
out := make([]string, 0, len(lines))
|
|
||||||
for _, line := range lines {
|
|
||||||
trimmed := strings.TrimSpace(line)
|
|
||||||
switch {
|
|
||||||
case trimmed == "":
|
|
||||||
out = append(out, "")
|
|
||||||
case strings.HasPrefix(trimmed, "- "):
|
|
||||||
out = append(out, " * "+strings.TrimSpace(strings.TrimPrefix(trimmed, "- ")))
|
|
||||||
default:
|
|
||||||
out = append(out, line)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return strings.Join(out, "\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func renderNodeBBManagedMarkdown(pageID, dokuText string) string {
|
|
||||||
generated, notes := splitDokuWikiNotes(dokuText)
|
|
||||||
body := dokuWikiToNodeBBMarkdown(generated)
|
|
||||||
parts := []string{
|
|
||||||
"[//]: # (sow-topdata-wiki:page=" + pageID + ")",
|
|
||||||
"[//]: # (sow-topdata-wiki:managed:start)",
|
|
||||||
body,
|
|
||||||
"[//]: # (sow-topdata-wiki:managed:end)",
|
|
||||||
}
|
|
||||||
notesMarkdown := dokuWikiToNodeBBMarkdown("===== Notes =====\n" + notes)
|
|
||||||
if strings.TrimSpace(notesMarkdown) != "" {
|
|
||||||
parts = append(parts, notesMarkdown)
|
|
||||||
}
|
|
||||||
return ensureTrailingNewline(strings.Join(parts, "\n"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func renderNodeBBManagedHTML(pageID, sourceText string, manualSections []wikiManualSection) string {
|
func renderNodeBBManagedHTML(pageID, sourceText string, manualSections []wikiManualSection) string {
|
||||||
body := sourceText
|
body := strings.TrimSpace(sourceText)
|
||||||
if !strings.Contains(sourceText, "<h1") {
|
|
||||||
body = dokuWikiToNodeBBHTML(sourceText)
|
|
||||||
}
|
|
||||||
hash := computeManagedHash(body)
|
hash := computeManagedHash(body)
|
||||||
parts := []string{
|
parts := []string{
|
||||||
"<!-- sow-topdata-wiki:page=" + pageID + " -->",
|
"<!-- sow-topdata-wiki:page=" + pageID + " -->",
|
||||||
"<!-- sow-topdata-wiki:managed:start hash=\"sha256:" + hash + "\" -->",
|
"<!-- sow-topdata-wiki:managed:start hash=\"sha256:" + hash + "\" -->",
|
||||||
strings.TrimSpace(body),
|
body,
|
||||||
"<!-- sow-topdata-wiki:managed:end -->",
|
"<!-- sow-topdata-wiki:managed:end -->",
|
||||||
}
|
}
|
||||||
renderedSections := extractManualRegions(body)
|
renderedSections := extractManualRegions(body)
|
||||||
@@ -2042,104 +1997,6 @@ func renderNodeBBManagedHTML(pageID, sourceText string, manualSections []wikiMan
|
|||||||
return ensureTrailingNewline(strings.Join(parts, "\n"))
|
return ensureTrailingNewline(strings.Join(parts, "\n"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func dokuWikiToNodeBBHTML(text string) string {
|
|
||||||
text = strings.ReplaceAll(strings.TrimSpace(text), "\r\n", "\n")
|
|
||||||
if text == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
lines := strings.Split(text, "\n")
|
|
||||||
var out []string
|
|
||||||
var paragraph []string
|
|
||||||
var list []string
|
|
||||||
var tableRows []string
|
|
||||||
|
|
||||||
flushParagraph := func() {
|
|
||||||
if len(paragraph) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
out = append(out, "<p>"+renderInlineHTML(strings.Join(paragraph, " "))+"</p>")
|
|
||||||
paragraph = nil
|
|
||||||
}
|
|
||||||
flushList := func() {
|
|
||||||
if len(list) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
out = append(out, "<ul>")
|
|
||||||
for _, item := range list {
|
|
||||||
out = append(out, "<li>"+renderInlineHTML(item)+"</li>")
|
|
||||||
}
|
|
||||||
out = append(out, "</ul>")
|
|
||||||
list = nil
|
|
||||||
}
|
|
||||||
flushTable := func() {
|
|
||||||
if len(tableRows) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
out = append(out, "<table><tbody>")
|
|
||||||
out = append(out, tableRows...)
|
|
||||||
out = append(out, "</tbody></table>")
|
|
||||||
tableRows = nil
|
|
||||||
}
|
|
||||||
flushBlocks := func() {
|
|
||||||
flushParagraph()
|
|
||||||
flushList()
|
|
||||||
flushTable()
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, line := range lines {
|
|
||||||
trimmed := strings.TrimSpace(line)
|
|
||||||
if trimmed == "" {
|
|
||||||
flushBlocks()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(trimmed, "<p class=\"wiki-callout") {
|
|
||||||
flushBlocks()
|
|
||||||
out = append(out, trimmed)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if heading, level, ok := parseDokuHeading(trimmed); ok {
|
|
||||||
flushBlocks()
|
|
||||||
out = append(out, fmt.Sprintf("<h%d>%s</h%d>", level, renderInlineHTML(heading), level))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(trimmed, " * ") || strings.HasPrefix(trimmed, "* ") || strings.HasPrefix(trimmed, "- ") {
|
|
||||||
flushParagraph()
|
|
||||||
flushTable()
|
|
||||||
item := strings.TrimSpace(strings.TrimPrefix(strings.TrimPrefix(strings.TrimPrefix(trimmed, " * "), "* "), "- "))
|
|
||||||
list = append(list, item)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if label, value, ok := parseFactLine(trimmed); ok {
|
|
||||||
flushParagraph()
|
|
||||||
flushList()
|
|
||||||
tableRows = append(tableRows, "<tr><th>"+html.EscapeString(label)+"</th><td>"+renderInlineHTML(value)+"</td></tr>")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
flushList()
|
|
||||||
flushTable()
|
|
||||||
for _, part := range strings.Split(trimmed, `\\`) {
|
|
||||||
part = strings.TrimSpace(part)
|
|
||||||
if part != "" {
|
|
||||||
paragraph = append(paragraph, part)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
flushBlocks()
|
|
||||||
return strings.TrimSpace(strings.Join(out, "\n"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseFactLine(line string) (string, string, bool) {
|
|
||||||
if !strings.HasPrefix(line, "**") {
|
|
||||||
return "", "", false
|
|
||||||
}
|
|
||||||
rest := strings.TrimPrefix(line, "**")
|
|
||||||
label, value, ok := strings.Cut(rest, ":**")
|
|
||||||
if !ok {
|
|
||||||
return "", "", false
|
|
||||||
}
|
|
||||||
return strings.TrimSpace(label), strings.Trim(strings.TrimSpace(value), `\`), true
|
|
||||||
}
|
|
||||||
|
|
||||||
func renderInlineHTML(text string) string {
|
func renderInlineHTML(text string) string {
|
||||||
var out strings.Builder
|
var out strings.Builder
|
||||||
for {
|
for {
|
||||||
@@ -2161,114 +2018,6 @@ func renderInlineHTML(text string) string {
|
|||||||
return out.String()
|
return out.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func splitDokuWikiNotes(text string) (string, string) {
|
|
||||||
const delimiter = "===== Notes ====="
|
|
||||||
before, after, ok := strings.Cut(text, delimiter)
|
|
||||||
if !ok {
|
|
||||||
return text, ""
|
|
||||||
}
|
|
||||||
return before, after
|
|
||||||
}
|
|
||||||
|
|
||||||
func dokuWikiToNodeBBMarkdown(text string) string {
|
|
||||||
text = strings.ReplaceAll(strings.TrimSpace(text), "\r\n", "\n")
|
|
||||||
if text == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
var out []string
|
|
||||||
inNotice := false
|
|
||||||
|
|
||||||
closeNotice := func() {
|
|
||||||
if inNotice {
|
|
||||||
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 == "" {
|
|
||||||
appendBlank()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(trimmed, "<WRAP") {
|
|
||||||
appendBlank()
|
|
||||||
inNotice = true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if trimmed == "</WRAP>" {
|
|
||||||
closeNotice()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if heading, level, ok := parseDokuHeading(trimmed); ok {
|
|
||||||
closeNotice()
|
|
||||||
appendBlank()
|
|
||||||
out = append(out, strings.Repeat("#", level)+" "+renderInlineNodeBBMarkdown(heading))
|
|
||||||
out = append(out, "")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(trimmed, " * ") || strings.HasPrefix(trimmed, "* ") || strings.HasPrefix(trimmed, "- ") {
|
|
||||||
item := strings.TrimSpace(strings.TrimPrefix(strings.TrimPrefix(strings.TrimPrefix(trimmed, " * "), "* "), "- "))
|
|
||||||
prefix := "- "
|
|
||||||
if inNotice {
|
|
||||||
prefix = "> - "
|
|
||||||
}
|
|
||||||
out = append(out, prefix+renderInlineNodeBBMarkdown(item))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
parts := strings.Split(trimmed, `\\`)
|
|
||||||
for _, part := range parts {
|
|
||||||
part = strings.TrimSpace(part)
|
|
||||||
if part != "" {
|
|
||||||
prefix := ""
|
|
||||||
if inNotice {
|
|
||||||
prefix = "> "
|
|
||||||
}
|
|
||||||
out = append(out, prefix+renderInlineNodeBBMarkdown(part))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
closeNotice()
|
|
||||||
return strings.TrimSpace(strings.Join(out, "\n"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseDokuHeading(line string) (string, int, bool) {
|
|
||||||
if !strings.HasPrefix(line, "=") || !strings.HasSuffix(line, "=") {
|
|
||||||
return "", 0, false
|
|
||||||
}
|
|
||||||
left := len(line) - len(strings.TrimLeft(line, "="))
|
|
||||||
right := len(line) - len(strings.TrimRight(line, "="))
|
|
||||||
if left == 0 || left != right {
|
|
||||||
return "", 0, false
|
|
||||||
}
|
|
||||||
title := strings.TrimSpace(line[left : len(line)-right])
|
|
||||||
if title == "" {
|
|
||||||
return "", 0, false
|
|
||||||
}
|
|
||||||
level := 7 - left
|
|
||||||
if level < 1 {
|
|
||||||
level = 1
|
|
||||||
}
|
|
||||||
if level > 6 {
|
|
||||||
level = 6
|
|
||||||
}
|
|
||||||
return title, level, true
|
|
||||||
}
|
|
||||||
|
|
||||||
func renderInlineNodeBBMarkdown(text string) string {
|
|
||||||
return renderWikiLinks(text)
|
|
||||||
}
|
|
||||||
|
|
||||||
func renderWikiLinks(text string) string {
|
func renderWikiLinks(text string) string {
|
||||||
var out strings.Builder
|
var out strings.Builder
|
||||||
for {
|
for {
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ func TestBuildNativeGeneratesAndSkipsWikiPages(t *testing.T) {
|
|||||||
if !strings.Contains(text, "<h1>Athletics</h1>") || !strings.Contains(text, `class="wiki-callout wiki-callout--status"`) || !strings.Contains(text, "This skill is unchanged from vanilla.") {
|
if !strings.Contains(text, "<h1>Athletics</h1>") || !strings.Contains(text, `class="wiki-callout wiki-callout--status"`) || !strings.Contains(text, "This skill is unchanged from vanilla.") {
|
||||||
t.Fatalf("unexpected generated page:\n%s", text)
|
t.Fatalf("unexpected generated page:\n%s", text)
|
||||||
}
|
}
|
||||||
if !strings.Contains(text, `<!-- sow-topdata-wiki:page=skills:athletics -->`) || strings.Contains(text, `wiki_slug=`) || !strings.Contains(text, `<!-- sow-topdata-wiki:managed:start hash="sha256:`) || !strings.Contains(text, "Ability: Strength.") || !strings.Contains(text, "<li>Climb</li>") {
|
if !strings.Contains(text, `<!-- sow-topdata-wiki:page=skills:athletics -->`) || strings.Contains(text, `wiki_slug=`) || !strings.Contains(text, `<!-- sow-topdata-wiki:managed:start hash="sha256:`) || !strings.Contains(text, "Ability: Strength.") || !strings.Contains(text, "Uses:<br>- Climb<br>- Swim") {
|
||||||
t.Fatalf("expected HTML managed markers and description to be rendered into wiki page:\n%s", text)
|
t.Fatalf("expected HTML managed markers and description to be rendered into wiki page:\n%s", text)
|
||||||
}
|
}
|
||||||
if strings.Contains(text, "[//]: #") || strings.Contains(text, "<WRAP>") {
|
if strings.Contains(text, "[//]: #") || strings.Contains(text, "<WRAP>") {
|
||||||
@@ -180,6 +180,44 @@ func TestBuildWikiRegeneratesMissingCachedPages(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRenderNodeBBManagedHTMLPreservesHeadinglessTemplateHTML(t *testing.T) {
|
||||||
|
source := strings.TrimSpace(`
|
||||||
|
<p class="wiki-callout wiki-callout--status">This is generated.</p>
|
||||||
|
<!-- sow-topdata-wiki:manual:start id="user_top" -->
|
||||||
|
<p></p>
|
||||||
|
<!-- sow-topdata-wiki:manual:end id="user_top" -->
|
||||||
|
<p>Generated description.</p>
|
||||||
|
<table class="wiki-facts"><tbody><tr><th scope="row">Hit Die</th><td>d6</td></tr></tbody></table>
|
||||||
|
<h3>Class Skills</h3>
|
||||||
|
<div class="wiki-media-row"><ul><li>[[Skills/Heal|Heal]]</li></ul></div>
|
||||||
|
`)
|
||||||
|
|
||||||
|
got := renderNodeBBManagedHTML("classes:adept", source, []wikiManualSection{
|
||||||
|
{ID: "user_top", Alias: "UserTopSection", InitialHTML: "<p></p>"},
|
||||||
|
{ID: "user_bottom", Alias: "UserBottomSection", InitialHTML: "<p></p>"},
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, want := range []string{
|
||||||
|
`<table class="wiki-facts">`,
|
||||||
|
`<h3>Class Skills</h3>`,
|
||||||
|
`<div class="wiki-media-row">`,
|
||||||
|
`<li>[[Skills/Heal|Heal]]</li>`,
|
||||||
|
} {
|
||||||
|
if !strings.Contains(got, want) {
|
||||||
|
t.Fatalf("expected generated HTML to preserve %q:\n%s", want, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.Contains(got, "<table") || strings.Contains(got, "<div") || strings.Contains(got, "<p><") {
|
||||||
|
t.Fatalf("expected generated HTML not to be escaped into plain text:\n%s", got)
|
||||||
|
}
|
||||||
|
if strings.Count(got, `manual:start id="user_top"`) != 1 {
|
||||||
|
t.Fatalf("expected existing user_top manual section not to be duplicated:\n%s", got)
|
||||||
|
}
|
||||||
|
if strings.Count(got, `manual:start id="user_bottom"`) != 1 {
|
||||||
|
t.Fatalf("expected missing user_bottom manual section to be bootstrapped once:\n%s", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestBuildNativeIndexesUnregisteredStatusPagesWithDeclaredNamespacePath(t *testing.T) {
|
func TestBuildNativeIndexesUnregisteredStatusPagesWithDeclaredNamespacePath(t *testing.T) {
|
||||||
root := testProjectRoot(t)
|
root := testProjectRoot(t)
|
||||||
mkdirAll(t, filepath.Join(root, "topdata", "data", "skills"))
|
mkdirAll(t, filepath.Join(root, "topdata", "data", "skills"))
|
||||||
|
|||||||
Reference in New Issue
Block a user