Rebuild split-rulebook journal parsing (vault sync 2026-08-18 to 08-23)

The bab5a62 vault commit split 9 rulebook monoliths into hub/ToC pages plus
per-topic-file subfolders (rules/NN. <Book>/<Chapter>/<Topic>.md). The
existing chapter-slicing parser (parseJournalBook) relied on finding a
duplicate TOC-vs-body heading pair per chapter; after the split each hub
heading only occurs once (in the ToC), so it was silently treating the
ToC's link-list body as if it were real chapter content.

Added parseSplitBookFolder (journal-parser.ts): walks a book's split
subfolder directly, one JournalDoc per Rozdzial/Aneks subfolder-or-file,
one JournalPage per topic file (or a single page for a standalone chapter
file). The hub's own Wstep intro paragraph is unaffected by the split (it's
still real prose, not a link) and continues to come from the existing
prefaceTitle preface-extraction logic.

Wired into build-journal-packs.ts for the 6 affected packs: core-rules-lore,
bestiary-lore, magic-book-lore, abyss, blood-magic-history, economy.
crimson-cult's rules source was never split into a subfolder, so it's left
on the old code path unchanged.

Also fixed a pre-existing derivePageTitle bug (page titles kept a leading
markdown heading marker, e.g. "## Wstep" instead of "Wstep") to match the
already-correct behavior in topical-parser.ts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-23 01:15:51 +02:00
parent 447b90dbb7
commit 7dd2af1ec5
2 changed files with 201 additions and 15 deletions
+55 -12
View File
@@ -15,7 +15,7 @@ import { mkdirSync, rmSync, writeFileSync, existsSync, readdirSync, statSync, re
import { dirname, resolve, basename, extname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { compilePack } from '@foundryvtt/foundryvtt-cli';
import { parseJournalBook, type JournalDoc, type JournalPage } from './parsers/journal-parser';
import { parseJournalBook, parseSplitBookFolder, type JournalDoc, type JournalPage } from './parsers/journal-parser';
import { parseTopicalFolder } from './parsers/topical-parser';
const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -37,6 +37,14 @@ interface SourceFile {
* as a synthetic journal entry with this title (e.g. "Wstęp").
*/
prefaceTitle?: string;
/**
* Since `bab5a62`, `path`'s chapter bodies were replaced with links into
* this per-topic subfolder (relative to repoRoot). When set, chapters are
* read from that folder instead of sliced out of `path` - `path` is then
* used only to extract `prefaceTitle`'s intro paragraph, which is still
* real prose in the hub file.
*/
splitFolder?: string;
}
interface PackConfig {
@@ -57,13 +65,17 @@ const PACK_CONFIGS: PackConfig[] = [
{
pack: 'core-rules-lore',
sourceBook: 'core-rules',
sources: [{ path: 'ObsidianNotes/rules/00. Podręcznik Gry.md' }],
sources: [
{ path: 'ObsidianNotes/rules/00. Podręcznik Gry.md', prefaceTitle: 'Wstęp', splitFolder: 'ObsidianNotes/rules/00. Podręcznik Gry' },
],
skip: /^(Talenty|Zaklęcia|Atrybuty|Umiejętności|Wyposażenie)/i,
},
{
pack: 'bestiary-lore',
sourceBook: 'bestiary',
sources: [{ path: 'ObsidianNotes/rules/06. Bestiariusz.md' }],
sources: [
{ path: 'ObsidianNotes/rules/06. Bestiariusz.md', prefaceTitle: 'Wstęp', splitFolder: 'ObsidianNotes/rules/06. Bestiariusz' },
],
skip: /^Talenty/i,
},
@@ -73,9 +85,9 @@ const PACK_CONFIGS: PackConfig[] = [
sourceBook: 'magic-book',
sources: [
// Lore intro lives inside the rules file (no dedicated lore/ file).
{ path: 'ObsidianNotes/rules/01. Księga Magii.md', prefaceTitle: 'Wstęp' },
{ path: 'ObsidianNotes/rules/01. Księga Magii.md', prefaceTitle: 'Wstęp', splitFolder: 'ObsidianNotes/rules/01. Księga Magii' },
],
skip: /^(Zaklęcia|Lista zaklęć|Rozdział V)/i,
skip: /^(Zaklęcia|Lista zaklęć)/i,
},
{
pack: 'abyss',
@@ -84,17 +96,17 @@ const PACK_CONFIGS: PackConfig[] = [
// Pure lore narrative.
{ path: 'ObsidianNotes/lore/02. Otchłań i Magia.md' },
// Rules intro (Wstęp only - mechanics chapters are skipped).
{ path: 'ObsidianNotes/rules/02. Klątwa Otchłani.md', prefaceTitle: 'Wstęp' },
{ path: 'ObsidianNotes/rules/02. Klątwa Otchłani.md', prefaceTitle: 'Wstęp', splitFolder: 'ObsidianNotes/rules/02. Klątwa Otchłani' },
],
skip: /^(Rozdział II - Talenty|Rozdział III - Dary|Rozdział IV - Choroby|Rozdział V - Zaklęcia|Rozdział X - Artefakty)/i,
skip: /^(Talenty|Dary|Choroby|Zaklęcia|Artefakty)/i,
},
{
pack: 'blood-magic-history',
sourceBook: 'arcanum-sanguinis',
sources: [
{ path: 'ObsidianNotes/rules/04. Arcanum Sanguinis.md', prefaceTitle: 'Wstęp' },
{ path: 'ObsidianNotes/rules/04. Arcanum Sanguinis.md', prefaceTitle: 'Wstęp', splitFolder: 'ObsidianNotes/rules/04. Arcanum Sanguinis' },
],
skip: /^Rozdział IV - Talenty/i,
skip: /^Talenty/i,
},
{
pack: 'crimson-cult',
@@ -102,7 +114,9 @@ const PACK_CONFIGS: PackConfig[] = [
sources: [
// Lore narrative (former Chwała Szkarłatnemu Kultowi lore section).
{ path: 'ObsidianNotes/lore/05. Szkarłatny Kult.md' },
// Rules intro only (spells/artifacts skipped).
// Rules intro only (spells/artifacts skipped). This book was NOT split
// into a per-topic subfolder (bab5a62) - it stayed a monolith, so the
// old heading-slicing approach still applies unmodified.
{ path: 'ObsidianNotes/rules/05. Vivat Patriarcha coccineus!.md', prefaceTitle: 'Wstęp' },
],
skip: /^(Rozdział I - Zaklęcia|Rozdział II - Artefakty)/i,
@@ -112,9 +126,9 @@ const PACK_CONFIGS: PackConfig[] = [
sourceBook: 'gold-steel-magic',
sources: [
{ path: 'ObsidianNotes/lore/03. Ekonomia Magicznego Świata.md' },
{ path: 'ObsidianNotes/rules/03. Złoto, Stal i Magia.md', prefaceTitle: 'Wstęp' },
{ path: 'ObsidianNotes/rules/03. Złoto, Stal i Magia.md', prefaceTitle: 'Wstęp', splitFolder: 'ObsidianNotes/rules/03. Złoto, Stal i Magia' },
],
skip: /^(Aneks A - Nowe Talenty|Aneks B - Przedmioty)/i,
skip: /^(Nowe Talenty|Przedmioty)/i,
},
// ── New: Humanity Guide ──
@@ -166,6 +180,35 @@ for (const cfg of PACK_CONFIGS) {
console.warn(` · skip source ${src.path}: file not found`);
continue;
}
if (src.splitFolder) {
const folderPath = resolve(repoRoot, src.splitFolder);
if (!existsSync(folderPath)) {
console.warn(` · skip splitFolder ${src.splitFolder}: folder not found`);
} else {
if (src.prefaceTitle) {
// Preface only - skip every real chapter (they come from the folder walk below).
packDocs.push(
...parseJournalBook({
bookPath,
pack: cfg.pack,
sourceBook: cfg.sourceBook,
prefaceTitle: src.prefaceTitle,
chapterSkip: /./,
}),
);
}
packDocs.push(
...parseSplitBookFolder({
folderPath,
pack: cfg.pack,
sourceBook: cfg.sourceBook,
chapterSkip: cfg.skip,
}),
);
}
continue;
}
const docs = parseJournalBook({
bookPath,
pack: cfg.pack,