Files
InsanityLab-web/apps/sito/scripts/campus/parse.mjs
T
AdrianoDev 510d7ca4ec Struttura: il sito esistente si sposta sotto apps/sito
Primo passo della separazione sito / piattaforme decisa da Adriano il 03/09:
due applicazioni autosufficienti, senza workspace di root, cosi' che ognuna
resti un progetto Astro completo ed estraibile.

Qui non cambia niente del codice: solo la radice. I pattern del .gitignore
che avevano una barra erano ancorati alla radice e dopo lo spostamento non
avrebbero piu' coperto niente (data/*.db*, uploads/*): resi validi a
qualunque profondita'.

Suite: 45 file, 347 test verdi prima e dopo lo spostamento.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EndnceRA5WnA6rvA5iV9WL
2026-09-03 11:05:54 +00:00

116 lines
4.6 KiB
JavaScript

// Parsing delle guide di studio (*.studio.md) del corso Biohacking Campus.
// Le intestazioni H1 sono eterogenee per tipo di guida: ogni regex qui sotto
// riconosce una famiglia e ne estrae titolo, relatore ed eventuale numero.
import { marked } from 'marked';
const H1_RE = /^#\s+Lezione\s+([0-9]+)\s+—\s+(.*?)\s+\(([^)]*)\)\s+—\s+Guida/u;
const ACCEL_RE = /^#\s+Accelerator\s+([0-9]+)\s+—\s+(.*?)\s+\(([^)]*)\)\s+—\s+Guida/u;
const LIVE_RE = /^#\s+Lezione\s+live\s+([0-9]+)\s+—\s+(.*?)\s+\(([^)]*)\)\s+—\s+Guida/u;
const SLIDE_RE = /^#\s+Slide\s+—\s+(.*?)\s+—\s+Guida/u;
const SPEAKER_RE = /^#\s+Intervento\s+—\s+(.*?)\s+\(([^)]*)\)\s+—\s+Guida/u;
const CONSUL_RE = /^#\s+Consulenza\s+—\s+(.*?)\s+\(([^)]*)\)\s+—\s+Guida/u;
const MASTER_RE = /^#\s+Masterclass\s+—\s+(.*?)\s+\(([^)]*)\)\s+—\s+Guida/u;
// I titoli Q&A sono eterogenei: cattura tutto fino a "— Guida allo studio".
const QA_RE = /^#\s+(.+?)\s+—\s+Guida allo studio\s*$/u;
const BOLD_RE = /\*\*(.+?)\*\*/u;
const PARTE_RE = /\((Parte\s+\d+)\)/u;
export function kindOf(slug) {
if (slug.startsWith('accelerator')) return 'accelerator';
if (slug.startsWith('lezione-live')) return 'live';
if (slug.startsWith('slide-')) return 'slide';
if (slug.startsWith('speaker-')) return 'speaker';
if (slug.startsWith('consulenza-')) return 'consulenza';
if (slug.startsWith('masterclass-')) return 'masterclass';
if (slug.startsWith('qa-')) return 'qa';
return 'lezione';
}
const LABEL_PREFIX = {
accelerator: 'A', live: 'LV', slide: 'SL', speaker: 'SP',
consulenza: 'C', masterclass: 'MC', qa: 'QA',
};
// Le immagini nei sorgenti puntano a "assets/..." relativo alla cartella del
// sito statico; nella sezione campus sono servite dalla route protetta.
export function rewriteAssets(html) {
return html.replaceAll('src="assets/', 'src="/campus/assets/');
}
function extractConcetti(bodyLines) {
const out = [];
let inSection = false;
for (const ln of bodyLines) {
if (ln.startsWith('## ')) {
inSection = ln.trim().toLowerCase().startsWith('## concetti chiave');
continue;
}
if (!inSection || !ln.trimStart().startsWith('- ')) continue;
const m = BOLD_RE.exec(ln);
if (m) out.push(m[1].trim().replace(/[.:;,]+$/, ''));
}
return out;
}
/**
* Parsa una guida.
* @param {string} raw contenuto del file .studio.md
* @param {string} slug nome file senza estensione
* @param {number} pos posizione della guida nel capitolo (1-based)
* @returns {{slug,kind,num,titleFull,titleShort,relatore,parte,label,concetti,html}}
*/
export function parseGuide(raw, slug, pos) {
const lines = raw.split('\n');
const h1 = lines[0];
const kind = kindOf(slug);
let num = pos, titleFull, titleShort, relatore, parte = '';
let m;
if (kind === 'qa' && (m = QA_RE.exec(h1))) {
titleFull = titleShort = m[1].trim();
relatore = 'Q&A del corso';
} else if (kind === 'masterclass' && (m = MASTER_RE.exec(h1))) {
titleFull = titleShort = m[1].trim();
relatore = m[2].trim();
} else if (kind === 'consulenza' && (m = CONSUL_RE.exec(h1))) {
titleFull = titleShort = m[1].trim();
relatore = m[2].trim();
} else if (kind === 'speaker' && (m = SPEAKER_RE.exec(h1))) {
titleFull = titleShort = m[1].trim();
relatore = m[2].trim();
} else if (kind === 'slide' && (m = SLIDE_RE.exec(h1))) {
titleFull = titleShort = m[1].trim();
relatore = 'Slide ufficiali del corso';
} else if (kind === 'live' && (m = LIVE_RE.exec(h1))) {
num = Number(m[1]);
const date = m[2].trim();
titleFull = `Lezione live ${num}${date}`;
titleShort = `Lezione live ${num}`;
relatore = `${m[3]} · ${date}`;
} else if (kind === 'accelerator' && (m = ACCEL_RE.exec(h1))) {
num = Number(m[1]);
const date = m[2].trim();
titleFull = `Accelerator ${num}${date}`;
titleShort = `Accelerator ${num}`;
relatore = `${m[3]} · ${date}`;
} else if (kind === 'lezione' && (m = H1_RE.exec(h1))) {
num = Number(m[1]);
titleFull = m[2];
relatore = m[3];
parte = PARTE_RE.exec(titleFull)?.[1] ?? '';
titleShort = titleFull.replace(/\s*\(Parte\s+\d+\)/u, '').trim();
} else {
throw new Error(`H1 non riconosciuto in ${slug}: ${JSON.stringify(h1)}`);
}
const bodyLines = lines.slice(1).filter((ln) => !ln.startsWith('> Sintesi di studio basata'));
const html = rewriteAssets(marked.parse(bodyLines.join('\n').trim(), { async: false }));
const label = kind === 'lezione'
? `L${num}${parte ? ` · ${parte}` : ''}`
: `${LABEL_PREFIX[kind]}${num}`;
return { slug, kind, num, titleFull, titleShort, relatore, parte, label, concetti: extractConcetti(bodyLines), html };
}