@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.
- package/LICENSE +21 -0
- package/README.md +622 -0
- package/index.d.ts +109 -0
- package/index.js +1 -0
- package/package.json +58 -0
- package/src/libraries/cache.class.php +101 -0
- package/src/libraries/fs.class.php +133 -0
- package/src/libraries/html.class.php +176 -0
- package/src/libraries/img.class.php +122 -0
- package/src/libraries/md.class.php +444 -0
- package/src/libraries/md.plugins.php +86 -0
- package/src/libraries/obf.class.php +23 -0
- package/src/libraries/prepros.class.php +157 -0
- package/src/libraries/prepros.plugins.php +28 -0
- package/src/libraries/std.class.php +28 -0
- package/src/libraries/str.class.php +47 -0
- package/src/libraries/yaml.class.php +453 -0
- package/src/prepros.js +107 -0
- package/src/prepros.php +45 -0
- package/src/utils/isbinary.js +79 -0
- package/src/utils/joinwith.js +12 -0
- package/src/utils.inc.php +29 -0
|
@@ -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 >
|
|
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
|
+
}
|