/**
* 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) => `
${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(``); } else { out.push(`${inline(title)}
${markdownToHtml(buf.slice(1).join('\n'))}
${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, }]; }