@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,453 @@
1
+ <?php
2
+
3
+ /**
4
+ * YAML — Parseur YAML léger, entièrement statique.
5
+ *
6
+ * Utilisation :
7
+ * $data = YAML::parse($yamlString); // mappings → stdClass (défaut)
8
+ * $data = YAML::parse($yamlString, true); // mappings → array associatif
9
+ * $data = YAML::parseFile('/chemin/vers/config.yaml');
10
+ *
11
+ * Supporte :
12
+ * - Scalaires typés (string, int, float, bool, null)
13
+ * - Guillemets simples et doubles (avec séquences d'échappement)
14
+ * - Blocs multi-lignes (| littéral et > replié, avec chomping -, +)
15
+ * - Plain scalars multi-lignes (continuation sans indicateur)
16
+ * - Mappings et séquences imbriqués
17
+ * - Collections inline [a, b] et {k: v}
18
+ * - Commentaires (#)
19
+ * - Documents multiples séparés par ---
20
+ */
21
+ class YAML
22
+ {
23
+ private function __construct() {}
24
+
25
+ // -------------------------------------------------------------------------
26
+ // Points d'entrée publics
27
+ // -------------------------------------------------------------------------
28
+
29
+ public static function parse(string $yaml, bool $assoc = false): mixed
30
+ {
31
+ $yaml = str_replace(["\r\n", "\r"], "\n", $yaml);
32
+ $lines = explode("\n", $yaml);
33
+ $pos = 0;
34
+
35
+ $documents = [];
36
+
37
+ while ($pos < count($lines)) {
38
+ $line = $lines[$pos];
39
+ if (preg_match('/^---/', $line)) { $pos++; continue; }
40
+ if (preg_match('/^\.\.\./', $line)) { $pos++; break; }
41
+
42
+ $doc = self::parseBlock($lines, $pos, 0, $assoc);
43
+ $documents[] = $doc;
44
+ }
45
+
46
+ return count($documents) === 1 ? $documents[0] : $documents;
47
+ }
48
+
49
+ public static function parseFile(string $path, bool $assoc = false): mixed
50
+ {
51
+ if (!is_readable($path)) {
52
+ throw new \RuntimeException("Impossible de lire le fichier : $path");
53
+ }
54
+ return self::parse(file_get_contents($path), $assoc);
55
+ }
56
+
57
+ // -------------------------------------------------------------------------
58
+ // Parsing récursif
59
+ // -------------------------------------------------------------------------
60
+
61
+ private static function parseBlock(array $lines, int &$pos, int $indent, bool $assoc = false): mixed
62
+ {
63
+ self::skipEmptyAndComments($lines, $pos);
64
+
65
+ if ($pos >= count($lines)) return null;
66
+
67
+ $line = $lines[$pos];
68
+ $lineIndent = self::getIndent($line);
69
+ $trimmed = ltrim($line);
70
+
71
+ if (str_starts_with($trimmed, '- ') || $trimmed === '-') {
72
+ return self::parseSequence($lines, $pos, $lineIndent, $assoc);
73
+ }
74
+
75
+ if (self::isMapping($trimmed)) {
76
+ return self::parseMapping($lines, $pos, $lineIndent, $assoc);
77
+ }
78
+
79
+ return null;
80
+ }
81
+
82
+ private static function parseMapping(array $lines, int &$pos, int $indent, bool $assoc = false): array|object
83
+ {
84
+ $result = [];
85
+
86
+ while ($pos < count($lines)) {
87
+ self::skipEmptyAndComments($lines, $pos);
88
+ if ($pos >= count($lines)) break;
89
+
90
+ $line = $lines[$pos];
91
+ $lineIndent = self::getIndent($line);
92
+ $trimmed = ltrim($line);
93
+
94
+ if ($lineIndent < $indent) break;
95
+ if ($lineIndent > $indent) break;
96
+ if (preg_match('/^(---|\.\.\.)\s*$/', $trimmed)) break;
97
+ if (!self::isMapping($trimmed)) break;
98
+
99
+ [$key, $rest] = self::splitKeyValue($trimmed);
100
+ $pos++;
101
+
102
+ if ($rest === null) {
103
+ // Valeur sur les lignes suivantes : sous-bloc ou plain scalar multi-ligne
104
+ self::skipEmptyAndComments($lines, $pos);
105
+ if ($pos < count($lines)) {
106
+ $nextIndent = self::getIndent($lines[$pos]);
107
+ if ($nextIndent > $indent) {
108
+ $nextTrimmed = ltrim($lines[$pos]);
109
+ // Plain scalar si ce n'est ni un mapping ni une séquence
110
+ if (!self::isMapping($nextTrimmed) && !str_starts_with($nextTrimmed, '- ')) {
111
+ $result[$key] = self::parseScalar(
112
+ self::collectPlainScalar($lines, $pos, $indent, '')
113
+ );
114
+ } else {
115
+ $result[$key] = self::parseBlock($lines, $pos, $nextIndent, $assoc);
116
+ }
117
+ } else {
118
+ $result[$key] = null;
119
+ }
120
+ } else {
121
+ $result[$key] = null;
122
+ }
123
+ } elseif ($rest === '|' || $rest === '|-' || $rest === '|+') {
124
+ $result[$key] = self::parseLiteralBlock($lines, $pos, $indent, $rest);
125
+ } elseif ($rest === '>' || $rest === '>-' || $rest === '>+') {
126
+ $result[$key] = self::parseFoldedBlock($lines, $pos, $indent, $rest);
127
+ } elseif ($rest !== '' && ($rest[0] === '[' || $rest[0] === '{')) {
128
+ $result[$key] = self::parseInlineCollection($rest, $assoc);
129
+ } else {
130
+ // Scalaire inline, peut être suivi de lignes de continuation
131
+ $result[$key] = self::parseScalar(
132
+ self::collectPlainScalar($lines, $pos, $indent, $rest)
133
+ );
134
+ }
135
+ }
136
+
137
+ return $assoc ? $result : (object) $result;
138
+ }
139
+
140
+ private static function parseSequence(array $lines, int &$pos, int $indent, bool $assoc = false): array
141
+ {
142
+ $result = [];
143
+
144
+ while ($pos < count($lines)) {
145
+ self::skipEmptyAndComments($lines, $pos);
146
+ if ($pos >= count($lines)) break;
147
+
148
+ $line = $lines[$pos];
149
+ $lineIndent = self::getIndent($line);
150
+ $trimmed = ltrim($line);
151
+
152
+ if ($lineIndent < $indent) break;
153
+ if ($lineIndent > $indent) break;
154
+ if (preg_match('/^(---|\.\.\.)\s*$/', $trimmed)) break;
155
+ if (!str_starts_with($trimmed, '- ') && $trimmed !== '-') break;
156
+
157
+ $itemContent = $trimmed === '-' ? '' : substr($trimmed, 2);
158
+ $pos++;
159
+
160
+ if ($itemContent === '') {
161
+ self::skipEmptyAndComments($lines, $pos);
162
+ if ($pos < count($lines)) {
163
+ $nextIndent = self::getIndent($lines[$pos]);
164
+ $result[] = $nextIndent > $indent
165
+ ? self::parseBlock($lines, $pos, $nextIndent, $assoc)
166
+ : null;
167
+ } else {
168
+ $result[] = null;
169
+ }
170
+ } elseif (self::isMapping($itemContent)) {
171
+ // Mapping inline dans la séquence :
172
+ // On reconstruit un tableau de lignes virtuel en préfixant la première clé
173
+ // avec fakeIndent, puis on délègue entièrement à parseMapping pour bénéficier
174
+ // de toute sa logique (|, >, plain scalars, sous-blocs…).
175
+ $fakeIndent = $lineIndent + 2;
176
+ $fakePrefix = str_repeat(' ', $fakeIndent);
177
+ $virtualLines = array_merge(
178
+ [$fakePrefix . $itemContent],
179
+ array_slice($lines, $pos)
180
+ );
181
+ $vPos = 0;
182
+ $itemMap = self::parseMapping($virtualLines, $vPos, $fakeIndent, $assoc);
183
+ $pos += max(0, $vPos - 1);
184
+ $result[] = $itemMap;
185
+ } elseif ($itemContent[0] === '[' || $itemContent[0] === '{') {
186
+ $result[] = self::parseInlineCollection($itemContent, $assoc);
187
+ } else {
188
+ $result[] = self::parseScalar($itemContent);
189
+ }
190
+ }
191
+
192
+ return $result;
193
+ }
194
+
195
+ // -------------------------------------------------------------------------
196
+ // Plain scalar multi-ligne
197
+ // -------------------------------------------------------------------------
198
+
199
+ /**
200
+ * Collecte un scalaire qui peut continuer sur des lignes plus indentées que $parentIndent.
201
+ * Les lignes de continuation sont jointes par un espace (repli implicite).
202
+ */
203
+ private static function collectPlainScalar(array $lines, int &$pos, int $parentIndent, string $first): string
204
+ {
205
+ $parts = $first !== '' ? [trim($first)] : [];
206
+
207
+ while ($pos < count($lines)) {
208
+ $raw = $lines[$pos];
209
+ $trimmed = trim($raw);
210
+
211
+ // Ligne vide : fin du scalaire
212
+ if ($trimmed === '') break;
213
+
214
+ $lineIndent = self::getIndent($raw);
215
+
216
+ // Retour à l'indentation parente ou moins : fin
217
+ if ($lineIndent <= $parentIndent) break;
218
+
219
+ // Commentaire seul sur la ligne : fin
220
+ if (str_starts_with($trimmed, '#')) break;
221
+
222
+ // C'est un mapping ou une séquence : fin
223
+ if (self::isMapping($trimmed) || str_starts_with($trimmed, '- ')) break;
224
+
225
+ $parts[] = $trimmed;
226
+ $pos++;
227
+ }
228
+
229
+ return implode(' ', $parts);
230
+ }
231
+
232
+ // -------------------------------------------------------------------------
233
+ // Blocs multi-lignes
234
+ // -------------------------------------------------------------------------
235
+
236
+ private static function parseLiteralBlock(array $lines, int &$pos, int $parentIndent, string $indicator): string
237
+ {
238
+ $blockLines = [];
239
+ $blockIndent = null;
240
+ $chomping = self::getChomping($indicator);
241
+
242
+ while ($pos < count($lines)) {
243
+ $raw = $lines[$pos];
244
+
245
+ if (trim($raw) === '') {
246
+ $blockLines[] = '';
247
+ $pos++;
248
+ continue;
249
+ }
250
+
251
+ $lineIndent = self::getIndent($raw);
252
+ if ($blockIndent === null) {
253
+ if ($lineIndent <= $parentIndent) break;
254
+ $blockIndent = $lineIndent;
255
+ }
256
+ if ($lineIndent < $blockIndent) break;
257
+
258
+ $blockLines[] = substr($raw, $blockIndent);
259
+ $pos++;
260
+ }
261
+
262
+ return self::applyChomping(implode("\n", $blockLines), $chomping);
263
+ }
264
+
265
+ private static function parseFoldedBlock(array $lines, int &$pos, int $parentIndent, string $indicator): string
266
+ {
267
+ $blockLines = [];
268
+ $blockIndent = null;
269
+ $chomping = self::getChomping($indicator);
270
+
271
+ while ($pos < count($lines)) {
272
+ $raw = $lines[$pos];
273
+
274
+ if (trim($raw) === '') {
275
+ $blockLines[] = '';
276
+ $pos++;
277
+ continue;
278
+ }
279
+
280
+ $lineIndent = self::getIndent($raw);
281
+ if ($blockIndent === null) {
282
+ if ($lineIndent <= $parentIndent) break;
283
+ $blockIndent = $lineIndent;
284
+ }
285
+ if ($lineIndent < $blockIndent) break;
286
+
287
+ $blockLines[] = rtrim(substr($raw, $blockIndent));
288
+ $pos++;
289
+ }
290
+
291
+ $folded = '';
292
+ $count = count($blockLines);
293
+ for ($i = 0; $i < $count; $i++) {
294
+ if ($blockLines[$i] === '') {
295
+ $folded .= "\n";
296
+ } elseif ($i < $count - 1 && $blockLines[$i + 1] !== '') {
297
+ $folded .= $blockLines[$i] . ' ';
298
+ } else {
299
+ $folded .= $blockLines[$i];
300
+ }
301
+ }
302
+
303
+ return self::applyChomping(rtrim($folded, ' '), $chomping);
304
+ }
305
+
306
+ // -------------------------------------------------------------------------
307
+ // Collections inline [a, b] {k: v}
308
+ // -------------------------------------------------------------------------
309
+
310
+ private static function parseInlineCollection(string $raw, bool $assoc = false): mixed
311
+ {
312
+ $raw = trim($raw);
313
+ if ($raw[0] === '[') return self::parseInlineSequence($raw, $assoc);
314
+ if ($raw[0] === '{') return self::parseInlineMapping($raw, $assoc);
315
+ return self::parseScalar($raw);
316
+ }
317
+
318
+ private static function parseInlineSequence(string $raw, bool $assoc = false): array
319
+ {
320
+ $inner = trim(substr($raw, 1, strrpos($raw, ']') - 1));
321
+ if ($inner === '') return [];
322
+
323
+ return array_map(
324
+ fn($item) => self::parseInlineCollection(trim($item), $assoc),
325
+ self::splitInline($inner)
326
+ );
327
+ }
328
+
329
+ private static function parseInlineMapping(string $raw, bool $assoc = false): array|object
330
+ {
331
+ $inner = trim(substr($raw, 1, strrpos($raw, '}') - 1));
332
+ if ($inner === '') return $assoc ? [] : new \stdClass();
333
+
334
+ $result = [];
335
+ foreach (self::splitInline($inner) as $pair) {
336
+ $colonPos = strpos($pair, ':');
337
+ if ($colonPos === false) continue;
338
+ $k = trim(substr($pair, 0, $colonPos));
339
+ $v = trim(substr($pair, $colonPos + 1));
340
+ $result[$k] = self::parseInlineCollection($v, $assoc);
341
+ }
342
+ return $assoc ? $result : (object) $result;
343
+ }
344
+
345
+ private static function splitInline(string $str): array
346
+ {
347
+ $parts = [];
348
+ $depth = 0;
349
+ $current = '';
350
+ $inSingle = false;
351
+ $inDouble = false;
352
+
353
+ for ($i = 0, $len = strlen($str); $i < $len; $i++) {
354
+ $c = $str[$i];
355
+
356
+ if ($c === "'" && !$inDouble) { $inSingle = !$inSingle; $current .= $c; continue; }
357
+ if ($c === '"' && !$inSingle) { $inDouble = !$inDouble; $current .= $c; continue; }
358
+ if ($inSingle || $inDouble) { $current .= $c; continue; }
359
+
360
+ if ($c === '[' || $c === '{') { $depth++; $current .= $c; continue; }
361
+ if ($c === ']' || $c === '}') { $depth--; $current .= $c; continue; }
362
+
363
+ if ($c === ',' && $depth === 0) { $parts[] = $current; $current = ''; continue; }
364
+ $current .= $c;
365
+ }
366
+
367
+ if ($current !== '') $parts[] = $current;
368
+ return $parts;
369
+ }
370
+
371
+ // -------------------------------------------------------------------------
372
+ // Scalaires
373
+ // -------------------------------------------------------------------------
374
+
375
+ private static function parseScalar(string $value): mixed
376
+ {
377
+ $value = trim($value);
378
+
379
+ if (!preg_match('/^[\'"]/', $value)) {
380
+ $value = rtrim(preg_replace('/(^|\s)#.*$/', '', $value));
381
+ }
382
+
383
+ if ($value === '') return null;
384
+
385
+ if (str_starts_with($value, '"') && str_ends_with($value, '"') && strlen($value) >= 2) {
386
+ return stripcslashes(substr($value, 1, -1));
387
+ }
388
+
389
+ if (str_starts_with($value, "'") && str_ends_with($value, "'") && strlen($value) >= 2) {
390
+ return str_replace("''", "'", substr($value, 1, -1));
391
+ }
392
+
393
+ if (in_array(strtolower($value), ['~', 'null'], true)) return null;
394
+ if (in_array(strtolower($value), ['true', 'yes', 'on'], true)) return true;
395
+ if (in_array(strtolower($value), ['false', 'no', 'off'], true)) return false;
396
+ if (preg_match('/^-?\d+$/', $value)) return (int) $value;
397
+ if (preg_match('/^-?\d+\.\d*([eE][+-]?\d+)?$/', $value)) return (float) $value;
398
+ if (in_array(strtolower($value), ['.inf', '+.inf'], true)) return INF;
399
+ if (strtolower($value) === '-.inf') return -INF;
400
+ if (strtolower($value) === '.nan') return NAN;
401
+
402
+ return $value;
403
+ }
404
+
405
+ // -------------------------------------------------------------------------
406
+ // Utilitaires
407
+ // -------------------------------------------------------------------------
408
+
409
+ private static function getIndent(string $line): int
410
+ {
411
+ return strlen($line) - strlen(ltrim($line));
412
+ }
413
+
414
+ private static function isMapping(string $trimmed): bool
415
+ {
416
+ return (bool) preg_match('/^(?:"[^"]*"|\'[^\']*\'|[^:\'"\[\{]+):\s?/', $trimmed);
417
+ }
418
+
419
+ private static function splitKeyValue(string $line): array
420
+ {
421
+ if (preg_match('/^("(?:[^"\\\\]|\\\\.)*"|\'(?:[^\']|\'\')*\'|[^:]+?):\s*(.*)$/', $line, $m)) {
422
+ $key = self::parseScalar($m[1]);
423
+ $val = trim($m[2]) !== '' ? trim($m[2]) : null;
424
+ return [$key, $val];
425
+ }
426
+ return [$line, null];
427
+ }
428
+
429
+ private static function skipEmptyAndComments(array $lines, int &$pos): void
430
+ {
431
+ while ($pos < count($lines)) {
432
+ $t = trim($lines[$pos]);
433
+ if ($t === '' || str_starts_with($t, '#')) $pos++;
434
+ else break;
435
+ }
436
+ }
437
+
438
+ private static function getChomping(string $indicator): string
439
+ {
440
+ if (str_ends_with($indicator, '-')) return 'strip';
441
+ if (str_ends_with($indicator, '+')) return 'keep';
442
+ return 'clip';
443
+ }
444
+
445
+ private static function applyChomping(string $text, string $chomping): string
446
+ {
447
+ return match ($chomping) {
448
+ 'strip' => rtrim($text, "\n"),
449
+ 'keep' => $text,
450
+ default => rtrim($text, "\n") . "\n",
451
+ };
452
+ }
453
+ }
package/src/prepros.js ADDED
@@ -0,0 +1,107 @@
1
+ import fs from 'fs';
2
+ import path, { dirname } from "path";
3
+ import { spawn } from 'child_process';
4
+ import { fileURLToPath } from 'url';
5
+ import { getPHPRuntime, getPHPRuntimeWithNetwork } from "@kirigami/php-wasm";
6
+ import isBinary from './utils/isbinary.js';
7
+ import joinWith from './utils/joinwith.js'
8
+ import yaml from "js-yaml";
9
+
10
+
11
+ const __project = process.cwd();
12
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
13
+ const __configpath = path.join(__project, 'kirigami.yaml');
14
+
15
+
16
+ const mountPath = (php, localPath, virtualDir) => {
17
+ const includeExtensions = new Set(['.php', '.json', '.yaml', '.md', '.db', ...(config?.prepros?.mountext || [])]);
18
+ const stat = fs.statSync(localPath);
19
+ if (stat.isDirectory()) {
20
+ php.mkdir(virtualDir);
21
+ for (const entry of fs.readdirSync(localPath, { withFileTypes: true })) {
22
+ mountPath(php, path.join(localPath, entry.name), virtualDir + '/' + entry.name);
23
+ }
24
+ } else {
25
+ const ext = path.extname(localPath).toLowerCase();
26
+ if (includeExtensions.has(ext)) {
27
+ const buf = fs.readFileSync(localPath);
28
+ php.writeFile(virtualDir, isBinary(buf) ? buf : buf.toString('utf8'));
29
+ }
30
+ }
31
+ }
32
+
33
+
34
+ const run = async (args) => {
35
+ php.setSpawnHandler((command, args, options) => spawn(command, args, options));
36
+ const output = await php.runStream({
37
+ scriptPath: '/prepros/prepros.php',
38
+ env: { PREPROS_ARGS: JSON.stringify(args), PREPROS_CONFIG: JSON.stringify(preprosConfig) }
39
+ });
40
+ const stdout = await output.stdoutText;
41
+ const stderr = await output.stderrText;
42
+ console.log(stdout);
43
+ let retobj;
44
+ try {
45
+ retobj = JSON.parse(stdout);
46
+ await Promise.all(retobj.files.map(async (file, i) => {
47
+ const buffer = php.readFileAsBuffer(file);
48
+ const dest = file.replace(/^\/project\//i, '');
49
+ fs.writeFileSync(path.join(__project, dest), buffer);
50
+ retobj.files[i] = dest;
51
+ }));
52
+ } catch(e) {
53
+ retobj = { success: false, error: 'Response parsing error.', response: stdout };
54
+ }
55
+ if (stderr) {
56
+ try {
57
+ retobj = JSON.parse(stderr);
58
+ } catch(e) {
59
+ retobj = { success: false, error: stderr };
60
+ }
61
+ }
62
+ return retobj;
63
+ }
64
+
65
+
66
+ const render = async (file = '.') => {
67
+ const target = path.resolve(__project, config?.kirigami?.root, file);
68
+ const fsvm = path.join('/project', config?.kirigami?.root, file).replace(/\\/g, '/');
69
+ mountPath(php, target, fsvm);
70
+ return run([fsvm]);
71
+ }
72
+
73
+
74
+ const sitemap = async (dir) => {
75
+ mountPath(php, __root, '/project/' + config?.kirigami?.root);
76
+ return run(['sitemap']);
77
+ }
78
+
79
+
80
+ if (!fs.existsSync(__configpath)) throw `Config file not found: ${__configpath}`;
81
+ const configContents = fs.readFileSync(__configpath, 'utf8');
82
+ const config = yaml.load(configContents);
83
+ if(!config) throw `Invalid config file: ${__configpath}`;
84
+
85
+
86
+ if(config?.kirigami?.root === undefined) throw `Missing prepros:root property in config file: ${__configpath}`;
87
+ const __root = path.join(__project, config.kirigami.root);
88
+ if (!fs.existsSync(__root)) throw `Invalid prepros:root path: ${__root}`;
89
+
90
+
91
+ const preprosConfig = config.prepros;
92
+ preprosConfig.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
93
+ preprosConfig.root = joinWith('/project/', config?.kirigami?.root);
94
+ preprosConfig.data = config.kirigami || {};
95
+
96
+
97
+ const php = await (preprosConfig.network ? getPHPRuntimeWithNetwork() : getPHPRuntime());
98
+
99
+
100
+ const mountPaths = [];
101
+ const __cache = path.join(__project, '.cache.db');
102
+ mountPath(php, __dirname, '/prepros');
103
+ mountPath(php, joinWith(__project, config?.kirigami?.root), joinWith('/project', config?.kirigami?.root));
104
+ if (fs.existsSync(__cache)) mountPath(php, __cache, '/project/.cache.db');
105
+
106
+
107
+ export { render, sitemap };
@@ -0,0 +1,45 @@
1
+ <?php
2
+
3
+ ini_set('display_errors', 1);
4
+ ini_set('log_errors', 1);
5
+ ini_set('error_log', 'php://stderr');
6
+ ini_set('html_errors', 0);
7
+ ini_set('error_reporting', 32767);
8
+ error_reporting(E_ALL);
9
+
10
+
11
+ $argv = array_merge(['prepros.php'], json_decode(getenv('PREPROS_ARGS'), true));
12
+ $config = json_decode(getenv('PREPROS_CONFIG'));
13
+
14
+
15
+ try {
16
+
17
+ date_default_timezone_set($config->timezone);
18
+ if (!isset($argv[1])) STD::error("Invalid argument.");
19
+ include(__DIR__ . '/utils.inc.php');
20
+ PREPROS::loadConfig($config);
21
+ if($argv[1] == 'sitemap') {
22
+ if(!PREPROS::sitemap()) STD::error("Can't produce the sitemap.");
23
+ STD::succeed(['files' => PREPROS::getExportedFiles()]);
24
+ }
25
+ elseif (!$target = realpath($argv[1])) STD::error("Invalid target.");
26
+ else if (is_dir($target)) {
27
+ $prj = new PREPROS($config);
28
+ foreach (FS::dig($target . '/*.php') as $file) {
29
+ $parent = pathinfo(pathinfo($file, PATHINFO_DIRNAME), PATHINFO_BASENAME);
30
+ if (strpos($parent, '_') === 0) continue;
31
+ if (strpos(pathinfo($file, PATHINFO_FILENAME), '_') !== 0) continue;
32
+ PREPROS::render($file);
33
+ }
34
+ } elseif (preg_match('#^_(.*)\.php$#i', pathinfo($target, PATHINFO_BASENAME), $m)) {
35
+ PREPROS::render($target);
36
+ } else {
37
+ throw new Exception("Invalid target.");
38
+ }
39
+
40
+ } catch(Exception $e) {
41
+ STD::error($e->getMessage());
42
+ }
43
+
44
+
45
+ STD::succeed(['files' => PREPROS::getExportedFiles()]);
@@ -0,0 +1,79 @@
1
+ // Table de magic bytes compilée une seule fois au module load
2
+ // Clé = premier octet, valeur = liste de signatures [octets, longueur à lire]
3
+ const MAGIC_MAP = new Map([
4
+ [0x89, [[[0x89, 0x50, 0x4E, 0x47]]]], // PNG
5
+ [0xFF, [[[0xFF, 0xD8, 0xFF]], [[0xFF, 0xFE]]]], // JPEG, UTF-16 LE (text BOM)
6
+ [0x47, [[[0x47, 0x49, 0x46]]]], // GIF
7
+ [0x25, [[[0x25, 0x50, 0x44, 0x46]]]], // PDF
8
+ [0x52, [[[0x52, 0x49, 0x46, 0x46]]]], // WEBP/WAV (RIFF)
9
+ [0x77, [[[0x77, 0x4F, 0x46, 0x46]], [[0x77, 0x4F, 0x46, 0x32]]]], // WOFF, WOFF2
10
+ [0x1F, [[[0x1F, 0x8B]]]], // GZIP
11
+ [0x50, [[[0x50, 0x4B, 0x03, 0x04]]]], // ZIP
12
+ [0xEF, [[[0xEF, 0xBB, 0xBF]]]], // UTF-8 BOM (text!)
13
+ [0xFE, [[[0xFE, 0xFF]]]], // UTF-16 BE BOM (text!)
14
+ [0x00, [[[0x00, 0x00, 0xFE, 0xFF]]]], // UTF-32 BE BOM (text!)
15
+ ]);
16
+
17
+ // BOMs texte → jamais binaire, early exit immédiat
18
+ const TEXT_BOMS = new Set([
19
+ '\xEF\xBB\xBF', // UTF-8
20
+ '\xFF\xFE', // UTF-16 LE
21
+ '\xFE\xFF', // UTF-16 BE
22
+ ]);
23
+
24
+ const SAMPLE_SIZE = 512; // 8 Ko c'est inutile pour un mount local
25
+
26
+ /**
27
+ * Détection binaire en 3 passes avec early exit agressif.
28
+ * Optimisé pour être appelé sur des centaines de fichiers au démarrage.
29
+ *
30
+ * @param {Buffer} buf - Buffer du fichier (readFileSync)
31
+ * @returns {boolean}
32
+ */
33
+ function isBinary(buf) {
34
+ const len = buf.length;
35
+ if (len === 0) return false;
36
+
37
+ const b0 = buf[0];
38
+
39
+ // Passe 1 — Magic bytes (O(1), max 4 comparaisons)
40
+ const candidates = MAGIC_MAP.get(b0);
41
+ if (candidates) {
42
+ for (const sigs of candidates) {
43
+ for (const sig of sigs) {
44
+ if (sig.every((b, i) => buf[i] === b)) {
45
+ // BOM texte → sortie immédiate "text"
46
+ if (b0 === 0xEF || b0 === 0xFE || (b0 === 0xFF && buf[1] === 0xFE)) return false;
47
+ if (b0 === 0x00 && buf[1] === 0x00) return false; // UTF-32
48
+ return true; // Binaire confirmé
49
+ }
50
+ }
51
+ }
52
+ }
53
+
54
+ const sample = Math.min(len, SAMPLE_SIZE);
55
+
56
+ // Passe 2 — Octet NUL (signal binaire quasi-certain, très fréquent dans les binaires)
57
+ // indexOf est natif C++ → beaucoup plus rapide qu'une boucle JS
58
+ if (buf.indexOf(0x00, 0, 'binary') !== -1 &&
59
+ buf.indexOf(0x00) < sample) return true;
60
+
61
+ // Passe 3 — Ratio statistique sur l'échantillon
62
+ let nonText = 0;
63
+ for (let i = 0; i < sample; i++) {
64
+ const b = buf[i];
65
+ if (b < 0x20) {
66
+ // Caractères de contrôle légitimes en texte
67
+ if (b !== 0x09 && b !== 0x0A && b !== 0x0D && b !== 0x0C && b !== 0x1B) {
68
+ nonText++;
69
+ }
70
+ } else if (b >= 0x80) {
71
+ nonText++;
72
+ }
73
+ }
74
+
75
+ return (nonText / sample) > 0.30;
76
+ }
77
+
78
+
79
+ export { isBinary as default }
@@ -0,0 +1,12 @@
1
+ function joinWith(part1, part2, separator = '/', prefix = '') {
2
+ let join = '';
3
+ let separatorsFound = 0;
4
+ if (part1.endsWith(separator)) { separatorsFound += 1; }
5
+ if (part2.startsWith(separator)) { separatorsFound += 1; }
6
+ if (separatorsFound === 0) { join = separator; }
7
+ else if (separatorsFound === 2) { part1 = part1.substr(0, part1.length - separator.length); }
8
+ if (part1.startsWith(prefix)) { prefix = ''; }
9
+ return prefix + part1 + join + part2;
10
+ }
11
+
12
+ export { joinWith as default }