feat: parse the script coverage table
This commit is contained in:
87
.obsidian/plugins/webinar-dash/main.js
vendored
87
.obsidian/plugins/webinar-dash/main.js
vendored
@@ -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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user