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:
@@ -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.") {
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
for _, expected := range []string{
|
||||
`1|[[feat/literate|Literate]], [[feat/simple-weapon-proficiency|Simple Weapon Proficiency]]||1`,
|
||||
`4||[[feat/weapon-specialization|Weapon Specialization]], [[feat/weapon-focus|Weapon Focus]]|1`,
|
||||
`1|[[Feats/Literate|Literate]], [[Feats/Simple_Weapon_Proficiency|Simple Weapon Proficiency]]||1`,
|
||||
`4||[[Feats/Weapon_Specialization|Weapon Specialization]], [[Feats/Weapon_Focus|Weapon Focus]]|1`,
|
||||
} {
|
||||
if !strings.Contains(got, expected) {
|
||||
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)
|
||||
}
|
||||
for _, want := range []string{
|
||||
`<li>[[skills/athletics|Athletics]]</li>`,
|
||||
`<li>[[skills/parry|Parry]]</li>`,
|
||||
`<li>[[Skills/Athletics|Athletics]]</li>`,
|
||||
`<li>[[Skills/Parry|Parry]]</li>`,
|
||||
} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Fatalf("expected rendered output to contain %q:\n%s", want, got)
|
||||
@@ -526,8 +526,8 @@ func TestWikiTemplateListFilterRendersSliceAsListItems(t *testing.T) {
|
||||
Title: "Fighter",
|
||||
Row: map[string]any{
|
||||
"GrantedFeats": []string{
|
||||
"[[feat/literate|Literate]]",
|
||||
"[[feat/armor-proficiency-light|Armor Proficiency (light)]]",
|
||||
"[[Feats/Literate|Literate]]",
|
||||
"[[Feats/Armor_Proficiency_light|Armor Proficiency (light)]]",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -538,8 +538,8 @@ func TestWikiTemplateListFilterRendersSliceAsListItems(t *testing.T) {
|
||||
}
|
||||
for _, want := range []string{
|
||||
`<td><ul class="wiki-list wiki-inline-list">`,
|
||||
`<li>[[feat/literate|Literate]]</li>`,
|
||||
`<li>[[feat/armor-proficiency-light|Armor Proficiency (light)]]</li>`,
|
||||
`<li>[[Feats/Literate|Literate]]</li>`,
|
||||
`<li>[[Feats/Armor_Proficiency_light|Armor Proficiency (light)]]</li>`,
|
||||
} {
|
||||
if !strings.Contains(got, want) {
|
||||
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{
|
||||
`<th scope="row">Ability Adjustments</th><td>+2 Wisdom, +2 Charisma</td>`,
|
||||
`<th scope="row">Favored Class</th><td>[[classes/paladin|Paladin]]</td>`,
|
||||
`<li>[[feat/darkvision|Darkvision]]</li>`,
|
||||
`<li>[[feat/daylight-aasimar|Daylight (aasimar)]]</li>`,
|
||||
`<th scope="row">Favored Class</th><td>[[Classes/Paladin|Paladin]]</td>`,
|
||||
`<li>[[Feats/Darkvision|Darkvision]]</li>`,
|
||||
`<li>[[Feats/Daylight_aasimar|Daylight (aasimar)]]</li>`,
|
||||
} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Fatalf("expected rendered output to contain %q:\n%s", want, got)
|
||||
@@ -746,9 +746,9 @@ tables:
|
||||
`<td>1st</td>`,
|
||||
`<td>+1</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>[[feat/weapon-specialization|Weapon Specialization]] first available.</td>`,
|
||||
`<td>[[Feats/Weapon_Specialization|Weapon Specialization]] first available.</td>`,
|
||||
} {
|
||||
if !strings.Contains(got, expected) {
|
||||
t.Fatalf("expected %q in rendered table:\n%s", expected, got)
|
||||
@@ -935,7 +935,7 @@ datasets:
|
||||
}
|
||||
for _, path := range []string{
|
||||
filepath.Join(root, ".cache", "wiki", "pages", "feat", "visible.html"),
|
||||
filepath.Join(root, ".cache", "wiki", "pages", "feat", "class", "only.html"),
|
||||
filepath.Join(root, ".cache", "wiki", "pages", "feat", "class_only.html"),
|
||||
} {
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
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)
|
||||
}
|
||||
}
|
||||
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) {
|
||||
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")
|
||||
if want := "feat:special:attacks:bull:rush"; got != want {
|
||||
if want := "feat:special:attacks_bull_rush"; got != want {
|
||||
t.Fatalf("expected normalized page ID %q, got %q", want, got)
|
||||
}
|
||||
if got := wikiPageIDForKey("feat:bardic_music_inspire_competence"); got != "feat:bardic_music_inspire_competence" {
|
||||
t.Fatalf("expected underscore to remain in page ID leaf, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -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{
|
||||
Version: wikiGeneratorVersion,
|
||||
Pages: []wikiPageIndexEntry{
|
||||
{PageID: "spells:darkness", PublicTarget: "spells/darkness", OutputPath: "pages/spells/darkness.html"},
|
||||
{PageID: "spells:gwildshape:driderdarkness", PublicTarget: "spells/darkness", OutputPath: "pages/spells/gwildshape/driderdarkness.html"},
|
||||
{PageID: "spells:darkness", PublicPath: "Spells/Darkness", OutputPath: "pages/spells/darkness.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") {
|
||||
t.Fatalf("expected duplicate public_target validation error, got %v", err)
|
||||
if err == nil || !strings.Contains(err.Error(), "duplicate wiki page-index public_path") {
|
||||
t.Fatalf("expected duplicate public_path validation error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseWikiPagePathOverridesRejectsInvalidAndDuplicateSlugs(t *testing.T) {
|
||||
func TestParseWikiPagePathDeclarationsRejectsLegacySlugOverrides(t *testing.T) {
|
||||
_, 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:
|
||||
slug_overrides:
|
||||
- page_id: "spells:pdk:fear"
|
||||
slug: "pdk-fear"
|
||||
- page_id: "spells:pdk:fear"
|
||||
slug: "fear-pdk"
|
||||
`), "wiki.yaml")
|
||||
if err == nil || !strings.Contains(err.Error(), `duplicate page_id "spells:pdk:fear"`) {
|
||||
t.Fatalf("expected duplicate page ID error, got %v", err)
|
||||
if err == nil || !strings.Contains(err.Error(), `page_paths.slug_overrides is retired`) {
|
||||
t.Fatalf("expected retired slug override error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWikiPageSlugOverridesChangeTargets(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) {
|
||||
func TestCanonicalWikiSegmentPreservesCaseAndUnderscoreSpaces(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
`Grandmaster's Battle Momentum`: "grandmasters-battle-momentum",
|
||||
`Bigby’s 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 & Noble Houses`: "elite-noble-houses",
|
||||
`Rock 'n' Roll`: "rock-n-roll",
|
||||
`Melf's Acid Arrow`: "melfs-acid-arrow",
|
||||
`Alpha!@#$%^&*()[]-=_+/?.,<>` + "`" + `~|\Omega`: "alpha-omega",
|
||||
`Æther Œuvre Øresund Straße Þorn Łódź Đelta`: "aether-oeuvre-oresund-strasse-thorn-lodz-delta",
|
||||
`Inspire Competence`: "Inspire_Competence",
|
||||
`Grandmaster's Battle Momentum`: "Grandmasters_Battle_Momentum",
|
||||
`Bigby’s 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 & Noble Houses`: "Elite_Noble_Houses",
|
||||
`Rock 'n' Roll`: "Rock_n_Roll",
|
||||
`Melf's Acid Arrow`: "Melfs_Acid_Arrow",
|
||||
`Alpha!@#$%^&*()[]-=_+/?.,<>` + "`" + `~|\Omega`: "Alpha_Omega",
|
||||
`Æther Œuvre Øresund Straße Þorn Łódź Đelta`: "Aether_Oeuvre_Oresund_Strasse_Thorn_Lodz_Delta",
|
||||
}
|
||||
for input, want := range tests {
|
||||
if got := wikiSlugifyTitle(input); got != want {
|
||||
t.Fatalf("wikiSlugifyTitle(%q) = %q, want %q", input, got, want)
|
||||
if got := canonicalWikiSegment(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) {
|
||||
ctx := &wikiContext{
|
||||
namespaceTitles: map[string]string{
|
||||
"feat": "Feats",
|
||||
},
|
||||
rowsByKey: map[string]map[string]any{
|
||||
"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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user