@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
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
final class PREPROS
|
|
5
5
|
{
|
|
6
6
|
|
|
7
|
-
public static mixed $config;
|
|
8
|
-
|
|
7
|
+
public static mixed $config;
|
|
8
|
+
public static string $file = '';
|
|
9
|
+
private static string $root = '';
|
|
9
10
|
private static array $files = [];
|
|
10
11
|
private static array $hooks = [];
|
|
11
12
|
private static array $tags = [];
|
|
@@ -19,6 +20,11 @@ final class PREPROS
|
|
|
19
20
|
if (!is_file(realpath(self::$root . $path))) continue;
|
|
20
21
|
else include_once(realpath(self::$root . $path));
|
|
21
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
|
+
}
|
|
22
28
|
}
|
|
23
29
|
|
|
24
30
|
|
|
@@ -26,8 +32,16 @@ final class PREPROS
|
|
|
26
32
|
{
|
|
27
33
|
$dir = pathinfo($file, PATHINFO_DIRNAME) . S;
|
|
28
34
|
$target = $dir . ltrim(pathinfo($file, PATHINFO_FILENAME), '_') . '.html';
|
|
29
|
-
|
|
30
|
-
|
|
35
|
+
if(!$file = realpath($file)) return false;
|
|
36
|
+
self::$file = $file;
|
|
37
|
+
|
|
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, '/') . '/');
|
|
44
|
+
|
|
31
45
|
$relroot = FS::getRelativePath($dir, self::$root);
|
|
32
46
|
$page = self::processHook('page_info', [$file, FS::phpFileInfo($file)]);
|
|
33
47
|
|
|
@@ -37,8 +51,9 @@ final class PREPROS
|
|
|
37
51
|
self::processHook('pre_render', file_get_contents($file));
|
|
38
52
|
|
|
39
53
|
ob_start();
|
|
54
|
+
self::processHook('pre_before', self::$config->before);
|
|
40
55
|
if (self::$config->before) include(realpath(self::$root . self::$config->before));
|
|
41
|
-
$header = ob_get_clean();
|
|
56
|
+
$header = self::processHook('post_before', ob_get_clean());
|
|
42
57
|
|
|
43
58
|
if(empty($content)) {
|
|
44
59
|
ob_start();
|
|
@@ -53,17 +68,25 @@ final class PREPROS
|
|
|
53
68
|
}
|
|
54
69
|
|
|
55
70
|
ob_start();
|
|
71
|
+
self::processHook('pre_after', self::$config->after);
|
|
56
72
|
if (self::$config->after) include(realpath(self::$root . self::$config->after));
|
|
57
|
-
$footer = ob_get_clean();
|
|
73
|
+
$footer = self::processHook('post_after', ob_get_clean());
|
|
58
74
|
|
|
59
75
|
$contents = $header . $body . $footer;
|
|
60
76
|
$contents = self::processTags($contents);
|
|
61
77
|
$contents = self::processHook('post_render', $contents);
|
|
62
78
|
|
|
79
|
+
if ((self::$config->head ?? true) !== false) {
|
|
80
|
+
$contents = self::injectHead($contents, $relroot);
|
|
81
|
+
}
|
|
82
|
+
|
|
63
83
|
if(!empty(self::$config->format)) $contents = HTML::format($contents);
|
|
64
84
|
|
|
85
|
+
$contents = self::replaceTokens($contents);
|
|
86
|
+
|
|
65
87
|
file_put_contents($target, $contents);
|
|
66
88
|
self::exportFile($target);
|
|
89
|
+
self::$file = '';
|
|
67
90
|
return realpath($target);
|
|
68
91
|
}
|
|
69
92
|
|
|
@@ -100,11 +123,103 @@ final class PREPROS
|
|
|
100
123
|
|
|
101
124
|
$dest = self::$root . 'sitemap.xml';
|
|
102
125
|
file_put_contents($dest, $dom->saveXML());
|
|
103
|
-
|
|
126
|
+
|
|
127
|
+
$destrobots = self::$root . 'robots.txt';
|
|
128
|
+
$urlrobots = rtrim(self::$config->data->baseurl, '/') . '/sitemap.xml';
|
|
129
|
+
file_put_contents($destrobots, "User-agent: *\nAllow: /\nSitemap: {$urlrobots}");
|
|
130
|
+
|
|
131
|
+
self::exportFile([$dest, $destrobots]);
|
|
104
132
|
return realpath($dest);
|
|
105
133
|
}
|
|
106
134
|
|
|
107
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
|
+
|
|
108
223
|
public static function exportFile(string|array $file): void
|
|
109
224
|
{
|
|
110
225
|
if(is_string($file)) $file = [$file];
|
|
@@ -118,7 +233,7 @@ final class PREPROS
|
|
|
118
233
|
public static function getExportedFiles(): array
|
|
119
234
|
{
|
|
120
235
|
$files = array_unique(self::$files);
|
|
121
|
-
sort($files);
|
|
236
|
+
// sort($files);
|
|
122
237
|
return $files;
|
|
123
238
|
}
|
|
124
239
|
|
|
@@ -132,19 +247,43 @@ final class PREPROS
|
|
|
132
247
|
}
|
|
133
248
|
|
|
134
249
|
|
|
135
|
-
public static function mount(string|array $patterns)
|
|
250
|
+
public static function mount(string|array $patterns)
|
|
251
|
+
{
|
|
136
252
|
$results = self::callJS('mount', ['patterns' => $patterns]);
|
|
137
|
-
if(!$results->success) return false;
|
|
253
|
+
if (!$results->success) return false;
|
|
138
254
|
return $results->results;
|
|
139
255
|
}
|
|
140
256
|
|
|
141
257
|
|
|
258
|
+
public static function fstat(string $path)
|
|
259
|
+
{
|
|
260
|
+
if (!$results = self::callJS('fstat', ['path' => $path])) return false;
|
|
261
|
+
if (!$results->success) return false;
|
|
262
|
+
if (!$results->results->exists) return false;
|
|
263
|
+
return $results->results;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
public static function backtraceFile()
|
|
268
|
+
{
|
|
269
|
+
$debug = debug_backtrace();
|
|
270
|
+
if(!isset($debug[1])) return false;
|
|
271
|
+
else return $debug[1]['file'];
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
|
|
142
275
|
public static function registerHook(string $hook, callable $clb)
|
|
143
276
|
{
|
|
144
277
|
self::$hooks[$hook][] = $clb;
|
|
145
278
|
}
|
|
146
279
|
|
|
147
280
|
|
|
281
|
+
public static function runHook(string $hook, $data = null)
|
|
282
|
+
{
|
|
283
|
+
return self::processHook($hook, $data);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
|
|
148
287
|
private static function processHook(string $hook, $data = null)
|
|
149
288
|
{
|
|
150
289
|
if (!empty(self::$hooks[$hook])) {
|
|
@@ -7,22 +7,72 @@ PREPROS::registerTag('markdown', function ($tag, $attrs, $body) {
|
|
|
7
7
|
});
|
|
8
8
|
|
|
9
9
|
|
|
10
|
+
PREPROS::registerTag('img', function ($tag, $attrs, $body) {
|
|
11
|
+
if(empty($attrs['asset'])) return $tag;
|
|
12
|
+
$width = 0;
|
|
13
|
+
$height = 0;
|
|
14
|
+
$cover = false;
|
|
15
|
+
if(isset($attrs['width'])) {
|
|
16
|
+
$width = $attrs['width'] ?? 0;
|
|
17
|
+
unset($attrs['width']);
|
|
18
|
+
}
|
|
19
|
+
if(isset($attrs['height'])) {
|
|
20
|
+
$height = $attrs['height'] ?? 0;
|
|
21
|
+
unset($attrs['height']);
|
|
22
|
+
}
|
|
23
|
+
if(isset($attrs['cover'])) {
|
|
24
|
+
$cover = true;
|
|
25
|
+
unset($attrs['cover']);
|
|
26
|
+
}
|
|
27
|
+
$attrs['src'] = IMG::asset($attrs['asset'], $width, $height, $cover, PREPROS::$file);
|
|
28
|
+
unset($attrs['asset']);
|
|
29
|
+
foreach($attrs as $k => $v) $props[] = $k.'="'.$v.'"';
|
|
30
|
+
return '<img'.(!empty($props) ? ' '.join(' ', $props): '').'>';
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
|
|
10
34
|
PREPROS::registerHook('page_info', function($info) {
|
|
11
|
-
|
|
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
|
+
}
|
|
12
44
|
foreach($page as $k => $v) {
|
|
13
45
|
$ext = strtolower(pathinfo($v, PATHINFO_EXTENSION));
|
|
14
46
|
if(in_array($ext, ['yaml', 'yml', 'json', 'md']) ) {
|
|
15
|
-
|
|
47
|
+
$isUrl = STR::is_url($v);
|
|
48
|
+
if($isUrl) $filename = $v;
|
|
16
49
|
else $filename = pathinfo(realpath($file), PATHINFO_DIRNAME) . '/' . $v;
|
|
17
|
-
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;
|
|
18
55
|
$page->{$k} = match ($ext) {
|
|
19
|
-
'yml', 'yaml' => YAML::
|
|
20
|
-
'json' => json_decode(
|
|
21
|
-
'md' => MD::toHtml(
|
|
56
|
+
'yml', 'yaml' => YAML::parse($raw),
|
|
57
|
+
'json' => json_decode($raw),
|
|
58
|
+
'md' => MD::toHtml($raw),
|
|
22
59
|
default => $filename,
|
|
23
60
|
};
|
|
24
61
|
}
|
|
25
62
|
}
|
|
26
63
|
}
|
|
27
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);
|
|
28
78
|
});
|