From 1bd8e634dd70363937eb1aa167e67253a14e998b Mon Sep 17 00:00:00 2001 From: Octoturge Date: Sat, 17 Oct 2026 10:50:12 +0200 Subject: [PATCH 1/2] chore: bump version to 1.3.21 Co-Authored-By: Claude Sonnet 5.5 --- module.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/module.json b/module.json index d032e00..2b23a29 100644 --- a/module.json +++ b/module.json @@ -2,9 +2,9 @@ "id": "hbm-rpg-v3-lore", "title": "Homebrew Magic: RPG v3 - Lore", "description": "Companion lore module for the HbM RPG v3 system. Provides journal entries for setting, deities, history, and narrative chapters from the official books.", - "version": "1.3.20", + "version": "1.3.21", "manifest": "https://vtt-content.octoturge.com/packages/module/hbm-rpg-v3-lore.json", - "download": "https://vtt-content.octoturge.com/packages/module/hbm-rpg-v3-lore-v1.3.20.zip", + "download": "https://vtt-content.octoturge.com/packages/module/hbm-rpg-v3-lore-v1.3.21.zip", "compatibility": { "minimum": "13", "verified": "14" diff --git a/package.json b/package.json index 690f3f9..66c6e5b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hbm-rpg-v3-lore", - "version": "1.3.20", + "version": "1.3.21", "description": "Foundry VTT lore companion module for Homebrew Magic: RPG v3.", "private": true, "type": "module", -- 2.54.0 From 070d702ef951e8420e5285c4f34c4bf6b0b671ff Mon Sep 17 00:00:00 2001 From: Octoturge Date: Tue, 20 Oct 2026 13:24:03 +0200 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20add=20=C5=9Aci=C4=85gawka=20Gracza?= =?UTF-8?q?=20/=20Mistrza=20Gry=20journal=20compendia?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 9 ++ module.json | 42 ++++++- package.json | 2 +- scripts/build-journal-packs.ts | 19 ++++ scripts/parsers/cheatsheet-parser.ts | 162 +++++++++++++++++++++++++++ 5 files changed, 231 insertions(+), 3 deletions(-) create mode 100644 scripts/parsers/cheatsheet-parser.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 70ed11d..5537d87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to the **HbM RPG v3 - Lore** Foundry VTT module. +## [1.4.0] - 2026-10-20 + +### Added +- Two new journal compendia built from the vault: **Ściągawka Gracza** (visible to players) and **Ściągawka Mistrza Gry** (GM only), one page per section, in the "Ściągawki - HbM RPG v3" pack folder. +- `scripts/parsers/cheatsheet-parser.ts`: markdown renderer with tables, code blocks, callouts, nested lists and checkboxes. + +### Changed +- All vault-derived journal packs recompiled from the 2026-10-20 vault state. + ## [1.1.0] - 2026-04-30 Version bump to track the companion system release. No content changes. diff --git a/module.json b/module.json index 2b23a29..032ca7d 100644 --- a/module.json +++ b/module.json @@ -2,9 +2,9 @@ "id": "hbm-rpg-v3-lore", "title": "Homebrew Magic: RPG v3 - Lore", "description": "Companion lore module for the HbM RPG v3 system. Provides journal entries for setting, deities, history, and narrative chapters from the official books.", - "version": "1.3.21", + "version": "1.4.0", "manifest": "https://vtt-content.octoturge.com/packages/module/hbm-rpg-v3-lore.json", - "download": "https://vtt-content.octoturge.com/packages/module/hbm-rpg-v3-lore-v1.3.21.zip", + "download": "https://vtt-content.octoturge.com/packages/module/hbm-rpg-v3-lore-v1.4.0.zip", "compatibility": { "minimum": "13", "verified": "14" @@ -39,6 +39,36 @@ } ], "packs": [ + { + "name": "cheatsheet-player", + "label": "Ściągawka Gracza", + "path": "packs/cheatsheet-player", + "type": "JournalEntry", + "ownership": { + "PLAYER": "OBSERVER", + "ASSISTANT": "OWNER" + }, + "flags": { + "hbm-rpg-v3-lore": { + "category": "cheat-sheet" + } + } + }, + { + "name": "cheatsheet-gm", + "label": "Ściągawka Mistrza Gry", + "path": "packs/cheatsheet-gm", + "type": "JournalEntry", + "ownership": { + "PLAYER": "NONE", + "ASSISTANT": "OWNER" + }, + "flags": { + "hbm-rpg-v3-lore": { + "category": "cheat-sheet" + } + } + }, { "name": "abyss", "label": "Otchłań", @@ -319,6 +349,14 @@ } ], "packFolders": [ + { + "name": "Ściągawki - HbM RPG v3", + "sorting": "a", + "packs": [ + "cheatsheet-player", + "cheatsheet-gm" + ] + }, { "name": "Lore - HbM RPG v3", "sorting": "a", diff --git a/package.json b/package.json index 66c6e5b..f9d287d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hbm-rpg-v3-lore", - "version": "1.3.21", + "version": "1.4.0", "description": "Foundry VTT lore companion module for Homebrew Magic: RPG v3.", "private": true, "type": "module", diff --git a/scripts/build-journal-packs.ts b/scripts/build-journal-packs.ts index bef4bda..ebd353f 100644 --- a/scripts/build-journal-packs.ts +++ b/scripts/build-journal-packs.ts @@ -17,6 +17,7 @@ import { fileURLToPath } from 'node:url'; import { compilePack } from '@foundryvtt/foundryvtt-cli'; import { parseJournalBook, parseSplitBookFolder, type JournalDoc, type JournalPage } from './parsers/journal-parser'; import { parseTopicalFolder } from './parsers/topical-parser'; +import { parseCheatsheet } from './parsers/cheatsheet-parser'; const __dirname = dirname(fileURLToPath(import.meta.url)); const moduleRoot = resolve(__dirname, '..'); @@ -163,6 +164,12 @@ const TOPICAL_CONFIGS: TopicalConfig[] = [ { folder: 'ObsidianNotes/adventures', pack: 'adventures' }, ]; +/** Single-page vault cheat sheets → one pack each (page per `##` section). */ +const CHEATSHEET_CONFIGS: Array<{ file: string; pack: string; id: string }> = [ + { file: 'ObsidianNotes/rules/Ściągawka Gracza.md', pack: 'cheatsheet-player', id: 'sciagawka-gracza' }, + { file: 'ObsidianNotes/rules/Ściągawka Mistrza Gry.md', pack: 'cheatsheet-gm', id: 'sciagawka-mistrza-gry' }, +]; + // --------------------------------------------------------------------------- // Main // --------------------------------------------------------------------------- @@ -235,6 +242,18 @@ for (const top of TOPICAL_CONFIGS) { allDocs.push(...docs); } +// ── Cheat sheets (one entry per pack, one page per section) ── +for (const cs of CHEATSHEET_CONFIGS) { + const filePath = resolve(repoRoot, cs.file); + if (!existsSync(filePath)) { + console.warn(` · skip cheat sheet ${cs.pack}: file not found (${cs.file})`); + continue; + } + const docs = parseCheatsheet({ filePath, pack: cs.pack, id: cs.id, sourceBook: 'cheat-sheet' }); + console.log(` · ${cs.pack}: ${docs[0]?.pages.length ?? 0} pages`); + allDocs.push(...docs); +} + console.log(`[lore] parsed ${allDocs.length} journal entries total`); if (existsSync(packsSrcDir)) rmSync(packsSrcDir, { recursive: true }); diff --git a/scripts/parsers/cheatsheet-parser.ts b/scripts/parsers/cheatsheet-parser.ts new file mode 100644 index 0000000..9f2c209 --- /dev/null +++ b/scripts/parsers/cheatsheet-parser.ts @@ -0,0 +1,162 @@ +/** + * Cheat-sheet parser: turns a single self-contained vault page (`rules/Ściągawka *.md`) + * into one JournalEntry with one page per `##` section. + * + * Unlike `journal-parser.ts` it renders the full Obsidian dialect these pages use: + * tables, fenced code blocks, `> [!callout]` blocks, nested lists, task checkboxes, + * horizontal rules, and inline bold / italic / code / links. Links point at vault + * files that don't exist in Foundry, so they are rendered as plain text. + */ + +import { readFileSync } from 'node:fs'; +import type { JournalDoc, JournalPage } from './journal-parser'; + +function esc(s: string): string { + return s.replace(/&/g, '&').replace(//g, '>'); +} + +function inline(raw: string): string { + const codes: string[] = []; + let s = raw.replace(/`([^`]+)`/g, (_m, c: string) => { + codes.push(`${esc(c)}`); + return `\u0000${codes.length - 1}\u0000`; + }); + s = esc(s); + s = s.replace(/\[\[([^\]|]+)\|([^\]]+)\]\]/g, '$2').replace(/\[\[([^\]]+)\]\]/g, '$1'); + s = s.replace(/\[([^\]]+)\]\((?:[^()]|\([^)]*\))*\)/g, '$1'); + s = s.replace(/\*\*(.+?)\*\*/g, '$1'); + s = s.replace(/(^|[^*])\*([^*\s][^*]*?)\*(?!\*)/g, '$1$2'); + return s.replace(/\u0000(\d+)\u0000/g, (_m, i: string) => codes[Number(i)]!); +} + +function splitRow(line: string): string[] { + return line.trim().replace(/^\|/, '').replace(/\|$/, '').split('|').map((c) => c.trim()); +} + +function renderTable(rows: string[]): string { + const head = splitRow(rows[0]!); + const body = rows.slice(2).map(splitRow); + const th = head.map((c) => `${inline(c)}`).join(''); + const tr = body.map((r) => `${r.map((c) => `${inline(c)}`).join('')}`).join(''); + return `${th}${tr}
`; +} + +interface ListItem { text: string; children: ListItem[]; ordered: boolean } + +function renderList(items: ListItem[]): string { + if (!items.length) return ''; + const tag = items[0]!.ordered ? 'ol' : 'ul'; + const lis = items.map((i) => `
  • ${inline(i.text)}${renderList(i.children)}
  • `).join(''); + return `<${tag}>${lis}`; +} + +/** Render markdown (no frontmatter, no leading H1) to Foundry-safe HTML. */ +export function markdownToHtml(md: string): string { + const lines = md.replace(/\r\n/g, '\n').split('\n'); + const out: string[] = []; + let i = 0; + while (i < lines.length) { + const line = lines[i]!; + if (!line.trim()) { i++; continue; } + + // fenced code + if (/^```/.test(line)) { + const buf: string[] = []; + i++; + while (i < lines.length && !/^```/.test(lines[i]!)) buf.push(lines[i++]!); + i++; + out.push(`
    ${esc(buf.join('\n'))}
    `); + continue; + } + // heading (### and deeper; ## is split into pages by the caller) + const h = /^(#{2,6})\s+(.*)$/.exec(line); + if (h) { out.push(`${inline(h[2]!)}`); i++; continue; } + if (/^-{3,}\s*$/.test(line)) { out.push('
    '); i++; continue; } + + // table + if (/^\|/.test(line) && /^\|?\s*:?-{3,}/.test(lines[i + 1] ?? '')) { + const rows: string[] = []; + while (i < lines.length && /^\|/.test(lines[i]!)) rows.push(lines[i++]!); + out.push(renderTable(rows)); + continue; + } + // blockquote / callout + if (/^>/.test(line)) { + const buf: string[] = []; + while (i < lines.length && /^>/.test(lines[i]!)) buf.push(lines[i++]!.replace(/^>\s?/, '')); + const co = /^\[!(\w+)\][+-]?\s*(.*)$/.exec(buf[0]!); + if (co) { + const title = co[2] || co[1]!; + out.push(`

    ${inline(title)}

    ${markdownToHtml(buf.slice(1).join('\n'))}
    `); + } else { + out.push(`
    ${markdownToHtml(buf.join('\n'))}
    `); + } + continue; + } + // list + if (/^\s*([-*]|\d+\.)\s+/.test(line)) { + const root: ListItem[] = []; + const stack: Array<{ indent: number; list: ListItem[] }> = [{ indent: -1, list: root }]; + while (i < lines.length && /^\s*([-*]|\d+\.)\s+/.test(lines[i]!)) { + const m = /^(\s*)([-*]|\d+\.)\s+(.*)$/.exec(lines[i]!)!; + const indent = m[1]!.replace(/\t/g, ' ').length; + const text = m[3]!.replace(/^\[( |x)\]\s+/i, (_x, c: string) => (c === ' ' ? '☐ ' : '☑ ')); + const item: ListItem = { text, children: [], ordered: /\d/.test(m[2]!) }; + while (stack.length > 1 && stack[stack.length - 1]!.indent >= indent) stack.pop(); + stack[stack.length - 1]!.list.push(item); + stack.push({ indent, list: item.children }); + i++; + } + out.push(renderList(root)); + continue; + } + // paragraph + const buf: string[] = []; + while (i < lines.length && lines[i]!.trim() && !/^(#{2,6}\s|```|>|\||-{3,}\s*$|\s*([-*]|\d+\.)\s+)/.test(lines[i]!)) buf.push(lines[i++]!); + if (!buf.length) { buf.push(lines[i++]!); } + out.push(`

    ${inline(buf.join(' '))}

    `); + } + return out.join('\n'); +} + +export interface CheatsheetConfig { + /** Absolute path to the vault markdown file. */ + filePath: string; + /** Pack id (e.g. `cheatsheet-player`). */ + pack: string; + /** Journal entry id / slug. */ + id: string; + /** Source book id for flags. */ + sourceBook: string; +} + +/** Parse a cheat sheet into one JournalDoc (intro page + one page per `##` section). */ +export function parseCheatsheet(cfg: CheatsheetConfig): JournalDoc[] { + const raw = readFileSync(cfg.filePath, 'utf8').replace(/\r\n/g, '\n'); + const body = raw.replace(/^---\n[\s\S]*?\n---\n/, ''); + const lines = body.split('\n'); + const titleIdx = lines.findIndex((l) => /^#\s+/.test(l)); + const title = titleIdx >= 0 ? lines[titleIdx]!.replace(/^#\s+/, '').trim() : cfg.id; + const rest = lines.slice(titleIdx + 1); + + const sections: Array<{ name: string; lines: string[] }> = [{ name: title, lines: [] }]; + let inFence = false; + for (const l of rest) { + if (/^```/.test(l)) inFence = !inFence; + const h2 = !inFence ? /^##\s+(.*)$/.exec(l) : null; + if (h2) sections.push({ name: h2[1]!.trim(), lines: [] }); + else sections[sections.length - 1]!.lines.push(l); + } + + const pages: JournalPage[] = sections + .filter((s) => s.lines.some((l) => l.trim())) + .map((s, idx) => ({ name: s.name, html: markdownToHtml(s.lines.join('\n')), sort: (idx + 1) * 100 })); + + return [{ + id: cfg.id, + name: title, + pack: cfg.pack, + source: { book: cfg.sourceBook, chapter: title, line: 1 }, + pages, + }]; +} -- 2.54.0