/**
* Logique pure partagée : génération markdown, lecture meta, échappements.
*/
import fs from 'node:fs'
import path from 'node:path'
export const FEATURED_START = ''
export const FEATURED_END = ''
export function escMd(s) {
if (typeof s !== 'string') return ''
return s.replace(/\|/g, '\\|')
}
export function escHtml(s) {
return String(s)
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
}
export function pickFeatured(items) {
const marked = items.filter((p) => p.featured).sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
if (marked.length) return marked.slice(0, 6)
return [...items].sort((a, b) => (a.order ?? 0) - (b.order ?? 0)).slice(0, 3)
}
/** @param {Function} sitePath - ex. import { sitePath } from './site-base.mjs' */
export function buildFeaturedHtml(items, sitePath) {
const featured = pickFeatured(items)
if (!featured.length) return ''
const lines = [
'',
' Présentations
',
' ', '')
return lines.join('\n')
}
export function readAllMeta(slidesRoot) {
if (!fs.existsSync(slidesRoot)) return []
const dirs = fs
.readdirSync(slidesRoot, { withFileTypes: true })
.filter((d) => d.isDirectory() && !d.name.startsWith('wip_') && !d.name.startsWith('_'))
.map((d) => d.name)
const list = []
for (const dir of dirs) {
const metaPath = path.join(slidesRoot, dir, 'meta.json')
if (!fs.existsSync(metaPath)) continue
const raw = fs.readFileSync(metaPath, 'utf8')
const meta = JSON.parse(raw)
if (meta.slug && meta.slug !== dir) {
console.warn(`[sync] slug "${meta.slug}" ≠ dossier "${dir}", le dossier fait foi pour les URLs Slidev`)
}
list.push({ ...meta, slug: dir, folder: dir })
}
list.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
return list
}
/** @param {Function} sitePath */
export function generateListPageMarkdown(items, sitePath) {
const lines = [
'---',
'title: Présentations',
'outline: deep',
'---',
'',
'# Présentations',
'',
'Decks disponibles — fiche portail et build Slidev statique.',
'',
'
',
]
for (const p of items) {
const detail = sitePath(`/presentations/${p.slug}/`)
/** Première planche (`/1`) + classe `vp-raw` : le routeur VitePress ignore ces liens (navigation réelle vers Slidev). */
const deck = sitePath(`/slides/${p.slug}/1`)
const tags = (p.tags || []).map((t) => `\`${escMd(t)}\``).join(' · ')
lines.push('
')
lines.push(` `)
lines.push(` ${escMd(p.description || '')}
`)
if (tags) lines.push(` ${tags}
`)
lines.push(' ')
lines.push(`
Fiche`)
lines.push(
`
Slides`,
)
lines.push('
')
lines.push(' ')
}
lines.push('
', '')
return lines.join('\n')
}
/** @param {Function} sitePath */
export function generateDetailPageMarkdown(p, sitePath) {
/** `vp-raw` : VitePress ne intercepte pas le clic (sinon 404 SPA sur /slides/…). */
const deck = sitePath(`/slides/${p.slug}/1`)
const tags = (p.tags || []).map((t) => `\`${t}\``).join(' · ')
const lines = [
'---',
`title: ${p.title.replace(/"/g, '\\"')}`,
'outline: deep',
'---',
'',
`# ${p.title}`,
'',
...(tags ? [`**Tags :** ${tags}`, ''] : []),
'## Résumé',
'',
p.description || '_Pas de description._',
'',
'## Accès',
'',
``,
` Voir les slides`,
` Ouvrir dans un nouvel onglet`,
`
`,
'',
'## Partager les slides',
'',
'Depuis la salle, scannez le code pour ouvrir les slides sur mobile.',
'',
``,
'',
'::: info À propos',
'Les slides sont servies en SPA statique (Slidev). Les chemins d’assets suivent le `base` défini au build.',
':::',
'',
]
return lines.filter((l) => l !== undefined).join('\n')
}
export function patchIndexFeaturedContent(indexContent, items, sitePath) {
const i0 = indexContent.indexOf(FEATURED_START)
const i1 = indexContent.indexOf(FEATURED_END)
if (i0 === -1 || i1 === -1 || i1 <= i0) {
return { ok: false, content: indexContent }
}
const before = indexContent.slice(0, i0 + FEATURED_START.length)
const after = indexContent.slice(i1)
const block = buildFeaturedHtml(items, sitePath)
const content = `${before}\n\n${block}\n\n${after}`
return { ok: true, content }
}