Slug joiner normalization

This commit is contained in:
2026-05-21 22:06:52 +02:00
parent 96afb64c7a
commit 43fee721db
5 changed files with 84 additions and 33 deletions
+1 -14
View File
@@ -715,20 +715,7 @@ func normalizeWikiTitle(value string) string {
}
func slugifyWikiTitle(value string) string {
var b strings.Builder
lastDash := false
for _, r := range strings.ToLower(strings.TrimSpace(value)) {
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') {
b.WriteRune(r)
lastDash = false
continue
}
if !lastDash && b.Len() > 0 {
b.WriteByte('-')
lastDash = true
}
}
return strings.Trim(b.String(), "-")
return slugifyWikiText(value, "")
}
func loadDeployManifest(path string) wikiDeployManifest {
+15
View File
@@ -168,6 +168,21 @@ func TestMatchExistingNodeBBPagePrefersGeneratedPublicSlug(t *testing.T) {
}
}
func TestDeploySlugifyWikiTitleMatchesGeneratedSlugPolicy(t *testing.T) {
tests := map[string]string{
`Grandmaster's Battle Momentum`: "grandmasters-battle-momentum",
`Bigbys Clenched Fist`: "bigbys-clenched-fist",
`Hardiness vs. Enchantments`: "hardiness-vs-enchantments",
`Clairaudience/Clairvoyance`: "clairaudience-clairvoyance",
`Élite &amp; Noble Houses`: "elite-noble-houses",
}
for input, want := range tests {
if got := slugifyWikiTitle(input); got != want {
t.Fatalf("slugifyWikiTitle(%q) = %q, want %q", input, got, want)
}
}
}
func TestDeployWikiCreatesNodeBBTopicAndWritesManifest(t *testing.T) {
root := t.TempDir()
sourceDir := filepath.Join(root, "pages")
+1 -19
View File
@@ -1682,25 +1682,7 @@ func wikiPublicTarget(namespace, slug string) string {
}
func wikiSlugifyTitle(value string) string {
value = html.UnescapeString(strings.TrimSpace(value))
var b strings.Builder
lastDash := false
for _, r := range strings.ToLower(value) {
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') {
b.WriteRune(r)
lastDash = false
continue
}
if b.Len() > 0 && !lastDash {
b.WriteByte('-')
lastDash = true
}
}
slug := strings.Trim(b.String(), "-")
if slug == "" {
return "topic"
}
return slug
return slugifyWikiText(value, "topic")
}
func wikiPageIDToRelPath(pageID string) string {
+19
View File
@@ -1239,6 +1239,25 @@ func TestWikiPageSlugOverridesChangeTargets(t *testing.T) {
}
}
func TestWikiSlugifyTitleNormalizesPunctuationAndSeparators(t *testing.T) {
tests := map[string]string{
`Grandmaster's Battle Momentum`: "grandmasters-battle-momentum",
`Bigbys Clenched Fist`: "bigbys-clenched-fist",
`"Quoted" Curly “Title”`: "quoted-curly-title",
`Hardiness vs. Enchantments`: "hardiness-vs-enchantments",
`Clairaudience/Clairvoyance`: "clairaudience-clairvoyance",
`Moon-on-a-Stick`: "moon-on-a-stick",
`Élite &amp; Noble Houses`: "elite-noble-houses",
`Rock 'n' Roll`: "rock-n-roll",
`Melf&#39;s Acid Arrow`: "melfs-acid-arrow",
}
for input, want := range tests {
if got := wikiSlugifyTitle(input); got != want {
t.Fatalf("wikiSlugifyTitle(%q) = %q, want %q", input, got, want)
}
}
}
func TestPublicWikiTargetUsesTargetTitleBeforeDisplayLabel(t *testing.T) {
ctx := &wikiContext{
rowsByKey: map[string]map[string]any{
+48
View File
@@ -0,0 +1,48 @@
package topdata
import (
"html"
"strings"
"unicode"
"golang.org/x/text/unicode/norm"
)
func slugifyWikiText(value string, fallback string) string {
value = html.UnescapeString(strings.TrimSpace(value))
var b strings.Builder
lastDash := false
for _, r := range norm.NFKD.String(strings.ToLower(value)) {
switch {
case r >= 'a' && r <= 'z':
b.WriteRune(r)
lastDash = false
case r >= '0' && r <= '9':
b.WriteRune(r)
lastDash = false
case unicode.Is(unicode.Mn, r):
continue
case isWikiSlugJoinerPunctuation(r):
continue
default:
if b.Len() > 0 && !lastDash {
b.WriteByte('-')
lastDash = true
}
}
}
slug := strings.Trim(b.String(), "-")
if slug == "" {
return fallback
}
return slug
}
func isWikiSlugJoinerPunctuation(r rune) bool {
switch r {
case '\'', '"', '`', '´', '', '', '', '', '“', '”', '„', '‟', '', '', '«', '»', '', '″', '', '':
return true
default:
return false
}
}