- 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>
41 lines
1.5 KiB
TypeScript
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> = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
|
|
|
|
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();
|
|
}
|