← 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 : [];
}