Files
decision/web/src/lib/markdown.ts
T
YvvandClaude Opus 5.5 7a022dd35e v3 : interface Svelte — accueil, groupes, lecteur du texte en vigueur, panneau contextuel, éditeur à assistant, trustinWallet
- Accueil sobre, menu complet à gauche (Décisions, Docs de référence, Mandats,
  Protocoles), intro demandée, quatre sections, connexion Ğ1.
- Lecteur : texte en vigueur en Literata, actions contextuelles par article
  (le vote, propositions, proposer), propositions en cours sous la « surface »
  avec leur niveau d'eau, aperçus « si les bien cotées passent » / « si tout
  passe », langue du texte fr/en/es (traductions éditoriales).
- Panneau : vote pour/contre, seuil et formule (MathML), inertie et son
  changement (voté en inertie +), coquille sans vote, traduction, signalement
  de contradiction, historique des corrections.
- trustinWallet (/acces) : groupes et membres, invitations à usage unique,
  administrateurs, retours d'usage.
- Contrôles : vitest (markdown, catalogues, anti-lexique), Playwright C1–C10 + M1.
- Sections du seed portées jusqu'à l'écran.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 02:58:34 +02:00

41 lines
1.5 KiB
TypeScript

// A deliberately tiny Markdown subset for the texts of reference: paragraphs,
// line breaks, lists, **bold**, *italic*, `code`. HTML is always escaped first,
// so a proposal can never inject markup.
const ESCAPES: Record<string, string> = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };
export function escapeHtml(s: string): string {
return s.replace(/[&<>"']/g, (c) => ESCAPES[c] ?? c);
}
export function inline(s: string): string {
return escapeHtml(s)
.replace(/`([^`]+)`/g, '<code>$1</code>')
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
.replace(/(^|[^*])\*([^*\s][^*]*?)\*(?!\*)/g, '$1<em>$2</em>');
}
const BULLET = /^\s*[-*]\s+/;
const NUMBERED = /^\s*\d+[.)]\s+/;
export function render(md: string): string {
const blocks = md.replace(/\r\n?/g, '\n').trim().split(/\n\s*\n/);
return blocks
.map((block) => {
const lines = block.split('\n').filter((l) => l.trim().length > 0);
if (lines.length > 0 && lines.every((l) => BULLET.test(l))) {
return `<ul>${lines.map((l) => `<li>${inline(l.replace(BULLET, ''))}</li>`).join('')}</ul>`;
}
if (lines.length > 0 && lines.every((l) => NUMBERED.test(l))) {
return `<ol>${lines.map((l) => `<li>${inline(l.replace(NUMBERED, ''))}</li>`).join('')}</ol>`;
}
return `<p>${lines.map(inline).join('<br>')}</p>`;
})
.join('');
}
/** Plain text, for one-line excerpts. */
export function plain(md: string): string {
return md.replace(/[*`]/g, '').replace(/^\s*[-*]\s+/gm, '').replace(/\s+/g, ' ').trim();
}