7baebebc09
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
121 lines
4.6 KiB
JavaScript
121 lines
4.6 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* sync-campus.mjs — importa le guide di studio del corso Biohacking Campus
|
|
* nella sezione riservata /campus del sito.
|
|
*
|
|
* COSA FA
|
|
* Legge i file `<slug>.studio.md` dal progetto sorgente, li converte in
|
|
* frammenti HTML e produce in `campus-content/`:
|
|
* - `nav.json` → aree, capitoli, guide (ordine, titoli, prev/next)
|
|
* - `pages/<slug>.html` → corpo della guida
|
|
* - `assets/` → immagini delle slide, copiate dal sorgente
|
|
* Le pagine sono renderizzate a runtime da src/pages/campus/ con lo stile
|
|
* del sito InsanityLab; qui non si genera alcun layout.
|
|
*
|
|
* USO
|
|
* npm run sync-campus [-- <path-progetto-biohacking-campus>]
|
|
* Default: /data/Sorgenti/AI-OS/projects/personale/biohacking-campus
|
|
*
|
|
* IDEMPOTENTE
|
|
* Rigenera nav.json e le pagine, rimuovendo i frammenti orfani. Gli assets
|
|
* sono copiati solo se mancanti o più vecchi del sorgente.
|
|
*/
|
|
import { readFile, writeFile, mkdir, readdir, rm, copyFile, stat } from 'node:fs/promises';
|
|
import { existsSync } from 'node:fs';
|
|
import { join, dirname, resolve } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { CHAPTERS, CH_COLORS, AREAS, sourceDirFor } from './campus/structure.mjs';
|
|
import { parseGuide } from './campus/parse.mjs';
|
|
|
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
const OUT_DIR = resolve(HERE, '..', 'campus-content');
|
|
const DEFAULT_SRC = '/data/Sorgenti/AI-OS/projects/personale/biohacking-campus';
|
|
|
|
async function copyTree(src, dest) {
|
|
await mkdir(dest, { recursive: true });
|
|
let copied = 0;
|
|
for (const entry of await readdir(src, { withFileTypes: true })) {
|
|
const from = join(src, entry.name);
|
|
const to = join(dest, entry.name);
|
|
if (entry.isDirectory()) {
|
|
copied += await copyTree(from, to);
|
|
continue;
|
|
}
|
|
const [s, d] = [await stat(from), existsSync(to) ? await stat(to) : null];
|
|
if (!d || d.mtimeMs < s.mtimeMs || d.size !== s.size) {
|
|
await copyFile(from, to);
|
|
copied++;
|
|
}
|
|
}
|
|
return copied;
|
|
}
|
|
|
|
async function main() {
|
|
const srcRoot = resolve(process.argv[2] ?? DEFAULT_SRC);
|
|
if (!existsSync(srcRoot)) {
|
|
console.error(`Progetto sorgente non trovato: ${srcRoot}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
const pagesDir = join(OUT_DIR, 'pages');
|
|
await mkdir(pagesDir, { recursive: true });
|
|
|
|
// 1. parsing di tutte le guide, capitolo per capitolo
|
|
const chapters = [];
|
|
const written = new Set();
|
|
const flat = []; // ordine globale di lettura, per prev/next
|
|
for (const [num, title, sub, slugs] of CHAPTERS) {
|
|
const lessons = [];
|
|
for (const [i, slug] of slugs.entries()) {
|
|
const file = join(srcRoot, sourceDirFor(slug), `${slug}.studio.md`);
|
|
if (!existsSync(file)) {
|
|
console.warn(` guida mancante, salto: ${slug}`);
|
|
continue;
|
|
}
|
|
const g = parseGuide(await readFile(file, 'utf8'), slug, i + 1);
|
|
await writeFile(join(pagesDir, `${slug}.html`), g.html);
|
|
written.add(`${slug}.html`);
|
|
const entry = {
|
|
slug: g.slug, kind: g.kind, label: g.label, titleFull: g.titleFull,
|
|
titleShort: g.titleShort, relatore: g.relatore, concetti: g.concetti.slice(0, 6),
|
|
chapter: num,
|
|
};
|
|
lessons.push(entry);
|
|
flat.push(entry);
|
|
}
|
|
chapters.push({ num, title, sub, color: CH_COLORS[num - 1], lessons });
|
|
}
|
|
|
|
// 2. prev/next lungo l'ordine globale, e area di appartenenza
|
|
const areaOf = new Map();
|
|
for (const [slug, , , , chapterNums] of AREAS)
|
|
for (const n of chapterNums) areaOf.set(n, slug);
|
|
flat.forEach((l, i) => {
|
|
l.area = areaOf.get(l.chapter);
|
|
l.prev = flat[i - 1] ? { slug: flat[i - 1].slug, area: areaOf.get(flat[i - 1].chapter), title: flat[i - 1].titleShort } : null;
|
|
l.next = flat[i + 1] ? { slug: flat[i + 1].slug, area: areaOf.get(flat[i + 1].chapter), title: flat[i + 1].titleShort } : null;
|
|
});
|
|
|
|
const byNum = new Map(chapters.map((c) => [c.num, c]));
|
|
const areas = AREAS.map(([slug, title, sub, color, chapterNums]) => ({
|
|
slug, title, sub, color,
|
|
chapters: chapterNums.map((n) => byNum.get(n)).filter(Boolean),
|
|
}));
|
|
|
|
await writeFile(join(OUT_DIR, 'nav.json'), JSON.stringify({ areas, count: flat.length }, null, 2));
|
|
|
|
// 3. rimozione dei frammenti orfani
|
|
let removed = 0;
|
|
for (const f of await readdir(pagesDir)) {
|
|
if (!written.has(f)) { await rm(join(pagesDir, f)); removed++; }
|
|
}
|
|
|
|
// 4. assets delle slide
|
|
const assetsSrc = join(srcRoot, 'sito', 'assets');
|
|
const copied = existsSync(assetsSrc) ? await copyTree(assetsSrc, join(OUT_DIR, 'assets')) : 0;
|
|
|
|
console.log(`Campus: ${flat.length} guide, ${areas.length} aree, ${copied} asset copiati, ${removed} pagine orfane rimosse.`);
|
|
}
|
|
|
|
await main();
|