@kirigami/php-prepros 1.1.0 → 1.6.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/README.md +1593 -869
- package/index.d.ts +145 -2
- package/index.js +1 -1
- package/package.json +4 -4
- package/src/imagebatch.php +67 -0
- package/src/libraries/aliases.inc.php +836 -0
- package/src/libraries/curl.class.php +20 -3
- package/src/libraries/fs.class.php +161 -6
- package/src/libraries/html.class.php +92 -22
- package/src/libraries/img.class.php +372 -17
- package/src/libraries/ld.class.php +804 -0
- package/src/libraries/md.class.php +502 -173
- package/src/libraries/md.plugins.php +25 -19
- package/src/libraries/normalizer.class.php +310 -0
- package/src/libraries/prepros.class.php +149 -10
- package/src/libraries/prepros.plugins.php +56 -6
- package/src/libraries/schema.class.php +550 -0
- package/src/libraries/scraper.class.php +64 -59
- package/src/libraries/std.class.php +3 -0
- package/src/libraries/str.class.php +50 -16
- package/src/libraries/yaml.class.php +66 -66
- package/src/phpjs/fstat.js +9 -0
- package/src/prepros.js +126 -47
- package/src/prepros.php +9 -2
- package/src/runenv.php +5 -2
- package/src/utils/getfilestats.js +30 -0
- package/src/utils/isbinary.js +14 -14
- package/src/utils.inc.php +20 -9
|
@@ -1,29 +1,29 @@
|
|
|
1
1
|
<?php
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* YAML —
|
|
4
|
+
* YAML — lightweight, fully static YAML parser.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
* $data = YAML::parse($yamlString); // mappings → stdClass (
|
|
8
|
-
* $data = YAML::parse($yamlString, true); // mappings → array
|
|
9
|
-
* $data = YAML::parseFile('/
|
|
6
|
+
* Usage:
|
|
7
|
+
* $data = YAML::parse($yamlString); // mappings → stdClass (default)
|
|
8
|
+
* $data = YAML::parse($yamlString, true); // mappings → associative array
|
|
9
|
+
* $data = YAML::parseFile('/path/to/config.yaml');
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
* -
|
|
13
|
-
* -
|
|
14
|
-
* -
|
|
15
|
-
* -
|
|
16
|
-
* -
|
|
17
|
-
* -
|
|
18
|
-
* -
|
|
19
|
-
* -
|
|
11
|
+
* Supports:
|
|
12
|
+
* - Typed scalars (string, int, float, bool, null)
|
|
13
|
+
* - Single and double quotes (with escape sequences)
|
|
14
|
+
* - Multi-line blocks (| literal and > folded, with chomping -, +)
|
|
15
|
+
* - Multi-line plain scalars (continuation with no indicator)
|
|
16
|
+
* - Nested mappings and sequences
|
|
17
|
+
* - Inline collections [a, b] and {k: v}
|
|
18
|
+
* - Comments (#)
|
|
19
|
+
* - Multiple documents separated by ---
|
|
20
20
|
*/
|
|
21
21
|
class YAML
|
|
22
22
|
{
|
|
23
23
|
private function __construct() {}
|
|
24
24
|
|
|
25
25
|
// -------------------------------------------------------------------------
|
|
26
|
-
//
|
|
26
|
+
// Public entry points
|
|
27
27
|
// -------------------------------------------------------------------------
|
|
28
28
|
|
|
29
29
|
public static function parse(string $yaml, bool $assoc = false): mixed
|
|
@@ -49,60 +49,60 @@ class YAML
|
|
|
49
49
|
public static function parseFile(string $path, bool $assoc = false): mixed
|
|
50
50
|
{
|
|
51
51
|
if (!is_readable($path)) {
|
|
52
|
-
throw new \RuntimeException("
|
|
52
|
+
throw new \RuntimeException("Cannot read file: $path");
|
|
53
53
|
}
|
|
54
54
|
return self::parse(file_get_contents($path), $assoc);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
58
|
+
* Loads a YAML or JSON file, then walks the result recursively and replaces
|
|
59
|
+
* any string value that matches a relative path to an existing YAML/JSON
|
|
60
|
+
* file with that file's deserialized content.
|
|
61
61
|
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
62
|
+
* Each included file is itself resolved relative to its own directory, and
|
|
63
|
+
* so on (recursive).
|
|
64
64
|
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
65
|
+
* If the string doesn't end in .yml/.yaml/.json, or the resolved file
|
|
66
|
+
* doesn't exist, the value is kept as-is.
|
|
67
67
|
*
|
|
68
|
-
*
|
|
68
|
+
* Circular references (e.g. A → B → A) throw a RuntimeException.
|
|
69
69
|
*
|
|
70
|
-
*
|
|
71
|
-
* $data = YAML::loadFile('/
|
|
72
|
-
* $data = YAML::loadFile('/
|
|
70
|
+
* Usage:
|
|
71
|
+
* $data = YAML::loadFile('/path/to/config.yaml');
|
|
72
|
+
* $data = YAML::loadFile('/path/to/config.yaml', true); // assoc arrays
|
|
73
73
|
*
|
|
74
|
-
* @param string $path
|
|
75
|
-
* @param bool $assoc true → mappings
|
|
74
|
+
* @param string $path Path to the root file (YAML or JSON).
|
|
75
|
+
* @param bool $assoc true → mappings as arrays, false → stdClass.
|
|
76
76
|
* @return mixed
|
|
77
77
|
*/
|
|
78
78
|
public static function loadFile(string $path, bool $assoc = false): mixed
|
|
79
79
|
{
|
|
80
80
|
$absolute = realpath($path);
|
|
81
81
|
if ($absolute === false || !is_readable($absolute)) {
|
|
82
|
-
throw new \RuntimeException("
|
|
82
|
+
throw new \RuntimeException("Cannot read file: $path");
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
return self::loadFileRecursive($absolute, $assoc, []);
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
// -------------------------------------------------------------------------
|
|
89
|
-
//
|
|
89
|
+
// Private methods for loadFile
|
|
90
90
|
// -------------------------------------------------------------------------
|
|
91
91
|
|
|
92
92
|
/**
|
|
93
|
-
*
|
|
94
|
-
*
|
|
93
|
+
* Loads and resolves a file, propagating the list of ancestors to detect
|
|
94
|
+
* cycles.
|
|
95
95
|
*
|
|
96
|
-
* @param string $absolute
|
|
96
|
+
* @param string $absolute Canonical absolute path of the file to load.
|
|
97
97
|
* @param bool $assoc
|
|
98
|
-
* @param string[] $ancestors
|
|
98
|
+
* @param string[] $ancestors Absolute paths of the files currently being processed.
|
|
99
99
|
* @return mixed
|
|
100
100
|
*/
|
|
101
101
|
private static function loadFileRecursive(string $absolute, bool $assoc, array $ancestors): mixed
|
|
102
102
|
{
|
|
103
103
|
if (in_array($absolute, $ancestors, true)) {
|
|
104
104
|
throw new \RuntimeException(
|
|
105
|
-
"
|
|
105
|
+
"Circular reference detected: " . implode(' → ', $ancestors) . " → $absolute"
|
|
106
106
|
);
|
|
107
107
|
}
|
|
108
108
|
|
|
@@ -113,7 +113,7 @@ class YAML
|
|
|
113
113
|
if ($ext === 'json') {
|
|
114
114
|
$data = json_decode($raw, $assoc, 512, JSON_THROW_ON_ERROR);
|
|
115
115
|
} else {
|
|
116
|
-
// .yml, .yaml
|
|
116
|
+
// .yml, .yaml or any other extension treated as YAML
|
|
117
117
|
$data = self::parse($raw, $assoc);
|
|
118
118
|
}
|
|
119
119
|
|
|
@@ -121,11 +121,11 @@ class YAML
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
/**
|
|
124
|
-
*
|
|
125
|
-
*
|
|
124
|
+
* Recursively walks a PHP value (stdClass object, array, string, scalar)
|
|
125
|
+
* and resolves references to external files.
|
|
126
126
|
*
|
|
127
127
|
* @param mixed $node
|
|
128
|
-
* @param string $dir
|
|
128
|
+
* @param string $dir Directory of the file that contains this node.
|
|
129
129
|
* @param bool $assoc
|
|
130
130
|
* @param string[] $ancestors
|
|
131
131
|
* @return mixed
|
|
@@ -150,14 +150,14 @@ class YAML
|
|
|
150
150
|
return $node;
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
-
// int, float, bool, null →
|
|
153
|
+
// int, float, bool, null → returned as-is
|
|
154
154
|
return $node;
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
/**
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
158
|
+
* If the string points to an existing YAML/JSON file (path relative to the
|
|
159
|
+
* $dir directory), loads that file recursively. Otherwise returns the
|
|
160
|
+
* original string.
|
|
161
161
|
*
|
|
162
162
|
* @param string $str
|
|
163
163
|
* @param string $dir
|
|
@@ -169,24 +169,24 @@ class YAML
|
|
|
169
169
|
{
|
|
170
170
|
$trimmed = trim($str);
|
|
171
171
|
|
|
172
|
-
//
|
|
172
|
+
// Quick filter on the extension
|
|
173
173
|
if (!preg_match('/\.(ya?ml|json)$/i', $trimmed)) {
|
|
174
174
|
return $str;
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
-
//
|
|
177
|
+
// Resolve the path relative to the parent file's directory
|
|
178
178
|
$candidate = $dir . DIRECTORY_SEPARATOR . $trimmed;
|
|
179
179
|
$absolute = realpath($candidate);
|
|
180
180
|
|
|
181
181
|
if ($absolute === false || !is_readable($absolute)) {
|
|
182
|
-
return $str; //
|
|
182
|
+
return $str; // File not found → plain string
|
|
183
183
|
}
|
|
184
184
|
|
|
185
185
|
return self::loadFileRecursive($absolute, $assoc, $ancestors);
|
|
186
186
|
}
|
|
187
187
|
|
|
188
188
|
// -------------------------------------------------------------------------
|
|
189
|
-
//
|
|
189
|
+
// Recursive parsing
|
|
190
190
|
// -------------------------------------------------------------------------
|
|
191
191
|
|
|
192
192
|
private static function parseBlock(array $lines, int &$pos, int $indent, bool $assoc = false): mixed
|
|
@@ -231,13 +231,13 @@ class YAML
|
|
|
231
231
|
$pos++;
|
|
232
232
|
|
|
233
233
|
if ($rest === null) {
|
|
234
|
-
//
|
|
234
|
+
// Value on the following lines: sub-block or multi-line plain scalar
|
|
235
235
|
self::skipEmptyAndComments($lines, $pos);
|
|
236
236
|
if ($pos < count($lines)) {
|
|
237
237
|
$nextIndent = self::getIndent($lines[$pos]);
|
|
238
238
|
if ($nextIndent > $indent) {
|
|
239
239
|
$nextTrimmed = ltrim($lines[$pos]);
|
|
240
|
-
// Plain scalar
|
|
240
|
+
// Plain scalar if it's neither a mapping nor a sequence
|
|
241
241
|
if (!self::isMapping($nextTrimmed) && !str_starts_with($nextTrimmed, '- ')) {
|
|
242
242
|
$result[$key] = self::parseScalar(
|
|
243
243
|
self::collectPlainScalar($lines, $pos, $indent, '')
|
|
@@ -258,7 +258,7 @@ class YAML
|
|
|
258
258
|
} elseif ($rest !== '' && ($rest[0] === '[' || $rest[0] === '{')) {
|
|
259
259
|
$result[$key] = self::parseInlineCollection($rest, $assoc);
|
|
260
260
|
} else {
|
|
261
|
-
//
|
|
261
|
+
// Inline scalar, may be followed by continuation lines
|
|
262
262
|
$result[$key] = self::parseScalar(
|
|
263
263
|
self::collectPlainScalar($lines, $pos, $indent, $rest)
|
|
264
264
|
);
|
|
@@ -299,10 +299,10 @@ class YAML
|
|
|
299
299
|
$result[] = null;
|
|
300
300
|
}
|
|
301
301
|
} elseif (self::isMapping($itemContent)) {
|
|
302
|
-
//
|
|
303
|
-
//
|
|
304
|
-
//
|
|
305
|
-
//
|
|
302
|
+
// Inline mapping inside the sequence:
|
|
303
|
+
// Rebuild a virtual array of lines by prefixing the first key
|
|
304
|
+
// with fakeIndent, then delegate entirely to parseMapping to get
|
|
305
|
+
// all of its logic (|, >, plain scalars, sub-blocks…).
|
|
306
306
|
$fakeIndent = $lineIndent + 2;
|
|
307
307
|
$fakePrefix = str_repeat(' ', $fakeIndent);
|
|
308
308
|
$virtualLines = array_merge(
|
|
@@ -324,12 +324,12 @@ class YAML
|
|
|
324
324
|
}
|
|
325
325
|
|
|
326
326
|
// -------------------------------------------------------------------------
|
|
327
|
-
//
|
|
327
|
+
// Multi-line plain scalar
|
|
328
328
|
// -------------------------------------------------------------------------
|
|
329
329
|
|
|
330
330
|
/**
|
|
331
|
-
*
|
|
332
|
-
*
|
|
331
|
+
* Collects a scalar that may continue on lines more indented than $parentIndent.
|
|
332
|
+
* Continuation lines are joined with a space (implicit folding).
|
|
333
333
|
*/
|
|
334
334
|
private static function collectPlainScalar(array $lines, int &$pos, int $parentIndent, string $first): string
|
|
335
335
|
{
|
|
@@ -339,18 +339,18 @@ class YAML
|
|
|
339
339
|
$raw = $lines[$pos];
|
|
340
340
|
$trimmed = trim($raw);
|
|
341
341
|
|
|
342
|
-
//
|
|
342
|
+
// Blank line: end of the scalar
|
|
343
343
|
if ($trimmed === '') break;
|
|
344
344
|
|
|
345
345
|
$lineIndent = self::getIndent($raw);
|
|
346
346
|
|
|
347
|
-
//
|
|
347
|
+
// Back to the parent indentation or less: end
|
|
348
348
|
if ($lineIndent <= $parentIndent) break;
|
|
349
349
|
|
|
350
|
-
//
|
|
350
|
+
// Comment alone on the line: end
|
|
351
351
|
if (str_starts_with($trimmed, '#')) break;
|
|
352
352
|
|
|
353
|
-
//
|
|
353
|
+
// It's a mapping or a sequence: end
|
|
354
354
|
if (self::isMapping($trimmed) || str_starts_with($trimmed, '- ')) break;
|
|
355
355
|
|
|
356
356
|
$parts[] = $trimmed;
|
|
@@ -361,7 +361,7 @@ class YAML
|
|
|
361
361
|
}
|
|
362
362
|
|
|
363
363
|
// -------------------------------------------------------------------------
|
|
364
|
-
//
|
|
364
|
+
// Multi-line blocks
|
|
365
365
|
// -------------------------------------------------------------------------
|
|
366
366
|
|
|
367
367
|
private static function parseLiteralBlock(array $lines, int &$pos, int $parentIndent, string $indicator): string
|
|
@@ -435,7 +435,7 @@ class YAML
|
|
|
435
435
|
}
|
|
436
436
|
|
|
437
437
|
// -------------------------------------------------------------------------
|
|
438
|
-
//
|
|
438
|
+
// Inline collections [a, b] {k: v}
|
|
439
439
|
// -------------------------------------------------------------------------
|
|
440
440
|
|
|
441
441
|
private static function parseInlineCollection(string $raw, bool $assoc = false): mixed
|
|
@@ -500,7 +500,7 @@ class YAML
|
|
|
500
500
|
}
|
|
501
501
|
|
|
502
502
|
// -------------------------------------------------------------------------
|
|
503
|
-
//
|
|
503
|
+
// Scalars
|
|
504
504
|
// -------------------------------------------------------------------------
|
|
505
505
|
|
|
506
506
|
private static function parseScalar(string $value): mixed
|
|
@@ -534,7 +534,7 @@ class YAML
|
|
|
534
534
|
}
|
|
535
535
|
|
|
536
536
|
// -------------------------------------------------------------------------
|
|
537
|
-
//
|
|
537
|
+
// Helpers
|
|
538
538
|
// -------------------------------------------------------------------------
|
|
539
539
|
|
|
540
540
|
private static function getIndent(string $line): int
|
package/src/prepros.js
CHANGED
|
@@ -8,6 +8,7 @@ import { walkFile } from '@kirigami/struct-walker';
|
|
|
8
8
|
import { getPHPRuntime, getPHPRuntimeWithNetwork } from "@kirigami/php-wasm";
|
|
9
9
|
|
|
10
10
|
|
|
11
|
+
const __modules = new Map;
|
|
11
12
|
const __project = process.cwd();
|
|
12
13
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
13
14
|
const __configpath = path.join(__project, 'kirigami.yaml');
|
|
@@ -20,39 +21,59 @@ const config = await walkFile(__configpath);
|
|
|
20
21
|
if(!config) throw `Invalid config file: ${__configpath}`;
|
|
21
22
|
|
|
22
23
|
|
|
23
|
-
|
|
24
24
|
const getPHPInstance = async () => {
|
|
25
25
|
if(!__php) {
|
|
26
26
|
if(config?.kirigami?.root === undefined) throw `Missing prepros:root property in config file: ${__configpath}`;
|
|
27
27
|
__root = path.join(__project, config.kirigami.root);
|
|
28
28
|
if (!fs.existsSync(__root)) throw `Invalid prepros:root path: ${__root}`;
|
|
29
29
|
|
|
30
|
+
if(!config.image) config.image = {};
|
|
31
|
+
if(!config.image.format) config.image.format = 'webp';
|
|
32
|
+
if(!config.image.source) config.image.source = 'assets/images/';
|
|
33
|
+
if(!config.image.dest) config.image.dest = 'images/';
|
|
34
|
+
|
|
30
35
|
const preprosConfig = config.prepros || {};
|
|
36
|
+
preprosConfig.image = config.image;
|
|
31
37
|
preprosConfig.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
32
38
|
preprosConfig.root = joinWith('/project/', config?.kirigami?.root);
|
|
33
39
|
preprosConfig.data = config.kirigami || {};
|
|
40
|
+
preprosConfig.jsonld = config.jsonld ?? null;
|
|
41
|
+
// The build task list — read by PREPROS::injectHead() to auto-wire each
|
|
42
|
+
// page's <head> with a <link>/<script> per sass/esbuild task output.
|
|
43
|
+
preprosConfig.tasks = config.tasks || [];
|
|
34
44
|
|
|
35
45
|
__php = await (preprosConfig.network ? getPHPRuntimeWithNetwork() : getPHPRuntime());
|
|
36
46
|
__php.setSpawnHandler((command, args, options) => spawn(command, args, options));
|
|
37
47
|
__php.preprosConfig = preprosConfig;
|
|
48
|
+
__php.setIniValues({
|
|
49
|
+
log_errors: 1,
|
|
50
|
+
html_errors: 0,
|
|
51
|
+
display_errors: 1,
|
|
52
|
+
error_reporting: 32767,
|
|
53
|
+
error_log: 'php://stderr',
|
|
54
|
+
memory_limit: '2G',
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
await mountPath(__dirname, '/prepros', __php);
|
|
58
|
+
await mountPath(joinWith(__project, config?.kirigami?.root), joinWith('/project', config?.kirigami?.root), __php);
|
|
38
59
|
|
|
39
60
|
const __cache = path.join(__project, '.cache.db');
|
|
40
61
|
const __cookie = path.join(__project, '.cookie.txt');
|
|
41
|
-
mountPath(
|
|
42
|
-
|
|
62
|
+
if (fs.existsSync(__cache)) await mountPath(__cache, '/project/.cache.db', __php);
|
|
63
|
+
if (fs.existsSync(__cookie)) await mountPath(__cookie, '/project/.cookie.txt', __php);
|
|
43
64
|
|
|
44
|
-
|
|
45
|
-
if (fs.existsSync(__cookie)) mountPath(__php, __cookie, '/project/.cookie.txt');
|
|
46
|
-
|
|
47
|
-
__php.onMessage(async (data) => {
|
|
65
|
+
__php.onMessage(async data => {
|
|
48
66
|
const msg = JSON.parse(data);
|
|
49
67
|
if(!msg.command) return { success: false, error: "Invalid command."};
|
|
50
68
|
const file = path.join(__dirname, 'phpjs', `${msg.command}.js`);
|
|
51
69
|
if(!fs.existsSync(file)) return { success: false, error: "Invalid command."};
|
|
52
|
-
|
|
53
70
|
try {
|
|
54
|
-
|
|
55
|
-
|
|
71
|
+
if(!__modules.has(msg.command)) {
|
|
72
|
+
const cmdModule = await import(pathToFileURL(file).href);
|
|
73
|
+
__modules.set(msg.command, cmdModule);
|
|
74
|
+
}
|
|
75
|
+
const mod = __modules.get(msg.command);
|
|
76
|
+
const results = await mod.default(__php, msg);
|
|
56
77
|
return JSON.stringify({
|
|
57
78
|
success: true,
|
|
58
79
|
results: results
|
|
@@ -63,35 +84,36 @@ const getPHPInstance = async () => {
|
|
|
63
84
|
error: typeof e == 'string' ? e : e.message
|
|
64
85
|
}
|
|
65
86
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
// console.log(msg);
|
|
69
|
-
// console.log(__dirname);
|
|
70
|
-
|
|
71
87
|
});
|
|
72
88
|
}
|
|
73
89
|
return __php;
|
|
74
90
|
}
|
|
75
91
|
|
|
76
92
|
|
|
77
|
-
const mountPath = (
|
|
93
|
+
const mountPath = async (localPath, virtualDir, php) => {
|
|
94
|
+
php = php || await getPHPInstance();
|
|
95
|
+
if(!path.isAbsolute(localPath)) localPath = path.join(__project, localPath);
|
|
96
|
+
virtualDir = virtualDir || path.posix.join('/project', localPath.replace(__project + path.sep, ''));
|
|
78
97
|
const includeExtensions = new Set(['.php', '.json', '.yaml', '.yml', '.md', '.db', '.txt', ...(config?.prepros?.mountext || [])]);
|
|
79
98
|
const stat = fs.statSync(localPath);
|
|
80
99
|
if (stat.isDirectory()) {
|
|
81
100
|
php.mkdirTree(virtualDir);
|
|
82
101
|
for (const entry of fs.readdirSync(localPath, { withFileTypes: true })) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
php.mkdirTree(parentDir);
|
|
102
|
+
const target = path.join(localPath, entry.name);
|
|
103
|
+
if(!fs.statSync(target).isDirectory()) {
|
|
104
|
+
const ext = path.extname(target).toLowerCase();
|
|
105
|
+
if (includeExtensions.has(ext)) {
|
|
106
|
+
await mountPath(target, virtualDir + '/' + entry.name, php);
|
|
107
|
+
}
|
|
108
|
+
} else {
|
|
109
|
+
await mountPath(target, virtualDir + '/' + entry.name, php);
|
|
92
110
|
}
|
|
93
|
-
php.writeFile(virtualDir, isBinary(buf) ? buf : buf.toString('utf8'));
|
|
94
111
|
}
|
|
112
|
+
} else {
|
|
113
|
+
const buf = fs.readFileSync(localPath);
|
|
114
|
+
const parentDir = virtualDir.substring(0, virtualDir.lastIndexOf('/'));
|
|
115
|
+
if (parentDir) php.mkdirTree(parentDir);
|
|
116
|
+
php.writeFile(virtualDir, isBinary(buf) ? buf : buf.toString('utf8'));
|
|
95
117
|
}
|
|
96
118
|
}
|
|
97
119
|
|
|
@@ -129,7 +151,6 @@ const run = async (args = [], script = null, mountfiles = []) => {
|
|
|
129
151
|
const buffer = php.readFileAsBuffer(resultPath);
|
|
130
152
|
retobj = JSON.parse(Buffer.from(buffer).toString('utf8'));
|
|
131
153
|
retobj.debug = stdout;
|
|
132
|
-
|
|
133
154
|
if(retobj.files) await Promise.all(retobj.files.map(async (file, i) => {
|
|
134
155
|
const fbuffer = php.readFileAsBuffer(file);
|
|
135
156
|
const dest = file.replace(/^\/project\//i, '');
|
|
@@ -139,23 +160,60 @@ const run = async (args = [], script = null, mountfiles = []) => {
|
|
|
139
160
|
retobj.files[i] = dest;
|
|
140
161
|
}));
|
|
141
162
|
} else {
|
|
142
|
-
|
|
163
|
+
// No result file: PHP exited before STD::succeed()/error() — a
|
|
164
|
+
// parse error, an OOM, a die(), a hard crash. Pull whatever
|
|
165
|
+
// explanation we can out of the streams instead of a silent pass.
|
|
166
|
+
const crash = extractPhpError(stderr) || extractPhpError(stdout);
|
|
167
|
+
retobj = {
|
|
168
|
+
success: false,
|
|
169
|
+
files: [],
|
|
170
|
+
error: crash || 'PHP produced no result (exited early — check the debug output).',
|
|
171
|
+
debug: stdout,
|
|
172
|
+
stderr,
|
|
173
|
+
};
|
|
143
174
|
}
|
|
144
175
|
} catch (e) {
|
|
145
|
-
retobj = { success: false, error: 'Response parsing error
|
|
176
|
+
retobj = { success: false, error: 'Response parsing error: ' + (e?.message || e), debug: stdout, stderr };
|
|
146
177
|
} finally {
|
|
147
178
|
if (await php.fileExists(resultPath)) {
|
|
148
179
|
await php.unlink(resultPath);
|
|
149
180
|
}
|
|
150
181
|
}
|
|
182
|
+
|
|
183
|
+
// A fatal that still managed to write a (stale/partial) result file, or
|
|
184
|
+
// warnings printed to stderr. Only a genuine fatal marker overrides a
|
|
185
|
+
// result the PHP side reported as a success; plain warnings are attached
|
|
186
|
+
// as `warnings` so a stray notice doesn't sink an otherwise-clean build.
|
|
151
187
|
if (stderr) {
|
|
152
|
-
|
|
188
|
+
const fatal = extractPhpError(stderr);
|
|
189
|
+
if (fatal && (!retobj || retobj.success !== false)) {
|
|
190
|
+
retobj = { success: false, files: [], error: fatal, debug: stdout, stderr };
|
|
191
|
+
} else if (retobj && retobj.success && !fatal) {
|
|
192
|
+
retobj.warnings = stderr.trim();
|
|
193
|
+
} else if (retobj && retobj.success === false && !retobj.stderr) {
|
|
194
|
+
retobj.stderr = stderr.trim();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Never hand back a failure without a usable message.
|
|
199
|
+
if (retobj && retobj.success === false && !retobj.error) {
|
|
200
|
+
retobj.error = retobj.message || retobj.stderr || 'Unknown error.';
|
|
153
201
|
}
|
|
154
202
|
|
|
155
203
|
return retobj;
|
|
156
204
|
}
|
|
157
205
|
|
|
158
206
|
|
|
207
|
+
// Pulls the first meaningful PHP error line out of a stdout/stderr blob
|
|
208
|
+
// (fatals, parse errors, uncaught throwables). Returns null when the text
|
|
209
|
+
// holds nothing that looks like a hard error.
|
|
210
|
+
const extractPhpError = (text) => {
|
|
211
|
+
if (!text) return null;
|
|
212
|
+
const m = text.match(/^.*\b(?:PHP\s+)?(?:Fatal error|Parse error|Uncaught\s+\w+|Recoverable fatal error)\b.*$/mi);
|
|
213
|
+
return m ? m[0].trim() : null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
|
|
159
217
|
const runenv = async (script, paths = [], ...args) => {
|
|
160
218
|
if(!script) throw "Missing PHP file.";
|
|
161
219
|
const file = path.resolve(script);
|
|
@@ -166,30 +224,51 @@ const runenv = async (script, paths = [], ...args) => {
|
|
|
166
224
|
}
|
|
167
225
|
|
|
168
226
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
227
|
+
// Run a batch of image jobs (resize / palette) through /prepros/imagebatch.php,
|
|
228
|
+
// i.e. the IMG class (GD + Imagick). Used by @kirigami/kirigami's `sass` task so
|
|
229
|
+
// that img-asset() / colors() and the PHP IMG::asset() / <img asset> tag share
|
|
230
|
+
// one engine. `jobs` is the list documented in imagebatch.php; the result is a
|
|
231
|
+
// PreprosResult plus a `colors` map ({ "<src>:<count>": ["#rrggbb", …] }).
|
|
232
|
+
const processImages = async (jobs = []) => {
|
|
233
|
+
if (!Array.isArray(jobs) || !jobs.length) return { success: true, files: [], colors: {} };
|
|
234
|
+
const result = await run(['imagebatch', JSON.stringify(jobs)], '/prepros/imagebatch.php');
|
|
235
|
+
if (!result.colors) result.colors = {};
|
|
236
|
+
if (!result.files) result.files = [];
|
|
237
|
+
return result;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
const render = async (file = '.', phpIncludes = []) => {
|
|
242
|
+
const target = path.resolve(config?.kirigami?.root, file);
|
|
172
243
|
const fsvm = path.join('/project', config?.kirigami?.root, file).replace(/\\/g, '/');
|
|
173
|
-
mountPath(
|
|
174
|
-
if(config?.prepros?.before)
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const
|
|
182
|
-
|
|
244
|
+
await mountPath(target);
|
|
245
|
+
if(config?.prepros?.before) await mountPath(path.resolve(config?.kirigami?.root, config?.prepros?.before));
|
|
246
|
+
if(config?.prepros?.after) await mountPath(path.resolve(config?.kirigami?.root, config?.prepros?.after));
|
|
247
|
+
|
|
248
|
+
// Extra PHP files contributed by plugins (the kiri 'prepros:php' hook):
|
|
249
|
+
// mounted outside /project and include_once'd once, before any page
|
|
250
|
+
// renders, so they can PREPROS::registerTag()/registerHook() from PHP.
|
|
251
|
+
if (Array.isArray(phpIncludes) && phpIncludes.length) {
|
|
252
|
+
const php = await getPHPInstance();
|
|
253
|
+
const mounted = [];
|
|
254
|
+
for (let i = 0; i < phpIncludes.length; i++) {
|
|
255
|
+
const abs = path.resolve(phpIncludes[i]);
|
|
256
|
+
if (!fs.existsSync(abs)) continue;
|
|
257
|
+
const virt = `/plugins/${i}_${path.basename(abs)}`;
|
|
258
|
+
await mountPath(abs, virt, php);
|
|
259
|
+
mounted.push(virt);
|
|
260
|
+
}
|
|
261
|
+
php.preprosConfig.phpIncludes = mounted;
|
|
183
262
|
}
|
|
263
|
+
|
|
184
264
|
return run([fsvm]);
|
|
185
265
|
}
|
|
186
266
|
|
|
187
267
|
|
|
188
|
-
const sitemap = async (
|
|
189
|
-
|
|
190
|
-
mountPath(php, __root, '/project/' + config?.kirigami?.root);
|
|
268
|
+
const sitemap = async () => {
|
|
269
|
+
await mountPath(config?.kirigami?.root);
|
|
191
270
|
return run(['sitemap']);
|
|
192
271
|
}
|
|
193
272
|
|
|
194
273
|
|
|
195
|
-
export { runenv, render, sitemap };
|
|
274
|
+
export { runenv, render, sitemap, mountPath, processImages };
|
package/src/prepros.php
CHANGED
|
@@ -24,8 +24,15 @@ try {
|
|
|
24
24
|
throw new Exception("Invalid target.");
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
} catch(
|
|
28
|
-
|
|
27
|
+
} catch(Throwable $e) {
|
|
28
|
+
// Catch Error/TypeError too, not just Exception, so a fatal in a template
|
|
29
|
+
// (a bad call, a null arg, …) still comes back as a structured result with
|
|
30
|
+
// a real message and the page that blew up — never a bare "Error: undefined".
|
|
31
|
+
STD::error([
|
|
32
|
+
'error' => $e->getMessage(),
|
|
33
|
+
'page' => PREPROS::$file ?: ($argv[1] ?? null),
|
|
34
|
+
'where' => $e->getFile() . ':' . $e->getLine(),
|
|
35
|
+
]);
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
|
package/src/runenv.php
CHANGED
|
@@ -10,8 +10,11 @@ try {
|
|
|
10
10
|
|
|
11
11
|
require_once($argv[1]);
|
|
12
12
|
|
|
13
|
-
} catch(
|
|
14
|
-
STD::error(
|
|
13
|
+
} catch(Throwable $e) {
|
|
14
|
+
STD::error([
|
|
15
|
+
'error' => $e->getMessage(),
|
|
16
|
+
'where' => $e->getFile() . ':' . $e->getLine(),
|
|
17
|
+
]);
|
|
15
18
|
}
|
|
16
19
|
|
|
17
20
|
STD::succeed(['files' => PREPROS::getExportedFiles()]);
|