@kirigami/php-prepros 1.2.0 → 1.6.1
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 +491 -12
- package/index.d.ts +101 -31
- package/index.js +1 -1
- package/package.json +1 -1
- package/src/imagebatch.php +67 -0
- package/src/libraries/aliases.inc.php +836 -0
- package/src/libraries/curl.class.php +15 -3
- package/src/libraries/fs.class.php +161 -6
- package/src/libraries/html.class.php +69 -0
- package/src/libraries/img.class.php +19 -4
- package/src/libraries/ld.class.php +804 -0
- package/src/libraries/md.class.php +26 -3
- package/src/libraries/md.plugins.php +7 -1
- package/src/libraries/prepros.class.php +119 -5
- package/src/libraries/prepros.plugins.php +32 -6
- package/src/libraries/std.class.php +3 -0
- package/src/libraries/str.class.php +1 -1
- package/src/prepros.js +80 -5
- package/src/prepros.php +9 -2
- package/src/runenv.php +5 -2
- package/src/utils.inc.php +11 -1
|
@@ -568,6 +568,10 @@ class MD {
|
|
|
568
568
|
// Processed before XSS encoding — re-injected as the very last step.
|
|
569
569
|
// ====================================================================
|
|
570
570
|
$pluginBlocks = [];
|
|
571
|
+
// Literal, XSS-escaped source of each captured tag, keyed by the same
|
|
572
|
+
// placeholder. Used to restore a `{% tag %}` that turns out to sit
|
|
573
|
+
// inside a code span / code block as verbatim text instead of expanding it.
|
|
574
|
+
$pluginLiterals = [];
|
|
571
575
|
|
|
572
576
|
/**
|
|
573
577
|
* Parses an argument string into an array.
|
|
@@ -596,7 +600,7 @@ class MD {
|
|
|
596
600
|
// Group 2: inline args (everything on the first line after the name)
|
|
597
601
|
// Group 3: multi-line body (present only for block tags)
|
|
598
602
|
'/\{%\s*([a-zA-Z0-9_-]+)([^\n%]*?)(?:\n([\s\S]*?))?\s*%\}/m',
|
|
599
|
-
function ($matches) use (&$pluginBlocks, $parseArgs): string {
|
|
603
|
+
function ($matches) use (&$pluginBlocks, &$pluginLiterals, $parseArgs): string {
|
|
600
604
|
$name = strtolower(trim($matches[1]));
|
|
601
605
|
$args = $parseArgs(trim($matches[2] ?? ''));
|
|
602
606
|
// $matches[3] exists only if the tag is multi-line
|
|
@@ -609,7 +613,8 @@ class MD {
|
|
|
609
613
|
|
|
610
614
|
$output = (self::$plugins[$name])($args, $body);
|
|
611
615
|
$placeholder = "\x02PLG" . count($pluginBlocks) . "\x03";
|
|
612
|
-
$pluginBlocks[$placeholder]
|
|
616
|
+
$pluginBlocks[$placeholder] = $output;
|
|
617
|
+
$pluginLiterals[$placeholder] = htmlspecialchars($matches[0], ENT_QUOTES, 'UTF-8');
|
|
613
618
|
return $placeholder;
|
|
614
619
|
},
|
|
615
620
|
$html
|
|
@@ -726,6 +731,16 @@ class MD {
|
|
|
726
731
|
return $placeholder;
|
|
727
732
|
}, $html);
|
|
728
733
|
|
|
734
|
+
// A `{% tag %}` sitting inside a code span or code block was captured
|
|
735
|
+
// by STEP 2 and is now a plugin placeholder embedded in the stored
|
|
736
|
+
// code. Swap those back for the literal (escaped) tag source so code
|
|
737
|
+
// shows `{% tag %}` verbatim instead of its rendered output — or a
|
|
738
|
+
// stray control-char placeholder that never gets restored.
|
|
739
|
+
if ($pluginLiterals) {
|
|
740
|
+
foreach ($codeBlocks as $k => $v) $codeBlocks[$k] = strtr($v, $pluginLiterals);
|
|
741
|
+
foreach ($inlineCodes as $k => $v) $inlineCodes[$k] = strtr($v, $pluginLiterals);
|
|
742
|
+
}
|
|
743
|
+
|
|
729
744
|
|
|
730
745
|
// ====================================================================
|
|
731
746
|
// STEP 3b: CHARACTER ESCAPING (\* \_ \# etc.)
|
|
@@ -1222,4 +1237,12 @@ class MD {
|
|
|
1222
1237
|
|
|
1223
1238
|
return $html;
|
|
1224
1239
|
}
|
|
1225
|
-
}
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
|
|
1243
|
+
// Default Markdown plugins ({% codepen %}, {% youtube %}, {% checklist %},
|
|
1244
|
+
// {% callout %}) — "registered out of the box" per the README. Loaded here so
|
|
1245
|
+
// every entrypoint (prepros.php, runenv.php, imagebatch.php) gets them without
|
|
1246
|
+
// an explicit include. A project can still MD::unregisterPlugin() any of them,
|
|
1247
|
+
// or MD::registerPlugin() its own with the same name to override.
|
|
1248
|
+
include_once(__DIR__ . '/md.plugins.php');
|
|
@@ -3,7 +3,13 @@
|
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
// =============================================================================
|
|
6
|
-
// DEFAULT PLUGINS
|
|
6
|
+
// DEFAULT MARKDOWN PLUGINS
|
|
7
|
+
//
|
|
8
|
+
// Auto-loaded from md.class.php, so these are available in every render without
|
|
9
|
+
// any setup ("registered out of the box"). To drop one, call
|
|
10
|
+
// MD::unregisterPlugin('name') (or md_unregister_plugin('name')) from an
|
|
11
|
+
// `includes` file; to change one, MD::registerPlugin() your own under the same
|
|
12
|
+
// name.
|
|
7
13
|
// =============================================================================
|
|
8
14
|
|
|
9
15
|
// --- CodePen (inline) ---
|
|
@@ -20,6 +20,11 @@ final class PREPROS
|
|
|
20
20
|
if (!is_file(realpath(self::$root . $path))) continue;
|
|
21
21
|
else include_once(realpath(self::$root . $path));
|
|
22
22
|
}
|
|
23
|
+
// Plugin-contributed PHP (kiri 'prepros:php' hook): absolute virtual
|
|
24
|
+
// paths, mounted outside the project by @kirigami/php-prepros.
|
|
25
|
+
if (!empty($config->phpIncludes)) foreach ($config->phpIncludes as $path) {
|
|
26
|
+
if (is_file($path)) include_once($path);
|
|
27
|
+
}
|
|
23
28
|
}
|
|
24
29
|
|
|
25
30
|
|
|
@@ -30,8 +35,12 @@ final class PREPROS
|
|
|
30
35
|
if(!$file = realpath($file)) return false;
|
|
31
36
|
self::$file = $file;
|
|
32
37
|
|
|
33
|
-
//
|
|
34
|
-
|
|
38
|
+
// absolute URL path of the page's dir, prefixed with the baseurl's
|
|
39
|
+
// subpath (if any) so it stays valid when the site is deployed under a
|
|
40
|
+
// subfolder, e.g. https://kirigami.github.io/template/
|
|
41
|
+
$basepath = rtrim((string)parse_url(self::$config->data->baseurl, PHP_URL_PATH), '/');
|
|
42
|
+
$reldir = str_replace('\\', '/', pathinfo(str_replace(realpath(self::$root), '', $file), PATHINFO_DIRNAME));
|
|
43
|
+
$absurl = preg_replace('#/+#', '/', $basepath . '/' . trim($reldir, '/') . '/');
|
|
35
44
|
|
|
36
45
|
$relroot = FS::getRelativePath($dir, self::$root);
|
|
37
46
|
$page = self::processHook('page_info', [$file, FS::phpFileInfo($file)]);
|
|
@@ -42,8 +51,9 @@ final class PREPROS
|
|
|
42
51
|
self::processHook('pre_render', file_get_contents($file));
|
|
43
52
|
|
|
44
53
|
ob_start();
|
|
54
|
+
self::processHook('pre_before', self::$config->before);
|
|
45
55
|
if (self::$config->before) include(realpath(self::$root . self::$config->before));
|
|
46
|
-
$header = ob_get_clean();
|
|
56
|
+
$header = self::processHook('post_before', ob_get_clean());
|
|
47
57
|
|
|
48
58
|
if(empty($content)) {
|
|
49
59
|
ob_start();
|
|
@@ -58,15 +68,22 @@ final class PREPROS
|
|
|
58
68
|
}
|
|
59
69
|
|
|
60
70
|
ob_start();
|
|
71
|
+
self::processHook('pre_after', self::$config->after);
|
|
61
72
|
if (self::$config->after) include(realpath(self::$root . self::$config->after));
|
|
62
|
-
$footer = ob_get_clean();
|
|
73
|
+
$footer = self::processHook('post_after', ob_get_clean());
|
|
63
74
|
|
|
64
75
|
$contents = $header . $body . $footer;
|
|
65
76
|
$contents = self::processTags($contents);
|
|
66
77
|
$contents = self::processHook('post_render', $contents);
|
|
67
78
|
|
|
79
|
+
if ((self::$config->head ?? true) !== false) {
|
|
80
|
+
$contents = self::injectHead($contents, $relroot);
|
|
81
|
+
}
|
|
82
|
+
|
|
68
83
|
if(!empty(self::$config->format)) $contents = HTML::format($contents);
|
|
69
84
|
|
|
85
|
+
$contents = self::replaceTokens($contents);
|
|
86
|
+
|
|
70
87
|
file_put_contents($target, $contents);
|
|
71
88
|
self::exportFile($target);
|
|
72
89
|
self::$file = '';
|
|
@@ -116,6 +133,93 @@ final class PREPROS
|
|
|
116
133
|
}
|
|
117
134
|
|
|
118
135
|
|
|
136
|
+
/**
|
|
137
|
+
* Auto-wires each page: a theme/FOUC guard as the first child of <head>, a
|
|
138
|
+
* <link> for every `sass` task output, a <script> (no `defer`, before
|
|
139
|
+
* </body>) for every `esbuild` task output, and — when `format` is on — a
|
|
140
|
+
* tiny de-indent script that flattens the leading whitespace HTML::format()
|
|
141
|
+
* adds inside `<pre><code>`. Paths use $relroot so they work at any depth.
|
|
142
|
+
*
|
|
143
|
+
* Runs unless `prepros.head` is `false`. A single task opts out with
|
|
144
|
+
* `head: false`. A file already referenced in the page is left alone.
|
|
145
|
+
* `###TIMESTAMP###` is expanded by replaceTokens() right after.
|
|
146
|
+
*/
|
|
147
|
+
private static function injectHead(string $contents, string $relroot): string
|
|
148
|
+
{
|
|
149
|
+
$inject = [];
|
|
150
|
+
|
|
151
|
+
// Theme guard — skip if the page already carries one.
|
|
152
|
+
if (!str_contains($contents, 'kirigami-theme')) {
|
|
153
|
+
$inject[] = '<script>(function(){var e=document.documentElement;'
|
|
154
|
+
. 'e.classList.add("js");try{var t=localStorage.getItem("kirigami-theme");'
|
|
155
|
+
. 'if(t==="dark"||t==="light")e.setAttribute("data-theme",t)}catch(x){}})();</script>';
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
$links = [];
|
|
159
|
+
$scripts = [];
|
|
160
|
+
|
|
161
|
+
// Undo the <pre><code> indentation HTML::format() added, at parse time
|
|
162
|
+
// (before first paint), for blocks a build-time highlighter didn't
|
|
163
|
+
// already flatten (those carry child <span>s).
|
|
164
|
+
if (!empty(self::$config->format)) {
|
|
165
|
+
$scripts[] = '<script>document.querySelectorAll("pre>code").forEach(function(c){'
|
|
166
|
+
. 'if(c.children.length)return;'
|
|
167
|
+
. 'var L=c.textContent.replace(/^\n+/,"").replace(/\s+$/,"").split("\n"),n=1/0;'
|
|
168
|
+
. 'L.forEach(function(l){if(l.trim())n=Math.min(n,l.match(/^\s*/)[0].length)});'
|
|
169
|
+
. 'if(n&&n<1/0)c.textContent=L.map(function(l){return l.slice(n)}).join("\n")});</script>';
|
|
170
|
+
}
|
|
171
|
+
foreach ((array) (self::$config->tasks ?? []) as $task) {
|
|
172
|
+
$task = (array) $task;
|
|
173
|
+
if (($task['head'] ?? true) === false) continue;
|
|
174
|
+
$entry = (string) ($task['entry'] ?? '');
|
|
175
|
+
if ($entry === '') continue;
|
|
176
|
+
|
|
177
|
+
if (($task['type'] ?? '') === 'sass') {
|
|
178
|
+
$out = preg_replace('/\.s?css$/', '.min.css', $entry);
|
|
179
|
+
if ($out !== $entry && !str_contains($contents, $out)) {
|
|
180
|
+
$links[] = '<link rel="stylesheet" href="' . $relroot . $out . '?###TIMESTAMP###">';
|
|
181
|
+
}
|
|
182
|
+
} elseif (($task['type'] ?? '') === 'esbuild') {
|
|
183
|
+
$out = preg_replace('/\.[jt]s$/', '.min.js', $entry);
|
|
184
|
+
if ($out !== $entry && !str_contains($contents, $out)) {
|
|
185
|
+
$scripts[] = '<script src="' . $relroot . $out . '?###TIMESTAMP###"></script>';
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
$head = implode("\n ", array_merge($inject, $links));
|
|
191
|
+
if ($head !== '') {
|
|
192
|
+
// Slot it right after <meta charset> when there is one (keep the
|
|
193
|
+
// charset first), otherwise right after <head>.
|
|
194
|
+
if (preg_match('/<meta\s+charset=[^>]*>/i', $contents)) {
|
|
195
|
+
$contents = preg_replace('/(<meta\s+charset=[^>]*>)/i', "$1\n " . $head, $contents, 1);
|
|
196
|
+
} elseif (preg_match('/<head\b[^>]*>/i', $contents)) {
|
|
197
|
+
$contents = preg_replace('/(<head\b[^>]*>)/i', "$1\n " . $head, $contents, 1);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if ($scripts && preg_match('/<\/body>/i', $contents)) {
|
|
201
|
+
$contents = preg_replace('/(<\/body>)/i', " " . implode("\n ", $scripts) . "\n$1", $contents, 1);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return $contents;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Expands the build-time text tokens in a generated file. Runs at render
|
|
210
|
+
* time (so `kiri build` / `kiri watch` previews show real values, not the
|
|
211
|
+
* literal `###YEAR###`), and again — harmlessly — on export.
|
|
212
|
+
*/
|
|
213
|
+
private static function replaceTokens(string $contents): string
|
|
214
|
+
{
|
|
215
|
+
return strtr($contents, [
|
|
216
|
+
'###YEAR###' => date('Y'),
|
|
217
|
+
'###TIMESTAMP###' => (string) time(),
|
|
218
|
+
'###TODAY###' => date('Y-m-d'),
|
|
219
|
+
]);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
|
|
119
223
|
public static function exportFile(string|array $file): void
|
|
120
224
|
{
|
|
121
225
|
if(is_string($file)) $file = [$file];
|
|
@@ -128,7 +232,11 @@ final class PREPROS
|
|
|
128
232
|
|
|
129
233
|
public static function getExportedFiles(): array
|
|
130
234
|
{
|
|
131
|
-
|
|
235
|
+
// array_unique() keeps the original keys, so any duplicate leaves a gap
|
|
236
|
+
// in the sequence — json_encode() would then emit a JSON object instead
|
|
237
|
+
// of an array and the JS side chokes ("retobj.files.map is not a
|
|
238
|
+
// function"). array_values() reindexes so it always serialises as a list.
|
|
239
|
+
$files = array_values(array_unique(self::$files));
|
|
132
240
|
// sort($files);
|
|
133
241
|
return $files;
|
|
134
242
|
}
|
|
@@ -174,6 +282,12 @@ final class PREPROS
|
|
|
174
282
|
}
|
|
175
283
|
|
|
176
284
|
|
|
285
|
+
public static function runHook(string $hook, $data = null)
|
|
286
|
+
{
|
|
287
|
+
return self::processHook($hook, $data);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
|
|
177
291
|
private static function processHook(string $hook, $data = null)
|
|
178
292
|
{
|
|
179
293
|
if (!empty(self::$hooks[$hook])) {
|
|
@@ -32,21 +32,47 @@ PREPROS::registerTag('img', function ($tag, $attrs, $body) {
|
|
|
32
32
|
|
|
33
33
|
|
|
34
34
|
PREPROS::registerHook('page_info', function($info) {
|
|
35
|
-
|
|
35
|
+
// The hook fires with [$file, $pageInfo]; every callback then returns just
|
|
36
|
+
// the (possibly modified) $pageInfo, so a callback registered after this
|
|
37
|
+
// one receives the bare object. Accept either shape.
|
|
38
|
+
if (is_array($info)) {
|
|
39
|
+
[$file, $page] = $info;
|
|
40
|
+
} else {
|
|
41
|
+
$page = $info;
|
|
42
|
+
$file = PREPROS::$file;
|
|
43
|
+
}
|
|
36
44
|
foreach($page as $k => $v) {
|
|
37
45
|
$ext = strtolower(pathinfo($v, PATHINFO_EXTENSION));
|
|
38
46
|
if(in_array($ext, ['yaml', 'yml', 'json', 'md']) ) {
|
|
39
|
-
|
|
47
|
+
$isUrl = STR::is_url($v);
|
|
48
|
+
if($isUrl) $filename = $v;
|
|
40
49
|
else $filename = pathinfo(realpath($file), PATHINFO_DIRNAME) . '/' . $v;
|
|
41
|
-
if(
|
|
50
|
+
if($isUrl || is_file($filename)) {
|
|
51
|
+
// Remote URLs always go through CURL::getContents — never
|
|
52
|
+
// file_get_contents, which this PHP build can't use for http(s).
|
|
53
|
+
$raw = $isUrl ? CURL::getContents($filename) : file_get_contents($filename);
|
|
54
|
+
if($raw === false) continue;
|
|
42
55
|
$page->{$k} = match ($ext) {
|
|
43
|
-
'yml', 'yaml' => YAML::
|
|
44
|
-
'json' => json_decode(
|
|
45
|
-
'md' => MD::toHtml(
|
|
56
|
+
'yml', 'yaml' => YAML::parse($raw),
|
|
57
|
+
'json' => json_decode($raw),
|
|
58
|
+
'md' => MD::toHtml($raw),
|
|
46
59
|
default => $filename,
|
|
47
60
|
};
|
|
48
61
|
}
|
|
49
62
|
}
|
|
50
63
|
}
|
|
51
64
|
return $page;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
// LD — capture the page under render, then inject the automatic
|
|
69
|
+
// schema.org JSON-LD `<script>` into its `<head>` once the HTML is assembled.
|
|
70
|
+
// Registered after the data-loading hook above so `$page` arrives resolved.
|
|
71
|
+
PREPROS::registerHook('page_info', function($page) {
|
|
72
|
+
LD::capture($page);
|
|
73
|
+
return $page;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
PREPROS::registerHook('post_render', function($html) {
|
|
77
|
+
return LD::inject($html);
|
|
52
78
|
});
|
|
@@ -24,6 +24,9 @@ class STD {
|
|
|
24
24
|
$return->files = PREPROS::getExportedFiles();
|
|
25
25
|
if(is_string($props)) $return->error = $props;
|
|
26
26
|
else foreach($props as $k => $v) $return->{$k} = $v;
|
|
27
|
+
// Consumers read `->error`; keep it populated whatever key the caller
|
|
28
|
+
// used (some pass `message`) so nothing ever surfaces as "undefined".
|
|
29
|
+
if(empty($return->error)) $return->error = $return->message ?? 'Unknown error.';
|
|
27
30
|
file_put_contents(self::RESULT_PATH, json_encode($return, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE), FILE_APPEND);
|
|
28
31
|
exit(1);
|
|
29
32
|
}
|
|
@@ -78,7 +78,7 @@ class STR
|
|
|
78
78
|
|
|
79
79
|
public static function is_url(string $str): bool
|
|
80
80
|
{
|
|
81
|
-
if (!$sheme = strtolower(@parse_url($str, PHP_URL_SCHEME))) return false;
|
|
81
|
+
if (!$sheme = strtolower((string) @parse_url($str, PHP_URL_SCHEME))) return false;
|
|
82
82
|
return in_array($sheme, ['http', 'https', 'itunes', 'ftp', 'ftps', 'ssh', 'ssl', 'sftp']);
|
|
83
83
|
}
|
|
84
84
|
|
package/src/prepros.js
CHANGED
|
@@ -37,6 +37,10 @@ const getPHPInstance = async () => {
|
|
|
37
37
|
preprosConfig.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
38
38
|
preprosConfig.root = joinWith('/project/', config?.kirigami?.root);
|
|
39
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 || [];
|
|
40
44
|
|
|
41
45
|
__php = await (preprosConfig.network ? getPHPRuntimeWithNetwork() : getPHPRuntime());
|
|
42
46
|
__php.setSpawnHandler((command, args, options) => spawn(command, args, options));
|
|
@@ -147,6 +151,9 @@ const run = async (args = [], script = null, mountfiles = []) => {
|
|
|
147
151
|
const buffer = php.readFileAsBuffer(resultPath);
|
|
148
152
|
retobj = JSON.parse(Buffer.from(buffer).toString('utf8'));
|
|
149
153
|
retobj.debug = stdout;
|
|
154
|
+
// json_encode() turns a PHP array with gaps in its integer keys
|
|
155
|
+
// into an object — normalise back to a list before we map over it.
|
|
156
|
+
if (retobj.files && !Array.isArray(retobj.files)) retobj.files = Object.values(retobj.files);
|
|
150
157
|
if(retobj.files) await Promise.all(retobj.files.map(async (file, i) => {
|
|
151
158
|
const fbuffer = php.readFileAsBuffer(file);
|
|
152
159
|
const dest = file.replace(/^\/project\//i, '');
|
|
@@ -156,23 +163,60 @@ const run = async (args = [], script = null, mountfiles = []) => {
|
|
|
156
163
|
retobj.files[i] = dest;
|
|
157
164
|
}));
|
|
158
165
|
} else {
|
|
159
|
-
|
|
166
|
+
// No result file: PHP exited before STD::succeed()/error() — a
|
|
167
|
+
// parse error, an OOM, a die(), a hard crash. Pull whatever
|
|
168
|
+
// explanation we can out of the streams instead of a silent pass.
|
|
169
|
+
const crash = extractPhpError(stderr) || extractPhpError(stdout);
|
|
170
|
+
retobj = {
|
|
171
|
+
success: false,
|
|
172
|
+
files: [],
|
|
173
|
+
error: crash || 'PHP produced no result (exited early — check the debug output).',
|
|
174
|
+
debug: stdout,
|
|
175
|
+
stderr,
|
|
176
|
+
};
|
|
160
177
|
}
|
|
161
178
|
} catch (e) {
|
|
162
|
-
retobj = { success: false, error: 'Response parsing error
|
|
179
|
+
retobj = { success: false, error: 'Response parsing error: ' + (e?.message || e), debug: stdout, stderr };
|
|
163
180
|
} finally {
|
|
164
181
|
if (await php.fileExists(resultPath)) {
|
|
165
182
|
await php.unlink(resultPath);
|
|
166
183
|
}
|
|
167
184
|
}
|
|
185
|
+
|
|
186
|
+
// A fatal that still managed to write a (stale/partial) result file, or
|
|
187
|
+
// warnings printed to stderr. Only a genuine fatal marker overrides a
|
|
188
|
+
// result the PHP side reported as a success; plain warnings are attached
|
|
189
|
+
// as `warnings` so a stray notice doesn't sink an otherwise-clean build.
|
|
168
190
|
if (stderr) {
|
|
169
|
-
|
|
191
|
+
const fatal = extractPhpError(stderr);
|
|
192
|
+
if (fatal && (!retobj || retobj.success !== false)) {
|
|
193
|
+
retobj = { success: false, files: [], error: fatal, debug: stdout, stderr };
|
|
194
|
+
} else if (retobj && retobj.success && !fatal) {
|
|
195
|
+
retobj.warnings = stderr.trim();
|
|
196
|
+
} else if (retobj && retobj.success === false && !retobj.stderr) {
|
|
197
|
+
retobj.stderr = stderr.trim();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Never hand back a failure without a usable message.
|
|
202
|
+
if (retobj && retobj.success === false && !retobj.error) {
|
|
203
|
+
retobj.error = retobj.message || retobj.stderr || 'Unknown error.';
|
|
170
204
|
}
|
|
171
205
|
|
|
172
206
|
return retobj;
|
|
173
207
|
}
|
|
174
208
|
|
|
175
209
|
|
|
210
|
+
// Pulls the first meaningful PHP error line out of a stdout/stderr blob
|
|
211
|
+
// (fatals, parse errors, uncaught throwables). Returns null when the text
|
|
212
|
+
// holds nothing that looks like a hard error.
|
|
213
|
+
const extractPhpError = (text) => {
|
|
214
|
+
if (!text) return null;
|
|
215
|
+
const m = text.match(/^.*\b(?:PHP\s+)?(?:Fatal error|Parse error|Uncaught\s+\w+|Recoverable fatal error)\b.*$/mi);
|
|
216
|
+
return m ? m[0].trim() : null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
|
|
176
220
|
const runenv = async (script, paths = [], ...args) => {
|
|
177
221
|
if(!script) throw "Missing PHP file.";
|
|
178
222
|
const file = path.resolve(script);
|
|
@@ -183,12 +227,43 @@ const runenv = async (script, paths = [], ...args) => {
|
|
|
183
227
|
}
|
|
184
228
|
|
|
185
229
|
|
|
186
|
-
|
|
230
|
+
// Run a batch of image jobs (resize / palette) through /prepros/imagebatch.php,
|
|
231
|
+
// i.e. the IMG class (GD + Imagick). Used by @kirigami/kirigami's `sass` task so
|
|
232
|
+
// that img-asset() / colors() and the PHP IMG::asset() / <img asset> tag share
|
|
233
|
+
// one engine. `jobs` is the list documented in imagebatch.php; the result is a
|
|
234
|
+
// PreprosResult plus a `colors` map ({ "<src>:<count>": ["#rrggbb", …] }).
|
|
235
|
+
const processImages = async (jobs = []) => {
|
|
236
|
+
if (!Array.isArray(jobs) || !jobs.length) return { success: true, files: [], colors: {} };
|
|
237
|
+
const result = await run(['imagebatch', JSON.stringify(jobs)], '/prepros/imagebatch.php');
|
|
238
|
+
if (!result.colors) result.colors = {};
|
|
239
|
+
if (!result.files) result.files = [];
|
|
240
|
+
return result;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
const render = async (file = '.', phpIncludes = []) => {
|
|
187
245
|
const target = path.resolve(config?.kirigami?.root, file);
|
|
188
246
|
const fsvm = path.join('/project', config?.kirigami?.root, file).replace(/\\/g, '/');
|
|
189
247
|
await mountPath(target);
|
|
190
248
|
if(config?.prepros?.before) await mountPath(path.resolve(config?.kirigami?.root, config?.prepros?.before));
|
|
191
249
|
if(config?.prepros?.after) await mountPath(path.resolve(config?.kirigami?.root, config?.prepros?.after));
|
|
250
|
+
|
|
251
|
+
// Extra PHP files contributed by plugins (the kiri 'prepros:php' hook):
|
|
252
|
+
// mounted outside /project and include_once'd once, before any page
|
|
253
|
+
// renders, so they can PREPROS::registerTag()/registerHook() from PHP.
|
|
254
|
+
if (Array.isArray(phpIncludes) && phpIncludes.length) {
|
|
255
|
+
const php = await getPHPInstance();
|
|
256
|
+
const mounted = [];
|
|
257
|
+
for (let i = 0; i < phpIncludes.length; i++) {
|
|
258
|
+
const abs = path.resolve(phpIncludes[i]);
|
|
259
|
+
if (!fs.existsSync(abs)) continue;
|
|
260
|
+
const virt = `/plugins/${i}_${path.basename(abs)}`;
|
|
261
|
+
await mountPath(abs, virt, php);
|
|
262
|
+
mounted.push(virt);
|
|
263
|
+
}
|
|
264
|
+
php.preprosConfig.phpIncludes = mounted;
|
|
265
|
+
}
|
|
266
|
+
|
|
192
267
|
return run([fsvm]);
|
|
193
268
|
}
|
|
194
269
|
|
|
@@ -199,4 +274,4 @@ const sitemap = async () => {
|
|
|
199
274
|
}
|
|
200
275
|
|
|
201
276
|
|
|
202
|
-
export { runenv, render, sitemap, mountPath };
|
|
277
|
+
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()]);
|
package/src/utils.inc.php
CHANGED
|
@@ -23,6 +23,7 @@ spl_autoload_register(function ($class) {
|
|
|
23
23
|
'FS' => 'fs.class.php',
|
|
24
24
|
'HTML' => 'html.class.php',
|
|
25
25
|
'IMG' => 'img.class.php',
|
|
26
|
+
'LD' => 'ld.class.php',
|
|
26
27
|
'MD' => 'md.class.php',
|
|
27
28
|
'NORM' => 'norm.class.php',
|
|
28
29
|
'OBF' => 'obf.class.php',
|
|
@@ -40,10 +41,19 @@ spl_autoload_register(function ($class) {
|
|
|
40
41
|
}, true, true);
|
|
41
42
|
|
|
42
43
|
|
|
44
|
+
|
|
43
45
|
$argv = array_merge(['prepros.php'], json_decode(getenv('PREPROS_ARGS'), true));
|
|
44
46
|
$config = json_decode(getenv('PREPROS_CONFIG'));
|
|
45
47
|
date_default_timezone_set($config->timezone);
|
|
46
48
|
chdir('/project');
|
|
47
49
|
|
|
50
|
+
require_once(__DIR__ . '/libraries/aliases.inc.php');
|
|
51
|
+
|
|
52
|
+
PREPROS::loadConfig($config);
|
|
48
53
|
|
|
49
|
-
|
|
54
|
+
// Bootstrap is done: config loaded, `includes` pulled in, aliases available.
|
|
55
|
+
// Fires once per process, before any page renders, for every entrypoint
|
|
56
|
+
// (prepros.php, runenv.php, imagebatch.php). A plugin registered from an
|
|
57
|
+
// `includes` file (or prepros.plugins.php) can hook here to pull in extra
|
|
58
|
+
// PHP files or wire itself up.
|
|
59
|
+
PREPROS::runHook('boot', $config);
|