@kirigami/php-prepros 3.2.0 → 3.2.2

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/package.json CHANGED
@@ -1,59 +1,59 @@
1
- {
2
- "name": "@kirigami/php-prepros",
3
- "version": "3.2.0",
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.2",
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
+ }
@@ -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 $child) {
192
- $inner .= static::renderNode($child, $depth + 1);
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 ($node->childNodes as $child) {
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 (!in_array(strtolower($child->nodeName), self::INLINE, true)) {
310
+ if (!static::isInline($child)) {
259
311
  return false;
260
312
  }
261
313
  // Recursive: the inline element must not contain block elements
@@ -746,8 +746,9 @@ final class LD
746
746
  if ($url === null || $url === '') return null;
747
747
  if (preg_match('#^(https?:)?//#', $url) || str_starts_with($url, 'data:')) return $url;
748
748
 
749
- $origin = preg_replace('#^(https?://[^/]+).*#', '$1', (string) (self::data()->baseurl ?? ''));
750
- return $origin . '/' . ltrim($url, '/');
749
+ // Against the whole baseurl, path included: a site on `user.github.io/repo` keeps `/repo`.
750
+ $base = rtrim((string) (self::data()->baseurl ?? ''), '/');
751
+ return $base . '/' . ltrim($url, '/');
751
752
  }
752
753
 
753
754
  /** @return array<int,string> */
@@ -1,57 +1,57 @@
1
- <?php
2
-
3
- /**
4
- * MD — Markdown renderer backed by the native `mdhtml` extension (CommonMark
5
- * + GFM via cmark-gfm 0.29.0.gfm.13, statically built into @kirigami/php-wasm
6
- * — see php-kirigami/php-mdhtml).
7
- *
8
- * Switched from the hand-written recursive-descent/regex renderer to this
9
- * native extension — see docs/DECISIONS.md and docs/STATUS.md in the repo
10
- * root for the full reasoning. Every MD::toHtml() extension (plugins, emoji,
11
- * GFM alerts, definition lists, heading anchors, ==highlight==/^sup^/~sub~)
12
- * is reimplemented in C, not just the CommonMark/GFM core — see php-mdhtml's
13
- * README for exactly what's covered.
14
- *
15
- * Kept the old implementation as MD_LEGACY (md-legacy.class.php) — untouched,
16
- * still autoloadable — as a rollback path.
17
- *
18
- * Usage:
19
- * $html = MD::toHtml($markdown);
20
- * MD::registerPlugin('name', function (array $args, string $body): string { ... });
21
- * MD::registerEmoji('kirigami', '📐');
22
- */
23
- class MD
24
- {
25
- public static function registerPlugin(string $name, callable $callback): void
26
- {
27
- \MDHtml\RegisterPlugin($name, $callback);
28
- }
29
-
30
- public static function unregisterPlugin(string $name): void
31
- {
32
- \MDHtml\UnregisterPlugin($name);
33
- }
34
-
35
- public static function getRegisteredPlugins(): array
36
- {
37
- return \MDHtml\GetRegisteredPlugins();
38
- }
39
-
40
- public static function registerEmoji(string $shortcode, string $char): void
41
- {
42
- \MDHtml\RegisterEmoji($shortcode, $char);
43
- }
44
-
45
- public static function toHtml(string $markdown): string
46
- {
47
- return \MDHtml\Render($markdown);
48
- }
49
- }
50
-
51
-
52
- // Default Markdown plugins ({% codepen %}, {% checklist %}, {% callout %},
53
- // {% img-asset %}) — "registered out of the box" per the README. Loaded here
54
- // so every entrypoint (prepros.php, runenv.php, imagebatch.php) gets them
55
- // without an explicit include. A project can still MD::unregisterPlugin() any
56
- // of them, or MD::registerPlugin() its own with the same name to override.
57
- include_once(__DIR__ . '/md.plugins.php');
1
+ <?php
2
+
3
+ /**
4
+ * MD — Markdown renderer backed by the native `mdhtml` extension (CommonMark
5
+ * + GFM via cmark-gfm 0.29.0.gfm.13, statically built into @kirigami/php-wasm
6
+ * — see php-kirigami/php-mdhtml).
7
+ *
8
+ * Switched from the hand-written recursive-descent/regex renderer to this
9
+ * native extension — see docs/DECISIONS.md and docs/STATUS.md in the repo
10
+ * root for the full reasoning. Every MD::toHtml() extension (plugins, emoji,
11
+ * GFM alerts, definition lists, heading anchors, ==highlight==/^sup^/~sub~)
12
+ * is reimplemented in C, not just the CommonMark/GFM core — see php-mdhtml's
13
+ * README for exactly what's covered.
14
+ *
15
+ * Kept the old implementation as MD_LEGACY (md-legacy.class.php) — untouched,
16
+ * still autoloadable — as a rollback path.
17
+ *
18
+ * Usage:
19
+ * $html = MD::toHtml($markdown);
20
+ * MD::registerPlugin('name', function (array $args, string $body): string { ... });
21
+ * MD::registerEmoji('kirigami', '📐');
22
+ */
23
+ class MD
24
+ {
25
+ public static function registerPlugin(string $name, callable $callback): void
26
+ {
27
+ \MDHtml\RegisterPlugin($name, $callback);
28
+ }
29
+
30
+ public static function unregisterPlugin(string $name): void
31
+ {
32
+ \MDHtml\UnregisterPlugin($name);
33
+ }
34
+
35
+ public static function getRegisteredPlugins(): array
36
+ {
37
+ return \MDHtml\GetRegisteredPlugins();
38
+ }
39
+
40
+ public static function registerEmoji(string $shortcode, string $char): void
41
+ {
42
+ \MDHtml\RegisterEmoji($shortcode, $char);
43
+ }
44
+
45
+ public static function toHtml(string $markdown): string
46
+ {
47
+ return \MDHtml\Render($markdown);
48
+ }
49
+ }
50
+
51
+
52
+ // Default Markdown plugins ({% codepen %}, {% callout %},
53
+ // {% img-asset %}) — "registered out of the box" per the README. Loaded here
54
+ // so every entrypoint (prepros.php, runenv.php, imagebatch.php) gets them
55
+ // without an explicit include. A project can still MD::unregisterPlugin() any
56
+ // of them, or MD::registerPlugin() its own with the same name to override.
57
+ include_once(__DIR__ . '/md.plugins.php');
@@ -1,103 +1,80 @@
1
- <?php
2
-
3
-
4
-
5
- // =============================================================================
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.
13
- // =============================================================================
14
-
15
- // --- CodePen (inline) ---
16
- // {% codepen PENid %}
17
- // {% codepen PENid user height %}
18
- MD::registerPlugin('codepen', function (array $args, string $body): string {
19
- $id = htmlspecialchars($args[0] ?? '', ENT_QUOTES, 'UTF-8');
20
- $user = htmlspecialchars($args[1] ?? 'anonymous', ENT_QUOTES, 'UTF-8');
21
- $height = intval($args[2] ?? 400);
22
- if ($id === '') return '<!-- codepen: missing id -->';
23
- return "<iframe height=\"{$height}\" style=\"width:100%\" "
24
- . "scrolling=\"no\" "
25
- . "src=\"https://codepen.io/{$user}/embed/{$id}?default-tab=result\" "
26
- . "frameborder=\"no\" loading=\"lazy\" allowfullscreen>"
27
- . "</iframe>";
28
- });
29
-
30
- // --- Checklist (multi-line block) ---
31
- // {% checklist
32
- // Do the dishes
33
- // Walk the dog
34
- // Read a book
35
- // %}
36
- //
37
- // Optionally with a title:
38
- // {% checklist "My tasks for today"
39
- // Task 1
40
- // Task 2
41
- // %}
42
- MD::registerPlugin('checklist', function (array $args, string $body): string {
43
- $title = !empty($args[0]) ? '<p class="checklist-title"><strong>' . htmlspecialchars($args[0], ENT_QUOTES, 'UTF-8') . '</strong></p>' : '';
44
- $items = array_filter(array_map('trim', explode("\n", $body)));
45
- if (empty($items)) return '<!-- checklist: no items -->';
46
- $html = "<div class=\"checklist\">{$title}<ul>\n";
47
- foreach ($items as $item) {
48
- $html .= ' <li><label><input type="checkbox" /> ' . htmlspecialchars($item, ENT_QUOTES, 'UTF-8') . '</label></li>' . "\n";
49
- }
50
- return $html . "</ul></div>";
51
- });
52
-
53
- // --- Custom callout (inline or block) ---
54
- // Inline: {% callout warning "Warning!" Content on one line %}
55
- // Block : {% callout danger "Title"
56
- // Paragraph 1
57
- //
58
- // Paragraph 2
59
- // %}
60
- MD::registerPlugin('callout', function (array $args, string $body): string {
61
- $validTypes = ['info', 'success', 'warning', 'danger'];
62
- $type = in_array($args[0] ?? '', $validTypes, true) ? $args[0] : 'info';
63
- $title = !empty($args[1]) ? htmlspecialchars($args[1], ENT_QUOTES, 'UTF-8') : '';
64
-
65
- // Content: $body for a block tag, otherwise the remaining args on the opening line
66
- if ($body !== '') {
67
- $content = htmlspecialchars($body, ENT_QUOTES, 'UTF-8');
68
- // Replace double line breaks with <br><br> to preserve paragraphs
69
- $content = str_replace("\n\n", '<br><br>', $content);
70
- $content = str_replace("\n", '<br>', $content);
71
- } else {
72
- $content = htmlspecialchars(implode(' ', array_slice($args, $title ? 2 : 1)), ENT_QUOTES, 'UTF-8');
73
- }
74
-
75
- $titleHtml = $title ? "<strong>{$title}</strong><br>" : '';
76
- return "<div class=\"callout callout-{$type}\">{$titleHtml}{$content}</div>";
77
- });
78
-
79
- // --- Image asset (inline) — same pipeline as <img asset>, via IMG::asset() ---
80
- // {% img-asset photo.jpg %}
81
- // {% img-asset photo.jpg 800 %}
82
- // {% img-asset photo.jpg 800 600 %}
83
- // {% img-asset photo.jpg 800 600 cover %}
84
- MD::registerPlugin('img-asset', function (array $args, string $body): string {
85
- $path = trim($args[0] ?? '');
86
- if ($path === '') return '<!-- img-asset: missing path -->';
87
-
88
- $width = isset($args[1]) ? (int) $args[1] : 0;
89
- $height = isset($args[2]) ? (int) $args[2] : 0;
90
- $cover = isset($args[3]) && strtolower($args[3]) === 'cover';
91
-
92
- // A resolvable path is required to actually call IMG::asset() — this
93
- // also protects a literal `{% img-asset path … %}` written as Markdown
94
- // *documentation* (inside a code span): the plugin still runs even when
95
- // its output ends up discarded for the literal text, so a placeholder
96
- // path must not throw.
97
- try {
98
- $src = IMG::asset($path, $width, $height, $cover, PREPROS::$file);
99
- } catch (Throwable $e) {
100
- return '<!-- img-asset: ' . htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8') . ' -->';
101
- }
102
- return '<img src="' . htmlspecialchars($src, ENT_QUOTES, 'UTF-8') . '" alt="">';
1
+ <?php
2
+
3
+
4
+
5
+ // =============================================================================
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.
13
+ // =============================================================================
14
+
15
+ // --- CodePen (inline) ---
16
+ // {% codepen PENid %}
17
+ // {% codepen PENid user height %}
18
+ MD::registerPlugin('codepen', function (array $args, string $body): string {
19
+ $id = htmlspecialchars($args[0] ?? '', ENT_QUOTES, 'UTF-8');
20
+ $user = htmlspecialchars($args[1] ?? 'anonymous', ENT_QUOTES, 'UTF-8');
21
+ $height = intval($args[2] ?? 400);
22
+ if ($id === '') return '<!-- codepen: missing id -->';
23
+ return "<iframe height=\"{$height}\" style=\"width:100%\" "
24
+ . "scrolling=\"no\" "
25
+ . "src=\"https://codepen.io/{$user}/embed/{$id}?default-tab=result\" "
26
+ . "frameborder=\"no\" loading=\"lazy\" allowfullscreen>"
27
+ . "</iframe>";
28
+ });
29
+
30
+ // --- Custom callout (inline or block) ---
31
+ // Inline: {% callout warning "Warning!" Content on one line %}
32
+ // Block : {% callout danger "Title"
33
+ // Paragraph 1
34
+ //
35
+ // Paragraph 2
36
+ // %}
37
+ MD::registerPlugin('callout', function (array $args, string $body): string {
38
+ $validTypes = ['info', 'success', 'warning', 'danger'];
39
+ $type = in_array($args[0] ?? '', $validTypes, true) ? $args[0] : 'info';
40
+ $title = !empty($args[1]) ? htmlspecialchars($args[1], ENT_QUOTES, 'UTF-8') : '';
41
+
42
+ // Content: $body for a block tag, otherwise the remaining args on the opening line
43
+ if ($body !== '') {
44
+ $content = htmlspecialchars($body, ENT_QUOTES, 'UTF-8');
45
+ // Replace double line breaks with <br><br> to preserve paragraphs
46
+ $content = str_replace("\n\n", '<br><br>', $content);
47
+ $content = str_replace("\n", '<br>', $content);
48
+ } else {
49
+ $content = htmlspecialchars(implode(' ', array_slice($args, $title ? 2 : 1)), ENT_QUOTES, 'UTF-8');
50
+ }
51
+
52
+ $titleHtml = $title ? "<strong>{$title}</strong><br>" : '';
53
+ return "<div class=\"callout callout-{$type}\">{$titleHtml}{$content}</div>";
54
+ });
55
+
56
+ // --- Image asset (inline) — same pipeline as <img asset>, via IMG::asset() ---
57
+ // {% img-asset photo.jpg %}
58
+ // {% img-asset photo.jpg 800 %}
59
+ // {% img-asset photo.jpg 800 600 %}
60
+ // {% img-asset photo.jpg 800 600 cover %}
61
+ MD::registerPlugin('img-asset', function (array $args, string $body): string {
62
+ $path = trim($args[0] ?? '');
63
+ if ($path === '') return '<!-- img-asset: missing path -->';
64
+
65
+ $width = isset($args[1]) ? (int) $args[1] : 0;
66
+ $height = isset($args[2]) ? (int) $args[2] : 0;
67
+ $cover = isset($args[3]) && strtolower($args[3]) === 'cover';
68
+
69
+ // A resolvable path is required to actually call IMG::asset() — this
70
+ // also protects a literal `{% img-asset path … %}` written as Markdown
71
+ // *documentation* (inside a code span): the plugin still runs even when
72
+ // its output ends up discarded for the literal text, so a placeholder
73
+ // path must not throw.
74
+ try {
75
+ $src = IMG::asset($path, $width, $height, $cover, PREPROS::$file);
76
+ } catch (Throwable $e) {
77
+ return '<!-- img-asset: ' . htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8') . ' -->';
78
+ }
79
+ return '<img src="' . htmlspecialchars($src, ENT_QUOTES, 'UTF-8') . '" alt="">';
103
80
  });
@@ -458,8 +458,9 @@ final class META
458
458
  $url = trim($url);
459
459
  if (preg_match('#^(https?:)?//#', $url) || str_starts_with($url, 'data:')) return $url;
460
460
 
461
- $origin = preg_replace('#^(https?://[^/]+).*#', '$1', (string) (self::data()->baseurl ?? ''));
462
- return $origin === '' ? $url : $origin . '/' . ltrim($url, '/');
461
+ // Against the whole baseurl, path included: a site on `user.github.io/repo` keeps `/repo`.
462
+ $base = rtrim((string) (self::data()->baseurl ?? ''), '/');
463
+ return $base === '' ? $url : $base . '/' . ltrim($url, '/');
463
464
  }
464
465
 
465
466
  /** `@user` / `user` / `https://twitter.com/user` → `@user`. */