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
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
// 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 };
|
||||
}
|
||||
Reference in New Issue
Block a user