@kirigami/php-prepros 3.1.1 → 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 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 // parse PHPDOC annotations
1570
- FS::getChildren(string $backtrace = ''): object[] // child _index.php pages, ordered by @position
1571
- FS::getBreadcrumb(string $backtrace = ''): object[] // ancestor _index.php pages, top-most first (opt-in via @breadcrumb)
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 its `@tag value` pairs as a `stdClass`. This is used internally to resolve page metadata and data-file annotations.
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.1.1",
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",
@@ -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 an `_index.php`, parses
56
- * that file's first PHPDOC block via {@see FS::phpFileInfo()} and returns
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
- $index = $subdir . '/_index.php';
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 . '/_index.php';
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
- if (!isset($files[$file])) {
176
- $tokens = token_get_all(file_get_contents($file));
177
- foreach ($tokens as $tok) {
178
- if (!is_array($tok)) continue;
179
- if ($tok[0] == T_DOC_COMMENT) {
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
- if (empty($block)) return new stdClass;
185
- $files[$file] = (object) self::parseDocBlock($block);
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 $files[$file];
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
- * @param string $block Raw `/** … *&#47;` doc-comment text.
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 `/** … *&#47;` 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 = trim($m[1]);
217
- $info[$current] = trim($m[2]);
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 . '/_index.php';
713
- if (is_file($index) && (str_replace('\\', '/', @realpath($index) ?: $index)) !== $selfNorm) {
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
- // Check every relative directory, but not the configured root's own name.
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
- if (!preg_match('/^_.*\.php$/i', $name)) return false;
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(empty($content)) {
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', true) as $file) {
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
  }