← back to Paul Conrad Cartoons
scripts/lib-stories.mjs
62 lines
// lib-stories.mjs — shared P24 story-list access for the Shadow Man article linker (TK-12247). $0, local.
// Runs the P24 repo's own daily-cartoons/extract_stories.mjs (sandboxed vm parse of real-news-data.js +
// stories-data.js, read-only) and returns its flat article list. Used by the matcher script and by the
// Inkwell server's GET /api/stories + POST /api/shadowman/link validation.
import { execFileSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import vm from 'node:vm';
export function p24Dir(explicit) {
const raw = explicit || process.env.P24_DIR || '~/Projects/crazy-news-channel';
return path.resolve(raw.replace(/^~/, process.env.HOME));
}
export function loadStories(dir) {
const d = p24Dir(dir);
const script = path.join(d, 'daily-cartoons', 'extract_stories.mjs');
if (!fs.existsSync(script)) throw new Error(`extract_stories.mjs not found under ${d}`);
const out = execFileSync(process.execPath, [script], { env: { ...process.env, P24_SITE_DIR: d }, maxBuffer: 32 * 1024 * 1024, timeout: 30000 });
const list = JSON.parse(out.toString('utf8'));
if (!Array.isArray(list)) throw new Error('extract_stories.mjs did not return an array');
return list;
}
// The article URL for a story: the original outlet link for real news, else the P24 article route
// written SITE-ROOT-relative ("index.html#/article/<id>"). Consumers resolve a relative value against
// wherever the P24 site lives (the exported cartoon page prefixes "../"; Inkwell prefixes P24_BASE).
export function storyUrl(s) {
return s.sourceUrl || `index.html#/article/${encodeURIComponent(s.id)}`;
}
export function storySourceLabel(s) {
return s.source === 'real-news' ? (s.sourceName || 'Real news') : 'P24';
}
// id -> { category, categoryLabel } from the raw story globals (same sandboxed vm pattern as
// extract_stories.mjs — never eval'd in this process). Used for the manifest entry's `section`.
export function storyCategories(dir) {
const d = p24Dir(dir);
const ctx = { window: {}, console };
vm.createContext(ctx);
for (const f of ['real-news-data.js', 'stories-data.js']) {
const p = path.join(d, f);
if (fs.existsSync(p)) vm.runInContext(fs.readFileSync(p, 'utf8'), ctx, { filename: f });
}
const map = new Map();
for (const arr of [ctx.window.P24_REAL_STORIES, ctx.window.P24_EXTRA_STORIES]) {
for (const s of Array.isArray(arr) ? arr : []) if (s && s.id) map.set(s.id, { category: s.category || null, categoryLabel: s.categoryLabel || null });
}
return map;
}
// Parse a P24 cartoons/manifest.js (window.P24_CARTOONS = [...];) in a sandbox. Returns [] if absent.
export function readManifest(dir) {
const p = path.join(p24Dir(dir), 'cartoons', 'manifest.js');
if (!fs.existsSync(p)) return [];
const ctx = { window: {} };
vm.createContext(ctx);
vm.runInContext(fs.readFileSync(p, 'utf8'), ctx, { filename: 'manifest.js' });
return Array.isArray(ctx.window.P24_CARTOONS) ? ctx.window.P24_CARTOONS : [];
}