@kirigami/php-prepros 1.7.0 → 1.7.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/README.md +44 -4
- package/package.json +1 -1
- package/src/libraries/fs.class.php +1 -1
- package/src/libraries/html.class.php +37 -10
- package/src/libraries/md.class.php +2 -2
- package/src/libraries/prepros.class.php +8 -6
- package/src/libraries/prepros.plugins.php +11 -2
- package/src/prepros.js +19 -7
package/README.md
CHANGED
|
@@ -34,6 +34,8 @@ 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)
|
|
38
|
+
- [What's new in 1.7.1](#whats-new-in-171)
|
|
37
39
|
- [What's new in 1.7.0](#whats-new-in-170)
|
|
38
40
|
- [What's new in 1.6.0](#whats-new-in-160)
|
|
39
41
|
- [What's new in 1.4.0](#whats-new-in-140)
|
|
@@ -107,6 +109,36 @@ Part of the **Kirigami** project ecosystem.
|
|
|
107
109
|
|
|
108
110
|
---
|
|
109
111
|
|
|
112
|
+
## What's new in 1.7.2
|
|
113
|
+
|
|
114
|
+
- **Cleaner formatted output around highlighted code.** The de-indent script
|
|
115
|
+
`PREPROS::injectHead()` adds when `prepros.format` is on now flattens the
|
|
116
|
+
`<pre><code>` indentation `HTML::format()` writes for *every* block, including
|
|
117
|
+
ones a build-time highlighter has wrapped in `<span>`s. It works on
|
|
118
|
+
`innerHTML` line by line and removes only the shared leading run (relative
|
|
119
|
+
indentation is kept). This lets [`@kirigami/plugin-highlight`](https://www.npmjs.com/package/@kirigami/plugin-highlight)
|
|
120
|
+
1.7.2+ re-indent its markup to line up with the rest of the document instead
|
|
121
|
+
of leaving it flush-left — the served HTML stays consistently indented, the
|
|
122
|
+
rendered code is still de-indented before the first paint.
|
|
123
|
+
|
|
124
|
+
- **`<markdown prose>` wraps in `.prose`.** With the `prose` attribute the
|
|
125
|
+
built-in tag emits `<div class="prose"> … </div>` so long-form Markdown picks
|
|
126
|
+
up `@kirigami/canva`'s `styles/prose` typography with no extra markup. Opt-in
|
|
127
|
+
(a bare `<markdown>` is unchanged); `class` / `id` on the tag land on the
|
|
128
|
+
wrapper.
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## What's new in 1.7.1
|
|
133
|
+
|
|
134
|
+
- **No side effects on import.** `kirigami.yaml` is now loaded on first use
|
|
135
|
+
(`render()` / `sitemap()` / `runenv()` / `processImages()`), not while the
|
|
136
|
+
module is being imported. `import '@kirigami/php-prepros'` from a directory
|
|
137
|
+
with no project no longer throws — which is what made `kiri build --help` /
|
|
138
|
+
`kiri export --help` / `kiri run --help` crash instead of printing their help.
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
110
142
|
## What's new in 1.7.0
|
|
111
143
|
|
|
112
144
|
- **`META`** class — a `<head>` SEO / social metadata generator, the companion
|
|
@@ -146,10 +178,11 @@ Part of the **Kirigami** project ecosystem.
|
|
|
146
178
|
- **`HTML::format()` indents `<pre><code>`.** A fenced code block's lines are
|
|
147
179
|
shifted to the block's nesting depth so the HTML source stays readable
|
|
148
180
|
(relative indentation preserved). The exact leading run is stripped again
|
|
149
|
-
before it's shown
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
`<pre>` and `<textarea>` are still emitted
|
|
181
|
+
before it's shown, by a small de-indent script `prepros.head` injects before
|
|
182
|
+
`</body>` (only when `format` is on). Since 1.7.2 the script works on
|
|
183
|
+
`innerHTML` line by line, so it also flattens blocks a build-time highlighter
|
|
184
|
+
has wrapped in `<span>`s. A bare `<pre>` and `<textarea>` are still emitted
|
|
185
|
+
byte-for-byte.
|
|
153
186
|
|
|
154
187
|
---
|
|
155
188
|
|
|
@@ -612,6 +645,13 @@ your template. All registered [MD plugins](#md-plugins) work inside it. See
|
|
|
612
645
|
</section>
|
|
613
646
|
```
|
|
614
647
|
|
|
648
|
+
Add the `prose` attribute — `<markdown prose>` — to wrap the output in
|
|
649
|
+
`<div class="prose">`, so it picks up the long-form typography of
|
|
650
|
+
[`@kirigami/canva`'s `styles/prose`](https://www.npmjs.com/package/@kirigami/canva)
|
|
651
|
+
with no extra markup. Any `class` / `id` on the tag lands on that wrapper
|
|
652
|
+
(`<markdown prose class="lede" id="intro">` → `<div class="prose lede" id="intro">`).
|
|
653
|
+
A bare `<markdown>` emits just the converted HTML, as before.
|
|
654
|
+
|
|
615
655
|
#### `<img asset="…">`
|
|
616
656
|
|
|
617
657
|
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.
|
|
3
|
+
"version": "1.7.2",
|
|
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('
|
|
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
|
-
|
|
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($
|
|
80
|
-
$isRaw = in_array($
|
|
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($
|
|
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
|
|
195
|
-
$
|
|
196
|
-
|
|
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
|
-
$
|
|
292
|
-
|
|
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 =
|
|
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
|
-
. "
|
|
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,
|
|
@@ -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)
|
|
165
|
-
//
|
|
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
|
-
. '
|
|
169
|
-
. '
|
|
170
|
-
. 'L.
|
|
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
|
-
$
|
|
6
|
-
|
|
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
|
|
package/src/prepros.js
CHANGED
|
@@ -12,16 +12,25 @@ const __modules = new Map;
|
|
|
12
12
|
const __project = process.cwd();
|
|
13
13
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
14
|
const __configpath = path.join(__project, 'kirigami.yaml');
|
|
15
|
-
let __root
|
|
16
|
-
let __php
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
15
|
+
let __root = null;
|
|
16
|
+
let __php = null;
|
|
17
|
+
let __config = null;
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
// Load and cache kirigami.yaml on first use. Deferred (not run at import) so
|
|
21
|
+
// `import '@kirigami/php-prepros'` has no side effects — e.g. `kiri build -h`
|
|
22
|
+
// can pull the module in with no project on disk.
|
|
23
|
+
const loadConfig = async () => {
|
|
24
|
+
if (__config) return __config;
|
|
25
|
+
if (!fs.existsSync(__configpath)) throw `Config file not found: ${__configpath}`;
|
|
26
|
+
__config = await walkFile(__configpath);
|
|
27
|
+
if (!__config) throw `Invalid config file: ${__configpath}`;
|
|
28
|
+
return __config;
|
|
29
|
+
}
|
|
22
30
|
|
|
23
31
|
|
|
24
32
|
const getPHPInstance = async () => {
|
|
33
|
+
const config = await loadConfig();
|
|
25
34
|
if(!__php) {
|
|
26
35
|
if(config?.kirigami?.root === undefined) throw `Missing prepros:root property in config file: ${__configpath}`;
|
|
27
36
|
__root = path.join(__project, config.kirigami.root);
|
|
@@ -100,6 +109,7 @@ const getPHPInstance = async () => {
|
|
|
100
109
|
|
|
101
110
|
|
|
102
111
|
const mountPath = async (localPath, virtualDir, php) => {
|
|
112
|
+
const config = await loadConfig();
|
|
103
113
|
php = php || await getPHPInstance();
|
|
104
114
|
if(!path.isAbsolute(localPath)) localPath = path.join(__project, localPath);
|
|
105
115
|
virtualDir = virtualDir || path.posix.join('/project', localPath.replace(__project + path.sep, ''));
|
|
@@ -251,6 +261,7 @@ const processImages = async (jobs = []) => {
|
|
|
251
261
|
|
|
252
262
|
|
|
253
263
|
const render = async (file = '.', phpIncludes = []) => {
|
|
264
|
+
const config = await loadConfig();
|
|
254
265
|
const target = path.resolve(config?.kirigami?.root, file);
|
|
255
266
|
const fsvm = path.join('/project', config?.kirigami?.root, file).replace(/\\/g, '/');
|
|
256
267
|
await mountPath(target);
|
|
@@ -278,6 +289,7 @@ const render = async (file = '.', phpIncludes = []) => {
|
|
|
278
289
|
|
|
279
290
|
|
|
280
291
|
const sitemap = async () => {
|
|
292
|
+
const config = await loadConfig();
|
|
281
293
|
await mountPath(config?.kirigami?.root);
|
|
282
294
|
return run(['sitemap']);
|
|
283
295
|
}
|