feat: parse the script coverage table

This commit is contained in:
meels
2026-07-28 13:45:24 +02:00
parent 3e28180f8b
commit 0304757982
2 changed files with 189 additions and 1 deletions

View File

@@ -65,6 +65,87 @@ function derivePipeline({ rawFiles, sourcePages }) {
return { processed, unprocessed, orphaned };
}
const VALID_STATUS = new Set(["covered", "partial", "absent"]);
const SEPARATOR_RE = /^\|?\s*:?-{2,}/;
function parseCoverageTable(markdown) {
const text = String(markdown);
const meta = { script: null, lastSynced: null };
const scriptM = text.match(/^\s*-\s*\*\*Script:\*\*\s*`([^`]+)`/m);
if (scriptM) meta.script = scriptM[1].trim();
const syncM = text.match(/^\s*-\s*\*\*Last synced:\*\*\s*(\S+)/m);
if (syncM) meta.lastSynced = syncM[1].trim();
const rows = [];
const errors = [];
let inTable = false;
text.split(/\r?\n/).forEach((line, i) => {
const t = line.trim();
if (!t.startsWith("|")) {
inTable = false;
return;
}
if (SEPARATOR_RE.test(t)) {
inTable = true;
return;
}
if (!inTable) return;
const body = t.endsWith("|") ? t.slice(1, -1) : t.slice(1);
const cells = body.split(/(?<!\\)\|/).map((c) => c.trim());
if (cells.length < 3) {
errors.push({ line: i + 1, text: t, reason: "expected at least 3 columns" });
return;
}
const status = cells[1].toLowerCase();
if (!VALID_STATUS.has(status)) {
errors.push({ line: i + 1, text: t, reason: `invalid status "${cells[1]}"` });
return;
}
// Capture up to the first ] | or backslash. Obsidian escapes the pipe in a
// piped wikilink inside a table cell, so the raw cell reads [[name\|alias]] —
// excluding the backslash is what keeps the trailing "\" out of the name.
const linkM = cells[0].match(/\[\[([^\]|\\]+)/);
const stationCell = cells[2];
rows.push({
concept: linkM ? linkM[1].trim() : cells[0],
status,
stations:
stationCell === "—" || stationCell === "-" || stationCell === ""
? []
: stationCell.split(",").map((s) => s.trim()).filter(Boolean),
pinned: (cells[3] || "").toLowerCase() === "yes",
line: i + 1,
});
});
return { meta, rows, errors };
}
function groupByStation(rows) {
const order = ["All stations", ...STATIONS, "No station"];
const buckets = new Map(order.map((k) => [k, []]));
for (const row of rows) {
let key;
if (row.stations.includes("all")) key = "All stations";
else if (row.stations.length === 0) key = "No station";
else key = row.stations[0];
if (!buckets.has(key)) buckets.set(key, []);
buckets.get(key).push(row);
}
const known = order.filter((k) => buckets.get(k).length > 0);
const unknown = [...buckets.keys()].filter((k) => !order.includes(k) && buckets.get(k).length > 0);
return [...known, ...unknown].map((station) => ({ station, rows: buckets.get(station) }));
}
function parseConfig(source) {
const cfg = Object.assign({}, DEFAULTS);
for (const line of String(source).split(/\r?\n/)) {
@@ -200,4 +281,8 @@ class WebinarDashPlugin extends PluginBase {
module.exports = WebinarDashPlugin;
module.exports.default = WebinarDashPlugin;
module.exports.__test__ = { parseConfig, extractRawPath, derivePipeline, STATIONS, DEFAULTS };
module.exports.__test__ = {
parseConfig, extractRawPath, derivePipeline,
parseCoverageTable, groupByStation,
STATIONS, DEFAULTS,
};