@kirigami/php-prepros 1.2.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 +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 +114 -4
- 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 +77 -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];
|
|
@@ -174,6 +278,12 @@ final class PREPROS
|
|
|
174
278
|
}
|
|
175
279
|
|
|
176
280
|
|
|
281
|
+
public static function runHook(string $hook, $data = null)
|
|
282
|
+
{
|
|
283
|
+
return self::processHook($hook, $data);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
|
|
177
287
|
private static function processHook(string $hook, $data = null)
|
|
178
288
|
{
|
|
179
289
|
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));
|
|
@@ -156,23 +160,60 @@ const run = async (args = [], script = null, mountfiles = []) => {
|
|
|
156
160
|
retobj.files[i] = dest;
|
|
157
161
|
}));
|
|
158
162
|
} else {
|
|
159
|
-
|
|
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
|
+
};
|
|
160
174
|
}
|
|
161
175
|
} catch (e) {
|
|
162
|
-
retobj = { success: false, error: 'Response parsing error
|
|
176
|
+
retobj = { success: false, error: 'Response parsing error: ' + (e?.message || e), debug: stdout, stderr };
|
|
163
177
|
} finally {
|
|
164
178
|
if (await php.fileExists(resultPath)) {
|
|
165
179
|
await php.unlink(resultPath);
|
|
166
180
|
}
|
|
167
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.
|
|
168
187
|
if (stderr) {
|
|
169
|
-
|
|
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.';
|
|
170
201
|
}
|
|
171
202
|
|
|
172
203
|
return retobj;
|
|
173
204
|
}
|
|
174
205
|
|
|
175
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
|
+
|
|
176
217
|
const runenv = async (script, paths = [], ...args) => {
|
|
177
218
|
if(!script) throw "Missing PHP file.";
|
|
178
219
|
const file = path.resolve(script);
|
|
@@ -183,12 +224,43 @@ const runenv = async (script, paths = [], ...args) => {
|
|
|
183
224
|
}
|
|
184
225
|
|
|
185
226
|
|
|
186
|
-
|
|
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 = []) => {
|
|
187
242
|
const target = path.resolve(config?.kirigami?.root, file);
|
|
188
243
|
const fsvm = path.join('/project', config?.kirigami?.root, file).replace(/\\/g, '/');
|
|
189
244
|
await mountPath(target);
|
|
190
245
|
if(config?.prepros?.before) await mountPath(path.resolve(config?.kirigami?.root, config?.prepros?.before));
|
|
191
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;
|
|
262
|
+
}
|
|
263
|
+
|
|
192
264
|
return run([fsvm]);
|
|
193
265
|
}
|
|
194
266
|
|
|
@@ -199,4 +271,4 @@ const sitemap = async () => {
|
|
|
199
271
|
}
|
|
200
272
|
|
|
201
273
|
|
|
202
|
-
export { runenv, render, sitemap, mountPath };
|
|
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()]);
|
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);
|