Files

164 lines
5.1 KiB
JavaScript
Raw Permalink Normal View History

2026-06-30 09:10:05 +10:00
const fs = require('fs');
function isTableRowLine(line) {
const t = line.trim();
return t.length > 0 && t.includes('|');
}
function isSeparatorLine(line) {
const t = line.trim();
if (!t.includes('-')) return false;
const cells = stripOuterPipes(t).split('|');
return cells.length > 0 && cells.every((c) => /^:?-{1,}:?$/.test(c.trim()));
}
function stripOuterPipes(t) {
let s = t;
if (s.startsWith('|')) s = s.slice(1);
if (s.endsWith('|')) s = s.slice(0, -1);
return s;
}
function splitRow(line) {
return stripOuterPipes(line.trim()).split('|').map((c) => c.trim());
}
function parseRollCell(raw) {
if (raw == null) return null;
const t = raw.trim().replace(/\*\*/g, '').trim();
if (t === '') return null;
let m = /^(\d+)\s*[-–—]\s*$/.exec(t);
if (m) return { min: parseInt(m[1], 10), max: 9999 };
m = /^(\d+)\s*\+\s*$/.exec(t);
if (m) return { min: parseInt(m[1], 10), max: 9999 };
m = /^(\d+)\s*or\s*(less|lower|fewer|under)$/i.exec(t);
if (m) return { min: 0, max: parseInt(m[1], 10) };
m = /^(\d+)\s*or\s*(more|higher|greater|above)$/i.exec(t);
if (m) return { min: parseInt(m[1], 10), max: 9999 };
m = /^(\d+)\s*[-–—]\s*(\d+)$/.exec(t);
if (m) return { min: parseInt(m[1], 10), max: parseInt(m[2], 10) };
m = /^(\d+)$/.exec(t);
if (m) {
const v = parseInt(m[1], 10);
return { min: v, max: v };
}
return null;
}
function inferDiceFromMax(maxRoll) {
const standard = [4, 6, 8, 10, 12, 20, 100];
const found = standard.find((d) => maxRoll <= d);
return `d${found || maxRoll}`;
}
function buildTableObject(columns, rowsRaw, headingText) {
if (columns.length === 1) {
const rows = rowsRaw.map((cells, idx) => ({
roll_min: idx + 1,
roll_max: idx + 1,
cells,
}));
return {
columns,
rows,
diceNotation: rows.length ? `d${rows.length}` : null,
tableType: 'list',
};
}
const headingDiceMatch = /\((\d*d\d+)\)/i.exec(headingText || '');
const diceFromHeading = headingDiceMatch ? headingDiceMatch[1].toLowerCase() : null;
const firstColHeader = columns[0];
const diceFromColumn = /^\d*d\d+$/i.test(firstColHeader.replace(/\s/g, ''))
? firstColHeader.toLowerCase()
: null;
const firstColRanges = rowsRaw.map((cells) => parseRollCell(cells[0]));
const allFirstColParsed = firstColRanges.length > 0 && firstColRanges.every((r) => r !== null);
if (!allFirstColParsed) {
// First column isn't actually a roll column (or some cells use unrecognized
// phrasing) - keep it as real data rather than silently discarding it.
if (firstColRanges.some((r) => r !== null)) {
console.warn(`"${headingText}": first column has a mix of roll-like and non-roll-like cells; keeping it as a data column.`);
}
const rows = rowsRaw.map((cells, idx) => ({ roll_min: idx + 1, roll_max: idx + 1, cells }));
return {
columns,
rows,
diceNotation: rows.length ? `d${rows.length}` : null,
tableType: 'list',
};
}
let maxRoll = 0;
const parsedRows = rowsRaw.map((cells, idx) => {
const range = firstColRanges[idx];
if (range.max < 9999) maxRoll = Math.max(maxRoll, range.max);
return { roll_min: range.min, roll_max: range.max, cells: cells.slice(1) };
});
const resultColumns = columns.slice(1);
const tableType = resultColumns.length > 1 ? 'multi' : 'single';
let diceNotation = diceFromHeading || diceFromColumn || null;
if (!diceNotation && maxRoll > 0) {
diceNotation = inferDiceFromMax(maxRoll);
}
return { columns: resultColumns, rows: parsedRows, diceNotation, tableType };
}
function extractFirstTable(bodyLines, headingText) {
for (let i = 0; i < bodyLines.length - 1; i++) {
if (isTableRowLine(bodyLines[i]) && isSeparatorLine(bodyLines[i + 1])) {
const columns = splitRow(bodyLines[i]);
const rowsRaw = [];
let j = i + 2;
while (j < bodyLines.length && isTableRowLine(bodyLines[j]) && !isSeparatorLine(bodyLines[j])) {
rowsRaw.push(splitRow(bodyLines[j]));
j++;
}
return buildTableObject(columns, rowsRaw, headingText);
}
}
return null;
}
function parseMarkdownFile(filePath) {
const content = fs.readFileSync(filePath, 'utf8');
const lines = content.split(/\r?\n/);
const root = { level: 0, headingText: null, table: null, children: [] };
const stack = [root];
let bodyLines = [];
let currentNode = null;
function flushBody() {
if (!currentNode) return;
const table = extractFirstTable(bodyLines, currentNode.headingText);
if (table) currentNode.table = table;
}
for (const line of lines) {
const m = /^(#{1,6})\s+(.+?)\s*$/.exec(line);
if (m) {
flushBody();
bodyLines = [];
const level = m[1].length;
const headingText = m[2].trim();
while (stack.length && stack[stack.length - 1].level >= level) stack.pop();
const parent = stack[stack.length - 1];
const node = { level, headingText, table: null, children: [] };
parent.children.push(node);
stack.push(node);
currentNode = node;
} else {
bodyLines.push(line);
}
}
flushBody();
return root.children;
}
module.exports = { parseMarkdownFile, parseRollCell };