@kirigami/php-prepros 1.0.3 → 1.0.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kirigami/php-prepros",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "PHP preprocessor for the Kirigami static site generator. Compile PHP page templates to clean, deployable HTML — with zero server dependency.",
5
5
  "keywords": [
6
6
  "kirigami",
@@ -44,8 +44,8 @@
44
44
  "test": "echo \"Error: no test specified\" && exit 1"
45
45
  },
46
46
  "dependencies": {
47
- "@kirigami/php-wasm": "8.5.9",
48
- "@kirigami/struct-walker": "1.0.2"
47
+ "@kirigami/php-wasm": "8.5.9-1",
48
+ "@kirigami/struct-walker": "1.0.3"
49
49
  },
50
50
  "repository": {
51
51
  "type": "git",
@@ -4,20 +4,18 @@
4
4
  class FS
5
5
  {
6
6
 
7
-
8
- public static function dig(string $path): iterable
7
+ public static function dig(string $path, bool $reverse = false): \Iterator
9
8
  {
10
9
  $patt = pathinfo($path, PATHINFO_BASENAME);
11
- $path = pathinfo($path, PATHINFO_DIRNAME);
12
- if ($path = realpath($path)) {
13
- $path .= '/';
14
- foreach (glob($path . $patt) as $file) {
15
- if (!is_dir($file)) yield $file;
16
- }
17
- foreach (glob($path . '*', GLOB_ONLYDIR) as $dir) {
18
- foreach (call_user_func(__METHOD__, $dir . '/' . $patt) as $file) yield $file;
19
- }
20
- }
10
+ $dir = pathinfo($path, PATHINFO_DIRNAME);
11
+
12
+ if (!($dir = realpath($dir))) return new \EmptyIterator();
13
+ $rdi = new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::UNIX_PATHS);
14
+ $filtered = new \RecursiveCallbackFilterIterator($rdi, fn(\SplFileInfo $current) => $current->isDir() || fnmatch($patt, $current->getFilename()));
15
+ $iterator = new \RecursiveIteratorIterator($filtered);
16
+ if (!$reverse) return $iterator;
17
+
18
+ return new \ArrayIterator(array_reverse(iterator_to_array($iterator, false)));
21
19
  }
22
20
 
23
21
 
@@ -107,6 +107,174 @@ class MD {
107
107
  return $out . "</{$type}>";
108
108
  }
109
109
 
110
+ // ========================================================================
111
+ // ÉMOJIS (syntaxe étendue) : :shortcode: → caractère unicode.
112
+ // Table non exhaustive mais couvrant les raccourcis les plus courants ;
113
+ // extensible via registerEmoji().
114
+ // ========================================================================
115
+ /** @var array<string, string> */
116
+ private static array $extraEmoji = [];
117
+
118
+ private static array $emojiMap = [
119
+ 'smile' => '😄', 'smiley' => '😃', 'grin' => '😁', 'joy' => '😂', 'rofl' => '🤣',
120
+ 'blush' => '😊', 'wink' => '😉', 'relaxed' => '☺️', 'slight_smile' => '🙂',
121
+ 'upside_down_face' => '🙃', 'innocent' => '😇', 'heart_eyes' => '😍', 'kissing_heart' => '😘',
122
+ 'thinking' => '🤔', 'neutral_face' => '😐', 'expressionless' => '😑', 'no_mouth' => '😶',
123
+ 'roll_eyes' => '🙄', 'smirk' => '😏', 'unamused' => '😒', 'grimacing' => '😬',
124
+ 'lying_face' => '🤥', 'relieved' => '😌', 'pensive' => '😔', 'sleepy' => '😪',
125
+ 'drooling_face' => '🤤', 'sleeping' => '😴', 'mask' => '😷', 'sunglasses' => '😎',
126
+ 'star_struck' => '🤩', 'partying_face' => '🥳', 'worried' => '😟', 'frowning' => '☹️',
127
+ 'confused' => '😕', 'slightly_frowning_face' => '🙁', 'cry' => '😢', 'sob' => '😭',
128
+ 'scream' => '😱', 'confounded' => '😖', 'persevere' => '😣', 'disappointed' => '😞',
129
+ 'sweat' => '😓', 'weary' => '😩', 'tired_face' => '😫', 'yawning_face' => '🥱',
130
+ 'triumph' => '😤', 'rage' => '😡', 'angry' => '😠', 'cursing_face' => '🤬',
131
+ 'exploding_head' => '🤯', 'flushed' => '😳', 'hot_face' => '🥵', 'cold_face' => '🥶',
132
+ 'scream_cat' => '🙀', 'nerd_face' => '🤓', 'monocle_face' => '🧐', 'zany_face' => '🤪',
133
+ 'raised_eyebrow' => '🤨', 'shushing_face' => '🤫', 'zipper_mouth_face' => '🤐',
134
+ 'heart' => '❤️', 'orange_heart' => '🧡', 'yellow_heart' => '💛', 'green_heart' => '💚',
135
+ 'blue_heart' => '💙', 'purple_heart' => '💜', 'black_heart' => '🖤', 'white_heart' => '🤍',
136
+ 'broken_heart' => '💔', 'two_hearts' => '💕', 'sparkling_heart' => '💖', 'heartbeat' => '💓',
137
+ 'thumbsup' => '👍', '+1' => '👍', 'thumbsdown' => '👎', '-1' => '👎',
138
+ 'clap' => '👏', 'raised_hands' => '🙌', 'pray' => '🙏', 'wave' => '👋',
139
+ 'ok_hand' => '👌', 'v' => '✌️', 'crossed_fingers' => '🤞', 'muscle' => '💪',
140
+ 'point_up' => '☝️', 'point_down' => '👇', 'point_left' => '👈', 'point_right' => '👉',
141
+ 'handshake' => '🤝', 'writing_hand' => '✍️', 'fire' => '🔥', 'star' => '⭐',
142
+ 'star2' => '🌟', 'sparkles' => '✨', 'zap' => '⚡', 'boom' => '💥', 'collision' => '💥',
143
+ 'rocket' => '🚀', 'tada' => '🎉', 'confetti_ball' => '🎊', 'gift' => '🎁',
144
+ 'balloon' => '🎈', 'trophy' => '🏆', 'medal' => '🏅', 'crown' => '👑',
145
+ 'gem' => '💎', 'moneybag' => '💰', 'dollar' => '💵', '100' => '💯',
146
+ 'warning' => '⚠️', 'no_entry' => '⛔', 'stop_sign' => '🛑', 'checkered_flag' => '🏁',
147
+ 'white_check_mark' => '✅', 'heavy_check_mark' => '✔️', 'x' => '❌', 'negative_squared_cross_mark' => '❎',
148
+ 'question' => '❓', 'grey_question' => '❔', 'exclamation' => '❗', 'bangbang' => '‼️',
149
+ 'interrobang' => '⁉️', 'bulb' => '💡', 'bell' => '🔔', 'no_bell' => '🔕',
150
+ 'lock' => '🔒', 'unlock' => '🔓', 'key' => '🔑', 'mag' => '🔍', 'link' => '🔗',
151
+ 'pushpin' => '📌', 'paperclip' => '📎', 'calendar' => '📅', 'clock' => '🕐',
152
+ 'hourglass' => '⌛', 'alarm_clock' => '⏰', 'memo' => '📝', 'pencil2' => '✏️',
153
+ 'book' => '📖', 'books' => '📚', 'newspaper' => '📰', 'email' => '📧',
154
+ 'envelope' => '✉️', 'inbox_tray' => '📥', 'outbox_tray' => '📤', 'package' => '📦',
155
+ 'file_folder' => '📁', 'open_file_folder' => '📂', 'clipboard' => '📋',
156
+ 'chart_with_upwards_trend' => '📈', 'chart_with_downwards_trend' => '📉', 'bar_chart' => '📊',
157
+ 'computer' => '💻', 'desktop_computer' => '🖥️', 'keyboard' => '⌨️', 'printer' => '🖨️',
158
+ 'phone' => '📱', 'iphone' => '📱', 'camera' => '📷', 'video_camera' => '📹',
159
+ 'tv' => '📺', 'radio' => '📻', 'battery' => '🔋', 'electric_plug' => '🔌',
160
+ 'bug' => '🐛', 'beetle' => '🪲', 'gear' => '⚙️', 'wrench' => '🔧', 'hammer' => '🔨',
161
+ 'nut_and_bolt' => '🔩', 'toolbox' => '🧰', 'test_tube' => '🧪', 'microscope' => '🔬',
162
+ 'satellite' => '🛰️', 'globe_with_meridians' => '🌐', 'earth_americas' => '🌎',
163
+ 'sun' => '☀️', 'sunny' => '☀️', 'partly_sunny' => '⛅', 'cloud' => '☁️',
164
+ 'rainbow' => '🌈', 'umbrella' => '☂️', 'snowflake' => '❄️', 'droplet' => '💧',
165
+ 'ocean' => '🌊', 'tent' => '⛺', 'camping' => '🏕️', 'mountain' => '⛰️',
166
+ 'evergreen_tree' => '🌲', 'deciduous_tree' => '🌳', 'palm_tree' => '🌴',
167
+ 'cactus' => '🌵', 'seedling' => '🌱', 'four_leaf_clover' => '🍀', 'maple_leaf' => '🍁',
168
+ 'dog' => '🐶', 'cat' => '🐱', 'mouse' => '🐭', 'rabbit' => '🐰', 'fox_face' => '🦊',
169
+ 'bear' => '🐻', 'panda_face' => '🐼', 'koala' => '🐨', 'tiger' => '🐯', 'lion' => '🦁',
170
+ 'cow' => '🐮', 'pig' => '🐷', 'frog' => '🐸', 'monkey_face' => '🐵', 'chicken' => '🐔',
171
+ 'penguin' => '🐧', 'bird' => '🐦', 'baby_chick' => '🐤', 'owl' => '🦉',
172
+ 'horse' => '🐴', 'unicorn' => '🦄', 'bee' => '🐝', 'butterfly' => '🦋', 'snail' => '🐌',
173
+ 'octopus' => '🐙', 'fish' => '🐟', 'dolphin' => '🐬', 'whale' => '🐳',
174
+ 'pizza' => '🍕', 'hamburger' => '🍔', 'fries' => '🍟', 'hotdog' => '🌭',
175
+ 'taco' => '🌮', 'sushi' => '🍣', 'ramen' => '🍜', 'spaghetti' => '🍝',
176
+ 'bread' => '🍞', 'cheese' => '🧀', 'egg' => '🥚', 'popcorn' => '🍿',
177
+ 'cookie' => '🍪', 'doughnut' => '🍩', 'cake' => '🍰', 'birthday' => '🎂',
178
+ 'candy' => '🍬', 'chocolate_bar' => '🍫', 'icecream' => '🍦', 'apple' => '🍎',
179
+ 'banana' => '🍌', 'grapes' => '🍇', 'watermelon' => '🍉', 'strawberry' => '🍓',
180
+ 'lemon' => '🍋', 'peach' => '🍑', 'coffee' => '☕', 'tea' => '🍵', 'beer' => '🍺',
181
+ 'beers' => '🍻', 'wine_glass' => '🍷', 'cocktail' => '🍸', 'tropical_drink' => '🍹',
182
+ 'champagne' => '🍾', 'soccer' => '⚽', 'basketball' => '🏀', 'football' => '🏈',
183
+ 'baseball' => '⚾', 'tennis' => '🎾', 'volleyball' => '🏐', 'rugby_football' => '🏉',
184
+ '8ball' => '🎱', 'golf' => '⛳', 'dart' => '🎯', 'video_game' => '🎮',
185
+ 'game_die' => '🎲', 'jigsaw' => '🧩', 'car' => '🚗', 'taxi' => '🚕', 'bus' => '🚌',
186
+ 'ambulance' => '🚑', 'fire_engine' => '🚒', 'police_car' => '🚓', 'bike' => '🚲',
187
+ 'airplane' => '✈️', 'helicopter' => '🚁', 'train' => '🚆', 'ship' => '🚢',
188
+ 'house' => '🏠', 'office' => '🏢', 'hospital' => '🏥', 'school' => '🏫',
189
+ 'church' => '⛪', 'castle' => '🏰', 'world_map' => '🗺️', 'flag_white' => '🏳️',
190
+ 'flag_black' => '🏴', 'checkered_flag2' => '🏁', 'eyes' => '👀', 'eye' => '👁️',
191
+ 'speech_balloon' => '💬', 'thought_balloon' => '💭', 'zzz' => '💤', 'boom2' => '💥',
192
+ 'sos' => '🆘', 'new' => '🆕', 'ok' => '🆗', 'up' => '🆙', 'cool' => '🆒',
193
+ 'free' => '🆓', 'id' => '🆔', 'ng' => '🆖',
194
+ ];
195
+
196
+ /**
197
+ * Enregistre (ou remplace) un raccourci emoji personnalisé.
198
+ */
199
+ public static function registerEmoji(string $shortcode, string $char): void {
200
+ self::$extraEmoji[strtolower(trim($shortcode, ':'))] = $char;
201
+ }
202
+
203
+ private static function emojiFor(string $shortcode): ?string {
204
+ $key = strtolower($shortcode);
205
+ return self::$extraEmoji[$key] ?? self::$emojiMap[$key] ?? null;
206
+ }
207
+
208
+ // ========================================================================
209
+ // LISTES DE DÉFINITION (syntaxe étendue)
210
+ // Terme
211
+ // : Définition
212
+ // Analyse procédurale ligne par ligne (plus sûre qu'une seule grosse
213
+ // regex pour regrouper plusieurs paires terme/définitions dans un même
214
+ // <dl>, séparées ou non par une ligne vide).
215
+ // ========================================================================
216
+ private static function isDefinitionColonLine(string $line): bool {
217
+ return (bool) preg_match('/^[ \t]*:[ \t]+.+$/', $line);
218
+ }
219
+
220
+ private static function looksLikeOtherBlock(string $line): bool {
221
+ $t = ltrim($line);
222
+ if ($t === '') return true;
223
+ if (str_starts_with($t, '<')) return true;
224
+ return (bool) preg_match('/^(?:#{1,6}[ \t]|>|```|\||[-*+][ \t]|\d+\.[ \t])/', $t);
225
+ }
226
+
227
+ private static function extractDefinitionLists(string $html): string {
228
+ $lines = explode("\n", $html);
229
+ $n = count($lines);
230
+ $out = [];
231
+ $i = 0;
232
+
233
+ while ($i < $n) {
234
+ $isTermStart = $i + 1 < $n
235
+ && !self::looksLikeOtherBlock($lines[$i])
236
+ && !self::isDefinitionColonLine($lines[$i])
237
+ && self::isDefinitionColonLine($lines[$i + 1]);
238
+
239
+ if (!$isTermStart) {
240
+ $out[] = $lines[$i];
241
+ $i++;
242
+ continue;
243
+ }
244
+
245
+ $dl = "<dl>\n";
246
+ while (true) {
247
+ $term = trim($lines[$i]);
248
+ $dl .= " <dt>{$term}</dt>\n";
249
+ $i++;
250
+ while ($i < $n && preg_match('/^[ \t]*:[ \t]+(.*)$/', $lines[$i], $m)) {
251
+ $dl .= " <dd>{$m[1]}</dd>\n";
252
+ $i++;
253
+ }
254
+
255
+ // Une seule ligne vide entre deux groupes reste dans le même <dl>
256
+ // si le groupe suivant est bien un nouveau terme.
257
+ if ($i < $n && trim($lines[$i]) === '') {
258
+ $j = $i;
259
+ while ($j < $n && trim($lines[$j]) === '') $j++;
260
+ if ($j + 1 < $n
261
+ && !self::looksLikeOtherBlock($lines[$j])
262
+ && !self::isDefinitionColonLine($lines[$j])
263
+ && self::isDefinitionColonLine($lines[$j + 1])
264
+ ) {
265
+ $i = $j;
266
+ continue;
267
+ }
268
+ }
269
+ break;
270
+ }
271
+ $dl .= "</dl>";
272
+ $out[] = $dl;
273
+ }
274
+
275
+ return implode("\n", $out);
276
+ }
277
+
110
278
  // ========================================================================
111
279
 
112
280
  public static function toHtml(string $markdown): string {
@@ -180,6 +348,38 @@ class MD {
180
348
  );
181
349
 
182
350
 
351
+ // ====================================================================
352
+ // ÉTAPE 2a : DÉFINITIONS DE NOTES DE BAS DE PAGE (footnotes)
353
+ // [^1]: Texte de la note.
354
+ // [^bignote]: Première ligne.
355
+ //
356
+ // Paragraphe suivant, indenté de 4 espaces ou 1 tabulation.
357
+ //
358
+ // `{ du code }`
359
+ // Extraites (et retirées du texte) AVANT les définitions de liens par
360
+ // référence, car [^label]: matcherait aussi leur regex sinon.
361
+ // Le contenu de chaque note est rendu via un appel récursif à
362
+ // toHtml() pour supporter plusieurs paragraphes, du code, etc.
363
+ // ====================================================================
364
+ $footnoteDefs = [];
365
+ $html = preg_replace_callback(
366
+ '/^\[\^([^\]\s]+)\]:[ \t]?([^\n]*)((?:\n(?:[ \t]{4}[^\n]*|[ \t]*))*)/m',
367
+ function ($m) use (&$footnoteDefs): string {
368
+ $label = strtolower(trim($m[1]));
369
+ $first = $m[2];
370
+ $rest = $m[3] ?? '';
371
+ $restLines = $rest !== '' ? explode("\n", $rest) : [];
372
+ $restLines = array_map(static function (string $l): string {
373
+ return preg_replace('/^(?:[ ]{4}|\t)/', '', $l);
374
+ }, $restLines);
375
+ $content = trim($first . "\n" . implode("\n", $restLines));
376
+ $footnoteDefs[$label] = self::toHtml($content);
377
+ return '';
378
+ },
379
+ $html
380
+ );
381
+
382
+
183
383
  // ====================================================================
184
384
  // ÉTAPE 2b : DÉFINITIONS DE LIENS PAR RÉFÉRENCE
185
385
  // [label]: https://example.com "Titre optionnel"
@@ -275,6 +475,18 @@ class MD {
275
475
  },
276
476
  $html
277
477
  );
478
+ // &#124; est la convention documentée (Markdown Extra / PHP Markdown)
479
+ // pour afficher un pipe littéral dans une cellule de tableau sans
480
+ // qu'il soit interprété comme séparateur de colonnes.
481
+ $html = preg_replace_callback(
482
+ '/&#124;/i',
483
+ function () use (&$escapes): string {
484
+ $placeholder = "\x02ESC" . count($escapes) . "\x03";
485
+ $escapes[$placeholder] = '|';
486
+ return $placeholder;
487
+ },
488
+ $html
489
+ );
278
490
 
279
491
 
280
492
  // ====================================================================
@@ -418,19 +630,19 @@ class MD {
418
630
  // ligne de tirets juste après une ligne de texte est un titre, pas un <hr>).
419
631
  // ====================================================================
420
632
  $html = preg_replace_callback(
421
- '/^(?![ \t]*(?:#{1,6}[ \t]|>|```|\||[-*+][ \t]|\d+\.[ \t]))[ \t]*(\S.*?)[ \t]*\n[ \t]*=+[ \t]*$/m',
633
+ '/^(?![ \t]*(?:#{1,6}[ \t]|>|```|\||[-*+][ \t]|\d+\.[ \t]))[ \t]*(\S.*?)[ \t]*(?:\{#([a-zA-Z0-9_\-:.]+)\}[ \t]*)?\n[ \t]*=+[ \t]*$/m',
422
634
  function ($matches) {
423
635
  $text = trim($matches[1]);
424
- $id = self::slugify($text);
636
+ $id = !empty($matches[2]) ? $matches[2] : self::slugify($text);
425
637
  return "<h1 id=\"{$id}\">{$text}</h1>";
426
638
  },
427
639
  $html
428
640
  );
429
641
  $html = preg_replace_callback(
430
- '/^(?![ \t]*(?:#{1,6}[ \t]|>|```|\||[-*+][ \t]|\d+\.[ \t]))[ \t]*(\S.*?)[ \t]*\n[ \t]*-+[ \t]*$/m',
642
+ '/^(?![ \t]*(?:#{1,6}[ \t]|>|```|\||[-*+][ \t]|\d+\.[ \t]))[ \t]*(\S.*?)[ \t]*(?:\{#([a-zA-Z0-9_\-:.]+)\}[ \t]*)?\n[ \t]*-+[ \t]*$/m',
431
643
  function ($matches) {
432
644
  $text = trim($matches[1]);
433
- $id = self::slugify($text);
645
+ $id = !empty($matches[2]) ? $matches[2] : self::slugify($text);
434
646
  return "<h2 id=\"{$id}\">{$text}</h2>";
435
647
  },
436
648
  $html
@@ -440,12 +652,16 @@ class MD {
440
652
  // ====================================================================
441
653
  // ÉTAPE 8 : TITRES (ATX : # à ######)
442
654
  // ====================================================================
443
- $html = preg_replace_callback('/^(#{1,6})[ \t]+(.+?)(?:[ \t]+#+)?$/m', function ($matches) {
444
- $level = strlen($matches[1]);
445
- $text = trim($matches[2]);
446
- $id = self::slugify($text);
447
- return "<h{$level} id=\"{$id}\">{$text}</h{$level}>";
448
- }, $html);
655
+ $html = preg_replace_callback(
656
+ '/^(#{1,6})[ \t]+(.+?)[ \t]*(?:\{#([a-zA-Z0-9_\-:.]+)\}[ \t]*)?(?:[ \t]+#+)?$/m',
657
+ function ($matches) {
658
+ $level = strlen($matches[1]);
659
+ $text = trim($matches[2]);
660
+ $id = !empty($matches[3]) ? $matches[3] : self::slugify($text);
661
+ return "<h{$level} id=\"{$id}\">{$text}</h{$level}>";
662
+ },
663
+ $html
664
+ );
449
665
 
450
666
 
451
667
  // ====================================================================
@@ -491,7 +707,39 @@ class MD {
491
707
 
492
708
 
493
709
  // ====================================================================
494
- // ÉTAPE 10 : TEXTE EN LIGNE (Gras, Italique, Barré)
710
+ // ÉTAPE 9b : LISTES DE DÉFINITION (syntaxe étendue)
711
+ // Terme
712
+ // : Définition
713
+ // ====================================================================
714
+ $html = self::extractDefinitionLists($html);
715
+
716
+
717
+ // ====================================================================
718
+ // ÉTAPE 9c : RÉFÉRENCES DE NOTES DE BAS DE PAGE [^label]
719
+ // Converties AVANT l'emphase pour ne pas entrer en collision avec le
720
+ // nouvel exposant ^texte^ (un [^1] suivi plus loin d'un [^2] sur la
721
+ // même ligne pourrait sinon être interprété comme ^1] ... [^2^).
722
+ // La numérotation est séquentielle, dans l'ordre de première
723
+ // apparition dans le texte (comme documenté).
724
+ // ====================================================================
725
+ $footnoteOrder = [];
726
+ $html = preg_replace_callback('/\[\^([^\]\s]+)\]/', function ($m) use (&$footnoteOrder, &$footnoteDefs): string {
727
+ $label = strtolower(trim($m[1]));
728
+ if (!isset($footnoteDefs[$label])) {
729
+ // Référence vers une note non définie : laissée telle quelle.
730
+ return $m[0];
731
+ }
732
+ if (!isset($footnoteOrder[$label])) {
733
+ $footnoteOrder[$label] = count($footnoteOrder) + 1;
734
+ }
735
+ $num = $footnoteOrder[$label];
736
+ return "<sup id=\"fnref:{$label}\"><a href=\"#fn:{$label}\">{$num}</a></sup>";
737
+ }, $html);
738
+
739
+
740
+ // ====================================================================
741
+ // ÉTAPE 10 : TEXTE EN LIGNE (Gras, Italique, Barré, Surlignage,
742
+ // Indice/Exposant, Emoji)
495
743
  // ====================================================================
496
744
  $html = preg_replace('/\*\*\*(.+?)\*\*\*/s', '<strong><em>$1</em></strong>', $html);
497
745
  $html = preg_replace('/___(.+?)___/s', '<strong><em>$1</em></strong>', $html);
@@ -501,7 +749,25 @@ class MD {
501
749
  // Le _ italique ne doit matcher qu'aux frontières de mots pour ne pas
502
750
  // capturer les snake_case, noms de packages (@php-wasm/node), etc.
503
751
  $html = preg_replace('/(?<!\w)_([^_\n]+)_(?!\w)/', '<em>$1</em>', $html);
752
+ // Surlignage ==texte== (syntaxe étendue)
753
+ $html = preg_replace('/==(.+?)==/s', '<mark>$1</mark>', $html);
754
+ // Barré ~~texte~~ — traité AVANT le sous-script (simple ~) pour que
755
+ // celui-ci ne matche pas la moitié d'une paire de tildes doubles.
504
756
  $html = preg_replace('/~~(.+?)~~/s', '<del>$1</del>', $html);
757
+ // Exposant ^texte^ (syntaxe étendue) — placé avant l'échappement des
758
+ // références de notes ([^label]) n'est pas un souci : celles-ci sont
759
+ // encadrées de crochets et ne forment donc pas de paire ^...^ isolée.
760
+ $html = preg_replace('/\^([^\^\n]+)\^/', '<sup>$1</sup>', $html);
761
+ // Sous-script ~texte~ (un seul tilde ; les ~~ ont déjà été consommés
762
+ // juste au-dessus par le barré).
763
+ $html = preg_replace('/~([^~\n]+)~/', '<sub>$1</sub>', $html);
764
+
765
+ // Émojis :shortcode: (syntaxe étendue) — les raccourcis inconnus sont
766
+ // laissés tels quels plutôt que silencieusement supprimés.
767
+ $html = preg_replace_callback('/:([a-zA-Z0-9_+\-]+):/', function ($m): string {
768
+ $emoji = self::emojiFor($m[1]);
769
+ return $emoji ?? $m[0];
770
+ }, $html);
505
771
 
506
772
 
507
773
  // ====================================================================
@@ -545,6 +811,15 @@ class MD {
545
811
  $html
546
812
  );
547
813
 
814
+ // URL nues https://... (syntaxe étendue : auto-link sans crochets).
815
+ // Exclut celles déjà entre guillemets/attributs (href="...") ou déjà
816
+ // transformées en lien pour ne pas les doubler.
817
+ $html = preg_replace(
818
+ '/(?<!["\'=>])\b(https?:\/\/[^\s<>"\')\]]+)/',
819
+ '<a href="$1" target="_blank" rel="noopener noreferrer">$1</a>',
820
+ $html
821
+ );
822
+
548
823
 
549
824
  // ====================================================================
550
825
  // ÉTAPE 12 : LIGNES SÉPARATRICES
@@ -561,6 +836,7 @@ class MD {
561
836
  // ====================================================================
562
837
  $blockStartTags = ['<h', '<pre', '<ul', '<ol', '<li', '<table', '<thead', '<tbody',
563
838
  '<tr', '<td', '<th', '<blockquote', '<div', '<hr', '<img',
839
+ '<dl', '<dt', '<dd',
564
840
  "\x02CB", "\x02PLG", "\x02BQ"];
565
841
 
566
842
  $isBlockLine = static function (string $line) use ($blockStartTags): bool {
@@ -622,6 +898,22 @@ class MD {
622
898
  // aucune regex Markdown ne peut les interpréter.
623
899
  $html = strtr($html, $escapes);
624
900
 
901
+
902
+ // ====================================================================
903
+ // ÉTAPE 15 : BLOC DES NOTES DE BAS DE PAGE
904
+ // Ajouté en fin de document, uniquement si au moins une note a été
905
+ // référencée (les notes définies mais jamais référencées sont
906
+ // silencieusement ignorées).
907
+ // ====================================================================
908
+ if (!empty($footnoteOrder)) {
909
+ $html .= "\n<div class=\"footnotes\">\n<ol>\n";
910
+ foreach ($footnoteOrder as $label => $num) {
911
+ $content = $footnoteDefs[$label];
912
+ $html .= " <li id=\"fn:{$label}\">{$content} <a href=\"#fnref:{$label}\" class=\"footnote-backref\">↩</a></li>\n";
913
+ }
914
+ $html .= "</ol>\n</div>";
915
+ }
916
+
625
917
  return $html;
626
918
  }
627
919
  }
package/src/prepros.php CHANGED
@@ -25,7 +25,7 @@ try {
25
25
  elseif (!$target = realpath($argv[1])) STD::error("Invalid target.");
26
26
  else if (is_dir($target)) {
27
27
  $prj = new PREPROS($config);
28
- foreach (FS::dig($target . '/*.php') as $file) {
28
+ foreach (FS::dig($target . '/*.php', true) as $file) {
29
29
  $parent = pathinfo(pathinfo($file, PATHINFO_DIRNAME), PATHINFO_BASENAME);
30
30
  if (strpos($parent, '_') === 0) continue;
31
31
  if (strpos(pathinfo($file, PATHINFO_FILENAME), '_') !== 0) continue;