@kirigami/php-prepros 3.1.0 → 3.2.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 +71 -6
- package/package.json +2 -2
- package/src/libraries/fs.class.php +171 -22
- 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/README.md
CHANGED
|
@@ -34,6 +34,7 @@ Part of the **Kirigami** project ecosystem.
|
|
|
34
34
|
|
|
35
35
|
- [@kirigami/php-prepros](#kirigamiphp-prepros)
|
|
36
36
|
- [Overview](#overview)
|
|
37
|
+
- [What's new in 3.2.0](#whats-new-in-320)
|
|
37
38
|
- [What's new in 3.0.1](#whats-new-in-301)
|
|
38
39
|
- [What's new in 3.0.0](#whats-new-in-300)
|
|
39
40
|
- [What's new in 2.0.0](#whats-new-in-200)
|
|
@@ -106,6 +107,15 @@ Part of the **Kirigami** project ecosystem.
|
|
|
106
107
|
|
|
107
108
|
---
|
|
108
109
|
|
|
110
|
+
## What's new in 3.2.0
|
|
111
|
+
|
|
112
|
+
- **Markdown pages.** An `_index.md` whose first lines are `@tag value` annotations is a page, like an `_index.php`: its body is rendered as Markdown and wrapped by the layouts. Without that header, or next to an `_index.php`, it stays a data file and is left alone. See [Markdown pages](#markdown-pages).
|
|
113
|
+
- **Inherited annotations.** `@@tag value` sets `tag` on the page and on every page below it; a child's `@tag` overrides it for that page only, a child's `@@tag` overrides it and passes the new value down. Works in PHPDOC blocks and Markdown headers. See [Inherited annotations (`@@`)](#inherited-annotations-).
|
|
114
|
+
- `FS::phpFileInfo()` reads Markdown headers and inherited values, and returns a fresh copy on each call: adding a key to the result no longer leaks into that page's own variables. `FS::getChildren()`, `FS::getBreadcrumb()`, the sitemap and the JSON-LD breadcrumb see `_index.md` pages.
|
|
115
|
+
- The `page_info` hook skips values that aren't strings instead of failing on them.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
109
119
|
## What's new in 3.0.1
|
|
110
120
|
|
|
111
121
|
A registered tag written inside Markdown code is no longer processed. `<markdown>`, `<img asset="...">` or a plugin tag shown in an inline code span (`` `<markdown prose>` ``) or a fenced block (` ``` ` / `~~~`) stays example text: before, a `<markdown>` in a code span paired with the real block's closing tag and broke the rest of the page, and an `<img asset>` in one became an empty image. Indented (four-space) code blocks are not recognized, since `<markdown>` bodies are indented; use a fence there.
|
|
@@ -653,7 +663,7 @@ Ordered list of build tasks, run in array order. **Consumed by the `kiri` CLI**,
|
|
|
653
663
|
|
|
654
664
|
## Writing pages
|
|
655
665
|
|
|
656
|
-
Source pages live in the directory pointed to by `kirigami.root`. The naming convention is straightforward: any file whose name starts with `_` and ends in `.php` is treated as a page source. The leading underscore is stripped in the output filename.
|
|
666
|
+
Source pages live in the directory pointed to by `kirigami.root`. The naming convention is straightforward: any file whose name starts with `_` and ends in `.php` is treated as a page source. The leading underscore is stripped in the output filename. An `_index.md` with an annotation header is a page too (see [Markdown pages](#markdown-pages)).
|
|
657
667
|
|
|
658
668
|
```
|
|
659
669
|
src/
|
|
@@ -662,6 +672,8 @@ src/
|
|
|
662
672
|
├── _index.php → src/index.html
|
|
663
673
|
├── about/
|
|
664
674
|
│ └── _index.php → src/about/index.html
|
|
675
|
+
├── notes/
|
|
676
|
+
│ └── _index.md → src/notes/index.html
|
|
665
677
|
└── blog/
|
|
666
678
|
├── _index.php → src/blog/index.html
|
|
667
679
|
└── _articles.yaml (data file, not compiled)
|
|
@@ -703,6 +715,57 @@ A value can wrap onto the following **indented** continuation lines:
|
|
|
703
715
|
*/
|
|
704
716
|
```
|
|
705
717
|
|
|
718
|
+
### Markdown pages
|
|
719
|
+
|
|
720
|
+
A page can be pure Markdown: an `_index.md` whose first lines are
|
|
721
|
+
annotations, written like PHPDOC tags without the comment around them.
|
|
722
|
+
|
|
723
|
+
```markdown
|
|
724
|
+
@title Hello, world
|
|
725
|
+
@type post
|
|
726
|
+
@date 2026-09-01
|
|
727
|
+
@abstract The first post.
|
|
728
|
+
|
|
729
|
+
Some **Markdown** text: the page body.
|
|
730
|
+
```
|
|
731
|
+
|
|
732
|
+
- The header is the `@tag value` lines at the top of the file (leading blank
|
|
733
|
+
lines allowed), with the same rules as a PHPDOC block: a value wraps onto
|
|
734
|
+
indented continuation lines. It ends at the first blank line, or the first
|
|
735
|
+
flush-left line that isn't a tag, where the body starts.
|
|
736
|
+
- The body goes through `MD::toHtml()` and becomes `$content`, so `@type`,
|
|
737
|
+
`@indent` and the layouts work as for a PHP page. PHP in the file is never
|
|
738
|
+
run. `@content other.md` in the header replaces the body.
|
|
739
|
+
- An `_index.md` **without** a header is not a page: it is left alone, as a
|
|
740
|
+
data file. So is one next to an `_index.php`, which is the page of that
|
|
741
|
+
folder (it can load the `.md` through an annotation).
|
|
742
|
+
- Only `_index.md` is a page; other `_*.md` files are data files as before.
|
|
743
|
+
|
|
744
|
+
### Inherited annotations (`@@`)
|
|
745
|
+
|
|
746
|
+
A tag written with two `@` applies to the page **and every page below it**
|
|
747
|
+
(the pages of its subfolders, and the other pages of its own folder when it is
|
|
748
|
+
an `_index`):
|
|
749
|
+
|
|
750
|
+
```php
|
|
751
|
+
/**
|
|
752
|
+
* @title Blog
|
|
753
|
+
* @@type post ← every page under blog/ is a post
|
|
754
|
+
* @@menu _menu.yaml ← loaded from blog/, wherever the page is
|
|
755
|
+
*/
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
- A child's `@tag` overrides the inherited value **for that page only**; its
|
|
759
|
+
own children still get the ancestor's value.
|
|
760
|
+
- A child's `@@tag` overrides it **and passes the new value down**.
|
|
761
|
+
- The nearest ancestor wins. Values come from the page file of each folder
|
|
762
|
+
above (`_index.php`, else `_index.md`), up to `kirigami.root`.
|
|
763
|
+
- A relative data-file value (`.yaml`/`.yml`/`.json`/`.md`) is resolved
|
|
764
|
+
against the folder of the page that declared it.
|
|
765
|
+
- In PHPDOC blocks and Markdown headers alike. `FS::getChildren()` and
|
|
766
|
+
`FS::getBreadcrumb()` entries include inherited values too, so
|
|
767
|
+
`@@breadcrumb true` turns breadcrumbs on for a whole section.
|
|
768
|
+
|
|
706
769
|
### Auto-loading data files
|
|
707
770
|
|
|
708
771
|
When an annotation value looks like a filename (with a `.yaml`, `.yml`, `.json`, or `.md` extension), it is automatically parsed and injected as a structured variable instead of a plain string.
|
|
@@ -1566,18 +1629,20 @@ Filesystem utilities.
|
|
|
1566
1629
|
```php
|
|
1567
1630
|
FS::dig(string $glob): iterable // recursive glob, yields file paths
|
|
1568
1631
|
FS::getRelativePath(string $from, string $to): string
|
|
1569
|
-
FS::phpFileInfo(string $file): object|false //
|
|
1570
|
-
FS::
|
|
1571
|
-
FS::
|
|
1632
|
+
FS::phpFileInfo(string $file): object|false // page annotations (PHPDOC or Markdown header, + inherited @@)
|
|
1633
|
+
FS::splitHeader(string $text): array // Markdown page → [annotations, body, @@ names]
|
|
1634
|
+
FS::indexFile(string $dir): ?string // the folder's _index.php, else _index.md, else null
|
|
1635
|
+
FS::getChildren(string $backtrace = ''): object[] // child _index pages, ordered by @position
|
|
1636
|
+
FS::getBreadcrumb(string $backtrace = ''): object[] // ancestor _index pages, top-most first (opt-in via @breadcrumb)
|
|
1572
1637
|
FS::rmdir(string $dir, bool $removeSelf = true): bool
|
|
1573
1638
|
FS::pathJoin(string ...$parts): string // URL-aware path join with .. resolution
|
|
1574
1639
|
```
|
|
1575
1640
|
|
|
1576
1641
|
`FS::dig()` is the workhorse of directory-wide builds — it recursively walks a glob pattern and yields every matching file path.
|
|
1577
1642
|
|
|
1578
|
-
`FS::phpFileInfo()` parses the first PHPDOC block of a PHP file and returns
|
|
1643
|
+
`FS::phpFileInfo()` parses the first PHPDOC block of a PHP file (or the header of a [Markdown page](#markdown-pages)), merges in the values it [inherits](#inherited-annotations-) from the pages above it, and returns the `@tag value` pairs as a `stdClass`. Each call returns a fresh copy, so adding a key to the result is safe. This is used internally to resolve page metadata and data-file annotations.
|
|
1579
1644
|
|
|
1580
|
-
`FS::getChildren()` (procedural: `fs_get_children()`) — usable only during a render — scans the folders directly below the calling template, keeps the ones that contain an `_index.php`, and returns one `stdClass` per child: the parsed PHPDOC of that `_index.php` plus a `->file` key with its absolute path. Entries are ordered by `@position` ascending (a page with no `@position` sorts as `999999`), then by folder name (natural, case-insensitive). Handy for building a section index or a navigation menu:
|
|
1645
|
+
`FS::getChildren()` (procedural: `fs_get_children()`) — usable only during a render — scans the folders directly below the calling template, keeps the ones that contain an `_index.php` or an `_index.md`, and returns one `stdClass` per child: the parsed PHPDOC of that `_index.php` plus a `->file` key with its absolute path. Entries are ordered by `@position` ascending (a page with no `@position` sorts as `999999`), then by folder name (natural, case-insensitive). Handy for building a section index or a navigation menu:
|
|
1581
1646
|
|
|
1582
1647
|
```php
|
|
1583
1648
|
<?php foreach (fs_get_children() as $page): ?>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kirigami/php-prepros",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "PHP preprocessor for the Kirigami static site generator. Compile PHP page templates to clean, deployable HTML — with zero server dependency.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"kirigami",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"test": "node --test --test-concurrency=1 \"test/*.test.js\""
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@kirigami/php-wasm": "8.5.11",
|
|
47
|
+
"@kirigami/php-wasm": "8.5.11-1",
|
|
48
48
|
"@kirigami/struct-walker": "1.0.6",
|
|
49
49
|
"picomatch": "^4.0.7"
|
|
50
50
|
},
|
|
@@ -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)) {
|
|
@@ -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
|
}
|