@mostajs/markdown-html 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +35 -0
  2. package/llms.txt +16 -0
  3. package/package.json +29 -0
  4. package/src/index.js +162 -0
package/README.md ADDED
@@ -0,0 +1,35 @@
1
+ # @mostajs/markdown-html
2
+
3
+ **Auteur** : Dr Hamid MADANI <drmdh@msn.com> · AGPL-3.0-or-later · **zéro dépendance** · ESM, sans build
4
+
5
+ Rendu **Markdown → HTML** pour servir un `README.md` comme page d'aide dans une app.
6
+
7
+ ```js
8
+ import { markdownPage, mdToHtml, MD_CSS } from '@mostajs/markdown-html';
9
+
10
+ const html = markdownPage(await readFile('README.md', 'utf8'), { title: 'Aide' });
11
+ ```
12
+
13
+ ## Périmètre (assumé)
14
+
15
+ Titres (+ ancres, table des matières), paragraphes, **gras**/*italique*/`code`/~~barré~~, listes à puces, numérotées et **cases à cocher**, blocs de code, **tableaux** (avec défilement horizontal), citations, règles, liens. Pas un moteur CommonMark complet : un rendu de doc interne, pas un CMS.
16
+
17
+ ## Sécurité
18
+
19
+ - **Le HTML d'un `.md` n'est jamais interprété** : tout est échappé. Un `<script>` collé dans un document **s'affiche**, il ne s'exécute pas. Un README vient d'un dépôt, d'un import ou d'un collègue — on ne lui fait pas confiance.
20
+ - **Un lien `javascript:` n'est pas rendu cliquable** (seuls `http(s)`, `mailto:` et les chemins relatifs le sont).
21
+
22
+ ## Thème
23
+
24
+ `MD_CSS` s'appuie sur les **tokens** de l'app s'ils existent (`var(--brand)`, `var(--text)`, `var(--focus-ring)`, `var(--radius-lg)`…), avec des replis sobres sinon. Propriétés **logiques** (`padding-inline-start`, `border-inline-start`) : compatible **RTL**.
25
+
26
+ ## API
27
+
28
+ | Export | Rôle |
29
+ |---|---|
30
+ | `mdToHtml(md)` | `{ html, toc }` — le rendu + la table des matières (titres de niveau 2) |
31
+ | `markdownPage(md, { title, head, toc, lang, dir })` | document HTML complet |
32
+ | `inline(text)` | fragments en ligne seuls |
33
+ | `MD_CSS` · `esc(s)` | feuille de style · échappement |
34
+
35
+ **14 tests verts** (`@mostajs/mjs-unit`) : `npm test`.
package/llms.txt ADDED
@@ -0,0 +1,16 @@
1
+ # @mostajs/markdown-html (0.1.0) — Markdown → HTML, zéro dépendance, sans build
2
+
3
+ QUOI : rendre un .md en HTML (README servi comme page d'aide dans une app).
4
+ API : mdToHtml(md)→{html,toc} · markdownPage(md,{title,head,toc,lang,dir})→document · inline(text) · MD_CSS · esc(s).
5
+ COUVRE : titres+ancres+TOC(h2), listes (puces/numérotées/cases à cocher), code (inline + blocs ```lang),
6
+ tableaux (+wrapper scrollable), citations, règles, liens, gras/italique/barré, paragraphes recollés.
7
+ PAS un CommonMark complet — périmètre assumé : doc interne.
8
+
9
+ SÉCURITÉ (testée) : tout est ÉCHAPPÉ — le HTML d'un .md n'est PAS interprété (<script> s'affiche, ne s'exécute pas) ;
10
+ les liens javascript: ne sont PAS rendus cliquables (seuls http(s)/mailto/relatifs le sont).
11
+
12
+ THÈME : MD_CSS consomme les tokens (var(--brand), var(--text), var(--focus-ring), var(--radius-lg)…) avec replis.
13
+ Propriétés logiques → compatible RTL.
14
+
15
+ CONSOMMATEUR : Palette Studio (palette.amia.fr/aide sert son propre README).
16
+ TESTS : 14 verts (mjs-unit).
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@mostajs/markdown-html",
3
+ "version": "0.1.0",
4
+ "description": "Rendu Markdown → HTML zéro dépendance, sans build : titres, listes, tableaux, code, citations, liens, cases à cocher + table des matières. Tout est échappé (le HTML d'un .md n'est PAS interprété). Pour servir un README comme page d'aide.",
5
+ "author": "Dr Hamid MADANI <drmdh@msn.com>",
6
+ "license": "AGPL-3.0-or-later",
7
+ "type": "module",
8
+ "main": "src/index.js",
9
+ "exports": {
10
+ ".": "./src/index.js"
11
+ },
12
+ "files": [
13
+ "src",
14
+ "README.md",
15
+ "llms.txt"
16
+ ],
17
+ "keywords": [
18
+ "markdown",
19
+ "html",
20
+ "no-build",
21
+ "zero-dependency",
22
+ "readme",
23
+ "docs",
24
+ "mostajs"
25
+ ],
26
+ "scripts": {
27
+ "test": "node test-scripts/unit/markdown-html.test.mjs"
28
+ }
29
+ }
package/src/index.js ADDED
@@ -0,0 +1,162 @@
1
+ // @mostajs/markdown-html — rendu Markdown → HTML, zéro dépendance, sans build.
2
+ // Author: Dr Hamid MADANI <drmdh@msn.com>
3
+ //
4
+ // Périmètre ASSUMÉ : le Markdown des README et docs de l'écosystème (titres, listes, tableaux,
5
+ // code, citations, liens, emphase, règles, cases à cocher). Ce n'est pas un moteur CommonMark
6
+ // complet — et ça n'a pas à l'être : un rendu de doc interne, pas un CMS.
7
+ //
8
+ // SÉCURITÉ : tout est ÉCHAPPÉ avant d'être rendu. Le HTML brut d'un document Markdown n'est PAS
9
+ // interprété — un `<script>` collé dans un .md s'affiche, il ne s'exécute pas. Ces documents
10
+ // peuvent venir d'un dépôt, d'un collègue ou d'un import : on ne leur fait pas confiance.
11
+
12
+ export const esc = (s = '') => String(s).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
13
+
14
+ /** Fragments en ligne : `code`, **gras**, *italique*, [lien](url), ~~barré~~. */
15
+ export function inline(text = '') {
16
+ let out = esc(text);
17
+ out = out.replace(/`([^`]+)`/g, (_, c) => `<code>${c}</code>`);
18
+ out = out.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
19
+ out = out.replace(/(^|[^*])\*([^*\n]+)\*/g, '$1<em>$2</em>');
20
+ out = out.replace(/~~([^~]+)~~/g, '<del>$1</del>');
21
+ // Liens : seuls http(s), mailto et les chemins relatifs sont rendus cliquables (pas de javascript:)
22
+ out = out.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (m, label, href) =>
23
+ (/^(https?:\/\/|mailto:|[./#])/i.test(href) ? `<a href="${href}">${label}</a>` : label));
24
+ return out;
25
+ }
26
+
27
+ const slug = (s) => s.toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '')
28
+ .replace(/[^a-z0-9\s-]/g, '').trim().replace(/\s+/g, '-');
29
+
30
+ /** Markdown → HTML. Retourne { html, toc } (la table des matières : les titres de niveau 2). */
31
+ export function mdToHtml(md = '') {
32
+ const lines = String(md).replace(/\r\n/g, '\n').split('\n');
33
+ const out = []; const toc = [];
34
+ let i = 0;
35
+ const closeList = (state) => { if (state.ul) { out.push('</ul>'); state.ul = false; } if (state.ol) { out.push('</ol>'); state.ol = false; } };
36
+ const state = { ul: false, ol: false };
37
+
38
+ while (i < lines.length) {
39
+ const line = lines[i];
40
+
41
+ // Bloc de code ```lang
42
+ if (/^\s*```/.test(line)) {
43
+ closeList(state);
44
+ const lang = line.replace(/^\s*```/, '').trim();
45
+ const buf = [];
46
+ i++;
47
+ while (i < lines.length && !/^\s*```/.test(lines[i])) buf.push(lines[i++]);
48
+ i++; // ferme la clôture
49
+ out.push(`<pre class="md-code"${lang ? ` data-lang="${esc(lang)}"` : ''}><code>${esc(buf.join('\n'))}</code></pre>`);
50
+ continue;
51
+ }
52
+
53
+ // Tableau | a | b |
54
+ if (/^\s*\|/.test(line) && /^\s*\|?[\s:-]+\|/.test(lines[i + 1] || '')) {
55
+ closeList(state);
56
+ const cells = (l) => l.trim().replace(/^\||\|$/g, '').split('|').map((c) => c.trim());
57
+ const head = cells(line);
58
+ i += 2;
59
+ const rows = [];
60
+ while (i < lines.length && /^\s*\|/.test(lines[i])) rows.push(cells(lines[i++]));
61
+ out.push(`<div class="md-tablewrap"><table class="md-table"><thead><tr>${head.map((h) => `<th>${inline(h)}</th>`).join('')}</tr></thead>`
62
+ + `<tbody>${rows.map((r) => `<tr>${r.map((c) => `<td>${inline(c)}</td>`).join('')}</tr>`).join('')}</tbody></table></div>`);
63
+ continue;
64
+ }
65
+
66
+ // Titres
67
+ const h = line.match(/^(#{1,6})\s+(.*)$/);
68
+ if (h) {
69
+ closeList(state);
70
+ const lvl = h[1].length; const txt = h[2].replace(/\s*#+\s*$/, '');
71
+ const id = slug(txt);
72
+ if (lvl === 2) toc.push({ id, text: txt.replace(/[*`]/g, '') });
73
+ out.push(`<h${lvl} id="${id}">${inline(txt)}</h${lvl}>`);
74
+ i++; continue;
75
+ }
76
+
77
+ // Règle horizontale
78
+ if (/^\s*(-{3,}|\*{3,}|_{3,})\s*$/.test(line)) { closeList(state); out.push('<hr>'); i++; continue; }
79
+
80
+ // Citation
81
+ if (/^\s*>\s?/.test(line)) {
82
+ closeList(state);
83
+ const buf = [];
84
+ while (i < lines.length && /^\s*>\s?/.test(lines[i])) buf.push(lines[i++].replace(/^\s*>\s?/, ''));
85
+ out.push(`<blockquote>${inline(buf.join(' '))}</blockquote>`);
86
+ continue;
87
+ }
88
+
89
+ // Listes (à puces, numérotées, cases à cocher)
90
+ const ul = line.match(/^\s*[-*+]\s+(.*)$/);
91
+ const ol = line.match(/^\s*\d+[.)]\s+(.*)$/);
92
+ if (ul || ol) {
93
+ if (ul && !state.ul) { closeList(state); out.push('<ul>'); state.ul = true; }
94
+ if (ol && !state.ol) { closeList(state); out.push('<ol>'); state.ol = true; }
95
+ let item = (ul || ol)[1];
96
+ const box = item.match(/^\[([ xX])\]\s+(.*)$/);
97
+ if (box) item = `<input type="checkbox" disabled${/[xX]/.test(box[1]) ? ' checked' : ''}> ${box[2]}`;
98
+ out.push(`<li>${box ? item.replace(/^(<input[^>]*>)\s*/, '$1 ') && item.replace(/^(<input[^>]*> )(.*)$/, (m, a, b) => a + inline(b)) : inline(item)}</li>`);
99
+ i++; continue;
100
+ }
101
+
102
+ // Ligne vide → ferme les listes
103
+ if (!line.trim()) { closeList(state); i++; continue; }
104
+
105
+ // Paragraphe (les lignes suivantes sont recollées)
106
+ closeList(state);
107
+ const buf = [line];
108
+ i++;
109
+ while (i < lines.length && lines[i].trim() && !/^\s*([-*+]\s|\d+[.)]\s|#{1,6}\s|>|\||```)/.test(lines[i])) buf.push(lines[i++]);
110
+ out.push(`<p>${inline(buf.join(' '))}</p>`);
111
+ }
112
+ closeList(state);
113
+ return { html: out.join('\n'), toc };
114
+ }
115
+
116
+ /** Feuille de style du rendu — s'appuie sur les tokens s'ils existent, sinon des replis sobres. */
117
+ export const MD_CSS = `
118
+ .md{max-width:80ch;margin:0 auto;padding:24px;color:var(--text,#0F172A);
119
+ font-family:var(--font-family,system-ui,sans-serif);line-height:1.65}
120
+ .md h1{font-size:var(--font-h1-size,32px);line-height:1.2;margin:0 0 8px}
121
+ .md h2{font-size:var(--font-h2-size,24px);margin:34px 0 10px;padding-bottom:6px;border-bottom:1px solid var(--line,#e2e8f0)}
122
+ .md h3{font-size:var(--font-h3-size,20px);margin:24px 0 8px}
123
+ .md h4{font-size:var(--font-bodyLg-size,16px);margin:18px 0 6px}
124
+ .md p{margin:0 0 12px}
125
+ .md a{color:var(--link,#2563EB)}
126
+ .md a:focus-visible,.md summary:focus-visible{outline:2px solid var(--focus-ring,#bc8b29);outline-offset:2px}
127
+ .md code{background:var(--surface-alt,#f1f5f9);border:1px solid var(--line,#e2e8f0);border-radius:5px;
128
+ padding:1px 5px;font-family:ui-monospace,monospace;font-size:.9em}
129
+ .md pre.md-code{background:var(--brand,#0F172A);color:#E2E8F0;border-radius:var(--radius-lg,12px);
130
+ padding:14px 16px;overflow-x:auto;margin:0 0 14px}
131
+ .md pre.md-code code{background:none;border:0;color:inherit;padding:0;font-size:13px;line-height:1.6}
132
+ .md blockquote{margin:0 0 14px;padding:10px 14px;border-inline-start:3px solid var(--accent,#C9952C);
133
+ background:var(--surface-alt,#f8fafc);border-radius:0 var(--radius-md,8px) var(--radius-md,8px) 0}
134
+ .md blockquote p{margin:0}
135
+ .md ul,.md ol{margin:0 0 12px;padding-inline-start:22px}
136
+ .md li{margin:4px 0}
137
+ .md hr{border:0;border-top:1px solid var(--line,#e2e8f0);margin:28px 0}
138
+ .md .md-tablewrap{overflow-x:auto;margin:0 0 16px}
139
+ .md table.md-table{width:100%;border-collapse:collapse;font-size:14px;background:var(--surface,#fff);
140
+ border:1px solid var(--line,#e2e8f0);border-radius:var(--radius-md,8px)}
141
+ .md table.md-table th{text-align:start;padding:8px 10px;background:var(--surface-alt,#f8fafc);
142
+ border-bottom:1px solid var(--line,#e2e8f0);font-weight:600;color:var(--text-muted,#64748b);font-size:12px}
143
+ .md table.md-table td{padding:8px 10px;border-bottom:1px solid var(--line,#e2e8f0);vertical-align:top}
144
+ .md .md-toc{background:var(--surface-alt,#f8fafc);border:1px solid var(--line,#e2e8f0);
145
+ border-radius:var(--radius-lg,12px);padding:12px 16px;margin:0 0 24px}
146
+ .md .md-toc ol{margin:0;padding-inline-start:20px}
147
+ .md .md-toc a{text-decoration:none}
148
+ .md .md-toc a:hover{text-decoration:underline}
149
+ `;
150
+
151
+ /** Document complet : le Markdown rendu, avec sa table des matières. */
152
+ export function markdownPage(md = '', { title = 'Aide', head = '', toc = true, lang = 'fr', dir = 'ltr' } = {}) {
153
+ const r = mdToHtml(md);
154
+ const nav = toc && r.toc.length
155
+ ? `<nav class="md-toc"><ol>${r.toc.map((t) => `<li><a href="#${t.id}">${esc(t.text)}</a></li>`).join('')}</ol></nav>`
156
+ : '';
157
+ return `<!doctype html><html lang="${esc(lang)}" dir="${esc(dir)}"><head><meta charset="utf-8">`
158
+ + `<meta name="viewport" content="width=device-width,initial-scale=1"><title>${esc(title)}</title>`
159
+ + `${head}<style>${MD_CSS}</style></head><body><article class="md">${nav}${r.html}</article></body></html>`;
160
+ }
161
+
162
+ export const moduleInfo = { name: '@mostajs/markdown-html', version: '0.1.0' };