Files
Talks/tools/presentation-contracts.mjs
T
Le Prévost-Corvellec Arnault 354c8cd9ef 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.
2026-04-11 15:59:20 +02:00

159 lines
5.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 }
}