Canonical path adherence (#10)
Reviewed-on: https://gitea.westgate.pw/ShadowsOverWestgate/sow-tools/pulls/10 Co-authored-by: vickydotbat <vickydotbat@tutamail.com> Co-committed-by: vickydotbat <vickydotbat@tutamail.com>
This commit is contained in:
@@ -66,7 +66,7 @@ type deployResult struct {
|
|||||||
type wikiDeployPage struct {
|
type wikiDeployPage struct {
|
||||||
PageID string
|
PageID string
|
||||||
Title string
|
Title string
|
||||||
PublicSlug string
|
PublicPath string
|
||||||
Namespace string
|
Namespace string
|
||||||
Content string
|
Content string
|
||||||
Hash string
|
Hash string
|
||||||
@@ -333,7 +333,7 @@ func collectLocalPages(sourceDir string, namespaces []string) (map[string]wikiDe
|
|||||||
pages[pageID] = wikiDeployPage{
|
pages[pageID] = wikiDeployPage{
|
||||||
PageID: pageID,
|
PageID: pageID,
|
||||||
Title: extractPageTitle(content, pageID),
|
Title: extractPageTitle(content, pageID),
|
||||||
PublicSlug: extractWikiPagePublicSlug(content),
|
PublicPath: extractWikiPagePublicPath(content),
|
||||||
Namespace: namespace,
|
Namespace: namespace,
|
||||||
Content: content,
|
Content: content,
|
||||||
Hash: computeManagedHash(content),
|
Hash: computeManagedHash(content),
|
||||||
@@ -636,10 +636,10 @@ func fetchNodeBBWikiPageMapping(page wikiDeployPage, cid int, remotePage nodeBBW
|
|||||||
}
|
}
|
||||||
|
|
||||||
func matchExistingNodeBBPage(page wikiDeployPage, remotePages []nodeBBWikiPage) (nodeBBWikiPage, bool) {
|
func matchExistingNodeBBPage(page wikiDeployPage, remotePages []nodeBBWikiPage) (nodeBBWikiPage, bool) {
|
||||||
if page.PublicSlug != "" {
|
if page.PublicPath != "" {
|
||||||
for _, candidate := range remotePages {
|
for _, candidate := range remotePages {
|
||||||
for _, slug := range []string{nodeBBPathLeaf(candidate.Slug), nodeBBPathLeaf(candidate.WikiPath)} {
|
for _, path := range []string{candidate.WikiPath, candidate.Slug} {
|
||||||
if slugifyWikiTitle(slug) == page.PublicSlug {
|
if normalizeWikiPath(path) == normalizeWikiPath(page.PublicPath) {
|
||||||
return candidate, true
|
return candidate, true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -653,12 +653,12 @@ func matchExistingNodeBBPage(page wikiDeployPage, remotePages []nodeBBWikiPage)
|
|||||||
}
|
}
|
||||||
desiredSlugs := map[string]struct{}{}
|
desiredSlugs := map[string]struct{}{}
|
||||||
for title := range desiredTitles {
|
for title := range desiredTitles {
|
||||||
if slug := slugifyWikiTitle(title); slug != "" {
|
if slug := strings.ToLower(canonicalWikiSegment(title)); slug != "" {
|
||||||
desiredSlugs[slug] = struct{}{}
|
desiredSlugs[slug] = struct{}{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, slug := range []string{pageIDLeaf(page.PageID), strings.ReplaceAll(pageIDRemainder(page.PageID), ":", "-")} {
|
for _, slug := range []string{pageIDLeaf(page.PageID), strings.ReplaceAll(pageIDRemainder(page.PageID), ":", "-")} {
|
||||||
if normalized := slugifyWikiTitle(slug); normalized != "" {
|
if normalized := strings.ToLower(canonicalWikiSegment(slug)); normalized != "" {
|
||||||
desiredSlugs[normalized] = struct{}{}
|
desiredSlugs[normalized] = struct{}{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -670,7 +670,7 @@ func matchExistingNodeBBPage(page wikiDeployPage, remotePages []nodeBBWikiPage)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, slug := range []string{nodeBBPathLeaf(candidate.Slug), nodeBBPathLeaf(candidate.WikiPath)} {
|
for _, slug := range []string{nodeBBPathLeaf(candidate.Slug), nodeBBPathLeaf(candidate.WikiPath)} {
|
||||||
if _, ok := desiredSlugs[slugifyWikiTitle(slug)]; ok {
|
if _, ok := desiredSlugs[strings.ToLower(canonicalWikiSegment(slug))]; ok {
|
||||||
return candidate, true
|
return candidate, true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -718,6 +718,20 @@ func slugifyWikiTitle(value string) string {
|
|||||||
return slugifyWikiText(value, "")
|
return slugifyWikiText(value, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func normalizeWikiPath(value string) string {
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
value = strings.TrimPrefix(value, "/wiki/")
|
||||||
|
value = strings.TrimPrefix(value, "wiki/")
|
||||||
|
parts := []string{}
|
||||||
|
for _, part := range strings.Split(value, "/") {
|
||||||
|
canonical := canonicalWikiSegment(part)
|
||||||
|
if canonical != "" {
|
||||||
|
parts = append(parts, strings.ToLower(canonical))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Join(parts, "/")
|
||||||
|
}
|
||||||
|
|
||||||
func loadDeployManifest(path string) wikiDeployManifest {
|
func loadDeployManifest(path string) wikiDeployManifest {
|
||||||
doc := wikiDeployManifest{Version: "nodebb-v1", Pages: map[string]wikiDeployManifestPage{}}
|
doc := wikiDeployManifest{Version: "nodebb-v1", Pages: map[string]wikiDeployManifestPage{}}
|
||||||
raw, err := os.ReadFile(path)
|
raw, err := os.ReadFile(path)
|
||||||
@@ -910,9 +924,9 @@ func renderArchivedWikiPage(pageID, title string) string {
|
|||||||
}, "\n"))
|
}, "\n"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractWikiPagePublicSlug(content string) string {
|
func extractWikiPagePublicPath(content string) string {
|
||||||
const pageMarker = "sow-topdata-wiki:page="
|
const pageMarker = "sow-topdata-wiki:page="
|
||||||
const slugField = "wiki_slug="
|
const pathField = "public_path="
|
||||||
start := strings.Index(content, pageMarker)
|
start := strings.Index(content, pageMarker)
|
||||||
if start < 0 {
|
if start < 0 {
|
||||||
return ""
|
return ""
|
||||||
@@ -922,15 +936,15 @@ func extractWikiPagePublicSlug(content string) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
marker := content[start : start+end]
|
marker := content[start : start+end]
|
||||||
fieldStart := strings.Index(marker, slugField)
|
fieldStart := strings.Index(marker, pathField)
|
||||||
if fieldStart < 0 {
|
if fieldStart < 0 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
value := marker[fieldStart+len(slugField):]
|
value := marker[fieldStart+len(pathField):]
|
||||||
if fields := strings.Fields(value); len(fields) > 0 {
|
if fields := strings.Fields(value); len(fields) > 0 {
|
||||||
slug := strings.TrimSpace(fields[0])
|
path := strings.Trim(strings.TrimSpace(fields[0]), `"`)
|
||||||
if slug != "" && slug == slugifyWikiTitle(slug) {
|
if path != "" && canonicalWikiPath(path) == path {
|
||||||
return slug
|
return path
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
@@ -153,34 +153,34 @@ func TestDeployWikiDryRunReadoptsMissingMappedPost(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMatchExistingNodeBBPagePrefersGeneratedPublicSlug(t *testing.T) {
|
func TestMatchExistingNodeBBPageIgnoresRetiredGeneratedPublicSlug(t *testing.T) {
|
||||||
page := wikiDeployPage{
|
page := wikiDeployPage{
|
||||||
PageID: "spells:pdk:fear",
|
PageID: "spells:pdk:fear",
|
||||||
Title: "Fear",
|
Title: "Fear",
|
||||||
PublicSlug: "pdk-fear",
|
PublicPath: "Spells/Fear",
|
||||||
}
|
}
|
||||||
matched, ok := matchExistingNodeBBPage(page, []nodeBBWikiPage{
|
matched, ok := matchExistingNodeBBPage(page, []nodeBBWikiPage{
|
||||||
{TID: 31, Title: "Fear", TitleLeaf: "Fear", WikiPath: "/wiki/spells/fear"},
|
{TID: 31, Title: "Fear", TitleLeaf: "Fear", WikiPath: "/wiki/spells/fear"},
|
||||||
{TID: 32, Title: "Fear", TitleLeaf: "Fear", WikiPath: "/wiki/spells/pdk-fear"},
|
{TID: 32, Title: "Fear", TitleLeaf: "Fear", WikiPath: "/wiki/spells/pdk-fear"},
|
||||||
})
|
})
|
||||||
if !ok || matched.TID != 32 {
|
if !ok || matched.TID != 31 {
|
||||||
t.Fatalf("expected generated public slug to select tid 32, got %#v ok=%t", matched, ok)
|
t.Fatalf("expected canonical title match to select tid 31, got %#v ok=%t", matched, ok)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDeploySlugifyWikiTitleMatchesGeneratedSlugPolicy(t *testing.T) {
|
func TestDeployCanonicalWikiSegmentMatchesGeneratedPathPolicy(t *testing.T) {
|
||||||
tests := map[string]string{
|
tests := map[string]string{
|
||||||
`Grandmaster's Battle Momentum`: "grandmasters-battle-momentum",
|
`Grandmaster's Battle Momentum`: "Grandmasters_Battle_Momentum",
|
||||||
`Bigby’s Clenched Fist`: "bigbys-clenched-fist",
|
`Bigby’s Clenched Fist`: "Bigbys_Clenched_Fist",
|
||||||
`Hardiness vs. Enchantments`: "hardiness-vs-enchantments",
|
`Hardiness vs. Enchantments`: "Hardiness_vs_Enchantments",
|
||||||
`Clairaudience/Clairvoyance`: "clairaudience-clairvoyance",
|
`Clairaudience/Clairvoyance`: "Clairaudience_Clairvoyance",
|
||||||
`Élite & Noble Houses`: "elite-noble-houses",
|
`Élite & Noble Houses`: "Elite_Noble_Houses",
|
||||||
`Alpha!@#$%^&*()[]-=_+/?.,<>` + "`" + `~|\Omega`: "alpha-omega",
|
`Alpha!@#$%^&*()[]-=_+/?.,<>` + "`" + `~|\Omega`: "Alpha_Omega",
|
||||||
`Æther Œuvre Øresund Straße Þorn Łódź Đelta`: "aether-oeuvre-oresund-strasse-thorn-lodz-delta",
|
`Æther Œuvre Øresund Straße Þorn Łódź Đelta`: "Aether_Oeuvre_Oresund_Strasse_Thorn_Lodz_Delta",
|
||||||
}
|
}
|
||||||
for input, want := range tests {
|
for input, want := range tests {
|
||||||
if got := slugifyWikiTitle(input); got != want {
|
if got := canonicalWikiSegment(input); got != want {
|
||||||
t.Fatalf("slugifyWikiTitle(%q) = %q, want %q", input, got, want)
|
t.Fatalf("canonicalWikiSegment(%q) = %q, want %q", input, got, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,8 +56,7 @@ type wikiPageIndexEntry struct {
|
|||||||
SourceDataset string `json:"source_dataset"`
|
SourceDataset string `json:"source_dataset"`
|
||||||
SourceKey string `json:"source_key"`
|
SourceKey string `json:"source_key"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
PublicSlug string `json:"public_slug"`
|
PublicPath string `json:"public_path"`
|
||||||
PublicTarget string `json:"public_target"`
|
|
||||||
Hash string `json:"hash"`
|
Hash string `json:"hash"`
|
||||||
OutputPath string `json:"output_path"`
|
OutputPath string `json:"output_path"`
|
||||||
EditPolicy string `json:"edit_policy"`
|
EditPolicy string `json:"edit_policy"`
|
||||||
@@ -119,7 +118,7 @@ type wikiContext struct {
|
|||||||
wikiDataProviders map[string]wikiDataProviderDefinition
|
wikiDataProviders map[string]wikiDataProviderDefinition
|
||||||
visibilityPolicy *wikiVisibilityDefinitions
|
visibilityPolicy *wikiVisibilityDefinitions
|
||||||
visibleWikiKeys map[string]bool
|
visibleWikiKeys map[string]bool
|
||||||
pageSlugOverrides map[string]string
|
namespaceTitles map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildWiki(p *project.Project, nativeResult BuildResult, force bool, progress func(string)) (wikiResult, error) {
|
func buildWiki(p *project.Project, nativeResult BuildResult, force bool, progress func(string)) (wikiResult, error) {
|
||||||
@@ -173,7 +172,12 @@ func buildWiki(p *project.Project, nativeResult BuildResult, force bool, progres
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return wikiResult{}, err
|
return wikiResult{}, err
|
||||||
}
|
}
|
||||||
ctx.pageSlugOverrides = pageSlugOverrides
|
_ = pageSlugOverrides
|
||||||
|
namespaceDeclarations, err := loadWikiNamespaceDeclarations(p)
|
||||||
|
if err != nil {
|
||||||
|
return wikiResult{}, err
|
||||||
|
}
|
||||||
|
ctx.namespaceTitles = wikiNamespaceTitles(namespaceDeclarations)
|
||||||
|
|
||||||
if err := os.RemoveAll(outputDir); err != nil {
|
if err := os.RemoveAll(outputDir); err != nil {
|
||||||
return wikiResult{}, fmt.Errorf("clean wiki output: %w", err)
|
return wikiResult{}, fmt.Errorf("clean wiki output: %w", err)
|
||||||
@@ -192,8 +196,8 @@ func buildWiki(p *project.Project, nativeResult BuildResult, force bool, progres
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
namespace := strings.SplitN(pageID, ":", 2)[0]
|
namespace := strings.SplitN(pageID, ":", 2)[0]
|
||||||
publicSlug := ctx.publicWikiPageSlug(pageID, title)
|
publicPath := ctx.publicWikiPath(namespace, title)
|
||||||
content = renderNodeBBManagedHTML(pageID, publicSlug, content, manualSections)
|
content = renderNodeBBManagedHTML(pageID, content, manualSections)
|
||||||
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
|
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -209,8 +213,7 @@ func buildWiki(p *project.Project, nativeResult BuildResult, force bool, progres
|
|||||||
SourceDataset: categoryFromWikiNamespace(namespace),
|
SourceDataset: categoryFromWikiNamespace(namespace),
|
||||||
SourceKey: pageID,
|
SourceKey: pageID,
|
||||||
Title: title,
|
Title: title,
|
||||||
PublicSlug: publicSlug,
|
PublicPath: publicPath,
|
||||||
PublicTarget: wikiPublicTarget(namespace, publicSlug),
|
|
||||||
Hash: computeManagedHash(content),
|
Hash: computeManagedHash(content),
|
||||||
OutputPath: filepath.ToSlash(rel),
|
OutputPath: filepath.ToSlash(rel),
|
||||||
EditPolicy: wikiEditPolicy(namespace),
|
EditPolicy: wikiEditPolicy(namespace),
|
||||||
@@ -327,7 +330,7 @@ func saveWikiPageIndex(path string, index wikiPageIndex) error {
|
|||||||
func validateWikiPageIndex(index wikiPageIndex) error {
|
func validateWikiPageIndex(index wikiPageIndex) error {
|
||||||
pageIDs := map[string]string{}
|
pageIDs := map[string]string{}
|
||||||
outputPaths := map[string]string{}
|
outputPaths := map[string]string{}
|
||||||
publicTargets := map[string]string{}
|
publicPaths := map[string]string{}
|
||||||
for _, entry := range index.Pages {
|
for _, entry := range index.Pages {
|
||||||
if previous, ok := pageIDs[entry.PageID]; ok {
|
if previous, ok := pageIDs[entry.PageID]; ok {
|
||||||
return fmt.Errorf("duplicate wiki page-index page_id %q for %s and %s", entry.PageID, previous, entry.OutputPath)
|
return fmt.Errorf("duplicate wiki page-index page_id %q for %s and %s", entry.PageID, previous, entry.OutputPath)
|
||||||
@@ -335,11 +338,11 @@ func validateWikiPageIndex(index wikiPageIndex) error {
|
|||||||
if previous, ok := outputPaths[entry.OutputPath]; ok {
|
if previous, ok := outputPaths[entry.OutputPath]; ok {
|
||||||
return fmt.Errorf("duplicate wiki page-index output_path %q for %s and %s", entry.OutputPath, previous, entry.PageID)
|
return fmt.Errorf("duplicate wiki page-index output_path %q for %s and %s", entry.OutputPath, previous, entry.PageID)
|
||||||
}
|
}
|
||||||
if entry.PublicTarget != "" {
|
if entry.PublicPath != "" {
|
||||||
if previous, ok := publicTargets[entry.PublicTarget]; ok {
|
if previous, ok := publicPaths[entry.PublicPath]; ok {
|
||||||
return fmt.Errorf("duplicate wiki page-index public_target %q for %s and %s", entry.PublicTarget, previous, entry.PageID)
|
return fmt.Errorf("duplicate wiki page-index public_path %q for %s and %s", entry.PublicPath, previous, entry.PageID)
|
||||||
}
|
}
|
||||||
publicTargets[entry.PublicTarget] = entry.PageID
|
publicPaths[entry.PublicPath] = entry.PageID
|
||||||
}
|
}
|
||||||
pageIDs[entry.PageID] = entry.OutputPath
|
pageIDs[entry.PageID] = entry.OutputPath
|
||||||
outputPaths[entry.OutputPath] = entry.PageID
|
outputPaths[entry.OutputPath] = entry.PageID
|
||||||
@@ -383,8 +386,7 @@ func indexUnregisteredWikiPages(rootDir, outputDir, stalePolicy string, index *w
|
|||||||
SourceDataset: categoryFromWikiNamespace(namespace),
|
SourceDataset: categoryFromWikiNamespace(namespace),
|
||||||
SourceKey: pageID,
|
SourceKey: pageID,
|
||||||
Title: extractHTMLTitle(string(raw), pageID),
|
Title: extractHTMLTitle(string(raw), pageID),
|
||||||
PublicSlug: wikiSlugifyTitle(extractHTMLTitle(string(raw), pageID)),
|
PublicPath: (&wikiContext{}).publicWikiPath(namespace, extractHTMLTitle(string(raw), pageID)),
|
||||||
PublicTarget: wikiPublicTarget(namespace, wikiSlugifyTitle(extractHTMLTitle(string(raw), pageID))),
|
|
||||||
Hash: computeManagedHash(string(raw)),
|
Hash: computeManagedHash(string(raw)),
|
||||||
OutputPath: filepath.ToSlash(relToRoot),
|
OutputPath: filepath.ToSlash(relToRoot),
|
||||||
EditPolicy: wikiEditPolicy(namespace),
|
EditPolicy: wikiEditPolicy(namespace),
|
||||||
@@ -1532,7 +1534,7 @@ func writeWikiFile(path, content string, manualSections []wikiManualSection) err
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
pageID := wikiRelPathToPageID(path)
|
pageID := wikiRelPathToPageID(path)
|
||||||
content = renderNodeBBManagedHTML(pageID, wikiSlugifyTitle(extractHTMLTitle(content, pageID)), content, manualSections)
|
content = renderNodeBBManagedHTML(pageID, content, manualSections)
|
||||||
return os.WriteFile(path, []byte(ensureTrailingNewline(content)), 0o644)
|
return os.WriteFile(path, []byte(ensureTrailingNewline(content)), 0o644)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1636,6 +1638,18 @@ func filterWikiPageIDs(pageStatuses map[string]string, namespace, status string)
|
|||||||
return pageIDs
|
return pageIDs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func wikiNamespaceTitles(declarations []wikiNamespaceDeclaration) map[string]string {
|
||||||
|
titles := map[string]string{}
|
||||||
|
for _, declaration := range declarations {
|
||||||
|
id := strings.TrimSpace(declaration.ID)
|
||||||
|
title := strings.TrimSpace(declaration.Title)
|
||||||
|
if id != "" && title != "" {
|
||||||
|
titles[id] = title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return titles
|
||||||
|
}
|
||||||
|
|
||||||
func wikiPageIDForKey(key string) string {
|
func wikiPageIDForKey(key string) string {
|
||||||
if key == "" {
|
if key == "" {
|
||||||
return ""
|
return ""
|
||||||
@@ -1655,17 +1669,10 @@ func wikiPageIDForKey(key string) string {
|
|||||||
if namespace == "" {
|
if namespace == "" {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
value = strings.NewReplacer("/", ":", "\\", ":", "_", ":").Replace(value)
|
value = strings.NewReplacer("/", ":", "\\", ":").Replace(value)
|
||||||
return namespace + ":" + value
|
return namespace + ":" + value
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *wikiContext) publicWikiPageSlug(pageID, title string) string {
|
|
||||||
if slug := ctx.pageSlugOverrides[pageID]; slug != "" {
|
|
||||||
return slug
|
|
||||||
}
|
|
||||||
return wikiSlugifyTitle(title)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ctx *wikiContext) publicWikiTargetForKey(key, fallbackTitle string) string {
|
func (ctx *wikiContext) publicWikiTargetForKey(key, fallbackTitle string) string {
|
||||||
pageID := wikiPageIDForKey(key)
|
pageID := wikiPageIDForKey(key)
|
||||||
if pageID == "" {
|
if pageID == "" {
|
||||||
@@ -1683,19 +1690,21 @@ func (ctx *wikiContext) publicWikiTargetForKey(key, fallbackTitle string) string
|
|||||||
title = key
|
title = key
|
||||||
}
|
}
|
||||||
namespace := strings.SplitN(pageID, ":", 2)[0]
|
namespace := strings.SplitN(pageID, ":", 2)[0]
|
||||||
return wikiPublicTarget(namespace, ctx.publicWikiPageSlug(pageID, title))
|
return ctx.publicWikiPath(namespace, title)
|
||||||
}
|
}
|
||||||
|
|
||||||
func wikiPublicTarget(namespace, slug string) string {
|
func (ctx *wikiContext) publicWikiPath(namespace, title string) string {
|
||||||
namespace = strings.TrimSpace(namespace)
|
namespaceTitle := namespaceTitle(namespace)
|
||||||
slug = strings.TrimSpace(slug)
|
if ctx != nil && ctx.namespaceTitles != nil {
|
||||||
if namespace == "" {
|
if declared := strings.TrimSpace(ctx.namespaceTitles[namespace]); declared != "" {
|
||||||
return slug
|
namespaceTitle = declared
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if slug == "" {
|
segments := []string{namespaceTitle}
|
||||||
return namespace
|
for _, part := range strings.Split(title, " :: ") {
|
||||||
|
segments = append(segments, part)
|
||||||
}
|
}
|
||||||
return namespace + "/" + slug
|
return canonicalWikiPath(segments...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func wikiSlugifyTitle(value string) string {
|
func wikiSlugifyTitle(value string) string {
|
||||||
@@ -1719,7 +1728,9 @@ func wikiRelPathToPageID(path string) string {
|
|||||||
func namespaceTitle(namespace string) string {
|
func namespaceTitle(namespace string) string {
|
||||||
switch namespace {
|
switch namespace {
|
||||||
case "itemtypes":
|
case "itemtypes":
|
||||||
return "Itemtypes"
|
return "Item Types"
|
||||||
|
case "feat":
|
||||||
|
return "Feats"
|
||||||
default:
|
default:
|
||||||
return strings.Title(namespace)
|
return strings.Title(namespace)
|
||||||
}
|
}
|
||||||
@@ -1924,14 +1935,14 @@ func renderNodeBBManagedMarkdown(pageID, dokuText string) string {
|
|||||||
return ensureTrailingNewline(strings.Join(parts, "\n"))
|
return ensureTrailingNewline(strings.Join(parts, "\n"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderNodeBBManagedHTML(pageID, publicSlug, sourceText string, manualSections []wikiManualSection) string {
|
func renderNodeBBManagedHTML(pageID, sourceText string, manualSections []wikiManualSection) string {
|
||||||
body := sourceText
|
body := sourceText
|
||||||
if !strings.Contains(sourceText, "<h1") {
|
if !strings.Contains(sourceText, "<h1") {
|
||||||
body = dokuWikiToNodeBBHTML(sourceText)
|
body = dokuWikiToNodeBBHTML(sourceText)
|
||||||
}
|
}
|
||||||
hash := computeManagedHash(body)
|
hash := computeManagedHash(body)
|
||||||
parts := []string{
|
parts := []string{
|
||||||
"<!-- sow-topdata-wiki:page=" + pageID + " wiki_slug=" + publicSlug + " -->",
|
"<!-- sow-topdata-wiki:page=" + pageID + " -->",
|
||||||
"<!-- sow-topdata-wiki:managed:start hash=\"sha256:" + hash + "\" -->",
|
"<!-- sow-topdata-wiki:managed:start hash=\"sha256:" + hash + "\" -->",
|
||||||
strings.TrimSpace(body),
|
strings.TrimSpace(body),
|
||||||
"<!-- sow-topdata-wiki:managed:end -->",
|
"<!-- sow-topdata-wiki:managed:end -->",
|
||||||
|
|||||||
@@ -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 wiki_slug=athletics -->`) || !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, "<li>Climb</li>") {
|
||||||
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>") {
|
||||||
@@ -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)
|
t.Fatalf("expected page-index entries to match generated HTML files, got index=%d files=%d", len(pageIndex.Pages), generatedHTMLCount)
|
||||||
}
|
}
|
||||||
indexText := string(indexRaw)
|
indexText := string(indexRaw)
|
||||||
if !strings.Contains(indexText, `"page_id": "skills:athletics"`) || !strings.Contains(indexText, `"public_slug": "athletics"`) || !strings.Contains(indexText, `"public_target": "skills/athletics"`) || !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)
|
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"`) {
|
if !strings.Contains(indexText, `"page_id": "meta:wikistatus"`) || !strings.Contains(indexText, `"output_path": "pages/meta/wikistatus.html"`) || !strings.Contains(indexText, `"edit_policy": "generated_only"`) {
|
||||||
@@ -424,8 +424,8 @@ providers:
|
|||||||
t.Fatalf("render template: %v", err)
|
t.Fatalf("render template: %v", err)
|
||||||
}
|
}
|
||||||
for _, expected := range []string{
|
for _, expected := range []string{
|
||||||
`1|[[feat/literate|Literate]], [[feat/simple-weapon-proficiency|Simple Weapon Proficiency]]||1`,
|
`1|[[Feats/Literate|Literate]], [[Feats/Simple_Weapon_Proficiency|Simple Weapon Proficiency]]||1`,
|
||||||
`4||[[feat/weapon-specialization|Weapon Specialization]], [[feat/weapon-focus|Weapon Focus]]|1`,
|
`4||[[Feats/Weapon_Specialization|Weapon Specialization]], [[Feats/Weapon_Focus|Weapon Focus]]|1`,
|
||||||
} {
|
} {
|
||||||
if !strings.Contains(got, expected) {
|
if !strings.Contains(got, expected) {
|
||||||
t.Fatalf("expected %q in rendered progression:\n%s", expected, got)
|
t.Fatalf("expected %q in rendered progression:\n%s", expected, got)
|
||||||
@@ -503,8 +503,8 @@ func TestWikiTemplateEachBlockRendersClassSkillsAsIndividualListItems(t *testing
|
|||||||
t.Fatalf("render template: %v", err)
|
t.Fatalf("render template: %v", err)
|
||||||
}
|
}
|
||||||
for _, want := range []string{
|
for _, want := range []string{
|
||||||
`<li>[[skills/athletics|Athletics]]</li>`,
|
`<li>[[Skills/Athletics|Athletics]]</li>`,
|
||||||
`<li>[[skills/parry|Parry]]</li>`,
|
`<li>[[Skills/Parry|Parry]]</li>`,
|
||||||
} {
|
} {
|
||||||
if !strings.Contains(got, want) {
|
if !strings.Contains(got, want) {
|
||||||
t.Fatalf("expected rendered output to contain %q:\n%s", want, got)
|
t.Fatalf("expected rendered output to contain %q:\n%s", want, got)
|
||||||
@@ -526,8 +526,8 @@ func TestWikiTemplateListFilterRendersSliceAsListItems(t *testing.T) {
|
|||||||
Title: "Fighter",
|
Title: "Fighter",
|
||||||
Row: map[string]any{
|
Row: map[string]any{
|
||||||
"GrantedFeats": []string{
|
"GrantedFeats": []string{
|
||||||
"[[feat/literate|Literate]]",
|
"[[Feats/Literate|Literate]]",
|
||||||
"[[feat/armor-proficiency-light|Armor Proficiency (light)]]",
|
"[[Feats/Armor_Proficiency_light|Armor Proficiency (light)]]",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -538,8 +538,8 @@ func TestWikiTemplateListFilterRendersSliceAsListItems(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, want := range []string{
|
for _, want := range []string{
|
||||||
`<td><ul class="wiki-list wiki-inline-list">`,
|
`<td><ul class="wiki-list wiki-inline-list">`,
|
||||||
`<li>[[feat/literate|Literate]]</li>`,
|
`<li>[[Feats/Literate|Literate]]</li>`,
|
||||||
`<li>[[feat/armor-proficiency-light|Armor Proficiency (light)]]</li>`,
|
`<li>[[Feats/Armor_Proficiency_light|Armor Proficiency (light)]]</li>`,
|
||||||
} {
|
} {
|
||||||
if !strings.Contains(got, want) {
|
if !strings.Contains(got, want) {
|
||||||
t.Fatalf("expected rendered output to contain %q:\n%s", want, got)
|
t.Fatalf("expected rendered output to contain %q:\n%s", want, got)
|
||||||
@@ -606,9 +606,9 @@ func TestWikiTemplateCanRenderRacialFactsAndFeatListInHTML(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, want := range []string{
|
for _, want := range []string{
|
||||||
`<th scope="row">Ability Adjustments</th><td>+2 Wisdom, +2 Charisma</td>`,
|
`<th scope="row">Ability Adjustments</th><td>+2 Wisdom, +2 Charisma</td>`,
|
||||||
`<th scope="row">Favored Class</th><td>[[classes/paladin|Paladin]]</td>`,
|
`<th scope="row">Favored Class</th><td>[[Classes/Paladin|Paladin]]</td>`,
|
||||||
`<li>[[feat/darkvision|Darkvision]]</li>`,
|
`<li>[[Feats/Darkvision|Darkvision]]</li>`,
|
||||||
`<li>[[feat/daylight-aasimar|Daylight (aasimar)]]</li>`,
|
`<li>[[Feats/Daylight_aasimar|Daylight (aasimar)]]</li>`,
|
||||||
} {
|
} {
|
||||||
if !strings.Contains(got, want) {
|
if !strings.Contains(got, want) {
|
||||||
t.Fatalf("expected rendered output to contain %q:\n%s", want, got)
|
t.Fatalf("expected rendered output to contain %q:\n%s", want, got)
|
||||||
@@ -746,9 +746,9 @@ tables:
|
|||||||
`<td>1st</td>`,
|
`<td>1st</td>`,
|
||||||
`<td>+1</td>`,
|
`<td>+1</td>`,
|
||||||
`<td>2</td>`,
|
`<td>2</td>`,
|
||||||
`<td>[[feat/literate|Literate]], [[feat/simple-weapon-proficiency|Simple Weapon Proficiency]]</td>`,
|
`<td>[[Feats/Literate|Literate]], [[Feats/Simple_Weapon_Proficiency|Simple Weapon Proficiency]]</td>`,
|
||||||
`<td>1-10</td>`,
|
`<td>1-10</td>`,
|
||||||
`<td>[[feat/weapon-specialization|Weapon Specialization]] first available.</td>`,
|
`<td>[[Feats/Weapon_Specialization|Weapon Specialization]] first available.</td>`,
|
||||||
} {
|
} {
|
||||||
if !strings.Contains(got, expected) {
|
if !strings.Contains(got, expected) {
|
||||||
t.Fatalf("expected %q in rendered table:\n%s", expected, got)
|
t.Fatalf("expected %q in rendered table:\n%s", expected, got)
|
||||||
@@ -935,7 +935,7 @@ datasets:
|
|||||||
}
|
}
|
||||||
for _, path := range []string{
|
for _, path := range []string{
|
||||||
filepath.Join(root, ".cache", "wiki", "pages", "feat", "visible.html"),
|
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", "class_only.html"),
|
||||||
} {
|
} {
|
||||||
if _, err := os.Stat(path); err != nil {
|
if _, err := os.Stat(path); err != nil {
|
||||||
t.Fatalf("expected visible wiki page %s: %v", path, err)
|
t.Fatalf("expected visible wiki page %s: %v", path, err)
|
||||||
@@ -951,7 +951,7 @@ datasets:
|
|||||||
t.Fatalf("expected hidden %q reference to be omitted from class page:\n%s", hidden, classText)
|
t.Fatalf("expected hidden %q reference to be omitted from class page:\n%s", hidden, classText)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, visible := range []string{"[[skills/athletics|Athletics]]", "[[feat/class-feat|Class Feat]]"} {
|
for _, visible := range []string{"[[Skills/Athletics|Athletics]]", "[[Feats/Class_Feat|Class Feat]]"} {
|
||||||
if !strings.Contains(classText, visible) {
|
if !strings.Contains(classText, visible) {
|
||||||
t.Fatalf("expected visible %q reference in class page:\n%s", visible, classText)
|
t.Fatalf("expected visible %q reference in class page:\n%s", visible, classText)
|
||||||
}
|
}
|
||||||
@@ -1319,11 +1319,23 @@ func TestWikiSourceDigestIncludesTemplates(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWikiPageIDForKeyNormalizesSlashAndUnderscoreSeparators(t *testing.T) {
|
func TestWikiPageIDForKeyNormalizesSlashSeparatorsOnly(t *testing.T) {
|
||||||
got := wikiPageIDForKey("feat:special/attacks_bull_rush")
|
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)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWikiPageIDToRelPathKeepsUnderscoreInFileLeaf(t *testing.T) {
|
||||||
|
if got, want := wikiPageIDToRelPath("feat:bardic_music_inspire_competence"), filepath.FromSlash("feat/bardic_music_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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSaveWikiPageIndexRejectsDuplicateOutputPaths(t *testing.T) {
|
func TestSaveWikiPageIndexRejectsDuplicateOutputPaths(t *testing.T) {
|
||||||
@@ -1339,80 +1351,77 @@ func TestSaveWikiPageIndexRejectsDuplicateOutputPaths(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSaveWikiPageIndexRejectsDuplicatePublicTargets(t *testing.T) {
|
func TestSaveWikiPageIndexRejectsDuplicatePublicPaths(t *testing.T) {
|
||||||
err := saveWikiPageIndex(filepath.Join(t.TempDir(), "page-index.json"), wikiPageIndex{
|
err := saveWikiPageIndex(filepath.Join(t.TempDir(), "page-index.json"), wikiPageIndex{
|
||||||
Version: wikiGeneratorVersion,
|
Version: wikiGeneratorVersion,
|
||||||
Pages: []wikiPageIndexEntry{
|
Pages: []wikiPageIndexEntry{
|
||||||
{PageID: "spells:darkness", PublicTarget: "spells/darkness", OutputPath: "pages/spells/darkness.html"},
|
{PageID: "spells:darkness", PublicPath: "Spells/Darkness", OutputPath: "pages/spells/darkness.html"},
|
||||||
{PageID: "spells:gwildshape:driderdarkness", PublicTarget: "spells/darkness", OutputPath: "pages/spells/gwildshape/driderdarkness.html"},
|
{PageID: "spells:gwildshape:driderdarkness", PublicPath: "Spells/Darkness", OutputPath: "pages/spells/gwildshape/driderdarkness.html"},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err == nil || !strings.Contains(err.Error(), "duplicate wiki page-index public_target") {
|
if err == nil || !strings.Contains(err.Error(), "duplicate wiki page-index public_path") {
|
||||||
t.Fatalf("expected duplicate public_target validation error, got %v", err)
|
t.Fatalf("expected duplicate public_path validation error, got %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseWikiPagePathOverridesRejectsInvalidAndDuplicateSlugs(t *testing.T) {
|
func TestParseWikiPagePathDeclarationsRejectsLegacySlugOverrides(t *testing.T) {
|
||||||
_, err := parseWikiPagePathDeclarations([]byte(`
|
_, err := parseWikiPagePathDeclarations([]byte(`
|
||||||
page_paths:
|
|
||||||
slug_overrides:
|
|
||||||
- page_id: "spells:pdk:fear"
|
|
||||||
slug: "Pdk Fear"
|
|
||||||
`), "wiki.yaml")
|
|
||||||
if err == nil || !strings.Contains(err.Error(), `invalid slug "Pdk Fear"`) {
|
|
||||||
t.Fatalf("expected invalid wiki slug error, got %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = parseWikiPagePathDeclarations([]byte(`
|
|
||||||
page_paths:
|
page_paths:
|
||||||
slug_overrides:
|
slug_overrides:
|
||||||
- page_id: "spells:pdk:fear"
|
- page_id: "spells:pdk:fear"
|
||||||
slug: "pdk-fear"
|
slug: "pdk-fear"
|
||||||
- page_id: "spells:pdk:fear"
|
|
||||||
slug: "fear-pdk"
|
|
||||||
`), "wiki.yaml")
|
`), "wiki.yaml")
|
||||||
if err == nil || !strings.Contains(err.Error(), `duplicate page_id "spells:pdk:fear"`) {
|
if err == nil || !strings.Contains(err.Error(), `page_paths.slug_overrides is retired`) {
|
||||||
t.Fatalf("expected duplicate page ID error, got %v", err)
|
t.Fatalf("expected retired slug override error, got %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWikiPageSlugOverridesChangeTargets(t *testing.T) {
|
func TestCanonicalWikiSegmentPreservesCaseAndUnderscoreSpaces(t *testing.T) {
|
||||||
ctx := &wikiContext{pageSlugOverrides: map[string]string{
|
|
||||||
"spells:pdk:fear": "pdk-fear",
|
|
||||||
}}
|
|
||||||
if got, want := ctx.publicWikiPageSlug("spells:pdk:fear", "Fear"), "pdk-fear"; got != want {
|
|
||||||
t.Fatalf("expected slug override %q, got %q", want, got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWikiSlugifyTitleNormalizesPunctuationAndSeparators(t *testing.T) {
|
|
||||||
tests := map[string]string{
|
tests := map[string]string{
|
||||||
`Grandmaster's Battle Momentum`: "grandmasters-battle-momentum",
|
`Inspire Competence`: "Inspire_Competence",
|
||||||
`Bigby’s Clenched Fist`: "bigbys-clenched-fist",
|
`Grandmaster's Battle Momentum`: "Grandmasters_Battle_Momentum",
|
||||||
`"Quoted" ‘Curly’ “Title”`: "quoted-curly-title",
|
`Bigby’s Clenched Fist`: "Bigbys_Clenched_Fist",
|
||||||
`Hardiness vs. Enchantments`: "hardiness-vs-enchantments",
|
`"Quoted" ‘Curly’ “Title”`: "Quoted_Curly_Title",
|
||||||
`Clairaudience/Clairvoyance`: "clairaudience-clairvoyance",
|
`Hardiness vs. Enchantments`: "Hardiness_vs_Enchantments",
|
||||||
`Moon-on-a-Stick`: "moon-on-a-stick",
|
`Clairaudience/Clairvoyance`: "Clairaudience_Clairvoyance",
|
||||||
`Élite & Noble Houses`: "elite-noble-houses",
|
`Moon-on-a-Stick`: "Moon_on_a_Stick",
|
||||||
`Rock 'n' Roll`: "rock-n-roll",
|
`Élite & Noble Houses`: "Elite_Noble_Houses",
|
||||||
`Melf's Acid Arrow`: "melfs-acid-arrow",
|
`Rock 'n' Roll`: "Rock_n_Roll",
|
||||||
`Alpha!@#$%^&*()[]-=_+/?.,<>` + "`" + `~|\Omega`: "alpha-omega",
|
`Melf's Acid Arrow`: "Melfs_Acid_Arrow",
|
||||||
`Æther Œuvre Øresund Straße Þorn Łódź Đelta`: "aether-oeuvre-oresund-strasse-thorn-lodz-delta",
|
`Alpha!@#$%^&*()[]-=_+/?.,<>` + "`" + `~|\Omega`: "Alpha_Omega",
|
||||||
|
`Æther Œuvre Øresund Straße Þorn Łódź Đelta`: "Aether_Oeuvre_Oresund_Strasse_Thorn_Lodz_Delta",
|
||||||
}
|
}
|
||||||
for input, want := range tests {
|
for input, want := range tests {
|
||||||
if got := wikiSlugifyTitle(input); got != want {
|
if got := canonicalWikiSegment(input); got != want {
|
||||||
t.Fatalf("wikiSlugifyTitle(%q) = %q, want %q", input, got, want)
|
t.Fatalf("canonicalWikiSegment(%q) = %q, want %q", input, got, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPublicWikiTargetUsesCanonicalNamespaceAndTitlePath(t *testing.T) {
|
||||||
|
ctx := &wikiContext{
|
||||||
|
namespaceTitles: map[string]string{
|
||||||
|
"feat": "Feats",
|
||||||
|
},
|
||||||
|
rowsByKey: map[string]map[string]any{
|
||||||
|
"feat:hardiness_versus_enchantments": {"FEAT": map[string]any{"tlk": map[string]any{"text": "Hardiness vs. Enchantments"}}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if got, want := ctx.publicWikiTargetForKey("feat:hardiness_versus_enchantments", "Display Alias"), "Feats/Hardiness_vs_Enchantments"; got != want {
|
||||||
|
t.Fatalf("expected canonical title path %q, got %q", want, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestPublicWikiTargetUsesTargetTitleBeforeDisplayLabel(t *testing.T) {
|
func TestPublicWikiTargetUsesTargetTitleBeforeDisplayLabel(t *testing.T) {
|
||||||
ctx := &wikiContext{
|
ctx := &wikiContext{
|
||||||
|
namespaceTitles: map[string]string{
|
||||||
|
"feat": "Feats",
|
||||||
|
},
|
||||||
rowsByKey: map[string]map[string]any{
|
rowsByKey: map[string]map[string]any{
|
||||||
"feat:hardiness:versus:enchantments": {"FEAT": "Hardiness vs. Enchantments"},
|
"feat:hardiness:versus:enchantments": {"FEAT": "Hardiness vs. Enchantments"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if got, want := ctx.publicWikiTargetForKey("feat:hardiness:versus:enchantments", "Display Alias"), "feat/hardiness-vs-enchantments"; got != want {
|
if got, want := ctx.publicWikiTargetForKey("feat:hardiness:versus:enchantments", "Display Alias"), "Feats/Hardiness_vs_Enchantments"; got != want {
|
||||||
t.Fatalf("expected target title slug %q, got %q", want, got)
|
t.Fatalf("expected target title slug %q, got %q", want, got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ func parseWikiPagePathDeclarations(raw []byte, path string) (map[string]string,
|
|||||||
if err := yaml.Unmarshal(raw, &doc); err != nil {
|
if err := yaml.Unmarshal(raw, &doc); err != nil {
|
||||||
return nil, fmt.Errorf("parse wiki page paths %s: %w", path, err)
|
return nil, fmt.Errorf("parse wiki page paths %s: %w", path, err)
|
||||||
}
|
}
|
||||||
|
if len(doc.PagePaths.SlugOverrides) > 0 {
|
||||||
|
return nil, fmt.Errorf("parse wiki page paths %s: page_paths.slug_overrides is retired; generated public wiki paths are title-derived", path)
|
||||||
|
}
|
||||||
overrides := map[string]string{}
|
overrides := map[string]string{}
|
||||||
for _, override := range doc.PagePaths.SlugOverrides {
|
for _, override := range doc.PagePaths.SlugOverrides {
|
||||||
pageID := strings.TrimSpace(override.PageID)
|
pageID := strings.TrimSpace(override.PageID)
|
||||||
|
|||||||
@@ -8,6 +8,59 @@ import (
|
|||||||
"golang.org/x/text/unicode/norm"
|
"golang.org/x/text/unicode/norm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func canonicalWikiSegment(value string) string {
|
||||||
|
value = html.UnescapeString(strings.TrimSpace(value))
|
||||||
|
var b strings.Builder
|
||||||
|
lastSeparator := false
|
||||||
|
for _, r := range norm.NFKD.String(value) {
|
||||||
|
transliterated := transliterateWikiSlugRune(unicode.ToLower(r))
|
||||||
|
switch {
|
||||||
|
case (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9'):
|
||||||
|
b.WriteRune(r)
|
||||||
|
lastSeparator = false
|
||||||
|
case r == '_':
|
||||||
|
if b.Len() > 0 && !lastSeparator {
|
||||||
|
b.WriteRune(r)
|
||||||
|
lastSeparator = true
|
||||||
|
}
|
||||||
|
case unicode.Is(unicode.Mn, r):
|
||||||
|
continue
|
||||||
|
case isWikiSlugJoinerPunctuation(r):
|
||||||
|
continue
|
||||||
|
case transliterated != "":
|
||||||
|
b.WriteString(matchWikiTransliterationCase(r, transliterated))
|
||||||
|
lastSeparator = false
|
||||||
|
default:
|
||||||
|
if b.Len() > 0 && !lastSeparator {
|
||||||
|
b.WriteByte('_')
|
||||||
|
lastSeparator = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Trim(b.String(), "_")
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchWikiTransliterationCase(r rune, value string) string {
|
||||||
|
if unicode.IsUpper(r) {
|
||||||
|
if len(value) == 1 {
|
||||||
|
return strings.ToUpper(value)
|
||||||
|
}
|
||||||
|
return strings.ToUpper(value[:1]) + value[1:]
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func canonicalWikiPath(segments ...string) string {
|
||||||
|
out := []string{}
|
||||||
|
for _, segment := range segments {
|
||||||
|
canonical := canonicalWikiSegment(segment)
|
||||||
|
if canonical != "" {
|
||||||
|
out = append(out, canonical)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Join(out, "/")
|
||||||
|
}
|
||||||
|
|
||||||
func slugifyWikiText(value string, fallback string) string {
|
func slugifyWikiText(value string, fallback string) string {
|
||||||
value = html.UnescapeString(strings.TrimSpace(value))
|
value = html.UnescapeString(strings.TrimSpace(value))
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
|
|||||||
Reference in New Issue
Block a user