@kirigami/php-prepros 1.7.1 → 1.8.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
  - [@kirigami/php-prepros](#kirigamiphp-prepros)
35
35
  - [Overview](#overview)
36
36
  - [Table of contents](#table-of-contents)
37
+ - [What's new in 1.7.2](#whats-new-in-172)
37
38
  - [What's new in 1.7.1](#whats-new-in-171)
38
39
  - [What's new in 1.7.0](#whats-new-in-170)
39
40
  - [What's new in 1.6.0](#whats-new-in-160)
@@ -108,6 +109,46 @@ Part of the **Kirigami** project ecosystem.
108
109
 
109
110
  ---
110
111
 
112
+ ## What's new in 1.8.0
113
+
114
+ - **`{% img-asset %}` — a new built-in Markdown plugin.** Same pipeline as the
115
+ `<img asset>` HTML tag (`IMG::asset()`: resize, cache, publish under
116
+ `image.dest`), usable straight from Markdown text:
117
+
118
+ ```
119
+ {% img-asset photo.jpg %}
120
+ {% img-asset photo.jpg 800 %}
121
+ {% img-asset photo.jpg 800 600 %}
122
+ {% img-asset photo.jpg 800 600 cover %}
123
+ ```
124
+
125
+ Positional args: source path (relative to `image.source`), width, height,
126
+ and the literal `cover` keyword. Registered in `md.plugins.php` alongside
127
+ `codepen`/`youtube`/`checklist`/`callout` — available out of the box, drop
128
+ it with `MD::unregisterPlugin('img-asset')` if you don't want it.
129
+
130
+ ---
131
+
132
+ ## What's new in 1.7.2
133
+
134
+ - **Cleaner formatted output around highlighted code.** The de-indent script
135
+ `PREPROS::injectHead()` adds when `prepros.format` is on now flattens the
136
+ `<pre><code>` indentation `HTML::format()` writes for *every* block, including
137
+ ones a build-time highlighter has wrapped in `<span>`s. It works on
138
+ `innerHTML` line by line and removes only the shared leading run (relative
139
+ indentation is kept). This lets [`@kirigami/plugin-highlight`](https://www.npmjs.com/package/@kirigami/plugin-highlight)
140
+ 1.7.2+ re-indent its markup to line up with the rest of the document instead
141
+ of leaving it flush-left — the served HTML stays consistently indented, the
142
+ rendered code is still de-indented before the first paint.
143
+
144
+ - **`<markdown prose>` wraps in `.prose`.** With the `prose` attribute the
145
+ built-in tag emits `<div class="prose"> … </div>` so long-form Markdown picks
146
+ up `@kirigami/canva`'s `styles/prose` typography with no extra markup. Opt-in
147
+ (a bare `<markdown>` is unchanged); `class` / `id` on the tag land on the
148
+ wrapper.
149
+
150
+ ---
151
+
111
152
  ## What's new in 1.7.1
112
153
 
113
154
  - **No side effects on import.** `kirigami.yaml` is now loaded on first use
@@ -157,10 +198,11 @@ Part of the **Kirigami** project ecosystem.
157
198
  - **`HTML::format()` indents `<pre><code>`.** A fenced code block's lines are
158
199
  shifted to the block's nesting depth so the HTML source stays readable
159
200
  (relative indentation preserved). The exact leading run is stripped again
160
- before it's shown — at build time by `@kirigami/plugin-highlight`, otherwise
161
- by a ~250-byte de-indent script `prepros.head` injects before `</body>` (only
162
- when `format` is on; it skips blocks a highlighter already flattened). A bare
163
- `<pre>` and `<textarea>` are still emitted byte-for-byte.
201
+ before it's shown, by a small de-indent script `prepros.head` injects before
202
+ `</body>` (only when `format` is on). Since 1.7.2 the script works on
203
+ `innerHTML` line by line, so it also flattens blocks a build-time highlighter
204
+ has wrapped in `<span>`s. A bare `<pre>` and `<textarea>` are still emitted
205
+ byte-for-byte.
164
206
 
165
207
  ---
166
208
 
@@ -623,6 +665,13 @@ your template. All registered [MD plugins](#md-plugins) work inside it. See
623
665
  </section>
624
666
  ```
625
667
 
668
+ Add the `prose` attribute — `<markdown prose>` — to wrap the output in
669
+ `<div class="prose">`, so it picks up the long-form typography of
670
+ [`@kirigami/canva`'s `styles/prose`](https://www.npmjs.com/package/@kirigami/canva)
671
+ with no extra markup. Any `class` / `id` on the tag lands on that wrapper
672
+ (`<markdown prose class="lede" id="intro">` → `<div class="prose lede" id="intro">`).
673
+ A bare `<markdown>` emits just the converted HTML, as before.
674
+
626
675
  #### `<img asset="…">`
627
676
 
628
677
  The HTML-side entry point of the image autogenerator — the exact same feature as
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kirigami/php-prepros",
3
- "version": "1.7.1",
3
+ "version": "1.8.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",
@@ -212,7 +212,7 @@ class FS
212
212
  $line = preg_replace('#\s*\*/\s*$#', '', $line);
213
213
  $line = preg_replace('#^[ \t]*\*[ \t]?#', '', $line, 1);
214
214
 
215
- if (preg_match('/^[ \t]*@([A-Za-z0-9_]+)[ \t]*(.*)$/', $line, $m)) {
215
+ if (preg_match('/^@([A-Za-z0-9_]+)[ \t]*(.*)$/', $line, $m)) {
216
216
  $current = trim($m[1]);
217
217
  $info[$current] = trim($m[2]);
218
218
  } elseif (trim($line) === '') {
@@ -27,6 +27,10 @@ class HTML
27
27
  'span', 'strong', 'sub', 'sup', 'textarea', 'time', 'tt', 'u', 'var',
28
28
  ];
29
29
 
30
+ // The HTML namespace URI Lexbor assigns to plain HTML elements (foreign
31
+ // content — SVG, MathML — gets its own namespace URI instead).
32
+ private const HTML_NS = 'http://www.w3.org/1999/xhtml';
33
+
30
34
  private const RAW = ['script', 'style'];
31
35
  // Whitespace-significant elements: their text content is emitted byte for
32
36
  // byte, never re-indented or collapsed (a code block must keep its line
@@ -71,13 +75,32 @@ class HTML
71
75
  };
72
76
  }
73
77
 
78
+ // Whether $node sits in the HTML namespace (as opposed to foreign content —
79
+ // SVG, MathML — whose element/attribute names carry meaningful case that
80
+ // Lexbor already restored while parsing).
81
+ private static function isHtmlNamespace(Dom\Node $node): bool
82
+ {
83
+ $ns = $node->namespaceURI ?? null;
84
+ return $ns === null || $ns === self::HTML_NS;
85
+ }
86
+
87
+ // The name to emit for $node: lowercase in the HTML namespace, verbatim
88
+ // (parser-adjusted) case in foreign content — e.g. `viewBox`, not `viewbox`.
89
+ private static function tagName(Dom\Node $node): string
90
+ {
91
+ return static::isHtmlNamespace($node) ? strtolower($node->nodeName) : $node->nodeName;
92
+ }
93
+
74
94
  private static function renderElement(Dom\Node $node, int $depth): string
75
95
  {
76
- $tag = strtolower($node->nodeName);
96
+ // VOID/RAW/VERBATIM/INLINE are HTML-only concepts — always look them up
97
+ // by the lowercase name, independent of the case emitted for $tag.
98
+ $lookup = strtolower($node->nodeName);
99
+ $tag = static::tagName($node);
77
100
  $pad = str_repeat(' ', $depth * self::INDENT);
78
101
  $attrs = static::renderAttrs($node);
79
- $isVoid = in_array($tag, self::VOID, true);
80
- $isRaw = in_array($tag, self::RAW, true);
102
+ $isVoid = in_array($lookup, self::VOID, true);
103
+ $isRaw = in_array($lookup, self::RAW, true);
81
104
 
82
105
  if ($isVoid) {
83
106
  return "{$pad}<{$tag}{$attrs}>\n";
@@ -114,7 +137,7 @@ class HTML
114
137
  // Whitespace-significant: emit the inner HTML exactly as parsed, so a
115
138
  // bare <pre>/<textarea> keeps its line breaks and indentation. Only the
116
139
  // opening tag is padded to the current depth.
117
- if (in_array($tag, self::VERBATIM, true)) {
140
+ if (in_array($lookup, self::VERBATIM, true)) {
118
141
  /** @var DOMElement $node */
119
142
  $token = "\x01VERB" . count(self::$verbatim) . "\x01";
120
143
  self::$verbatim[$token] = $node->innerHTML;
@@ -191,9 +214,10 @@ class HTML
191
214
  if ($child->nodeType !== XML_ELEMENT_NODE) {
192
215
  continue;
193
216
  }
194
- $tag = strtolower($child->nodeName);
195
- $attrs = static::renderAttrs($child);
196
- if (in_array($tag, self::VOID, true)) {
217
+ $tag = static::tagName($child);
218
+ $lookup = strtolower($child->nodeName);
219
+ $attrs = static::renderAttrs($child);
220
+ if (in_array($lookup, self::VOID, true)) {
197
221
  $out .= "<{$tag}{$attrs}>";
198
222
  } else {
199
223
  $out .= "<{$tag}{$attrs}>" . static::renderInline($child) . "</{$tag}>";
@@ -283,13 +307,16 @@ class HTML
283
307
  private static function renderAttrs(Dom\Node $node): string
284
308
  {
285
309
  $out = '';
286
- /** @var DOMElement $node */
310
+ /** @var DOMElement|DOM\Node $node */
287
311
  if (!$node->hasAttributes()) {
288
312
  return $out;
289
313
  }
314
+ $isHtml = static::isHtmlNamespace($node);
290
315
  foreach ($node->attributes as $attr) {
291
- $name = strtolower(trim((string) $attr->name));
292
- if (in_array($name, self::BOOLEAN_ATTRS, true)) {
316
+ $raw = trim((string) $attr->name);
317
+ $lookup = strtolower($raw);
318
+ $name = $isHtml ? $lookup : $raw;
319
+ if (in_array($lookup, self::BOOLEAN_ATTRS, true)) {
293
320
  $out .= ' ' . $name;
294
321
  } else {
295
322
  $out .= ' ' . $name . '="' . htmlspecialchars((string) $attr->value, ENT_QUOTES | ENT_HTML5, 'UTF-8') . '"';
@@ -807,11 +807,11 @@ class MD {
807
807
  $label = htmlspecialchars($matches[2], ENT_QUOTES, 'UTF-8');
808
808
  $content = preg_replace('/^>\s?\[!(?:NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\n?/m', '', $matches[1]);
809
809
  $content = preg_replace('/^>[ \t]?/m', '', $content);
810
- $content = htmlspecialchars(trim($content), ENT_QUOTES, 'UTF-8');
810
+ $content = self::toHtml(trim($content));
811
811
  $placeholder = "\x02BQ" . count($blockquotes) . "\x03";
812
812
  $blockquotes[$placeholder] = "<div class=\"markdown-alert markdown-alert-{$type}\">"
813
813
  . "<p class=\"markdown-alert-title\">{$label}</p>"
814
- . "<p>{$content}</p></div>";
814
+ . "{$content}</div>";
815
815
  // The trailing \n consumed by the regex is re-injected after
816
816
  // the placeholder so the following blank line doesn't merge
817
817
  // with the placeholder's line (which would break, for example,
@@ -89,4 +89,21 @@ MD::registerPlugin('callout', function (array $args, string $body): string {
89
89
 
90
90
  $titleHtml = $title ? "<strong>{$title}</strong><br>" : '';
91
91
  return "<div class=\"callout callout-{$type}\">{$titleHtml}{$content}</div>";
92
+ });
93
+
94
+ // --- Image asset (inline) — same pipeline as <img asset>, via IMG::asset() ---
95
+ // {% img-asset photo.jpg %}
96
+ // {% img-asset photo.jpg 800 %}
97
+ // {% img-asset photo.jpg 800 600 %}
98
+ // {% img-asset photo.jpg 800 600 cover %}
99
+ MD::registerPlugin('img-asset', function (array $args, string $body): string {
100
+ $path = trim($args[0] ?? '');
101
+ if ($path === '') return '<!-- img-asset: missing path -->';
102
+
103
+ $width = isset($args[1]) ? (int) $args[1] : 0;
104
+ $height = isset($args[2]) ? (int) $args[2] : 0;
105
+ $cover = isset($args[3]) && strtolower($args[3]) === 'cover';
106
+
107
+ $src = IMG::asset($path, $width, $height, $cover, PREPROS::$file);
108
+ return '<img src="' . htmlspecialchars($src, ENT_QUOTES, 'UTF-8') . '" alt="">';
92
109
  });
@@ -161,14 +161,16 @@ final class PREPROS
161
161
  $scripts = [];
162
162
 
163
163
  // Undo the <pre><code> indentation HTML::format() added, at parse time
164
- // (before first paint), for blocks a build-time highlighter didn't
165
- // already flatten (those carry child <span>s).
164
+ // (before first paint). Works on innerHTML line by line so it also
165
+ // flattens blocks a build-time highlighter has wrapped in <span>s
166
+ // (@kirigami/plugin-highlight re-indents its output to match the
167
+ // formatter); only the shared leading run is removed, relative
168
+ // indentation is kept.
166
169
  if (!empty(self::$config->format)) {
167
170
  $scripts[] = '<script>document.querySelectorAll("pre>code").forEach(function(c){'
168
- . 'if(c.children.length)return;'
169
- . 'var L=c.textContent.replace(/^\n+/,"").replace(/\s+$/,"").split("\n"),n=1/0;'
170
- . 'L.forEach(function(l){if(l.trim())n=Math.min(n,l.match(/^\s*/)[0].length)});'
171
- . 'if(n&&n<1/0)c.textContent=L.map(function(l){return l.slice(n)}).join("\n")});</script>';
171
+ . 'var L=c.innerHTML.replace(/^\n+/,"").replace(/\n[^\S\n]*$/,"").split("\n"),n=1/0;'
172
+ . 'L.forEach(function(l){if(l.trim())n=Math.min(n,l.match(/^[ \t]*/)[0].length)});'
173
+ . 'if(n&&n<1/0)c.innerHTML=L.map(function(l){return l.slice(n)}).join("\n")});</script>';
172
174
  }
173
175
  foreach ((array) (self::$config->tasks ?? []) as $task) {
174
176
  $task = (array) $task;
@@ -2,8 +2,17 @@
2
2
 
3
3
 
4
4
  PREPROS::registerTag('markdown', function ($tag, $attrs, $body) {
5
- $body = STR::trimIndent($body);
6
- return MD::toHtml($body);
5
+ $html = MD::toHtml(STR::trimIndent($body));
6
+
7
+ // `<markdown prose>` wraps the output in the canva `.prose` container so
8
+ // long-form Markdown picks up the typographic styles with no extra markup.
9
+ // Opt-in (a bare `<markdown>` is unchanged); extra classes / id on the tag
10
+ // land on the wrapper: `<markdown prose class="lede" id="intro">`.
11
+ if (empty($attrs['prose'])) return $html;
12
+
13
+ $class = trim('prose ' . (is_string($attrs['class'] ?? null) ? $attrs['class'] : ''));
14
+ $id = is_string($attrs['id'] ?? null) ? ' id="' . htmlspecialchars($attrs['id'], ENT_QUOTES) . '"' : '';
15
+ return '<div class="' . htmlspecialchars($class, ENT_QUOTES) . '"' . $id . ">\n{$html}\n</div>";
7
16
  });
8
17
 
9
18