@kirigami/php-prepros 3.1.1 → 3.2.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 +2071 -1999
- package/package.json +59 -59
- package/src/libraries/fs.class.php +171 -22
- package/src/libraries/html.class.php +56 -4
- package/src/libraries/ld.class.php +2 -2
- package/src/libraries/prepros.class.php +18 -5
- package/src/libraries/prepros.plugins.php +1 -0
- package/src/prepros.php +1 -1
package/package.json
CHANGED
|
@@ -1,59 +1,59 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@kirigami/php-prepros",
|
|
3
|
-
"version": "3.
|
|
4
|
-
"description": "PHP preprocessor for the Kirigami static site generator. Compile PHP page templates to clean, deployable HTML — with zero server dependency.",
|
|
5
|
-
"keywords": [
|
|
6
|
-
"kirigami",
|
|
7
|
-
"php",
|
|
8
|
-
"preprocessor",
|
|
9
|
-
"static-site-generator",
|
|
10
|
-
"ssg",
|
|
11
|
-
"wasm",
|
|
12
|
-
"php-wasm",
|
|
13
|
-
"markdown",
|
|
14
|
-
"yaml",
|
|
15
|
-
"github-pages",
|
|
16
|
-
"pages"
|
|
17
|
-
],
|
|
18
|
-
"license": "GPL-3.0-or-later",
|
|
19
|
-
"author": "Maxime Larrivée-Roy",
|
|
20
|
-
"type": "module",
|
|
21
|
-
"main": "index.js",
|
|
22
|
-
"types": "index.d.ts",
|
|
23
|
-
"files": [
|
|
24
|
-
"index.js",
|
|
25
|
-
"index.d.ts",
|
|
26
|
-
"src/",
|
|
27
|
-
"LICENSE",
|
|
28
|
-
"README.md"
|
|
29
|
-
],
|
|
30
|
-
"engines": {
|
|
31
|
-
"node": ">=24.0.0",
|
|
32
|
-
"npm": ">=10.2.3"
|
|
33
|
-
},
|
|
34
|
-
"exports": {
|
|
35
|
-
".": {
|
|
36
|
-
"import": "./index.js"
|
|
37
|
-
},
|
|
38
|
-
"./package.json": "./package.json"
|
|
39
|
-
},
|
|
40
|
-
"publishConfig": {
|
|
41
|
-
"access": "public"
|
|
42
|
-
},
|
|
43
|
-
"scripts": {
|
|
44
|
-
"test": "node --test --test-concurrency=1 \"test/*.test.js\""
|
|
45
|
-
},
|
|
46
|
-
"dependencies": {
|
|
47
|
-
"@kirigami/php-wasm": "8.5.11-1",
|
|
48
|
-
"@kirigami/struct-walker": "1.0.6",
|
|
49
|
-
"picomatch": "^4.0.7"
|
|
50
|
-
},
|
|
51
|
-
"repository": {
|
|
52
|
-
"type": "git",
|
|
53
|
-
"url": "git+https://github.com/php-kirigami/kirigami.git"
|
|
54
|
-
},
|
|
55
|
-
"homepage": "https://php-kirigami.github.io",
|
|
56
|
-
"bugs": {
|
|
57
|
-
"url": "https://github.com/php-kirigami/kirigami/issues"
|
|
58
|
-
}
|
|
59
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@kirigami/php-prepros",
|
|
3
|
+
"version": "3.2.1",
|
|
4
|
+
"description": "PHP preprocessor for the Kirigami static site generator. Compile PHP page templates to clean, deployable HTML — with zero server dependency.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"kirigami",
|
|
7
|
+
"php",
|
|
8
|
+
"preprocessor",
|
|
9
|
+
"static-site-generator",
|
|
10
|
+
"ssg",
|
|
11
|
+
"wasm",
|
|
12
|
+
"php-wasm",
|
|
13
|
+
"markdown",
|
|
14
|
+
"yaml",
|
|
15
|
+
"github-pages",
|
|
16
|
+
"pages"
|
|
17
|
+
],
|
|
18
|
+
"license": "GPL-3.0-or-later",
|
|
19
|
+
"author": "Maxime Larrivée-Roy",
|
|
20
|
+
"type": "module",
|
|
21
|
+
"main": "index.js",
|
|
22
|
+
"types": "index.d.ts",
|
|
23
|
+
"files": [
|
|
24
|
+
"index.js",
|
|
25
|
+
"index.d.ts",
|
|
26
|
+
"src/",
|
|
27
|
+
"LICENSE",
|
|
28
|
+
"README.md"
|
|
29
|
+
],
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=24.0.0",
|
|
32
|
+
"npm": ">=10.2.3"
|
|
33
|
+
},
|
|
34
|
+
"exports": {
|
|
35
|
+
".": {
|
|
36
|
+
"import": "./index.js"
|
|
37
|
+
},
|
|
38
|
+
"./package.json": "./package.json"
|
|
39
|
+
},
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"test": "node --test --test-concurrency=1 \"test/*.test.js\""
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@kirigami/php-wasm": "8.5.11-1",
|
|
48
|
+
"@kirigami/struct-walker": "1.0.6",
|
|
49
|
+
"picomatch": "^4.0.7"
|
|
50
|
+
},
|
|
51
|
+
"repository": {
|
|
52
|
+
"type": "git",
|
|
53
|
+
"url": "git+https://github.com/php-kirigami/kirigami.git"
|
|
54
|
+
},
|
|
55
|
+
"homepage": "https://php-kirigami.github.io",
|
|
56
|
+
"bugs": {
|
|
57
|
+
"url": "https://github.com/php-kirigami/kirigami/issues"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -52,8 +52,8 @@ class FS
|
|
|
52
52
|
* Lists the immediate child pages of the calling template.
|
|
53
53
|
*
|
|
54
54
|
* Scans the directories directly below the folder of the file that
|
|
55
|
-
* called this function, keeps the ones that hold
|
|
56
|
-
*
|
|
55
|
+
* called this function, keeps the ones that hold a page file (see
|
|
56
|
+
* {@see FS::indexFile()}), parses its header via {@see FS::phpFileInfo()} and returns
|
|
57
57
|
* the pages ordered by `@position` ascending (a missing `@position`
|
|
58
58
|
* counts as 999999), then by folder name.
|
|
59
59
|
*
|
|
@@ -76,10 +76,8 @@ class FS
|
|
|
76
76
|
|
|
77
77
|
$children = [];
|
|
78
78
|
foreach (glob($dir . '/*', GLOB_ONLYDIR) as $subdir) {
|
|
79
|
-
|
|
80
|
-
if (!is_file($index)) continue;
|
|
79
|
+
if (!$index = self::indexFile($subdir)) continue;
|
|
81
80
|
$info = FS::phpFileInfo($index) ?: new stdClass;
|
|
82
|
-
$info = clone $info;
|
|
83
81
|
$info->file = realpath($index);
|
|
84
82
|
$position = (isset($info->position) && is_numeric($info->position)) ? (int) $info->position : 999999;
|
|
85
83
|
$children[] = ['position' => $position, 'name' => pathinfo($subdir, PATHINFO_BASENAME), 'info' => $info];
|
|
@@ -99,7 +97,7 @@ class FS
|
|
|
99
97
|
*
|
|
100
98
|
* Starting from the folder *above* the caller's own folder (the current
|
|
101
99
|
* page is never part of its own trail), it walks the parent directories
|
|
102
|
-
* upward, collecting the `_index.php` of each one via
|
|
100
|
+
* upward, collecting the page file (`_index.php` or `_index.md`) of each one via
|
|
103
101
|
* {@see FS::phpFileInfo()}. The walk stops at the source root, or at the
|
|
104
102
|
* first ancestor `_index.php` that does not carry an active `@breadcrumb`
|
|
105
103
|
* tag — that page is a pure separator and is left out of the result.
|
|
@@ -138,11 +136,9 @@ class FS
|
|
|
138
136
|
$dir = $parent;
|
|
139
137
|
if (strncmp($dir . '/', $root . '/', strlen($root) + 1) !== 0) break; // above the source root
|
|
140
138
|
|
|
141
|
-
$index = $dir
|
|
142
|
-
if (is_file($index)) {
|
|
139
|
+
if ($index = self::indexFile($dir)) {
|
|
143
140
|
$info = FS::phpFileInfo($index) ?: new stdClass;
|
|
144
141
|
if (!self::truthy($info->breadcrumb ?? null)) break; // separator page: stop, exclude it
|
|
145
|
-
$info = clone $info;
|
|
146
142
|
$info->file = realpath($index);
|
|
147
143
|
$trail[] = $info;
|
|
148
144
|
}
|
|
@@ -168,23 +164,170 @@ class FS
|
|
|
168
164
|
}
|
|
169
165
|
|
|
170
166
|
|
|
167
|
+
/**
|
|
168
|
+
* The page file of a directory: its `_index.php`, else its `_index.md`,
|
|
169
|
+
* else null. A folder holding both is a PHP page, and the `.md` is data.
|
|
170
|
+
*/
|
|
171
|
+
public static function indexFile(string $dir): ?string
|
|
172
|
+
{
|
|
173
|
+
$dir = rtrim($dir, '\/');
|
|
174
|
+
foreach (['/_index.php', '/_index.md'] as $name) {
|
|
175
|
+
if (is_file($dir . $name)) return $dir . $name;
|
|
176
|
+
}
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Parses a page's header annotations and returns them as a `stdClass`
|
|
183
|
+
* (a fresh copy on each call: callers may add keys without them leaking
|
|
184
|
+
* into the page's own variables).
|
|
185
|
+
*
|
|
186
|
+
* - `.php`: the file's first PHPDOC block ({@see FS::parseDocBlock()}).
|
|
187
|
+
* - `.md`: the `@tag value` lines at the top of the file, up to the
|
|
188
|
+
* first line that is neither a tag nor an indented continuation
|
|
189
|
+
* ({@see FS::splitHeader()}).
|
|
190
|
+
*
|
|
191
|
+
* Inheritance: an `@@tag value` sets `tag` on the page and on every page
|
|
192
|
+
* below it (see {@see FS::inheritedInfo()}). A page's own `@tag`
|
|
193
|
+
* overrides an inherited value for that page only; its own `@@tag`
|
|
194
|
+
* overrides it and passes the new value down.
|
|
195
|
+
*/
|
|
171
196
|
public static function phpFileInfo(string $file): object|bool
|
|
172
197
|
{
|
|
173
198
|
static $files = [];
|
|
174
199
|
if (!$file = realpath($file)) return false;
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
200
|
+
$files[$file] ??= (object) array_merge(self::inheritedInfo($file), self::rawInfo($file)[0]);
|
|
201
|
+
return clone $files[$file];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* A file's own annotations, as `[all tags, tags passed down (@@)]`.
|
|
207
|
+
*
|
|
208
|
+
* @return array{0: array<string,string>, 1: array<string,string>}
|
|
209
|
+
*/
|
|
210
|
+
private static function rawInfo(string $file): array
|
|
211
|
+
{
|
|
212
|
+
static $files = [];
|
|
213
|
+
if (isset($files[$file])) return $files[$file];
|
|
214
|
+
|
|
215
|
+
$inherit = [];
|
|
216
|
+
if (strtolower(pathinfo($file, PATHINFO_EXTENSION)) === 'md') {
|
|
217
|
+
[$info, , $inherit] = self::splitHeader(file_get_contents($file));
|
|
218
|
+
} else {
|
|
219
|
+
$block = null;
|
|
220
|
+
foreach (token_get_all(file_get_contents($file)) as $tok) {
|
|
221
|
+
if (is_array($tok) && $tok[0] == T_DOC_COMMENT) {
|
|
180
222
|
$block = $tok[1];
|
|
181
223
|
break;
|
|
182
224
|
}
|
|
183
225
|
}
|
|
184
|
-
|
|
185
|
-
|
|
226
|
+
$info = $block ? self::parseDocBlock($block, $inherit) : [];
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return $files[$file] = [$info, array_intersect_key($info, array_flip($inherit))];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* The `@@` values a page inherits: those of the page file of every
|
|
235
|
+
* folder above it within `kirigami.root`, top-most first, a nearer
|
|
236
|
+
* ancestor winning. A non-index page (`_post.php`) also inherits from
|
|
237
|
+
* its own folder's `_index`. A relative data-file value
|
|
238
|
+
* (`@@menu _menu.yaml`) is rewritten relative to the inheriting page, so
|
|
239
|
+
* it still points at the file next to the page that declared it.
|
|
240
|
+
*
|
|
241
|
+
* @return array<string,string>
|
|
242
|
+
*/
|
|
243
|
+
private static function inheritedInfo(string $file): array
|
|
244
|
+
{
|
|
245
|
+
if (!isset(PREPROS::$config->root) || !$root = realpath(PREPROS::$config->root)) return [];
|
|
246
|
+
$root = rtrim(str_replace('\\', '/', $root), '/');
|
|
247
|
+
$self = str_replace('\\', '/', $file);
|
|
248
|
+
$dir = dirname($self);
|
|
249
|
+
$in = fn(string $d) => $d === $root || str_starts_with($d, $root . '/');
|
|
250
|
+
if (!$in($dir)) return [];
|
|
251
|
+
|
|
252
|
+
$dirs = [];
|
|
253
|
+
$isIndex = strtolower(pathinfo($self, PATHINFO_FILENAME)) === '_index';
|
|
254
|
+
for ($d = $isIndex ? dirname($dir) : $dir; $in($d); $d = dirname($d)) {
|
|
255
|
+
array_unshift($dirs, $d);
|
|
256
|
+
if ($d === $root) break;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
$vars = [];
|
|
260
|
+
foreach ($dirs as $d) {
|
|
261
|
+
$index = self::indexFile($d);
|
|
262
|
+
if (!$index || !$index = realpath($index)) continue;
|
|
263
|
+
foreach (self::rawInfo($index)[1] as $k => $v) {
|
|
264
|
+
$ext = strtolower(pathinfo($v, PATHINFO_EXTENSION));
|
|
265
|
+
if (in_array($ext, ['yaml', 'yml', 'json', 'md'], true) && !STR::is_url($v) && is_file($d . '/' . $v)) {
|
|
266
|
+
$v = self::relativeTo($dir, realpath($d . '/' . $v));
|
|
267
|
+
}
|
|
268
|
+
$vars[$k] = $v;
|
|
269
|
+
}
|
|
186
270
|
}
|
|
187
|
-
return $
|
|
271
|
+
return $vars;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
/** Path of `$file` relative to the directory `$from` (`../x/y.yaml`). */
|
|
276
|
+
private static function relativeTo(string $from, string $file): string
|
|
277
|
+
{
|
|
278
|
+
$from = explode('/', trim(str_replace('\\', '/', $from), '/'));
|
|
279
|
+
$to = explode('/', trim(str_replace('\\', '/', $file), '/'));
|
|
280
|
+
while ($from && $to && $from[0] === $to[0]) {
|
|
281
|
+
array_shift($from);
|
|
282
|
+
array_shift($to);
|
|
283
|
+
}
|
|
284
|
+
return str_repeat('../', count($from)) . implode('/', $to);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Splits a Markdown page into its header annotations and its body.
|
|
290
|
+
*
|
|
291
|
+
* The header is the `@tag value` lines at the top of the file (leading
|
|
292
|
+
* blank lines allowed), with the same rules as a PHPDOC block: a value
|
|
293
|
+
* wraps onto following indented lines. It ends at the first blank line
|
|
294
|
+
* (consumed) or flush-left line that isn't a tag, where the body starts.
|
|
295
|
+
* An `@@tag` is a tag passed down to child pages too. A file with no
|
|
296
|
+
* header is all body.
|
|
297
|
+
*
|
|
298
|
+
* @return array{0: array<string,string>, 1: string, 2: string[]}
|
|
299
|
+
* [annotations, body, names of the `@@` tags]
|
|
300
|
+
*/
|
|
301
|
+
public static function splitHeader(string $text): array
|
|
302
|
+
{
|
|
303
|
+
$lines = preg_split('/\r\n|\r|\n/', preg_replace('/^\xEF\xBB\xBF/', '', $text));
|
|
304
|
+
$info = [];
|
|
305
|
+
$inherit = [];
|
|
306
|
+
$current = null;
|
|
307
|
+
$count = count($lines);
|
|
308
|
+
$i = 0;
|
|
309
|
+
|
|
310
|
+
while ($i < $count && trim($lines[$i]) === '') $i++;
|
|
311
|
+
if ($i === $count || $lines[$i][0] !== '@') return [[], $text, []];
|
|
312
|
+
|
|
313
|
+
for (; $i < $count; $i++) {
|
|
314
|
+
$line = $lines[$i];
|
|
315
|
+
if (preg_match('/^@(@?)([A-Za-z0-9_]+)[ \t]*(.*)$/', $line, $m)) {
|
|
316
|
+
$current = $m[2];
|
|
317
|
+
$info[$current] = trim($m[3]);
|
|
318
|
+
$inherit = array_diff($inherit, [$current]);
|
|
319
|
+
if ($m[1] !== '') $inherit[] = $current;
|
|
320
|
+
} elseif (str_starts_with($line, '@')) {
|
|
321
|
+
$current = null;
|
|
322
|
+
} elseif ($current !== null && trim($line) !== '' && preg_match('/^[ \t]/', $line)) {
|
|
323
|
+
$info[$current] = trim($info[$current] . ' ' . trim($line));
|
|
324
|
+
} else {
|
|
325
|
+
if (trim($line) === '') $i++; // the separating blank line
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return [$info, implode("\n", array_slice($lines, $i)), array_values($inherit)];
|
|
188
331
|
}
|
|
189
332
|
|
|
190
333
|
|
|
@@ -198,10 +341,14 @@ class FS
|
|
|
198
341
|
* up to the next `@tag`, a blank line, a flush-left prose line, or the end
|
|
199
342
|
* of the block; continuation lines are joined with a single space.
|
|
200
343
|
*
|
|
201
|
-
*
|
|
344
|
+
* `@@tag` is a tag like any other, whose name is also added to `$inherit`
|
|
345
|
+
* (values passed down to child pages).
|
|
346
|
+
*
|
|
347
|
+
* @param string $block Raw `/** … */` doc-comment text.
|
|
348
|
+
* @param string[] $inherit Receives the names of the `@@` tags.
|
|
202
349
|
* @return array<string,string>
|
|
203
350
|
*/
|
|
204
|
-
private static function parseDocBlock(string $block): array
|
|
351
|
+
private static function parseDocBlock(string $block, array &$inherit = []): array
|
|
205
352
|
{
|
|
206
353
|
$info = [];
|
|
207
354
|
$current = null;
|
|
@@ -212,9 +359,11 @@ class FS
|
|
|
212
359
|
$line = preg_replace('#\s*\*/\s*$#', '', $line);
|
|
213
360
|
$line = preg_replace('#^[ \t]*\*[ \t]?#', '', $line, 1);
|
|
214
361
|
|
|
215
|
-
if (preg_match('/^@([A-Za-z0-9_]+)[ \t]*(.*)$/', $line, $m)) {
|
|
216
|
-
$current =
|
|
217
|
-
$info[$current] = trim($m[
|
|
362
|
+
if (preg_match('/^@(@?)([A-Za-z0-9_]+)[ \t]*(.*)$/', $line, $m)) {
|
|
363
|
+
$current = $m[2];
|
|
364
|
+
$info[$current] = trim($m[3]);
|
|
365
|
+
$inherit = array_values(array_diff($inherit, [$current]));
|
|
366
|
+
if ($m[1] !== '') $inherit[] = $current;
|
|
218
367
|
} elseif (trim($line) === '') {
|
|
219
368
|
$current = null; // blank line ends a value
|
|
220
369
|
} elseif ($current !== null && preg_match('/^[ \t]/', $line)) {
|
|
@@ -25,6 +25,7 @@ class HTML
|
|
|
25
25
|
'code', 'dfn', 'em', 'i', 'img', 'input', 'kbd', 'label', 'map',
|
|
26
26
|
'mark', 'object', 'output', 'q', 's', 'samp', 'select', 'small',
|
|
27
27
|
'span', 'strong', 'sub', 'sup', 'textarea', 'time', 'tt', 'u', 'var',
|
|
28
|
+
'bdi', 'data', 'del', 'font', 'ins', 'rp', 'rt', 'ruby', 'strike', 'wbr',
|
|
28
29
|
];
|
|
29
30
|
|
|
30
31
|
// The HTML namespace URI Lexbor assigns to plain HTML elements (foreign
|
|
@@ -188,20 +189,63 @@ class HTML
|
|
|
188
189
|
}
|
|
189
190
|
|
|
190
191
|
$inner = '';
|
|
191
|
-
foreach ($children as $
|
|
192
|
-
|
|
192
|
+
foreach (static::groupInlineRuns($children) as $group) {
|
|
193
|
+
if (is_array($group)) {
|
|
194
|
+
// Text and inline elements that follow each other: one line, so
|
|
195
|
+
// the spaces between them survive ("<b>bold</b>, next").
|
|
196
|
+
$inner .= str_repeat(' ', ($depth + 1) * self::INDENT) . trim(static::renderInlineNodes($group)) . "\n";
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
$inner .= static::renderNode($group, $depth + 1);
|
|
193
200
|
}
|
|
194
201
|
|
|
195
202
|
return "{$pad}<{$tag}{$attrs}>\n{$inner}{$pad}</{$tag}>\n";
|
|
196
203
|
}
|
|
197
204
|
|
|
205
|
+
// Splits a block's children into single nodes and runs (arrays) of
|
|
206
|
+
// consecutive text/inline nodes that contain real text. A run without text
|
|
207
|
+
// (a row of <a><img></a>, say) stays as separate nodes, one per line.
|
|
208
|
+
private static function groupInlineRuns(array $children): array
|
|
209
|
+
{
|
|
210
|
+
$groups = [];
|
|
211
|
+
$run = [];
|
|
212
|
+
$flush = static function () use (&$groups, &$run) {
|
|
213
|
+
$hasText = false;
|
|
214
|
+
foreach ($run as $node) {
|
|
215
|
+
if ($node->nodeType === XML_TEXT_NODE && trim($node->nodeValue) !== '') $hasText = true;
|
|
216
|
+
}
|
|
217
|
+
if ($hasText) $groups[] = $run;
|
|
218
|
+
else foreach ($run as $node) $groups[] = $node;
|
|
219
|
+
$run = [];
|
|
220
|
+
};
|
|
221
|
+
foreach ($children as $child) {
|
|
222
|
+
$isInline = $child->nodeType === XML_TEXT_NODE
|
|
223
|
+
|| ($child->nodeType === XML_ELEMENT_NODE
|
|
224
|
+
&& static::isInline($child)
|
|
225
|
+
&& static::hasOnlyInlineChildren($child));
|
|
226
|
+
if ($isInline) {
|
|
227
|
+
$run[] = $child;
|
|
228
|
+
} else {
|
|
229
|
+
$flush();
|
|
230
|
+
$groups[] = $child;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
$flush();
|
|
234
|
+
return $groups;
|
|
235
|
+
}
|
|
236
|
+
|
|
198
237
|
// Serializes a node's inline content on a single line, collapsing any run
|
|
199
238
|
// of spaces/tabs/newlines in the source text to a single space (equivalent
|
|
200
239
|
// to HTML's whitespace-collapsing behavior).
|
|
201
240
|
private static function renderInline(Dom\Node $node): string
|
|
241
|
+
{
|
|
242
|
+
return static::renderInlineNodes($node->childNodes);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
private static function renderInlineNodes(iterable $nodes): string
|
|
202
246
|
{
|
|
203
247
|
$out = '';
|
|
204
|
-
foreach ($
|
|
248
|
+
foreach ($nodes as $child) {
|
|
205
249
|
if ($child->nodeType === XML_TEXT_NODE) {
|
|
206
250
|
$text = preg_replace('/\s+/', ' ', $child->nodeValue);
|
|
207
251
|
$out .= htmlspecialchars($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
|
@@ -244,6 +288,14 @@ class HTML
|
|
|
244
288
|
return $elementCount <= 1;
|
|
245
289
|
}
|
|
246
290
|
|
|
291
|
+
// A known inline element, or a custom element (its name always has a
|
|
292
|
+
// hyphen), which browsers lay out inline unless a stylesheet says otherwise.
|
|
293
|
+
private static function isInline(Dom\Node $node): bool
|
|
294
|
+
{
|
|
295
|
+
$name = strtolower($node->nodeName);
|
|
296
|
+
return in_array($name, self::INLINE, true) || str_contains($name, '-');
|
|
297
|
+
}
|
|
298
|
+
|
|
247
299
|
// Checks that every direct descendant is inline (text, inline void, inline elements)
|
|
248
300
|
// The inline elements themselves must not contain block elements
|
|
249
301
|
private static function hasOnlyInlineChildren(Dom\Node $node): bool
|
|
@@ -255,7 +307,7 @@ class HTML
|
|
|
255
307
|
if ($child->nodeType !== XML_ELEMENT_NODE) {
|
|
256
308
|
return false;
|
|
257
309
|
}
|
|
258
|
-
if (!
|
|
310
|
+
if (!static::isInline($child)) {
|
|
259
311
|
return false;
|
|
260
312
|
}
|
|
261
313
|
// Recursive: the inline element must not contain block elements
|
|
@@ -709,8 +709,8 @@ final class LD
|
|
|
709
709
|
for ($guard = 0; $guard < 50; $guard++) {
|
|
710
710
|
if (strncmp($dir . '/', $root . '/', strlen($root) + 1) !== 0) break;
|
|
711
711
|
|
|
712
|
-
$index = $dir
|
|
713
|
-
if (
|
|
712
|
+
$index = FS::indexFile($dir);
|
|
713
|
+
if ($index && (str_replace('\\', '/', @realpath($index) ?: $index)) !== $selfNorm) {
|
|
714
714
|
$info = FS::phpFileInfo($index) ?: new stdClass;
|
|
715
715
|
$fallback = $dir === $root ? self::config()->name : self::humanize(basename($dir));
|
|
716
716
|
$trail[] = [
|
|
@@ -30,8 +30,10 @@ final class PREPROS
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
// Only page files beneath the configured source root are publishable
|
|
34
|
-
//
|
|
33
|
+
// Only page files beneath the configured source root are publishable: a
|
|
34
|
+
// `_*.php`, or an `_index.md` that starts with an `@tag` header, in a
|
|
35
|
+
// folder that has no `_index.php` (otherwise the `.md` is left alone as
|
|
36
|
+
// data). Check every relative directory, but not the configured root's own name.
|
|
35
37
|
public static function isPage(string $file): bool
|
|
36
38
|
{
|
|
37
39
|
$file = realpath($file);
|
|
@@ -41,7 +43,9 @@ final class PREPROS
|
|
|
41
43
|
if (!str_starts_with($file, $root)) return false;
|
|
42
44
|
$parts = explode('/', substr($file, strlen($root)));
|
|
43
45
|
$name = array_pop($parts);
|
|
44
|
-
|
|
46
|
+
$isMd = strcasecmp($name, '_index.md') === 0 && !is_file(dirname($file) . '/_index.php')
|
|
47
|
+
&& FS::splitHeader(file_get_contents($file))[0] !== [];
|
|
48
|
+
if (!$isMd && !preg_match('/^_.*\.php$/i', $name)) return false;
|
|
45
49
|
foreach ($parts as $part) {
|
|
46
50
|
if (str_starts_with($part, '_')) return false;
|
|
47
51
|
}
|
|
@@ -66,6 +70,13 @@ final class PREPROS
|
|
|
66
70
|
$relroot = FS::getRelativePath($dir, self::$root);
|
|
67
71
|
$page = self::processHook('page_info', [$file, FS::phpFileInfo($file)]);
|
|
68
72
|
|
|
73
|
+
// A Markdown page is never executed: its body (below the header) is
|
|
74
|
+
// the page content, unless the header points `@content` elsewhere.
|
|
75
|
+
$isMd = strtolower(pathinfo($file, PATHINFO_EXTENSION)) === 'md';
|
|
76
|
+
if ($isMd && !isset($page->content)) {
|
|
77
|
+
$page->content = MD::toHtml(FS::splitHeader(file_get_contents($file))[1]);
|
|
78
|
+
}
|
|
79
|
+
|
|
69
80
|
extract((array)self::$config->data);
|
|
70
81
|
extract((array)$page);
|
|
71
82
|
|
|
@@ -76,7 +87,8 @@ final class PREPROS
|
|
|
76
87
|
if (self::$config->before) include(realpath(self::$root . self::$config->before));
|
|
77
88
|
$header = self::processHook('post_before', ob_get_clean());
|
|
78
89
|
|
|
79
|
-
if(
|
|
90
|
+
if ($isMd) $body = (string) $content;
|
|
91
|
+
elseif(empty($content)) {
|
|
80
92
|
ob_start();
|
|
81
93
|
include($file);
|
|
82
94
|
$body = ob_get_clean();
|
|
@@ -130,10 +142,11 @@ final class PREPROS
|
|
|
130
142
|
{
|
|
131
143
|
$paths = [];
|
|
132
144
|
$root = realpath(self::$root);
|
|
133
|
-
foreach (FS::dig($root . '/_index.php') as $file) {
|
|
145
|
+
foreach ([...FS::dig($root . '/_index.php'), ...FS::dig($root . '/_index.md')] as $file) {
|
|
134
146
|
if (!self::isPage($file)) continue;
|
|
135
147
|
$paths[] = str_replace('\\', '/', ltrim(str_replace($root, '', pathinfo(realpath($file), PATHINFO_DIRNAME)), DIRECTORY_SEPARATOR));
|
|
136
148
|
}
|
|
149
|
+
sort($paths);
|
|
137
150
|
if (empty($paths)) $paths[] = '';
|
|
138
151
|
$dom = new DOMDocument('1.0', 'UTF-8');
|
|
139
152
|
$dom->formatOutput = true;
|
|
@@ -51,6 +51,7 @@ PREPROS::registerHook('page_info', function($info) {
|
|
|
51
51
|
$file = PREPROS::$file;
|
|
52
52
|
}
|
|
53
53
|
foreach($page as $k => $v) {
|
|
54
|
+
if (!is_string($v)) continue; // a key a hook or helper added
|
|
54
55
|
$ext = strtolower(pathinfo($v, PATHINFO_EXTENSION));
|
|
55
56
|
if(in_array($ext, ['yaml', 'yml', 'json', 'md']) ) {
|
|
56
57
|
$isUrl = STR::is_url($v);
|
package/src/prepros.php
CHANGED
|
@@ -13,7 +13,7 @@ try {
|
|
|
13
13
|
elseif (!$target = realpath($argv[1])) STD::error("Invalid target.");
|
|
14
14
|
else if (is_dir($target)) {
|
|
15
15
|
$prj = new PREPROS($config);
|
|
16
|
-
foreach (FS::dig($target . '/*.php',
|
|
16
|
+
foreach ([...FS::dig($target . '/*.php'), ...FS::dig($target . '/_index.md')] as $file) {
|
|
17
17
|
if (!PREPROS::isPage($file)) continue;
|
|
18
18
|
PREPROS::render($file);
|
|
19
19
|
}
|