- 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.
159 lines
5.5 KiB
JavaScript
159 lines
5.5 KiB
JavaScript
/**
|
||
* 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, '&')
|
||
.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 '<!-- (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 }
|
||
}
|