@kirigami/php-prepros 1.0.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.
@@ -0,0 +1,444 @@
1
+ <?php
2
+
3
+ class MD {
4
+
5
+ // ========================================================================
6
+ // SYSTÈME DE PLUGINS
7
+ //
8
+ // SYNTAXE INLINE (args sur la même ligne) :
9
+ // {% nom_plugin arg1 arg2 "arg avec espaces" %}
10
+ //
11
+ // SYNTAXE BLOC (contenu multi-ligne) :
12
+ // {% nom_plugin arg1 arg2
13
+ // ligne de contenu 1
14
+ // ligne de contenu 2
15
+ // %}
16
+ //
17
+ // Le callback reçoit toujours (array $args, string $body) :
18
+ // - $args : tableau des arguments passés sur la ligne d'ouverture
19
+ // - $body : contenu multi-ligne (vide "" pour les tags inline)
20
+ //
21
+ // Exemples :
22
+ // GithubReadmeParser::registerPlugin('codepen', function(array $args, string $body): string {
23
+ // $id = htmlspecialchars($args[0] ?? '', ENT_QUOTES, 'UTF-8');
24
+ // return "<iframe src=\"https://codepen.io/embed/{$id}\"></iframe>";
25
+ // });
26
+ //
27
+ // GithubReadmeParser::registerPlugin('checklist', function(array $args, string $body): string {
28
+ // $items = array_filter(explode("\n", trim($body)));
29
+ // $html = '<ul class="checklist">';
30
+ // foreach ($items as $item) {
31
+ // $html .= '<li><input type="checkbox" /> ' . htmlspecialchars(trim($item), ENT_QUOTES, 'UTF-8') . '</li>';
32
+ // }
33
+ // return $html . '</ul>';
34
+ // });
35
+ // ========================================================================
36
+
37
+ /** @var array<string, callable(string[]): string> */
38
+ private static array $plugins = [];
39
+
40
+ /**
41
+ * Enregistre un plugin par son nom.
42
+ *
43
+ * @param string $name Nom du tag, ex: "codepen"
44
+ * @param callable $callback function(array $args): string
45
+ * $args[0] = premier argument, $args[1] = second, etc.
46
+ */
47
+ public static function registerPlugin(string $name, callable $callback): void {
48
+ self::$plugins[strtolower(trim($name))] = $callback;
49
+ }
50
+
51
+ /**
52
+ * Supprime un plugin enregistré.
53
+ */
54
+ public static function unregisterPlugin(string $name): void {
55
+ unset(self::$plugins[strtolower(trim($name))]);
56
+ }
57
+
58
+ /**
59
+ * Retourne la liste des plugins enregistrés.
60
+ *
61
+ * @return string[]
62
+ */
63
+ public static function getRegisteredPlugins(): array {
64
+ return array_keys(self::$plugins);
65
+ }
66
+
67
+ // ========================================================================
68
+
69
+ public static function toHtml(string $markdown): string {
70
+
71
+ // ====================================================================
72
+ // ÉTAPE 1 : Normalisation des fins de ligne
73
+ // ====================================================================
74
+ $html = str_replace(["\r\n", "\r"], "\n", $markdown);
75
+
76
+
77
+ // ====================================================================
78
+ // ÉTAPE 2 : PLUGINS
79
+ // Deux formes supportées :
80
+ //
81
+ // INLINE : {% nom arg1 "arg 2" %}
82
+ // → $args = ['arg1', 'arg 2'], $body = ''
83
+ //
84
+ // BLOC : {% nom arg1\ncontenu\nsur\nplusieurs lignes\n%}
85
+ // → $args = ['arg1'], $body = "contenu\nsur\nplusieurs lignes"
86
+ //
87
+ // Les deux sont capturés par une seule regex qui distingue la présence
88
+ // d'un saut de ligne après les args (bloc) ou non (inline).
89
+ // Traités avant l'encodage XSS — réinjectés en toute dernière étape.
90
+ // ====================================================================
91
+ $pluginBlocks = [];
92
+
93
+ /**
94
+ * Parse une chaîne d'arguments en tableau.
95
+ * Supporte les mots simples, "guillemets doubles" et 'simples'.
96
+ */
97
+ $parseArgs = static function (string $rawArgs): array {
98
+ $args = [];
99
+ if (trim($rawArgs) === '') return $args;
100
+ preg_match_all(
101
+ '/"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"|\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\'|(\S+)/',
102
+ $rawArgs,
103
+ $m
104
+ );
105
+ foreach ($m[0] as $i => $_) {
106
+ $args[] = $m[1][$i] !== ''
107
+ ? stripslashes($m[1][$i])
108
+ : ($m[2][$i] !== ''
109
+ ? stripslashes($m[2][$i])
110
+ : $m[3][$i]);
111
+ }
112
+ return $args;
113
+ };
114
+
115
+ $html = preg_replace_callback(
116
+ // Groupe 1 : nom du plugin
117
+ // Groupe 2 : args inline (tout ce qui est sur la première ligne après le nom)
118
+ // Groupe 3 : corps multi-ligne (présent seulement pour les tags blocs)
119
+ '/\{%\s*([a-zA-Z0-9_-]+)([^\n%]*?)(?:\n([\s\S]*?))?\s*%\}/m',
120
+ function ($matches) use (&$pluginBlocks, $parseArgs): string {
121
+ $name = strtolower(trim($matches[1]));
122
+ $args = $parseArgs(trim($matches[2] ?? ''));
123
+ // $matches[3] existe uniquement si le tag est multi-ligne
124
+ $body = isset($matches[3]) ? trim($matches[3]) : '';
125
+
126
+ if (!isset(self::$plugins[$name])) {
127
+ // Plugin inconnu : préservé encodé plutôt que silencieusement supprimé
128
+ return htmlspecialchars($matches[0], ENT_QUOTES, 'UTF-8');
129
+ }
130
+
131
+ $output = (self::$plugins[$name])($args, $body);
132
+ $placeholder = "\x02PLG" . count($pluginBlocks) . "\x03";
133
+ $pluginBlocks[$placeholder] = $output;
134
+ return $placeholder;
135
+ },
136
+ $html
137
+ );
138
+
139
+
140
+ // ====================================================================
141
+ // ÉTAPE 3 : BLOCS DE CODE (```lang ... ```)
142
+ // ====================================================================
143
+ $codeBlocks = [];
144
+ $html = preg_replace_callback('/^```([a-zA-Z0-9_+-]*)\n([\s\S]*?)\n^```/m', function ($matches) use (&$codeBlocks) {
145
+ $lang = !empty($matches[1]) ? ' class="language-' . htmlspecialchars($matches[1], ENT_QUOTES, 'UTF-8') . '"' : '';
146
+ $code = htmlspecialchars($matches[2], ENT_QUOTES, 'UTF-8');
147
+ $placeholder = "\x02CB" . count($codeBlocks) . "\x03";
148
+ $codeBlocks[$placeholder] = "<pre><code{$lang}>{$code}</code></pre>";
149
+ return $placeholder;
150
+ }, $html);
151
+
152
+ // Code inline (`...`)
153
+ $inlineCodes = [];
154
+ $html = preg_replace_callback('/`([^`\n]+)`/', function ($matches) use (&$inlineCodes) {
155
+ $code = htmlspecialchars($matches[1], ENT_QUOTES, 'UTF-8');
156
+ $placeholder = "\x02IC" . count($inlineCodes) . "\x03";
157
+ $inlineCodes[$placeholder] = "<code>{$code}</code>";
158
+ return $placeholder;
159
+ }, $html);
160
+
161
+
162
+ // ====================================================================
163
+ // ÉTAPE 3b : ALERTES GFM ET BLOCKQUOTES
164
+ // Traités avant l'encodage XSS car le caractère > serait encodé en &gt;
165
+ // et les regex ne matcheraient plus.
166
+ // ====================================================================
167
+ $blockquotes = [];
168
+
169
+ // Alertes GFM (> [!NOTE], etc.) — plus spécifique, traité en premier
170
+ $html = preg_replace_callback(
171
+ '/^(>\s*\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\n(?:>[ \t]?[^\n]*\n?)*)/m',
172
+ function ($matches) use (&$blockquotes): string {
173
+ $type = strtolower($matches[2]);
174
+ $label = htmlspecialchars($matches[2], ENT_QUOTES, 'UTF-8');
175
+ $content = preg_replace('/^>\s?\[!(?:NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\n?/m', '', $matches[1]);
176
+ $content = preg_replace('/^>[ \t]?/m', '', $content);
177
+ $content = htmlspecialchars(trim($content), ENT_QUOTES, 'UTF-8');
178
+ $placeholder = "\x02BQ" . count($blockquotes) . "\x03";
179
+ $blockquotes[$placeholder] = "<div class=\"markdown-alert markdown-alert-{$type}\">"
180
+ . "<p class=\"markdown-alert-title\">{$label}</p>"
181
+ . "<p>{$content}</p></div>";
182
+ return $placeholder;
183
+ },
184
+ $html
185
+ );
186
+
187
+ // Blockquotes standards
188
+ $html = preg_replace_callback('/^((?:>[ \t]?[^\n]*\n?)+)/m', function ($matches) use (&$blockquotes): string {
189
+ $content = preg_replace('/^>[ \t]?/m', '', $matches[1]);
190
+ // Les deux espaces trailing sont laissés tels quels : toHtml() les gère lui-même
191
+ $inner = self::toHtml(trim($content));
192
+ $placeholder = "\x02BQ" . count($blockquotes) . "\x03";
193
+ $blockquotes[$placeholder] = "<blockquote>{$inner}</blockquote>";
194
+ return $placeholder;
195
+ }, $html);
196
+
197
+
198
+ // ====================================================================
199
+ // ÉTAPE 4 : Encodage XSS global
200
+ // ====================================================================
201
+ $html = htmlspecialchars($html, ENT_NOQUOTES, 'UTF-8');
202
+
203
+
204
+ // ====================================================================
205
+ // ÉTAPE 5 : TABLEAUX GFM
206
+ // Supporte les lignes avec ou sans pipe final (| col | ou | col)
207
+ // ====================================================================
208
+ $html = preg_replace_callback(
209
+ '/^(\|[^\n]+\|?\n)([ \t]*\|[ \t]*:?-+:?[ \t]*(?:\|[ \t]*:?-+:?[ \t]*)*\|?\n)((?:\|[^\n]+\|?\n?)+)/m',
210
+ function ($matches) {
211
+ $parseRow = function (string $line): array {
212
+ return array_values(array_filter(
213
+ array_map('trim', explode('|', trim($line, "| \t\n")))
214
+ ));
215
+ };
216
+
217
+ $headers = $parseRow($matches[1]);
218
+ $alignments = [];
219
+ $sepCells = $parseRow($matches[2]);
220
+ foreach ($sepCells as $sep) {
221
+ $left = str_starts_with(trim($sep), ':');
222
+ $right = str_ends_with(trim($sep), ':');
223
+ if ($left && $right) $alignments[] = ' style="text-align:center"';
224
+ elseif ($right) $alignments[] = ' style="text-align:right"';
225
+ elseif ($left) $alignments[] = ' style="text-align:left"';
226
+ else $alignments[] = '';
227
+ }
228
+
229
+ $out = "<table>\n <thead>\n <tr>\n";
230
+ foreach ($headers as $i => $header) {
231
+ $align = $alignments[$i] ?? '';
232
+ $out .= " <th{$align}>{$header}</th>\n";
233
+ }
234
+ $out .= " </tr>\n </thead>\n <tbody>\n";
235
+
236
+ $bodyLines = array_filter(explode("\n", trim($matches[3])));
237
+ foreach ($bodyLines as $line) {
238
+ $cells = $parseRow($line);
239
+ $out .= " <tr>\n";
240
+ foreach ($cells as $i => $cell) {
241
+ $align = $alignments[$i] ?? '';
242
+ $out .= " <td{$align}>{$cell}</td>\n";
243
+ }
244
+ $out .= " </tr>\n";
245
+ }
246
+ $out .= " </tbody>\n</table>";
247
+ return $out;
248
+ },
249
+ $html
250
+ );
251
+
252
+
253
+ // ====================================================================
254
+ // ÉTAPE 6 : (Alertes GFM et blockquotes traités à l'étape 3b)
255
+ // ====================================================================
256
+
257
+
258
+ // ====================================================================
259
+ // ÉTAPE 7 : LISTES DE TÂCHES (GFM checkboxes)
260
+ // ====================================================================
261
+ $html = preg_replace('/^[ \t]*[-*+] \[ \] (.+)$/m', '<li class="task-item"><input type="checkbox" disabled /> $1</li>', $html);
262
+ $html = preg_replace('/^[ \t]*[-*+] \[[xX]\] (.+)$/m', '<li class="task-item"><input type="checkbox" checked disabled /> $1</li>', $html);
263
+
264
+
265
+ // ====================================================================
266
+ // ÉTAPE 8 : TITRES (ATX : # à ######)
267
+ // ====================================================================
268
+ $html = preg_replace_callback('/^(#{1,6})[ \t]+(.+?)(?:[ \t]+#+)?$/m', function ($matches) {
269
+ $level = strlen($matches[1]);
270
+ $text = trim($matches[2]);
271
+ $id = strtolower(preg_replace('/[^\w\- ]/u', '', $text));
272
+ $id = preg_replace('/\s+/', '-', trim($id));
273
+ return "<h{$level} id=\"{$id}\">{$text}</h{$level}>";
274
+ }, $html);
275
+
276
+
277
+ // ====================================================================
278
+ // ÉTAPE 9 : LISTES (puces et ordonnées)
279
+ // ====================================================================
280
+ $html = preg_replace_callback(
281
+ '/^([ \t]*\d+\. .+(?:\n[ \t]*\d+\. .+)*)/m',
282
+ function ($matches) {
283
+ $items = preg_split('/\n/', trim($matches[1]));
284
+ $out = "<ol>\n";
285
+ foreach ($items as $item) {
286
+ $text = preg_replace('/^[ \t]*\d+\. /', '', $item);
287
+ $out .= " <li>{$text}</li>\n";
288
+ }
289
+ return $out . "</ol>";
290
+ },
291
+ $html
292
+ );
293
+
294
+ $html = preg_replace_callback(
295
+ '/^([ \t]*[-*+] (?!\[[ xX]\] ).+(?:\n[ \t]*[-*+] (?!\[[ xX]\] ).+)*)/m',
296
+ function ($matches) {
297
+ $items = preg_split('/\n/', trim($matches[1]));
298
+ $out = "<ul>\n";
299
+ foreach ($items as $item) {
300
+ $text = preg_replace('/^[ \t]*[-*+] /', '', $item);
301
+ $out .= " <li>{$text}</li>\n";
302
+ }
303
+ return $out . "</ul>";
304
+ },
305
+ $html
306
+ );
307
+
308
+ $html = preg_replace_callback(
309
+ '/(?:<li class="task-item">.*<\/li>\n?)+/s',
310
+ function ($matches) {
311
+ return "<ul class=\"task-list\">\n" . $matches[0] . "</ul>\n";
312
+ },
313
+ $html
314
+ );
315
+
316
+
317
+ // ====================================================================
318
+ // ÉTAPE 10 : TEXTE EN LIGNE (Gras, Italique, Barré)
319
+ // ====================================================================
320
+ $html = preg_replace('/\*\*\*(.+?)\*\*\*/s', '<strong><em>$1</em></strong>', $html);
321
+ $html = preg_replace('/___(.+?)___/s', '<strong><em>$1</em></strong>', $html);
322
+ $html = preg_replace('/\*\*(.+?)\*\*/s', '<strong>$1</strong>', $html);
323
+ $html = preg_replace('/__(.+?)__/s', '<strong>$1</strong>', $html);
324
+ $html = preg_replace('/\*(.+?)\*/s', '<em>$1</em>', $html);
325
+ // Le _ italique ne doit matcher qu'aux frontières de mots pour ne pas
326
+ // capturer les snake_case, noms de packages (@php-wasm/node), etc.
327
+ $html = preg_replace('/(?<!\w)_([^_\n]+)_(?!\w)/', '<em>$1</em>', $html);
328
+ $html = preg_replace('/~~(.+?)~~/s', '<del>$1</del>', $html);
329
+
330
+
331
+ // ====================================================================
332
+ // ÉTAPE 11 : LIENS & IMAGES
333
+ // Les liens externes (https?://) reçoivent target="_blank" + rel="noopener noreferrer".
334
+ // Les liens internes (/page, #anchor, ../truc) n'en reçoivent pas.
335
+ // ====================================================================
336
+ $html = preg_replace(
337
+ '/!\[([^\]]*)\]\(([^)\s]+)(?:\s+"([^"]*)")?\)/',
338
+ '<img src="$2" alt="$1" title="$3" loading="lazy" />',
339
+ $html
340
+ );
341
+
342
+ // Liens markdown [texte](url "titre optionnel")
343
+ $html = preg_replace_callback(
344
+ '/\[([^\]]+)\]\(([^)\s]+)(?:\s+"([^"]*)")?\)/',
345
+ function ($m): string {
346
+ $text = $m[1];
347
+ $href = $m[2];
348
+ $title = isset($m[3]) && $m[3] !== '' ? ' title="' . $m[3] . '"' : '';
349
+ $extern = preg_match('/^https?:\/\//i', $href)
350
+ ? ' target="_blank" rel="noopener noreferrer"'
351
+ : '';
352
+ return "<a href=\"{$href}\"{$title}{$extern}>{$text}</a>";
353
+ },
354
+ $html
355
+ );
356
+
357
+ // Liens automatiques <https://...>
358
+ $html = preg_replace(
359
+ '/<(https?:\/\/[^>]+)>/',
360
+ '<a href="$1" target="_blank" rel="noopener noreferrer">$1</a>',
361
+ $html
362
+ );
363
+
364
+ // URL nues https://...
365
+ $html = preg_replace(
366
+ '/(?<!["\'=>])\b(https?:\/\/[^\s<>"\')\]]+)/',
367
+ '<a href="$1" target="_blank" rel="noopener noreferrer">$1</a>',
368
+ $html
369
+ );
370
+
371
+
372
+ // ====================================================================
373
+ // ÉTAPE 12 : LIGNES SÉPARATRICES
374
+ // ====================================================================
375
+ $html = preg_replace('/^(?:[-*_][ \t]*){3,}$/m', '<hr />', $html);
376
+
377
+
378
+ // ====================================================================
379
+ // ÉTAPE 13 : PARAGRAPHES
380
+ // Stratégie : on traite ligne par ligne. Les lignes qui commencent par
381
+ // une balise block-level ou un placeholder sont laissées telles quelles.
382
+ // Les lignes de texte brut consécutives sont accumulées puis wrappées
383
+ // dans un <p> quand on rencontre une ligne block ou une ligne vide.
384
+ // ====================================================================
385
+ $blockStartTags = ['<h', '<pre', '<ul', '<ol', '<li', '<table', '<thead', '<tbody',
386
+ '<tr', '<td', '<th', '<blockquote', '<div', '<hr', '<img',
387
+ '</ul>', '</ol>', '</table>', '</blockquote>', '</div>',
388
+ "\x02CB", "\x02IC", "\x02PLG", "\x02BQ"];
389
+
390
+ $isBlockLine = static function (string $line) use ($blockStartTags): bool {
391
+ $t = ltrim($line);
392
+ if ($t === '') return false;
393
+ foreach ($blockStartTags as $tag) {
394
+ if (str_starts_with($t, $tag)) return true;
395
+ }
396
+ return false;
397
+ };
398
+
399
+ $lines = explode("\n", $html);
400
+ $output = [];
401
+ $textBuffer = [];
402
+
403
+ $flushBuffer = static function () use (&$textBuffer, &$output): void {
404
+ if (empty($textBuffer)) return;
405
+ $content = implode("\n", $textBuffer);
406
+ if (trim($content) !== '') {
407
+ // Deux espaces en fin de ligne → <br> (convention markdown standard)
408
+ $content = preg_replace('/ $/m', '<br>', $content);
409
+ // Saut de ligne simple → espace (comportement GitHub)
410
+ // Sauf si déjà converti en <br> ci-dessus
411
+ $content = preg_replace('/(?<!r>)\n/', ' ', $content);
412
+ $output[] = '<p>' . trim($content) . '</p>';
413
+ }
414
+ $textBuffer = [];
415
+ };
416
+
417
+ foreach ($lines as $line) {
418
+ if ($isBlockLine($line)) {
419
+ $flushBuffer();
420
+ $output[] = $line;
421
+ } elseif (trim($line) === '') {
422
+ // Ligne vide = séparateur de paragraphe
423
+ $flushBuffer();
424
+ } else {
425
+ $textBuffer[] = $line;
426
+ }
427
+ }
428
+ $flushBuffer();
429
+
430
+ $html = implode("\n", $output);
431
+
432
+
433
+ // ====================================================================
434
+ // ÉTAPE 14 : Réinjecter les placeholders
435
+ // ====================================================================
436
+ $html = strtr($html, $pluginBlocks);
437
+ $html = strtr($html, $blockquotes);
438
+ $html = strtr($html, $codeBlocks);
439
+ $html = strtr($html, $inlineCodes);
440
+
441
+ return $html;
442
+ }
443
+ }
444
+
@@ -0,0 +1,86 @@
1
+ <?php
2
+
3
+
4
+
5
+ // =============================================================================
6
+ // PLUGINS FOURNIS PAR DÉFAUT (optionnels, décommentez ce dont vous avez besoin)
7
+ // =============================================================================
8
+
9
+ // --- CodePen (inline) ---
10
+ // {% codepen PENid %}
11
+ // {% codepen PENid user height %}
12
+ MD::registerPlugin('codepen', function (array $args, string $body): string {
13
+ $id = htmlspecialchars($args[0] ?? '', ENT_QUOTES, 'UTF-8');
14
+ $user = htmlspecialchars($args[1] ?? 'anonymous', ENT_QUOTES, 'UTF-8');
15
+ $height = intval($args[2] ?? 400);
16
+ if ($id === '') return '<!-- codepen: id manquant -->';
17
+ return "<iframe height=\"{$height}\" style=\"width:100%\" "
18
+ . "scrolling=\"no\" "
19
+ . "src=\"https://codepen.io/{$user}/embed/{$id}?default-tab=result\" "
20
+ . "frameborder=\"no\" loading=\"lazy\" allowfullscreen>"
21
+ . "</iframe>";
22
+ });
23
+
24
+ // --- YouTube (inline) ---
25
+ // {% youtube VIDEO_ID %}
26
+ // {% youtube VIDEO_ID 560 315 %}
27
+ MD::registerPlugin('youtube', function (array $args, string $body): string {
28
+ $id = htmlspecialchars($args[0] ?? '', ENT_QUOTES, 'UTF-8');
29
+ $width = intval($args[1] ?? 560);
30
+ $height = intval($args[2] ?? 315);
31
+ if ($id === '') return '<!-- youtube: id manquant -->';
32
+ return "<iframe width=\"{$width}\" height=\"{$height}\" "
33
+ . "src=\"https://www.youtube.com/embed/{$id}\" "
34
+ . "title=\"YouTube video player\" frameborder=\"0\" loading=\"lazy\" "
35
+ . "allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\" "
36
+ . "allowfullscreen></iframe>";
37
+ });
38
+
39
+ // --- Checklist (bloc multi-ligne) ---
40
+ // {% checklist
41
+ // Faire la vaisselle
42
+ // Promener le chien
43
+ // Lire un livre
44
+ // %}
45
+ //
46
+ // Optionnellement avec un titre :
47
+ // {% checklist "Mes tâches du jour"
48
+ // Tâche 1
49
+ // Tâche 2
50
+ // %}
51
+ MD::registerPlugin('checklist', function (array $args, string $body): string {
52
+ $title = !empty($args[0]) ? '<p class="checklist-title"><strong>' . htmlspecialchars($args[0], ENT_QUOTES, 'UTF-8') . '</strong></p>' : '';
53
+ $items = array_filter(array_map('trim', explode("\n", $body)));
54
+ if (empty($items)) return '<!-- checklist: aucun élément -->';
55
+ $html = "<div class=\"checklist\">{$title}<ul>\n";
56
+ foreach ($items as $item) {
57
+ $html .= ' <li><label><input type="checkbox" /> ' . htmlspecialchars($item, ENT_QUOTES, 'UTF-8') . '</label></li>' . "\n";
58
+ }
59
+ return $html . "</ul></div>";
60
+ });
61
+
62
+ // --- Avertissement personnalisé (inline ou bloc) ---
63
+ // Inline : {% callout warning "Attention !" Contenu sur une ligne %}
64
+ // Bloc : {% callout danger "Titre"
65
+ // Paragraphe 1
66
+ //
67
+ // Paragraphe 2
68
+ // %}
69
+ MD::registerPlugin('callout', function (array $args, string $body): string {
70
+ $validTypes = ['info', 'success', 'warning', 'danger'];
71
+ $type = in_array($args[0] ?? '', $validTypes, true) ? $args[0] : 'info';
72
+ $title = !empty($args[1]) ? htmlspecialchars($args[1], ENT_QUOTES, 'UTF-8') : '';
73
+
74
+ // Contenu : $body si tag bloc, sinon les args restants sur la ligne d'ouverture
75
+ if ($body !== '') {
76
+ $content = htmlspecialchars($body, ENT_QUOTES, 'UTF-8');
77
+ // Remplace les doubles sauts de ligne par des <br><br> pour préserver les paragraphes
78
+ $content = str_replace("\n\n", '<br><br>', $content);
79
+ $content = str_replace("\n", '<br>', $content);
80
+ } else {
81
+ $content = htmlspecialchars(implode(' ', array_slice($args, $title ? 2 : 1)), ENT_QUOTES, 'UTF-8');
82
+ }
83
+
84
+ $titleHtml = $title ? "<strong>{$title}</strong><br>" : '';
85
+ return "<div class=\"callout callout-{$type}\">{$titleHtml}{$content}</div>";
86
+ });
@@ -0,0 +1,23 @@
1
+ <?php
2
+
3
+ class OBF {
4
+
5
+
6
+ public static function encode(mixed $obj): string {
7
+ $json = json_encode($obj);
8
+ $base64 = base64_encode($json);
9
+ $rot13 = str_rot13($base64);
10
+ $deflate = gzencode($rot13);
11
+ return substr($deflate, 2);
12
+ }
13
+
14
+
15
+ public static function decode(string $str): mixed {
16
+ $inflate = gzdecode("\x1f\x8b" . $str);
17
+ $base64 = str_rot13($inflate);
18
+ $json = base64_decode($base64);
19
+ $data = json_decode($json);
20
+ return $data;
21
+ }
22
+
23
+ }