Script sync-campus: importa le guide del corso in campus-content/
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
#!/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();
|
||||
Reference in New Issue
Block a user