@imenam/mcp-messenger 1.0.34 → 1.0.36
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.
- package/README.md +8 -2
- package/dist/src/core/access-store.d.ts +5 -0
- package/dist/src/core/access-store.js +17 -0
- package/dist/src/core/store.js +223 -223
- package/dist/src/index.js +21 -21
- package/dist/src/server/app.js +6 -3
- package/dist/src/server/ui/app.js +62 -1
- package/dist/src/server/ui/markdown.js +340 -340
- package/dist/src/server/ui/styles.css +3826 -3820
- package/package.json +64 -61
- package/dist/src/cli/generate-token.d.ts +0 -7
- package/dist/src/cli/generate-token.js +0 -12
|
@@ -1,340 +1,340 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Rendu Markdown de la console.
|
|
3
|
-
*
|
|
4
|
-
* Les agents s'écrivent en markdown : un corps de message affiché tel quel se lit mal
|
|
5
|
-
* (titres en dièses, listes en tirets, blocs de code noyés dans le texte). Ce module
|
|
6
|
-
* traduit ce markdown en DOM.
|
|
7
|
-
*
|
|
8
|
-
* Il construit des nœuds, jamais du HTML : tout texte venu d'un message passe par
|
|
9
|
-
* `textContent`, donc rien de ce qu'un agent écrit ne peut devenir une balise. Les seules
|
|
10
|
-
* URL suivies sont http, https et mailto ; le reste retombe en texte.
|
|
11
|
-
*
|
|
12
|
-
* La couverture est celle du markdown qu'écrivent les agents : titres, listes, citations,
|
|
13
|
-
* code, tableaux, filets, emphase, liens. Pas de HTML brut, pas de notes de bas de page.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
const FENCE = /^ {0,3}(`{3,}|~{3,})\s*([^`\s]*)\s*$/;
|
|
17
|
-
const HEADING = /^ {0,3}(#{1,6})\s+(.*?)\s*#*\s*$/;
|
|
18
|
-
const RULE = /^ {0,3}([-*_])[ \t]*(?:\1[ \t]*){2,}$/;
|
|
19
|
-
const QUOTE = /^ {0,3}>[ \t]?(.*)$/;
|
|
20
|
-
const BULLET = /^(\s*)([-*+])[ \t]+(.*)$/;
|
|
21
|
-
const ORDERED = /^(\s*)(\d{1,9})[.)][ \t]+(.*)$/;
|
|
22
|
-
const TABLE_RULE = /^ {0,3}\|?[ \t]*:?-+:?[ \t]*(\|[ \t]*:?-+:?[ \t]*)+\|?[ \t]*$/;
|
|
23
|
-
const SAFE_HREF = /^(https?:\/\/|mailto:)/i;
|
|
24
|
-
|
|
25
|
-
/** Rend un corps de message et renvoie un fragment prêt à être inséré. */
|
|
26
|
-
export function renderMarkdown(source) {
|
|
27
|
-
const fragment = document.createDocumentFragment();
|
|
28
|
-
const lines = String(source ?? "")
|
|
29
|
-
.replace(/\r\n?/g, "\n")
|
|
30
|
-
.split("\n");
|
|
31
|
-
for (const child of parseBlocks(lines)) fragment.appendChild(child);
|
|
32
|
-
return fragment;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// --- Blocs -----------------------------------------------------------------
|
|
36
|
-
|
|
37
|
-
function parseBlocks(lines) {
|
|
38
|
-
const out = [];
|
|
39
|
-
let i = 0;
|
|
40
|
-
|
|
41
|
-
while (i < lines.length) {
|
|
42
|
-
const line = lines[i];
|
|
43
|
-
|
|
44
|
-
if (!line.trim()) {
|
|
45
|
-
i += 1;
|
|
46
|
-
continue;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const fence = FENCE.exec(line);
|
|
50
|
-
if (fence) {
|
|
51
|
-
const closing = new RegExp(`^ {0,3}\\${fence[1][0]}{${fence[1].length},}\\s*$`);
|
|
52
|
-
const body = [];
|
|
53
|
-
i += 1;
|
|
54
|
-
while (i < lines.length && !closing.test(lines[i])) {
|
|
55
|
-
body.push(lines[i]);
|
|
56
|
-
i += 1;
|
|
57
|
-
}
|
|
58
|
-
i += 1; // la clôture, ou la fin du texte si le bloc n'a jamais été refermé
|
|
59
|
-
out.push(codeBlock(body.join("\n"), fence[2]));
|
|
60
|
-
continue;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const heading = HEADING.exec(line);
|
|
64
|
-
if (heading) {
|
|
65
|
-
const el = document.createElement(`h${heading[1].length}`);
|
|
66
|
-
el.className = "md-heading";
|
|
67
|
-
renderInline(heading[2], el);
|
|
68
|
-
out.push(el);
|
|
69
|
-
i += 1;
|
|
70
|
-
continue;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if (RULE.test(line)) {
|
|
74
|
-
const hr = document.createElement("hr");
|
|
75
|
-
hr.className = "md-rule";
|
|
76
|
-
out.push(hr);
|
|
77
|
-
i += 1;
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (QUOTE.test(line)) {
|
|
82
|
-
const inner = [];
|
|
83
|
-
while (i < lines.length && QUOTE.test(lines[i])) {
|
|
84
|
-
inner.push(QUOTE.exec(lines[i])[1]);
|
|
85
|
-
i += 1;
|
|
86
|
-
}
|
|
87
|
-
const quote = document.createElement("blockquote");
|
|
88
|
-
quote.className = "md-quote";
|
|
89
|
-
for (const child of parseBlocks(inner)) quote.appendChild(child);
|
|
90
|
-
out.push(quote);
|
|
91
|
-
continue;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (BULLET.test(line) || ORDERED.test(line)) {
|
|
95
|
-
const [list, next] = parseList(lines, i);
|
|
96
|
-
out.push(list);
|
|
97
|
-
i = next;
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (opensTable(lines, i)) {
|
|
102
|
-
const [table, next] = parseTable(lines, i);
|
|
103
|
-
if (table) {
|
|
104
|
-
out.push(table);
|
|
105
|
-
i = next;
|
|
106
|
-
continue;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Paragraphe : tout jusqu'à la ligne vide ou au premier bloc d'un autre genre.
|
|
111
|
-
const text = [];
|
|
112
|
-
while (i < lines.length && lines[i].trim() && !(text.length > 0 && startsBlock(lines, i))) {
|
|
113
|
-
text.push(lines[i].trim());
|
|
114
|
-
i += 1;
|
|
115
|
-
}
|
|
116
|
-
const paragraph = document.createElement("p");
|
|
117
|
-
paragraph.className = "md-p";
|
|
118
|
-
renderInline(text.join("\n"), paragraph);
|
|
119
|
-
out.push(paragraph);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return out;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/** Une ligne qui, au milieu d'un paragraphe, ouvre malgré tout un autre bloc. */
|
|
126
|
-
function startsBlock(lines, i) {
|
|
127
|
-
const line = lines[i];
|
|
128
|
-
return (
|
|
129
|
-
FENCE.test(line) ||
|
|
130
|
-
HEADING.test(line) ||
|
|
131
|
-
RULE.test(line) ||
|
|
132
|
-
QUOTE.test(line) ||
|
|
133
|
-
BULLET.test(line) ||
|
|
134
|
-
ORDERED.test(line) ||
|
|
135
|
-
opensTable(lines, i)
|
|
136
|
-
);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function opensTable(lines, i) {
|
|
140
|
-
return i + 1 < lines.length && lines[i].includes("|") && TABLE_RULE.test(lines[i + 1]);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function codeBlock(code, language) {
|
|
144
|
-
const pre = document.createElement("pre");
|
|
145
|
-
pre.className = "md-code";
|
|
146
|
-
const el = document.createElement("code");
|
|
147
|
-
if (language) {
|
|
148
|
-
el.className = `language-${language}`;
|
|
149
|
-
pre.dataset.language = language;
|
|
150
|
-
}
|
|
151
|
-
el.textContent = code;
|
|
152
|
-
pre.appendChild(el);
|
|
153
|
-
return pre;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Une liste et tout ce qu'elle contient. L'indentation seule décide de l'imbrication :
|
|
158
|
-
* une ligne plus rentrée que sa puce appartient à l'article ouvert, et c'est en le
|
|
159
|
-
* relisant comme un texte à part entière que la sous-liste apparaît.
|
|
160
|
-
*/
|
|
161
|
-
function parseList(lines, start) {
|
|
162
|
-
const ordered = !BULLET.test(lines[start]);
|
|
163
|
-
const first = ordered ? ORDERED.exec(lines[start]) : BULLET.exec(lines[start]);
|
|
164
|
-
const indent = first[1].length;
|
|
165
|
-
|
|
166
|
-
const list = document.createElement(ordered ? "ol" : "ul");
|
|
167
|
-
list.className = "md-list";
|
|
168
|
-
if (ordered && Number(first[2]) !== 1) list.start = Number(first[2]);
|
|
169
|
-
|
|
170
|
-
let i = start;
|
|
171
|
-
while (i < lines.length) {
|
|
172
|
-
if (!lines[i].trim()) {
|
|
173
|
-
// Une ligne vide ne ferme la liste que si plus rien derrière ne lui appartient.
|
|
174
|
-
if (!continues(lines, i, indent)) break;
|
|
175
|
-
i += 1;
|
|
176
|
-
continue;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const match = ordered ? ORDERED.exec(lines[i]) : BULLET.exec(lines[i]);
|
|
180
|
-
if (!match || match[1].length !== indent) break;
|
|
181
|
-
|
|
182
|
-
const own = [match[3]];
|
|
183
|
-
i += 1;
|
|
184
|
-
while (i < lines.length && (lines[i].trim() ? leadingSpaces(lines[i]) > indent : continues(lines, i, indent + 1))) {
|
|
185
|
-
own.push(lines[i].slice(Math.min(leadingSpaces(lines[i]), indent + 2)));
|
|
186
|
-
i += 1;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
const item = document.createElement("li");
|
|
190
|
-
item.className = "md-item";
|
|
191
|
-
const children = parseBlocks(own);
|
|
192
|
-
// Un article d'une seule ligne n'a pas besoin d'un paragraphe autour de son texte.
|
|
193
|
-
if (children.length === 1 && children[0].tagName === "P") item.append(...children[0].childNodes);
|
|
194
|
-
else for (const child of children) item.appendChild(child);
|
|
195
|
-
list.appendChild(item);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
return [list, i];
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/** Vrai si, après la ligne vide `i`, du texte reste rentré d'au moins `indent`. */
|
|
202
|
-
function continues(lines, i, indent) {
|
|
203
|
-
for (let j = i + 1; j < lines.length; j += 1) {
|
|
204
|
-
if (!lines[j].trim()) continue;
|
|
205
|
-
return leadingSpaces(lines[j]) >= indent;
|
|
206
|
-
}
|
|
207
|
-
return false;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function leadingSpaces(line) {
|
|
211
|
-
return line.length - line.replace(/^[ \t]*/, "").length;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function parseTable(lines, start) {
|
|
215
|
-
const header = splitRow(lines[start]);
|
|
216
|
-
const alignments = splitRow(lines[start + 1]).map((cell) => {
|
|
217
|
-
const left = cell.startsWith(":");
|
|
218
|
-
const right = cell.endsWith(":");
|
|
219
|
-
if (left && right) return "center";
|
|
220
|
-
if (right) return "right";
|
|
221
|
-
return left ? "left" : null;
|
|
222
|
-
});
|
|
223
|
-
if (header.length !== alignments.length) return [null, start];
|
|
224
|
-
|
|
225
|
-
const table = document.createElement("table");
|
|
226
|
-
table.className = "md-table";
|
|
227
|
-
const thead = document.createElement("thead");
|
|
228
|
-
thead.appendChild(row(header, alignments, "th"));
|
|
229
|
-
table.appendChild(thead);
|
|
230
|
-
|
|
231
|
-
const tbody = document.createElement("tbody");
|
|
232
|
-
let i = start + 2;
|
|
233
|
-
while (i < lines.length && lines[i].trim() && lines[i].includes("|")) {
|
|
234
|
-
tbody.appendChild(row(splitRow(lines[i]), alignments, "td"));
|
|
235
|
-
i += 1;
|
|
236
|
-
}
|
|
237
|
-
table.appendChild(tbody);
|
|
238
|
-
return [table, i];
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function splitRow(line) {
|
|
242
|
-
return line
|
|
243
|
-
.trim()
|
|
244
|
-
.replace(/^\|/, "")
|
|
245
|
-
.replace(/\|$/, "")
|
|
246
|
-
.split("|")
|
|
247
|
-
.map((cell) => cell.trim());
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function row(cells, alignments, tag) {
|
|
251
|
-
const tr = document.createElement("tr");
|
|
252
|
-
alignments.forEach((align, index) => {
|
|
253
|
-
const cell = document.createElement(tag);
|
|
254
|
-
if (align) cell.style.textAlign = align;
|
|
255
|
-
renderInline(cells[index] ?? "", cell);
|
|
256
|
-
tr.appendChild(cell);
|
|
257
|
-
});
|
|
258
|
-
return tr;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// --- Enrichissements de ligne ----------------------------------------------
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* Les motifs sont confrontés au texte à chaque tour, et celui qui accroche le plus tôt
|
|
265
|
-
* l'emporte. Le code passe en premier à égalité de position : son contenu ne doit plus
|
|
266
|
-
* être relu, un `**` dans un extrait de code est du code et non de la graisse.
|
|
267
|
-
*/
|
|
268
|
-
const INLINE = [
|
|
269
|
-
{ re: /(`+)([\s\S]+?)\1/, build: (m) => textNode("code", "md-inline-code", m[2].trim()) },
|
|
270
|
-
// La cible tolère une paire de parenthèses : une URL en contient parfois, et le lien
|
|
271
|
-
// ne doit pas se refermer sur la première rencontrée.
|
|
272
|
-
{ re: /!\[([^\]]*)\]\(((?:[^\s()]|\([^\s()]*\))+)(?:\s+"[^"]*")?\)/, build: (m) => image(m[1], m[2]) },
|
|
273
|
-
{ re: /\[([^\]]*)\]\(((?:[^\s()]|\([^\s()]*\))+)(?:\s+"[^"]*")?\)/, build: (m) => link(m[2], m[1]) },
|
|
274
|
-
{ re: /<((?:https?:\/\/|mailto:)[^\s>]+)>/, build: (m) => link(m[1], m[1]) },
|
|
275
|
-
{ re: /(\*\*|__)(?=\S)([\s\S]+?\S)\1/, build: (m) => inlineNode("strong", "md-strong", m[2]) },
|
|
276
|
-
{ re: /(?<![\w*])(\*|_)(?=\S)([^*_\n]+?\S)\1(?![\w*])/, build: (m) => inlineNode("em", "md-em", m[2]) },
|
|
277
|
-
{ re: /~~(?=\S)([\s\S]+?\S)~~/, build: (m) => inlineNode("del", "md-del", m[1]) },
|
|
278
|
-
{ re: /(?<![("[])https?:\/\/[^\s<>"'`]*[^\s<>"'`.,;:!?)\]]/, build: (m) => link(m[0], m[0]) },
|
|
279
|
-
];
|
|
280
|
-
|
|
281
|
-
function renderInline(text, parent) {
|
|
282
|
-
let rest = text;
|
|
283
|
-
|
|
284
|
-
while (rest) {
|
|
285
|
-
let best = null;
|
|
286
|
-
for (const pattern of INLINE) {
|
|
287
|
-
const match = pattern.re.exec(rest);
|
|
288
|
-
if (match && (!best || match.index < best.match.index)) best = { match, pattern };
|
|
289
|
-
if (best && best.match.index === 0) break;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
if (!best) break;
|
|
293
|
-
appendText(parent, rest.slice(0, best.match.index));
|
|
294
|
-
parent.appendChild(best.pattern.build(best.match));
|
|
295
|
-
rest = rest.slice(best.match.index + best.match[0].length);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
appendText(parent, rest);
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
/** Les retours à la ligne d'un paragraphe sont voulus : un agent aligne, il n'habille pas. */
|
|
302
|
-
function appendText(parent, text) {
|
|
303
|
-
if (!text) return;
|
|
304
|
-
text.split("\n").forEach((part, index) => {
|
|
305
|
-
if (index > 0) parent.appendChild(document.createElement("br"));
|
|
306
|
-
if (part) parent.appendChild(document.createTextNode(part));
|
|
307
|
-
});
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
function textNode(tag, className, text) {
|
|
311
|
-
const el = document.createElement(tag);
|
|
312
|
-
el.className = className;
|
|
313
|
-
el.textContent = text;
|
|
314
|
-
return el;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
function inlineNode(tag, className, text) {
|
|
318
|
-
const el = document.createElement(tag);
|
|
319
|
-
el.className = className;
|
|
320
|
-
renderInline(text, el);
|
|
321
|
-
return el;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
function link(href, label) {
|
|
325
|
-
if (!SAFE_HREF.test(href)) return document.createTextNode(label || href);
|
|
326
|
-
const el = document.createElement("a");
|
|
327
|
-
el.className = "md-link";
|
|
328
|
-
el.href = href;
|
|
329
|
-
el.target = "_blank";
|
|
330
|
-
el.rel = "noopener noreferrer";
|
|
331
|
-
appendText(el, label || href);
|
|
332
|
-
return el;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
/** Une image distante est montrée par son intitulé et son lien : la console ne va rien chercher dehors. */
|
|
336
|
-
function image(alt, src) {
|
|
337
|
-
const el = link(src, alt || src);
|
|
338
|
-
if (el.nodeType === 1) el.classList.add("md-image-link");
|
|
339
|
-
return el;
|
|
340
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Rendu Markdown de la console.
|
|
3
|
+
*
|
|
4
|
+
* Les agents s'écrivent en markdown : un corps de message affiché tel quel se lit mal
|
|
5
|
+
* (titres en dièses, listes en tirets, blocs de code noyés dans le texte). Ce module
|
|
6
|
+
* traduit ce markdown en DOM.
|
|
7
|
+
*
|
|
8
|
+
* Il construit des nœuds, jamais du HTML : tout texte venu d'un message passe par
|
|
9
|
+
* `textContent`, donc rien de ce qu'un agent écrit ne peut devenir une balise. Les seules
|
|
10
|
+
* URL suivies sont http, https et mailto ; le reste retombe en texte.
|
|
11
|
+
*
|
|
12
|
+
* La couverture est celle du markdown qu'écrivent les agents : titres, listes, citations,
|
|
13
|
+
* code, tableaux, filets, emphase, liens. Pas de HTML brut, pas de notes de bas de page.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const FENCE = /^ {0,3}(`{3,}|~{3,})\s*([^`\s]*)\s*$/;
|
|
17
|
+
const HEADING = /^ {0,3}(#{1,6})\s+(.*?)\s*#*\s*$/;
|
|
18
|
+
const RULE = /^ {0,3}([-*_])[ \t]*(?:\1[ \t]*){2,}$/;
|
|
19
|
+
const QUOTE = /^ {0,3}>[ \t]?(.*)$/;
|
|
20
|
+
const BULLET = /^(\s*)([-*+])[ \t]+(.*)$/;
|
|
21
|
+
const ORDERED = /^(\s*)(\d{1,9})[.)][ \t]+(.*)$/;
|
|
22
|
+
const TABLE_RULE = /^ {0,3}\|?[ \t]*:?-+:?[ \t]*(\|[ \t]*:?-+:?[ \t]*)+\|?[ \t]*$/;
|
|
23
|
+
const SAFE_HREF = /^(https?:\/\/|mailto:)/i;
|
|
24
|
+
|
|
25
|
+
/** Rend un corps de message et renvoie un fragment prêt à être inséré. */
|
|
26
|
+
export function renderMarkdown(source) {
|
|
27
|
+
const fragment = document.createDocumentFragment();
|
|
28
|
+
const lines = String(source ?? "")
|
|
29
|
+
.replace(/\r\n?/g, "\n")
|
|
30
|
+
.split("\n");
|
|
31
|
+
for (const child of parseBlocks(lines)) fragment.appendChild(child);
|
|
32
|
+
return fragment;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// --- Blocs -----------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
function parseBlocks(lines) {
|
|
38
|
+
const out = [];
|
|
39
|
+
let i = 0;
|
|
40
|
+
|
|
41
|
+
while (i < lines.length) {
|
|
42
|
+
const line = lines[i];
|
|
43
|
+
|
|
44
|
+
if (!line.trim()) {
|
|
45
|
+
i += 1;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const fence = FENCE.exec(line);
|
|
50
|
+
if (fence) {
|
|
51
|
+
const closing = new RegExp(`^ {0,3}\\${fence[1][0]}{${fence[1].length},}\\s*$`);
|
|
52
|
+
const body = [];
|
|
53
|
+
i += 1;
|
|
54
|
+
while (i < lines.length && !closing.test(lines[i])) {
|
|
55
|
+
body.push(lines[i]);
|
|
56
|
+
i += 1;
|
|
57
|
+
}
|
|
58
|
+
i += 1; // la clôture, ou la fin du texte si le bloc n'a jamais été refermé
|
|
59
|
+
out.push(codeBlock(body.join("\n"), fence[2]));
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const heading = HEADING.exec(line);
|
|
64
|
+
if (heading) {
|
|
65
|
+
const el = document.createElement(`h${heading[1].length}`);
|
|
66
|
+
el.className = "md-heading";
|
|
67
|
+
renderInline(heading[2], el);
|
|
68
|
+
out.push(el);
|
|
69
|
+
i += 1;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (RULE.test(line)) {
|
|
74
|
+
const hr = document.createElement("hr");
|
|
75
|
+
hr.className = "md-rule";
|
|
76
|
+
out.push(hr);
|
|
77
|
+
i += 1;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (QUOTE.test(line)) {
|
|
82
|
+
const inner = [];
|
|
83
|
+
while (i < lines.length && QUOTE.test(lines[i])) {
|
|
84
|
+
inner.push(QUOTE.exec(lines[i])[1]);
|
|
85
|
+
i += 1;
|
|
86
|
+
}
|
|
87
|
+
const quote = document.createElement("blockquote");
|
|
88
|
+
quote.className = "md-quote";
|
|
89
|
+
for (const child of parseBlocks(inner)) quote.appendChild(child);
|
|
90
|
+
out.push(quote);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (BULLET.test(line) || ORDERED.test(line)) {
|
|
95
|
+
const [list, next] = parseList(lines, i);
|
|
96
|
+
out.push(list);
|
|
97
|
+
i = next;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (opensTable(lines, i)) {
|
|
102
|
+
const [table, next] = parseTable(lines, i);
|
|
103
|
+
if (table) {
|
|
104
|
+
out.push(table);
|
|
105
|
+
i = next;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Paragraphe : tout jusqu'à la ligne vide ou au premier bloc d'un autre genre.
|
|
111
|
+
const text = [];
|
|
112
|
+
while (i < lines.length && lines[i].trim() && !(text.length > 0 && startsBlock(lines, i))) {
|
|
113
|
+
text.push(lines[i].trim());
|
|
114
|
+
i += 1;
|
|
115
|
+
}
|
|
116
|
+
const paragraph = document.createElement("p");
|
|
117
|
+
paragraph.className = "md-p";
|
|
118
|
+
renderInline(text.join("\n"), paragraph);
|
|
119
|
+
out.push(paragraph);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Une ligne qui, au milieu d'un paragraphe, ouvre malgré tout un autre bloc. */
|
|
126
|
+
function startsBlock(lines, i) {
|
|
127
|
+
const line = lines[i];
|
|
128
|
+
return (
|
|
129
|
+
FENCE.test(line) ||
|
|
130
|
+
HEADING.test(line) ||
|
|
131
|
+
RULE.test(line) ||
|
|
132
|
+
QUOTE.test(line) ||
|
|
133
|
+
BULLET.test(line) ||
|
|
134
|
+
ORDERED.test(line) ||
|
|
135
|
+
opensTable(lines, i)
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function opensTable(lines, i) {
|
|
140
|
+
return i + 1 < lines.length && lines[i].includes("|") && TABLE_RULE.test(lines[i + 1]);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function codeBlock(code, language) {
|
|
144
|
+
const pre = document.createElement("pre");
|
|
145
|
+
pre.className = "md-code";
|
|
146
|
+
const el = document.createElement("code");
|
|
147
|
+
if (language) {
|
|
148
|
+
el.className = `language-${language}`;
|
|
149
|
+
pre.dataset.language = language;
|
|
150
|
+
}
|
|
151
|
+
el.textContent = code;
|
|
152
|
+
pre.appendChild(el);
|
|
153
|
+
return pre;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Une liste et tout ce qu'elle contient. L'indentation seule décide de l'imbrication :
|
|
158
|
+
* une ligne plus rentrée que sa puce appartient à l'article ouvert, et c'est en le
|
|
159
|
+
* relisant comme un texte à part entière que la sous-liste apparaît.
|
|
160
|
+
*/
|
|
161
|
+
function parseList(lines, start) {
|
|
162
|
+
const ordered = !BULLET.test(lines[start]);
|
|
163
|
+
const first = ordered ? ORDERED.exec(lines[start]) : BULLET.exec(lines[start]);
|
|
164
|
+
const indent = first[1].length;
|
|
165
|
+
|
|
166
|
+
const list = document.createElement(ordered ? "ol" : "ul");
|
|
167
|
+
list.className = "md-list";
|
|
168
|
+
if (ordered && Number(first[2]) !== 1) list.start = Number(first[2]);
|
|
169
|
+
|
|
170
|
+
let i = start;
|
|
171
|
+
while (i < lines.length) {
|
|
172
|
+
if (!lines[i].trim()) {
|
|
173
|
+
// Une ligne vide ne ferme la liste que si plus rien derrière ne lui appartient.
|
|
174
|
+
if (!continues(lines, i, indent)) break;
|
|
175
|
+
i += 1;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const match = ordered ? ORDERED.exec(lines[i]) : BULLET.exec(lines[i]);
|
|
180
|
+
if (!match || match[1].length !== indent) break;
|
|
181
|
+
|
|
182
|
+
const own = [match[3]];
|
|
183
|
+
i += 1;
|
|
184
|
+
while (i < lines.length && (lines[i].trim() ? leadingSpaces(lines[i]) > indent : continues(lines, i, indent + 1))) {
|
|
185
|
+
own.push(lines[i].slice(Math.min(leadingSpaces(lines[i]), indent + 2)));
|
|
186
|
+
i += 1;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const item = document.createElement("li");
|
|
190
|
+
item.className = "md-item";
|
|
191
|
+
const children = parseBlocks(own);
|
|
192
|
+
// Un article d'une seule ligne n'a pas besoin d'un paragraphe autour de son texte.
|
|
193
|
+
if (children.length === 1 && children[0].tagName === "P") item.append(...children[0].childNodes);
|
|
194
|
+
else for (const child of children) item.appendChild(child);
|
|
195
|
+
list.appendChild(item);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return [list, i];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Vrai si, après la ligne vide `i`, du texte reste rentré d'au moins `indent`. */
|
|
202
|
+
function continues(lines, i, indent) {
|
|
203
|
+
for (let j = i + 1; j < lines.length; j += 1) {
|
|
204
|
+
if (!lines[j].trim()) continue;
|
|
205
|
+
return leadingSpaces(lines[j]) >= indent;
|
|
206
|
+
}
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function leadingSpaces(line) {
|
|
211
|
+
return line.length - line.replace(/^[ \t]*/, "").length;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function parseTable(lines, start) {
|
|
215
|
+
const header = splitRow(lines[start]);
|
|
216
|
+
const alignments = splitRow(lines[start + 1]).map((cell) => {
|
|
217
|
+
const left = cell.startsWith(":");
|
|
218
|
+
const right = cell.endsWith(":");
|
|
219
|
+
if (left && right) return "center";
|
|
220
|
+
if (right) return "right";
|
|
221
|
+
return left ? "left" : null;
|
|
222
|
+
});
|
|
223
|
+
if (header.length !== alignments.length) return [null, start];
|
|
224
|
+
|
|
225
|
+
const table = document.createElement("table");
|
|
226
|
+
table.className = "md-table";
|
|
227
|
+
const thead = document.createElement("thead");
|
|
228
|
+
thead.appendChild(row(header, alignments, "th"));
|
|
229
|
+
table.appendChild(thead);
|
|
230
|
+
|
|
231
|
+
const tbody = document.createElement("tbody");
|
|
232
|
+
let i = start + 2;
|
|
233
|
+
while (i < lines.length && lines[i].trim() && lines[i].includes("|")) {
|
|
234
|
+
tbody.appendChild(row(splitRow(lines[i]), alignments, "td"));
|
|
235
|
+
i += 1;
|
|
236
|
+
}
|
|
237
|
+
table.appendChild(tbody);
|
|
238
|
+
return [table, i];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function splitRow(line) {
|
|
242
|
+
return line
|
|
243
|
+
.trim()
|
|
244
|
+
.replace(/^\|/, "")
|
|
245
|
+
.replace(/\|$/, "")
|
|
246
|
+
.split("|")
|
|
247
|
+
.map((cell) => cell.trim());
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function row(cells, alignments, tag) {
|
|
251
|
+
const tr = document.createElement("tr");
|
|
252
|
+
alignments.forEach((align, index) => {
|
|
253
|
+
const cell = document.createElement(tag);
|
|
254
|
+
if (align) cell.style.textAlign = align;
|
|
255
|
+
renderInline(cells[index] ?? "", cell);
|
|
256
|
+
tr.appendChild(cell);
|
|
257
|
+
});
|
|
258
|
+
return tr;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// --- Enrichissements de ligne ----------------------------------------------
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Les motifs sont confrontés au texte à chaque tour, et celui qui accroche le plus tôt
|
|
265
|
+
* l'emporte. Le code passe en premier à égalité de position : son contenu ne doit plus
|
|
266
|
+
* être relu, un `**` dans un extrait de code est du code et non de la graisse.
|
|
267
|
+
*/
|
|
268
|
+
const INLINE = [
|
|
269
|
+
{ re: /(`+)([\s\S]+?)\1/, build: (m) => textNode("code", "md-inline-code", m[2].trim()) },
|
|
270
|
+
// La cible tolère une paire de parenthèses : une URL en contient parfois, et le lien
|
|
271
|
+
// ne doit pas se refermer sur la première rencontrée.
|
|
272
|
+
{ re: /!\[([^\]]*)\]\(((?:[^\s()]|\([^\s()]*\))+)(?:\s+"[^"]*")?\)/, build: (m) => image(m[1], m[2]) },
|
|
273
|
+
{ re: /\[([^\]]*)\]\(((?:[^\s()]|\([^\s()]*\))+)(?:\s+"[^"]*")?\)/, build: (m) => link(m[2], m[1]) },
|
|
274
|
+
{ re: /<((?:https?:\/\/|mailto:)[^\s>]+)>/, build: (m) => link(m[1], m[1]) },
|
|
275
|
+
{ re: /(\*\*|__)(?=\S)([\s\S]+?\S)\1/, build: (m) => inlineNode("strong", "md-strong", m[2]) },
|
|
276
|
+
{ re: /(?<![\w*])(\*|_)(?=\S)([^*_\n]+?\S)\1(?![\w*])/, build: (m) => inlineNode("em", "md-em", m[2]) },
|
|
277
|
+
{ re: /~~(?=\S)([\s\S]+?\S)~~/, build: (m) => inlineNode("del", "md-del", m[1]) },
|
|
278
|
+
{ re: /(?<![("[])https?:\/\/[^\s<>"'`]*[^\s<>"'`.,;:!?)\]]/, build: (m) => link(m[0], m[0]) },
|
|
279
|
+
];
|
|
280
|
+
|
|
281
|
+
function renderInline(text, parent) {
|
|
282
|
+
let rest = text;
|
|
283
|
+
|
|
284
|
+
while (rest) {
|
|
285
|
+
let best = null;
|
|
286
|
+
for (const pattern of INLINE) {
|
|
287
|
+
const match = pattern.re.exec(rest);
|
|
288
|
+
if (match && (!best || match.index < best.match.index)) best = { match, pattern };
|
|
289
|
+
if (best && best.match.index === 0) break;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (!best) break;
|
|
293
|
+
appendText(parent, rest.slice(0, best.match.index));
|
|
294
|
+
parent.appendChild(best.pattern.build(best.match));
|
|
295
|
+
rest = rest.slice(best.match.index + best.match[0].length);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
appendText(parent, rest);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** Les retours à la ligne d'un paragraphe sont voulus : un agent aligne, il n'habille pas. */
|
|
302
|
+
function appendText(parent, text) {
|
|
303
|
+
if (!text) return;
|
|
304
|
+
text.split("\n").forEach((part, index) => {
|
|
305
|
+
if (index > 0) parent.appendChild(document.createElement("br"));
|
|
306
|
+
if (part) parent.appendChild(document.createTextNode(part));
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function textNode(tag, className, text) {
|
|
311
|
+
const el = document.createElement(tag);
|
|
312
|
+
el.className = className;
|
|
313
|
+
el.textContent = text;
|
|
314
|
+
return el;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function inlineNode(tag, className, text) {
|
|
318
|
+
const el = document.createElement(tag);
|
|
319
|
+
el.className = className;
|
|
320
|
+
renderInline(text, el);
|
|
321
|
+
return el;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function link(href, label) {
|
|
325
|
+
if (!SAFE_HREF.test(href)) return document.createTextNode(label || href);
|
|
326
|
+
const el = document.createElement("a");
|
|
327
|
+
el.className = "md-link";
|
|
328
|
+
el.href = href;
|
|
329
|
+
el.target = "_blank";
|
|
330
|
+
el.rel = "noopener noreferrer";
|
|
331
|
+
appendText(el, label || href);
|
|
332
|
+
return el;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** Une image distante est montrée par son intitulé et son lien : la console ne va rien chercher dehors. */
|
|
336
|
+
function image(alt, src) {
|
|
337
|
+
const el = link(src, alt || src);
|
|
338
|
+
if (el.nodeType === 1) el.classList.add("md-image-link");
|
|
339
|
+
return el;
|
|
340
|
+
}
|