159 lines
4.3 KiB
JavaScript
159 lines
4.3 KiB
JavaScript
#!/usr/bin/env node
|
|
"use strict";
|
|
|
|
const SCAN_KINDS = [
|
|
{ subdir: "entities", pageType: "entity", applicable: true },
|
|
{ subdir: "topics", pageType: "topic", applicable: true },
|
|
{ subdir: "sources", pageType: "source", applicable: true },
|
|
{ subdir: "comparisons", pageType: "comparison", applicable: true },
|
|
{ subdir: "queries", pageType: "query", applicable: false },
|
|
{ subdir: "synthesis", pageType: "synthesis", applicable: false }
|
|
];
|
|
|
|
function sortedUnique(values) {
|
|
return Array.from(new Set(values)).sort();
|
|
}
|
|
|
|
function extractFrontmatter(text) {
|
|
if (!text.startsWith("---\n") && !text.startsWith("---\r\n")) {
|
|
return { hasFrontmatter: false, frontmatter: "", body: text };
|
|
}
|
|
|
|
const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)([\s\S]*)$/);
|
|
if (!match) {
|
|
return { hasFrontmatter: false, frontmatter: "", body: text };
|
|
}
|
|
|
|
return {
|
|
hasFrontmatter: true,
|
|
frontmatter: match[1],
|
|
body: match[2]
|
|
};
|
|
}
|
|
|
|
function normalizeSourceToken(token) {
|
|
const trimmed = String(token || "").trim();
|
|
if (!trimmed) return null;
|
|
|
|
let value = trimmed;
|
|
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
value = value.slice(1, -1).trim();
|
|
}
|
|
|
|
return value || null;
|
|
}
|
|
|
|
function parseInlineSources(raw) {
|
|
const trimmed = raw.trim();
|
|
if (trimmed === "[]") return { ok: true, values: [] };
|
|
if (!(trimmed.startsWith("[") && trimmed.endsWith("]"))) {
|
|
return { ok: false, values: [] };
|
|
}
|
|
|
|
const inner = trimmed.slice(1, -1).trim();
|
|
if (!inner) return { ok: true, values: [] };
|
|
|
|
const values = inner
|
|
.split(",")
|
|
.map(normalizeSourceToken)
|
|
.filter(Boolean);
|
|
|
|
return { ok: true, values };
|
|
}
|
|
|
|
function parseSourcesFrontmatter(frontmatter) {
|
|
if (!frontmatter) {
|
|
return { hasField: false, parsed: false, sources: [], signalAvailable: false };
|
|
}
|
|
|
|
const lines = frontmatter.split(/\r?\n/);
|
|
|
|
for (let index = 0; index < lines.length; index += 1) {
|
|
const match = lines[index].match(/^sources:\s*(.*)$/);
|
|
if (!match) continue;
|
|
|
|
const rest = match[1].trim();
|
|
if (rest) {
|
|
if (!rest.startsWith("[")) {
|
|
const single = normalizeSourceToken(rest);
|
|
return {
|
|
hasField: true,
|
|
parsed: Boolean(single),
|
|
sources: single ? [single] : [],
|
|
signalAvailable: Boolean(single)
|
|
};
|
|
}
|
|
|
|
const parsedInline = parseInlineSources(rest);
|
|
return {
|
|
hasField: true,
|
|
parsed: parsedInline.ok,
|
|
sources: parsedInline.ok ? sortedUnique(parsedInline.values) : [],
|
|
signalAvailable: parsedInline.ok && parsedInline.values.length > 0
|
|
};
|
|
}
|
|
|
|
const collected = [];
|
|
let parsed = true;
|
|
let consumed = 0;
|
|
|
|
for (let cursor = index + 1; cursor < lines.length; cursor += 1) {
|
|
const line = lines[cursor];
|
|
if (!line.trim()) {
|
|
consumed += 1;
|
|
continue;
|
|
}
|
|
if (/^[^\s-]/.test(line)) break;
|
|
const itemMatch = line.match(/^\s*-\s*(.+)$/);
|
|
if (!itemMatch) {
|
|
parsed = false;
|
|
consumed += 1;
|
|
continue;
|
|
}
|
|
const token = normalizeSourceToken(itemMatch[1]);
|
|
if (token) collected.push(token);
|
|
consumed += 1;
|
|
}
|
|
|
|
index += consumed;
|
|
return {
|
|
hasField: true,
|
|
parsed,
|
|
sources: parsed ? sortedUnique(collected) : [],
|
|
signalAvailable: parsed && collected.length > 0
|
|
};
|
|
}
|
|
|
|
return { hasField: false, parsed: false, sources: [], signalAvailable: false };
|
|
}
|
|
|
|
function evaluateSourceSignalEligibility({ pageType, frontmatter }) {
|
|
const kind = SCAN_KINDS.find((k) => k.pageType === pageType);
|
|
if (!kind || !kind.applicable) {
|
|
return { eligible: false, reason: "not_applicable", sources: [] };
|
|
}
|
|
|
|
const parsed = parseSourcesFrontmatter(frontmatter);
|
|
|
|
if (!parsed.hasField) {
|
|
return { eligible: false, reason: "missing_sources", sources: [] };
|
|
}
|
|
if (!parsed.parsed) {
|
|
return { eligible: false, reason: "invalid_sources", sources: [] };
|
|
}
|
|
if (parsed.sources.length === 0) {
|
|
return { eligible: false, reason: "empty_sources", sources: [] };
|
|
}
|
|
|
|
return { eligible: true, reason: "ok", sources: parsed.sources };
|
|
}
|
|
|
|
module.exports = {
|
|
SCAN_KINDS,
|
|
extractFrontmatter,
|
|
evaluateSourceSignalEligibility,
|
|
normalizeSourceToken,
|
|
parseSourcesFrontmatter,
|
|
sortedUnique
|
|
};
|