Add Makefile, package.json, and initial configuration for VitePress and Slidev integration

- Introduced a Makefile for streamlined commands related to building, running, and managing presentations.
- Created package.json to define project metadata, dependencies, and scripts for development and testing.
- Added .gitignore entries to exclude build artifacts and temporary files from version control.
- Included playwright.config.mjs for end-to-end testing setup with Playwright.
- Added README.md for project documentation, outlining setup instructions and development guidelines.
This commit is contained in:
Le Prévost-Corvellec Arnault
2026-04-11 15:59:20 +02:00
parent 10f7798afb
commit 354c8cd9ef
70 changed files with 16388 additions and 2469 deletions
+158
View File
@@ -0,0 +1,158 @@
/**
* 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 = '<!-- FEATURED_START -->'
export const FEATURED_END = '<!-- FEATURED_END -->'
export function escMd(s) {
if (typeof s !== 'string') return ''
return s.replace(/\|/g, '\\|')
}
export function escHtml(s) {
return String(s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
}
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 '<!-- (aucune présentation) -->'
const lines = [
'<section class="talks-section talks-section--home" aria-labelledby="featured-heading">',
' <h2 id="featured-heading" class="section-title">Présentations</h2>',
' <div class="talks-grid">',
]
for (const p of featured) {
const href = sitePath(`/presentations/${p.slug}/`)
const meta = `Présentation · ${p.slug}`
lines.push(` <a class="talk-card" href="${escHtml(href)}">`)
lines.push(` <h2>${escHtml(p.title)}</h2>`)
lines.push(` <p>${escHtml(p.description || '')}</p>`)
lines.push(` <span class="meta">${escHtml(meta)}</span>`)
lines.push(' </a>')
}
lines.push(' </div>', '</section>')
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.',
'',
'<div class="talks-grid">',
]
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(' <article class="talk-card talk-card--list">')
lines.push(` <h2><a href="${detail}">${escMd(p.title)}</a></h2>`)
lines.push(` <p>${escMd(p.description || '')}</p>`)
if (tags) lines.push(` <p class="meta">${tags}</p>`)
lines.push(' <div class="talk-card__actions">')
lines.push(` <a class="vp-button" href="${detail}">Fiche</a>`)
lines.push(
` <a class="vp-button vp-button--alt vp-raw" href="${deck}" rel="noopener">Slides</a>`,
)
lines.push(' </div>')
lines.push(' </article>')
}
lines.push('</div>', '')
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',
'',
`<p class="pres-actions">`,
` <a class="vp-button vp-raw" href="${deck}">Voir les slides</a>`,
` <a class="vp-button vp-button--alt vp-raw" href="${deck}" target="_blank" rel="noopener noreferrer">Ouvrir dans un nouvel onglet</a>`,
`</p>`,
'',
'## Partager les slides',
'',
'Depuis la salle, scannez le code pour ouvrir les slides sur mobile.',
'',
`<QrEncode path="${deck}" />`,
'',
'::: 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 }
}