First Commit
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
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 };
|
||||
Reference in New Issue
Block a user