@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.
@@ -1,29 +1,29 @@
1
1
  <?php
2
2
 
3
3
  /**
4
- * YAML — Parseur YAML léger, entièrement statique.
4
+ * YAML — lightweight, fully static YAML parser.
5
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');
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
- * 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 ---
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
- // Points d'entrée publics
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("Impossible de lire le fichier : $path");
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
- * Charge un fichier YAML ou JSON, puis parcourt récursivement le résultat
59
- * et remplace toute valeur string qui correspond à un chemin relatif vers
60
- * un fichier YAML/JSON existant par le contenu désérialisé de ce fichier.
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
- * Chaque fichier inclus est lui-même résolu relativement à son propre
63
- * répertoire, et ainsi de suite (récursif).
62
+ * Each included file is itself resolved relative to its own directory, and
63
+ * so on (recursive).
64
64
  *
65
- * Si la chaîne ne se termine pas par .yml/.yaml/.json, ou si le fichier
66
- * résolu n'existe pas, la valeur est conservée telle quelle.
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
- * Les références circulaires (ex. A → B → A) lèvent une RuntimeException.
68
+ * Circular references (e.g. A → B → A) throw a RuntimeException.
69
69
  *
70
- * Utilisation :
71
- * $data = YAML::loadFile('/chemin/vers/config.yaml');
72
- * $data = YAML::loadFile('/chemin/vers/config.yaml', true); // arrays assoc
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 Chemin vers le fichier racine (YAML ou JSON).
75
- * @param bool $assoc true → mappings en array, false → stdClass.
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("Impossible de lire le fichier : $path");
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
- // Méthodes privées pour loadFile
89
+ // Private methods for loadFile
90
90
  // -------------------------------------------------------------------------
91
91
 
92
92
  /**
93
- * Charge et résout un fichier, en propageant la liste des ancêtres pour
94
- * détecter les cycles.
93
+ * Loads and resolves a file, propagating the list of ancestors to detect
94
+ * cycles.
95
95
  *
96
- * @param string $absolute Chemin absolu canonique du fichier à charger.
96
+ * @param string $absolute Canonical absolute path of the file to load.
97
97
  * @param bool $assoc
98
- * @param string[] $ancestors Chemins absolus des fichiers en cours de traitement.
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
- "Référence circulaire détectée : " . implode(' → ', $ancestors) . " → $absolute"
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 ou autre extension traitée comme 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
- * Parcourt récursivement une valeur PHP (objet stdClass, array, string,
125
- * scalaire) et résout les références vers des fichiers externes.
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 Répertoire du fichier qui contient ce nœud.
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 → retourné tel quel
153
+ // int, float, bool, null → returned as-is
154
154
  return $node;
155
155
  }
156
156
 
157
157
  /**
158
- * Si la chaîne pointe vers un fichier YAML/JSON existant (chemin relatif
159
- * au répertoire $dir), charge ce fichier récursivement. Sinon retourne la
160
- * chaîne d'origine.
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
- // Filtre rapide sur l'extension
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
- // Résolution du chemin relatif au répertoire du fichier parent
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; // Fichier introuvable → string ordinaire
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
- // Parsing récursif
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
- // Valeur sur les lignes suivantes : sous-bloc ou plain scalar multi-ligne
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 si ce n'est ni un mapping ni une séquence
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
- // Scalaire inline, peut être suivi de lignes de continuation
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
- // Mapping inline dans la séquence :
303
- // On reconstruit un tableau de lignes virtuel en préfixant la première clé
304
- // avec fakeIndent, puis on délègue entièrement à parseMapping pour bénéficier
305
- // de toute sa logique (|, >, plain scalars, sous-blocs…).
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
- // Plain scalar multi-ligne
327
+ // Multi-line plain scalar
328
328
  // -------------------------------------------------------------------------
329
329
 
330
330
  /**
331
- * Collecte un scalaire qui peut continuer sur des lignes plus indentées que $parentIndent.
332
- * Les lignes de continuation sont jointes par un espace (repli implicite).
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
- // Ligne vide : fin du scalaire
342
+ // Blank line: end of the scalar
343
343
  if ($trimmed === '') break;
344
344
 
345
345
  $lineIndent = self::getIndent($raw);
346
346
 
347
- // Retour à l'indentation parente ou moins : fin
347
+ // Back to the parent indentation or less: end
348
348
  if ($lineIndent <= $parentIndent) break;
349
349
 
350
- // Commentaire seul sur la ligne : fin
350
+ // Comment alone on the line: end
351
351
  if (str_starts_with($trimmed, '#')) break;
352
352
 
353
- // C'est un mapping ou une séquence : fin
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
- // Blocs multi-lignes
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
- // Collections inline [a, b] {k: v}
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
- // Scalaires
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
- // Utilitaires
537
+ // Helpers
538
538
  // -------------------------------------------------------------------------
539
539
 
540
540
  private static function getIndent(string $line): int
@@ -0,0 +1,9 @@
1
+ import path from "path";
2
+ import getFileStats from "../utils/getfilestats.js";
3
+
4
+ const __root = process.cwd();
5
+
6
+ export default async function fstat(php, msg) {
7
+ const file = path.join(__root, msg.path);
8
+ return getFileStats(file);
9
+ }
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(__php, __dirname, '/prepros');
42
- mountPath(__php, joinWith(__project, config?.kirigami?.root), joinWith('/project', config?.kirigami?.root));
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
- if (fs.existsSync(__cache)) mountPath(__php, __cache, '/project/.cache.db');
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
- const cmdModule = await import(pathToFileURL(file).href);
55
- const results = await cmdModule.default(__php, msg);
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 = (php, localPath, virtualDir) => {
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
- mountPath(php, path.join(localPath, entry.name), virtualDir + '/' + entry.name);
84
- }
85
- } else {
86
- const ext = path.extname(localPath).toLowerCase();
87
- if (includeExtensions.has(ext)) {
88
- const buf = fs.readFileSync(localPath);
89
- const parentDir = virtualDir.substring(0, virtualDir.lastIndexOf('/'));
90
- if (parentDir) {
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
- retobj = { success: true, files: [], debug: stdout };
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.' + e, debug: stdout, stderr };
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
- retobj = { success: false, error: stderr, debug: stdout };
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
- const render = async (file = '.') => {
170
- const php = await getPHPInstance();
171
- const target = path.resolve(__project, config?.kirigami?.root, file);
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(php, target, fsvm);
174
- if(config?.prepros?.before) {
175
- const include = path.resolve(__project, config?.kirigami?.root, config?.prepros?.before);
176
- const dest = path.join('/project', config?.kirigami?.root, config?.prepros?.before).replace(/\\/g, '/');
177
- mountPath(php, include, dest);
178
- }
179
- if(config?.prepros?.after) {
180
- const include = path.resolve(__project, config?.kirigami?.root, config?.prepros?.after);
181
- const dest = path.join('/project', config?.kirigami?.root, config?.prepros?.after).replace(/\\/g, '/');
182
- mountPath(php, include, dest);
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 (dir) => {
189
- const php = await getPHPInstance();
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(Exception $e) {
28
- STD::error(['message' => $e->getMessage()]);
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(Exception $e) {
14
- STD::error($e->getMessage());
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()]);