@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,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 };
|
package/src/prepros.php
ADDED
|
@@ -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 }
|