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:
@@ -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, '&')
|
||||
.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 }
|
||||
}
|
||||
Reference in New Issue
Block a user