@kirigami/php-prepros 1.1.0 → 1.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 +1114 -869
- package/index.d.ts +75 -2
- package/index.js +1 -1
- package/package.json +4 -4
- package/src/libraries/curl.class.php +5 -0
- package/src/libraries/html.class.php +23 -22
- package/src/libraries/img.class.php +356 -16
- package/src/libraries/md.class.php +476 -170
- package/src/libraries/md.plugins.php +19 -19
- package/src/libraries/normalizer.class.php +310 -0
- package/src/libraries/prepros.class.php +36 -7
- package/src/libraries/prepros.plugins.php +24 -0
- package/src/libraries/schema.class.php +550 -0
- package/src/libraries/scraper.class.php +64 -59
- package/src/libraries/str.class.php +49 -15
- package/src/libraries/yaml.class.php +66 -66
- package/src/phpjs/fstat.js +9 -0
- package/src/prepros.js +51 -44
- package/src/utils/getfilestats.js +30 -0
- package/src/utils/isbinary.js +14 -14
- package/src/utils.inc.php +9 -8
package/README.md
CHANGED
|
@@ -1,869 +1,1114 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
[](https://www.npmjs.com/package/@kirigami/php-prepros)
|
|
13
|
+
[](./LICENSE)
|
|
14
|
+
[](https://nodejs.org)
|
|
15
|
+
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Overview
|
|
21
|
+
|
|
22
|
+
Build full static websites in PHP — with zero server, zero runtime dependency, zero compromise on expressiveness. Write your pages as regular PHP files, annotate them with a PHPDOC header, and let `php-prepros` compile everything to clean, deployable HTML.
|
|
23
|
+
|
|
24
|
+
It is the perfect solution for **GitHub Pages**. Since it runs entirely in Node.js, it is fully compatible with **GitHub Actions**, allowing you to automate your deployment pipeline effortlessly.
|
|
25
|
+
|
|
26
|
+
Part of the **Kirigami** project ecosystem.
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
## Table of contents
|
|
33
|
+
|
|
34
|
+
- [@kirigami/php-prepros](#kirigamiphp-prepros)
|
|
35
|
+
- [Overview](#overview)
|
|
36
|
+
- [Table of contents](#table-of-contents)
|
|
37
|
+
- [What's new in 1.2.0](#whats-new-in-120)
|
|
38
|
+
- [How it works](#how-it-works)
|
|
39
|
+
- [Installation](#installation)
|
|
40
|
+
- [Configuration — `kirigami.yaml`](#configuration--kirigamiyaml)
|
|
41
|
+
- [`kirigami` block](#kirigami-block)
|
|
42
|
+
- [`prepros` block](#prepros-block)
|
|
43
|
+
- [`image` block](#image-block)
|
|
44
|
+
- [`plugins` block](#plugins-block)
|
|
45
|
+
- [`esbuild` / `sass` blocks](#esbuild--sass-blocks)
|
|
46
|
+
- [`export` block](#export-block)
|
|
47
|
+
- [`scripts` block](#scripts-block)
|
|
48
|
+
- [`tasks` block](#tasks-block)
|
|
49
|
+
- [Writing pages](#writing-pages)
|
|
50
|
+
- [PHPDOC header](#phpdoc-header)
|
|
51
|
+
- [Auto-loading data files](#auto-loading-data-files)
|
|
52
|
+
- [`@content` and `@indent`](#content-and-indent)
|
|
53
|
+
- [JavaScript API](#javascript-api)
|
|
54
|
+
- [`render(file?)`](#renderfile)
|
|
55
|
+
- [`sitemap()`](#sitemap)
|
|
56
|
+
- [`runenv(script, paths?, ...args)`](#runenvscript-paths-args)
|
|
57
|
+
- [`mountPath(localPath, virtualDir?, php?)`](#mountpathlocalpath-virtualdir-php)
|
|
58
|
+
- [PHP classes reference](#php-classes-reference)
|
|
59
|
+
- [PREPROS](#prepros)
|
|
60
|
+
- [`PREPROS::render(string $file)`](#preprosrenderstring-file)
|
|
61
|
+
- [`PREPROS::sitemap()`](#preprossitemap)
|
|
62
|
+
- [`PREPROS::mount(string|array $patterns)`](#preprosmountstringarray-patterns)
|
|
63
|
+
- [`PREPROS::exportFile(string $file)`](#preprosexportfilestring-file)
|
|
64
|
+
- [MD](#md)
|
|
65
|
+
- [Plugin API](#plugin-api)
|
|
66
|
+
- [HTML](#html)
|
|
67
|
+
- [YAML](#yaml)
|
|
68
|
+
- [SCHEMA](#schema)
|
|
69
|
+
- [CACHE](#cache)
|
|
70
|
+
- [IMG](#img)
|
|
71
|
+
- [FS](#fs)
|
|
72
|
+
- [STR](#str)
|
|
73
|
+
- [ARR](#arr)
|
|
74
|
+
- [CURL](#curl)
|
|
75
|
+
- [SCRAPER](#scraper)
|
|
76
|
+
- [OBF](#obf)
|
|
77
|
+
- [STD](#std)
|
|
78
|
+
- [Bundled polyfills](#bundled-polyfills)
|
|
79
|
+
- [Plugin system](#plugin-system)
|
|
80
|
+
- [PREPROS tags](#prepros-tags)
|
|
81
|
+
- [PREPROS hooks](#prepros-hooks)
|
|
82
|
+
- [MD plugins](#md-plugins)
|
|
83
|
+
- [Built-in plugins](#built-in-plugins)
|
|
84
|
+
- [`{% callout type ["Title"] content %}`](#-callout-type-title-content-)
|
|
85
|
+
- [`{% youtube id [width height] %}`](#-youtube-id-width-height-)
|
|
86
|
+
- [`{% codepen id [user height] %}`](#-codepen-id-user-height-)
|
|
87
|
+
- [`{% checklist ["Title"] items %}`](#-checklist-title-items-)
|
|
88
|
+
- [Extending the `<markdown>` tag](#extending-the-markdown-tag)
|
|
89
|
+
- [Requirements](#requirements)
|
|
90
|
+
- [License](#license)
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## What's new in 1.2.0
|
|
95
|
+
|
|
96
|
+
- **`SCHEMA`** class — a pure-PHP, dependency-free JSON Schema validator
|
|
97
|
+
(Draft-7 style, Ajv-like API: `isValid()` / `validate()` / `getErrors()`).
|
|
98
|
+
- **`IMG::asset()` / `IMG::palette()`** — static helpers powering kirigami-core's
|
|
99
|
+
`img-asset()` and `colors()` Sass functions: on-demand resize/convert of a
|
|
100
|
+
source image, and cached representative-colour extraction.
|
|
101
|
+
- **`IMG` now handles vector and exotic formats** — SVG, EPS, AI, PDF (rasterized
|
|
102
|
+
via Imagick), plus HEIC / TIFF / BMP, on top of GD's JPEG / PNG / GIF / WebP /
|
|
103
|
+
AVIF.
|
|
104
|
+
- **`MD` emoji shortcodes** — `:rocket:` → 🚀 from a large built-in map, extendable
|
|
105
|
+
with `MD::registerEmoji()`.
|
|
106
|
+
- **`MD` footnotes and definition lists** — `[^1]` / `[^1]: …`, and `Term` / `: …`.
|
|
107
|
+
- **`MD` inline HTML is now sanitized** against a tag/attribute allowlist rather
|
|
108
|
+
than passed through verbatim.
|
|
109
|
+
- **`STR::normalize()`** — Unicode NFD + combining-mark stripping;
|
|
110
|
+
**`STR::slug($str, $sep = '')`** now takes a separator (pass `'-'` for a
|
|
111
|
+
hyphenated slug).
|
|
112
|
+
- **Bundled `Normalizer` polyfill** — `ext-intl` isn't in the WASM build, so a
|
|
113
|
+
polyfill keeps `Normalizer::normalize()` (and `STR::normalize()` / `slug()`)
|
|
114
|
+
working.
|
|
115
|
+
|
|
116
|
+
Earlier, in 1.1.x: `PREPROS::mount()` + the `mountPath()` / `runenv()` JS
|
|
117
|
+
exports, the `SCRAPER` and `CURL` and `ARR` classes, `YAML::loadFile()`, the
|
|
118
|
+
`STR::is_url()` / `html_entities_decode()` / `shorthash()` / `slug()` helpers,
|
|
119
|
+
the `{% youtube %}` / `{% codepen %}` / `{% checklist %}` MD plugins, and the
|
|
120
|
+
`@content` / `@indent` PHPDOC annotations.
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## How it works
|
|
125
|
+
|
|
126
|
+
`@kirigami/php-prepros` runs your PHP source files inside a **WebAssembly PHP 8.x runtime** ([`@kirigami/php-wasm`](https://www.npmjs.com/package/@kirigami/php-wasm)), entirely in Node.js — no PHP installation required on the host machine.
|
|
127
|
+
|
|
128
|
+
The lifecycle of a page build looks like this:
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
_index.php ──▶ PHP (wasm) ──▶ processTags() ──▶ HTML::format() ──▶ index.html
|
|
132
|
+
│
|
|
133
|
+
├── before.php (optional layout header)
|
|
134
|
+
├── after.php (optional layout footer)
|
|
135
|
+
└── PHPDOC annotations resolved (yaml / json / md / url)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Files are mounted into the WebAssembly virtual filesystem on demand. Only `.php`, `.json`, `.yaml`, `.yml`, `.md`, `.db`, `.txt` and any extra extensions listed in `prepros.mountext` are mounted automatically, keeping memory usage low. Anything else can be mounted on demand with [`PREPROS::mount()`](#preprosmountstringarray-patterns).
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Installation
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
npm install @kirigami/php-prepros
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Configuration — `kirigami.yaml`
|
|
151
|
+
|
|
152
|
+
Every project **must** have a `kirigami.yaml` at its root. The preprocessor reads it at startup and throws if it is absent or invalid.
|
|
153
|
+
|
|
154
|
+
`@kirigami/php-prepros` itself only acts on three blocks — **`kirigami:`**, **`prepros:`**, and **`image:`**. The remaining blocks (**`plugins:`**, **`esbuild:`**, **`sass:`**, **`export:`**, **`scripts:`**, **`tasks:`**) are consumed by the [`kiri`](https://www.npmjs.com/package/@kirigami/kirigami) CLI that drives the build; they are documented here for completeness because everything lives in the one file. The full file is validated against [`kirigami.schema.json`](https://github.com/php-kirigami/kirigami/blob/main/packages/kirigami/kirigami.schema.json), also served for editor autocompletion:
|
|
155
|
+
|
|
156
|
+
```yaml
|
|
157
|
+
# yaml-language-server: $schema=https://cdn.jsdelivr.net/npm/@kirigami/kirigami/kirigami.schema.json
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
```yaml
|
|
161
|
+
kirigami:
|
|
162
|
+
# ── Required ──────────────────────────────────────────────────────────
|
|
163
|
+
project: My Website # Site name. Printed in the CLI banner, exposed as $project.
|
|
164
|
+
baseurl: https://example.com # Deployed root URL, no trailing slash. Used for sitemap.xml.
|
|
165
|
+
root: src # Source directory containing your _*.php pages.
|
|
166
|
+
|
|
167
|
+
# ── Optional ────────────────────────────────────────────────────────
|
|
168
|
+
banner: assets/banner.txt # Text file stamped as a license banner on exported files.
|
|
169
|
+
|
|
170
|
+
# ── Arbitrary project data ──────────────────────────────────────────
|
|
171
|
+
# Everything else under `kirigami:` is free-form. The whole block is
|
|
172
|
+
# extracted as PHP variables and made available in every page, in
|
|
173
|
+
# before.php/after.php, and anywhere PREPROS::$config->data is read.
|
|
174
|
+
author: Jane Doe
|
|
175
|
+
email: hello@example.com
|
|
176
|
+
gtag: G-XXXXXXXXXX
|
|
177
|
+
description: A short description of the site, useful for <meta name="description">.
|
|
178
|
+
keywords:
|
|
179
|
+
- keyword one
|
|
180
|
+
- keyword two
|
|
181
|
+
|
|
182
|
+
prepros:
|
|
183
|
+
before: _layouts/header.php # Included before every page body.
|
|
184
|
+
after: _layouts/footer.php # Included after every page body.
|
|
185
|
+
format: true # Pretty-print the HTML output (default: false).
|
|
186
|
+
network: true # Allow HTTP fetches in PHPDOC @tag annotations / CURL / SCRAPER.
|
|
187
|
+
mountext: # Extra file extensions to auto-mount into the wasm fs,
|
|
188
|
+
- .svg # in addition to the defaults (.php .json .yaml .yml .md .db .txt).
|
|
189
|
+
- .webp
|
|
190
|
+
includes: # PHP files auto-included once, before any page renders.
|
|
191
|
+
- _lib/functions.php
|
|
192
|
+
|
|
193
|
+
image: # Image autogenerator — powers IMG::asset() / IMG::palette().
|
|
194
|
+
format: webp # webp | avif (default: webp)
|
|
195
|
+
source: assets/images # Source folder, relative to cwd() (default: assets/images)
|
|
196
|
+
dest: images # Output folder, relative to kirigami.root (default: images)
|
|
197
|
+
|
|
198
|
+
plugins:
|
|
199
|
+
- name: "@kirigami/plugin-highlight"
|
|
200
|
+
active: true
|
|
201
|
+
options:
|
|
202
|
+
style: canva
|
|
203
|
+
color: black
|
|
204
|
+
|
|
205
|
+
esbuild:
|
|
206
|
+
# minify: false
|
|
207
|
+
|
|
208
|
+
sass:
|
|
209
|
+
style: expanded
|
|
210
|
+
|
|
211
|
+
export:
|
|
212
|
+
path: dist
|
|
213
|
+
ignore: ["*.psd", "notes/"]
|
|
214
|
+
|
|
215
|
+
scripts:
|
|
216
|
+
- name: convert-images
|
|
217
|
+
mount: ["assets/images/**/*.jpg"]
|
|
218
|
+
trigger: before-build # before-build | before-export | after-export
|
|
219
|
+
|
|
220
|
+
tasks:
|
|
221
|
+
- name: js-core
|
|
222
|
+
type: esbuild
|
|
223
|
+
entry: scripts/kirigami.core.js
|
|
224
|
+
|
|
225
|
+
- name: scss-core
|
|
226
|
+
type: sass
|
|
227
|
+
entry: styles/kirigami.core.scss
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### `kirigami` block
|
|
231
|
+
|
|
232
|
+
Core project settings. **Read by `php-prepros`.** The entire block is extracted into PHP variables and made available in every page template, `before.php`, `after.php`, and `prepros.includes` files — `$project`, `$author`, `$gtag`, etc. are available with no further setup, and also as `PREPROS::$config->data`.
|
|
233
|
+
|
|
234
|
+
| Key | Required | Description |
|
|
235
|
+
|-----|----------|--------------|
|
|
236
|
+
| `project` | ✅ | Human-readable site name. Exposed as `$project`. |
|
|
237
|
+
| `baseurl` | ✅ | Root URL of the deployed site, no trailing slash. Used to build absolute `<loc>` entries in `sitemap.xml`; exposed as `$baseurl`. |
|
|
238
|
+
| `root` | ✅ | Path (relative to the project root) to the directory containing your `_*.php` source pages. Build fails immediately if missing or if the path doesn't exist. |
|
|
239
|
+
| `banner` | — | Path (relative to the project root) to a text file stamped as a license/copyright banner on exported `.js`/`.css`/`.html` files during `kiri export`. May contain the `###DATE###` token, replaced with today's date. Falls back to an auto-generated banner. |
|
|
240
|
+
| *anything else* | — | Free-form key/value pairs (strings, numbers, booleans, lists, nested maps — anything valid YAML). Every key is extracted as a PHP variable (`$author`, `$gtag`, …). Use this for contact info, social links, analytics IDs, SEO keywords, or any project data you want available everywhere. |
|
|
241
|
+
|
|
242
|
+
### `prepros` block
|
|
243
|
+
|
|
244
|
+
Options for the PHP → HTML compiler. **Read by `php-prepros`.** Declaring this block (even empty) also makes `kiri` prepend a forced `prepros` task on every build/export/watch.
|
|
245
|
+
|
|
246
|
+
| Key | Type | Default | Description |
|
|
247
|
+
|-----|------|---------|--------------|
|
|
248
|
+
| `before` | `string` | — | Path (relative to `kirigami.root`) to a PHP file included **before** every page's body. Typically your `<head>`/layout opening. |
|
|
249
|
+
| `after` | `string` | — | Path (relative to `kirigami.root`) to a PHP file included **after** every page's body. Typically your layout closing. |
|
|
250
|
+
| `format` | `bool` | `false` | Pretty-print the compiled HTML via [`HTML::format()`](#html) before writing it to disk. |
|
|
251
|
+
| `network` | `bool` | `false` | Enables outbound HTTP(S) inside the WASM PHP runtime. Required for PHPDOC `@tag https://…` annotations that fetch remote `.yaml`/`.json`/`.md` data (see [Auto-loading data files](#auto-loading-data-files)), and for the `CURL` / `SCRAPER` classes. |
|
|
252
|
+
| `mountext` | `string[]` | `[]` | Extra file extensions to mount automatically into the virtual filesystem alongside the built-in `.php`, `.json`, `.yaml`, `.yml`, `.md`, `.db`, `.txt`. Use this for assets your PHP code reads directly (e.g. `.svg`, `.webp`). Files with extensions not in this set are skipped during mounting — mount them on demand with [`PREPROS::mount()`](#preprosmountstringarray-patterns) instead. |
|
|
253
|
+
| `includes` | `string[]` | `[]` | PHP files (relative to `kirigami.root`) `include_once`'d once, right after config is loaded — before any page renders. The natural place to `PREPROS::registerTag()`, `PREPROS::registerHook()`, or `MD::registerPlugin()`. |
|
|
254
|
+
|
|
255
|
+
### `image` block
|
|
256
|
+
|
|
257
|
+
Options for the image autogenerator. **Read by `php-prepros`** — these are what [`IMG::asset()` / `IMG::palette()`](#img) (and kirigami-core's `img-asset()` / `colors()` Sass functions) resolve against. Optional; the defaults below apply even when the block is absent.
|
|
258
|
+
|
|
259
|
+
| Key | Type | Default | Description |
|
|
260
|
+
|-----|------|---------|--------------|
|
|
261
|
+
| `format` | `string` | `webp` | Output format for generated images: `webp` or `avif`. |
|
|
262
|
+
| `source` | `string` | `assets/images` | Folder holding the source images, relative to `cwd()`. |
|
|
263
|
+
| `dest` | `string` | `images` | Destination folder for generated images, relative to `kirigami.root`. |
|
|
264
|
+
|
|
265
|
+
### `plugins` block
|
|
266
|
+
|
|
267
|
+
List of Kirigami plugins. **Consumed by the `kiri` CLI** (see [`@kirigami/sdk`](https://www.npmjs.com/package/@kirigami/sdk)), not by `php-prepros` directly.
|
|
268
|
+
|
|
269
|
+
| Key | Required | Description |
|
|
270
|
+
|-----|----------|--------------|
|
|
271
|
+
| `name` | ✅ | Plugin package name. Must match `@kirigami/plugin-*`, `<scope>/kirigami-plugin-*`, or `kirigami-plugin-*`. |
|
|
272
|
+
| `active` | ✅ | Whether the plugin is loaded. |
|
|
273
|
+
| `options` | — | Free-form object passed to the plugin; its shape depends on the plugin. |
|
|
274
|
+
|
|
275
|
+
### `esbuild` / `sass` blocks
|
|
276
|
+
|
|
277
|
+
Free-form objects. **Consumed by the `kiri` CLI.** There is no fixed key set: whatever you put here is spread straight into the underlying library call for every matching task, *after* Kirigami's own defaults — so it can also override them (`minify`, `target`, `style: "compressed"`, source maps, …). Refer to esbuild's [`BuildOptions`](https://esbuild.github.io/api/#build-api) and Dart Sass's [`Options`](https://sass-lang.com/documentation/js-api/interfaces/options/) for what's accepted. Writing the key with nothing under it parses to `null` in YAML, equivalent to omitting the block.
|
|
278
|
+
|
|
279
|
+
`sass:` additionally recognizes two keys that are **not** passed to Dart Sass:
|
|
280
|
+
|
|
281
|
+
| Key | Type | Description |
|
|
282
|
+
|-----|------|--------------|
|
|
283
|
+
| `before` | `string` / `string[]` | Extra `.scss` files compiled **before** the task entry (paths relative to `cwd()`). |
|
|
284
|
+
| `after` | `string` / `string[]` | Extra `.scss` files compiled **after** the task entry. |
|
|
285
|
+
|
|
286
|
+
### `export` block
|
|
287
|
+
|
|
288
|
+
Options for `kiri export`. **Consumed by the `kiri` CLI.** Optional.
|
|
289
|
+
|
|
290
|
+
| Key | Type | Default | Description |
|
|
291
|
+
|-----|------|---------|--------------|
|
|
292
|
+
| `path` | `string` | `dist` | Output directory for `kiri export`, relative to the project root. |
|
|
293
|
+
| `ignore` | `string[]` | `[]` | Extra gitignore-style patterns excluded from the export copy, on top of Kirigami's built-in exclusions. |
|
|
294
|
+
|
|
295
|
+
### `scripts` block
|
|
296
|
+
|
|
297
|
+
Named PHP scripts. **Consumed by the `kiri` CLI**, which runs each `scripts/<name>.php` through [`runenv()`](#runenvscript-paths-args) — so the full `php-prepros` class library is available and `kirigami.yaml`'s `kirigami` block is exposed as `PREPROS::$config->data`.
|
|
298
|
+
|
|
299
|
+
| Key | Required | Description |
|
|
300
|
+
|-----|----------|--------------|
|
|
301
|
+
| `name` | ✅ | Must match an existing `scripts/<name>.php` file. Run with `kiri run <name> [args...]`; extra CLI arguments are forwarded as `$argv` entries. |
|
|
302
|
+
| `mount` | — | Glob patterns (relative to the project root) of extra local files to mount into the sandbox before the script runs. |
|
|
303
|
+
| `trigger` | — | Fire the script automatically: `before-build` (start of `build` and `export`), `before-export` (very start of `export`), or `after-export` (once `export` has finished). |
|
|
304
|
+
|
|
305
|
+
### `tasks` block
|
|
306
|
+
|
|
307
|
+
Ordered list of build tasks, run in array order. **Consumed by the `kiri` CLI**, on top of the implicit `prepros` task (added when the `prepros` block is present) and the implicit `dist` task (added during `kiri export`).
|
|
308
|
+
|
|
309
|
+
| `type` | Purpose | Required fields | Optional |
|
|
310
|
+
|--------|---------|-----------------|----------|
|
|
311
|
+
| `esbuild` | Bundle/minify a JS/TS entry. Build + watch. Output: `<entry>.min.js`. | `name`, `type`, `entry` | `force` |
|
|
312
|
+
| `sass` | Compile a `.scss`/`.sass` entry, minified with csso on export. Build + watch. Output: `<entry>.min.css`. | `name`, `type`, `entry` | `force` |
|
|
313
|
+
| `prepros` | Render pages + `sitemap.xml`. Watch only (runs on build/export only when forced/implicit). | `name`, `type` | `target`, `force` |
|
|
314
|
+
| `dist` | Copy `kirigami.root` into an output dir, stamping the banner. Forced/implicit only. | `name`, `type`, `path` | `ignore`, `force` |
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
## Writing pages
|
|
319
|
+
|
|
320
|
+
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.
|
|
321
|
+
|
|
322
|
+
```
|
|
323
|
+
src/
|
|
324
|
+
├── _layouts/
|
|
325
|
+
├── _lib/
|
|
326
|
+
├── _index.php → src/index.html
|
|
327
|
+
├── about/
|
|
328
|
+
│ └── _index.php → src/about/index.html
|
|
329
|
+
└── blog/
|
|
330
|
+
├── _index.php → src/blog/index.html
|
|
331
|
+
└── _articles.yaml (data file, not compiled)
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
Directories whose name starts with `_` (e.g. `_layouts/`, `_lib/`) are skipped entirely during directory-wide builds.
|
|
335
|
+
|
|
336
|
+
### PHPDOC header
|
|
337
|
+
|
|
338
|
+
Every page starts with a PHP docblock that drives metadata and data loading:
|
|
339
|
+
|
|
340
|
+
```php
|
|
341
|
+
<?php
|
|
342
|
+
/**
|
|
343
|
+
* @name about
|
|
344
|
+
* @title About us
|
|
345
|
+
* @abstract A short description of this page.
|
|
346
|
+
*/
|
|
347
|
+
?>
|
|
348
|
+
<section>
|
|
349
|
+
<h1><?php echo $title; ?></h1>
|
|
350
|
+
<p><?php echo $abstract; ?></p>
|
|
351
|
+
</section>
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
All annotations are injected as PHP variables (`$name`, `$title`, `$abstract`, …). You can define any custom annotation you need.
|
|
355
|
+
|
|
356
|
+
Annotations are also available as variables in `before` and `after` PHP included files, so you can write proper metas in the HTML header.
|
|
357
|
+
|
|
358
|
+
### Auto-loading data files
|
|
359
|
+
|
|
360
|
+
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.
|
|
361
|
+
|
|
362
|
+
```php
|
|
363
|
+
<?php
|
|
364
|
+
/**
|
|
365
|
+
* @name medias
|
|
366
|
+
* @articles _articles.yaml
|
|
367
|
+
*/
|
|
368
|
+
?>
|
|
369
|
+
<?php foreach ($articles as $article): ?>
|
|
370
|
+
<a href="<?php echo $article->lien; ?>">
|
|
371
|
+
<?php echo $article->titre; ?>
|
|
372
|
+
</a>
|
|
373
|
+
<?php endforeach; ?>
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
| Extension | Parsed as |
|
|
377
|
+
|-----------|-----------|
|
|
378
|
+
| `.yaml` / `.yml` | `stdClass` object (or array of objects for sequences) |
|
|
379
|
+
| `.json` | Result of `json_decode()` |
|
|
380
|
+
| `.md` | HTML string via `MD::toHtml()` |
|
|
381
|
+
|
|
382
|
+
When `network: true` is set in `kirigami.yaml`, annotation values that start with `http://` or `https://` are fetched from the network and parsed the same way:
|
|
383
|
+
|
|
384
|
+
```php
|
|
385
|
+
/**
|
|
386
|
+
* @posts https://api.example.com/posts.json
|
|
387
|
+
*/
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
### `@content` and `@indent`
|
|
391
|
+
|
|
392
|
+
Two special annotation names change how a page's body is assembled:
|
|
393
|
+
|
|
394
|
+
- **`@content`** — if a `content` variable already resolves to a non-empty value (typically because it's a `.md`/`.yaml`/`.json` annotation that auto-loaded into HTML/data, see above), it is used **as-is** as the page body, and the PHP file itself is **not executed** for its output. This is handy for pages that are pure data/markdown wrapped by a shared layout.
|
|
395
|
+
- **`@indent`** — when set to a number, every line of the rendered body is prefixed with that many spaces before being wrapped by `before.php`/`after.php`. Useful for keeping generated HTML readable when a page is nested inside indented layout markup.
|
|
396
|
+
|
|
397
|
+
```php
|
|
398
|
+
<?php
|
|
399
|
+
/**
|
|
400
|
+
* @name changelog
|
|
401
|
+
* @title Changelog
|
|
402
|
+
* @content _changelog.md
|
|
403
|
+
* @indent 4
|
|
404
|
+
*/
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
---
|
|
408
|
+
|
|
409
|
+
## JavaScript API
|
|
410
|
+
|
|
411
|
+
```js
|
|
412
|
+
import { render, sitemap, runenv, mountPath } from '@kirigami/php-prepros';
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
### `render(file?)`
|
|
416
|
+
|
|
417
|
+
Compile a single PHP page or a whole directory.
|
|
418
|
+
|
|
419
|
+
```js
|
|
420
|
+
// Compile one page
|
|
421
|
+
const result = await render('about/_index.php');
|
|
422
|
+
|
|
423
|
+
// Compile everything under src/
|
|
424
|
+
const result = await render('.');
|
|
425
|
+
|
|
426
|
+
// Compile everything (uses kirigami.root from config)
|
|
427
|
+
const result = await render();
|
|
428
|
+
```
|
|
429
|
+
> Paths used by `render()` are all relative to the `kirigami.root` configuration.
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
**Returns** `Promise<PreprosResult>`:
|
|
433
|
+
|
|
434
|
+
```ts
|
|
435
|
+
interface PreprosResult {
|
|
436
|
+
success: boolean;
|
|
437
|
+
files: string[]; // relative paths of every file written
|
|
438
|
+
error?: string; // present only on failure
|
|
439
|
+
}
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
### `sitemap()`
|
|
443
|
+
|
|
444
|
+
Generate `sitemap.xml` at the source root.
|
|
445
|
+
|
|
446
|
+
```js
|
|
447
|
+
const result = await sitemap();
|
|
448
|
+
// result.files === ['src/sitemap.xml']
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
### `runenv(script, paths?, ...args)`
|
|
452
|
+
|
|
453
|
+
Run an arbitrary PHP script — not a page template — inside the very same sandboxed WASM environment used for `render()`, with the full `php-prepros` class library autoloaded and `kirigami.yaml`'s `kirigami` block available as `PREPROS::$config->data`. Useful for one-off maintenance scripts, data migrations, or CLI-style tooling that needs `CACHE`, `SCRAPER`, `IMG`, etc. without going through the page-rendering pipeline.
|
|
454
|
+
|
|
455
|
+
```js
|
|
456
|
+
// Run a standalone PHP script
|
|
457
|
+
const result = await runenv('scripts/purge-cache.php');
|
|
458
|
+
|
|
459
|
+
// Also mount extra local paths/files into the sandbox before running
|
|
460
|
+
const result = await runenv('scripts/build-og-images.php', ['assets/photos']);
|
|
461
|
+
|
|
462
|
+
// Extra arguments are appended and available as $argv[2], $argv[3], … in the script
|
|
463
|
+
const result = await runenv('scripts/import.php', [], '--force');
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
- `script` — path to a PHP file **inside the project**, executed with `require_once`.
|
|
467
|
+
- `paths` — optional array of extra local paths (files or directories) to mount into the sandbox before the script runs.
|
|
468
|
+
- `...args` — extra string arguments appended to the script's `$argv`.
|
|
469
|
+
|
|
470
|
+
**Returns** `Promise<PreprosResult>`, following the same shape as `render()`. Inside the script, call `PREPROS::exportFile()` for any file you want listed in `result.files`.
|
|
471
|
+
|
|
472
|
+
### `mountPath(localPath, virtualDir?, php?)`
|
|
473
|
+
|
|
474
|
+
The JavaScript-side counterpart to [`PREPROS::mount()`](#preprosmountstringarray-patterns). Mounts a local file or directory — recursively, preserving structure — into the WASM sandbox's virtual filesystem, ahead of (or between) calls to `render()`, `sitemap()`, or `runenv()`. Useful when a Node-side build step needs to make extra local files visible to PHP before rendering starts.
|
|
475
|
+
|
|
476
|
+
```js
|
|
477
|
+
import { mountPath, render } from '@kirigami/php-prepros';
|
|
478
|
+
|
|
479
|
+
// Mount a single file at its natural virtual path (/project/<relative path>)
|
|
480
|
+
await mountPath('assets/data/team.yaml');
|
|
481
|
+
|
|
482
|
+
// Mount a whole directory, at a custom virtual path
|
|
483
|
+
await mountPath('vendor/fonts', '/project/fonts');
|
|
484
|
+
|
|
485
|
+
await render();
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
- `localPath` — path to a local file or directory. Relative paths are resolved against the project root.
|
|
489
|
+
- `virtualDir` — optional destination path inside the WASM filesystem. Defaults to `/project/<localPath relative to the project root>` when omitted.
|
|
490
|
+
- `php` — optional WASM PHP instance to mount into. Defaults to the shared singleton instance (the same one used internally by `render()`/`sitemap()`/`runenv()`), creating it if needed.
|
|
491
|
+
|
|
492
|
+
Mounting a **directory** only copies files whose extension is one of the defaults (`.php`, `.json`, `.yaml`, `.yml`, `.md`, `.db`, `.txt`) or listed in `prepros.mountext`, same as automatic root mounting. Mounting a **single file directly** copies it regardless of extension — this is the simplest way to make an arbitrary asset (an image, a font, a CSV, …) available to PHP without adding its extension to `prepros.mountext` project-wide.
|
|
493
|
+
|
|
494
|
+
**Returns** `Promise<void>`.
|
|
495
|
+
|
|
496
|
+
---
|
|
497
|
+
|
|
498
|
+
## PHP classes reference
|
|
499
|
+
|
|
500
|
+
All classes are autoloaded — no manual `require` needed inside your page files.
|
|
501
|
+
|
|
502
|
+
---
|
|
503
|
+
|
|
504
|
+
### PREPROS
|
|
505
|
+
|
|
506
|
+
The core engine. Manages the rendering pipeline, tag processing, hooks, mounting, and file export.
|
|
507
|
+
|
|
508
|
+
```php
|
|
509
|
+
// Available inside page templates and included files.
|
|
510
|
+
PREPROS::$config // stdClass — full resolved config; ->data is the kirigami: block,
|
|
511
|
+
// ->image the image: block, plus before/after/format/… from prepros:
|
|
512
|
+
PREPROS::registerTag(string $tag, callable $callback)
|
|
513
|
+
PREPROS::registerHook(string $hook, callable $callback)
|
|
514
|
+
PREPROS::mount(string|array $patterns)
|
|
515
|
+
PREPROS::exportFile(string|array $absolutePath)
|
|
516
|
+
PREPROS::getExportedFiles(): string[]
|
|
517
|
+
PREPROS::fstat(string $path) // stat a file in the WASM FS (or false)
|
|
518
|
+
PREPROS::backtraceFile() // path of the page currently rendering
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
#### `PREPROS::render(string $file)`
|
|
522
|
+
|
|
523
|
+
Internal method called once per source file. Orchestrates the full pipeline:
|
|
524
|
+
|
|
525
|
+
1. Resolves PHPDOC metadata and auto-loads data files.
|
|
526
|
+
2. Fires the `pre_render` hook with the raw source contents.
|
|
527
|
+
3. Includes `before.php`, the page body (or `@content`, see [above](#content-and-indent)), and `after.php` into a single string.
|
|
528
|
+
4. Processes all registered custom HTML tags.
|
|
529
|
+
5. Fires the `post_render` hook on the assembled HTML.
|
|
530
|
+
6. Optionally pretty-prints via `HTML::format()` (when `format: true`).
|
|
531
|
+
7. Writes the output `.html` file.
|
|
532
|
+
|
|
533
|
+
#### `PREPROS::sitemap()`
|
|
534
|
+
|
|
535
|
+
Scans the source tree for `_index.php` files and generates a standards-compliant `sitemap.xml` (Sitemaps 0.9), using `kirigami.baseurl` as the root URL.
|
|
536
|
+
|
|
537
|
+
#### `PREPROS::mount(string|array $patterns)`
|
|
538
|
+
|
|
539
|
+
Mounts additional local project files into the WASM virtual filesystem, on demand, from one or more glob patterns evaluated against the project root (via `picomatch`). Unlike the automatic mounting done for `kirigami.root` (limited to `.php`, `.json`, `.yaml`, `.yml`, `.md`, `.db`, `.txt`, and `prepros.mountext`), `mount()` copies **any** matching file, regardless of extension.
|
|
540
|
+
|
|
541
|
+
```php
|
|
542
|
+
// Mount every .webp under assets/, wherever the page needs them
|
|
543
|
+
PREPROS::mount('assets/**/*.webp');
|
|
544
|
+
|
|
545
|
+
// Multiple patterns at once
|
|
546
|
+
PREPROS::mount(['data/**/*.csv', 'vendor/fonts/*.woff2']);
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
Returns an array of the virtual paths (under `/project/...`) that were mounted, or `false` on failure.
|
|
550
|
+
|
|
551
|
+
#### `PREPROS::exportFile(string $file)`
|
|
552
|
+
|
|
553
|
+
Marks a file as a build output so it gets surfaced in `PreprosResult.files`. Called automatically by `render()`, `sitemap()`, `CACHE::set()`, and `CURL`. Call it manually if your custom code writes additional files.
|
|
554
|
+
|
|
555
|
+
---
|
|
556
|
+
|
|
557
|
+
### MD
|
|
558
|
+
|
|
559
|
+
Markdown-to-HTML converter with a plugin system for custom shortcodes.
|
|
560
|
+
|
|
561
|
+
```php
|
|
562
|
+
$html = MD::toHtml(string $markdown): string;
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
Supports the full GitHub Flavored Markdown subset, plus a few extensions:
|
|
566
|
+
|
|
567
|
+
- ATX (`#` … `######`) and Setext headings, with auto-generated `id` attributes
|
|
568
|
+
- Ordered and unordered lists, including nested
|
|
569
|
+
- GFM task lists (`- [ ]` / `- [x]`)
|
|
570
|
+
- GFM tables with column alignment
|
|
571
|
+
- GFM alerts (`> [!NOTE]`, `> [!WARNING]`, etc.)
|
|
572
|
+
- Blockquotes (recursive)
|
|
573
|
+
- Fenced code blocks with language class
|
|
574
|
+
- Inline code
|
|
575
|
+
- Bold, italic, bold+italic, strikethrough
|
|
576
|
+
- Links with automatic `target="_blank" rel="noopener noreferrer"` for external URLs
|
|
577
|
+
- Images with `loading="lazy"`
|
|
578
|
+
- Auto-linked bare URLs
|
|
579
|
+
- Horizontal rules
|
|
580
|
+
- Hard line breaks (trailing double space → `<br>`)
|
|
581
|
+
- **Footnotes** — `[^1]` references and `[^1]: …` definitions (multi-paragraph)
|
|
582
|
+
- **Definition lists** — `Term` / `: Definition`
|
|
583
|
+
- **Emoji shortcodes** — `:rocket:` → 🚀, from a built-in map (see `MD::registerEmoji()`)
|
|
584
|
+
- **Sanitized inline HTML** — raw tags are filtered against an allowlist of tags and attributes, not passed through verbatim
|
|
585
|
+
|
|
586
|
+
#### Plugin API
|
|
587
|
+
|
|
588
|
+
Extend Markdown with custom shortcode tags:
|
|
589
|
+
|
|
590
|
+
```php
|
|
591
|
+
// Inline tag {% tagname arg1 "arg with spaces" %}
|
|
592
|
+
// Block tag {% tagname arg1
|
|
593
|
+
// body content
|
|
594
|
+
// %}
|
|
595
|
+
|
|
596
|
+
MD::registerPlugin(string $name, callable $callback): void
|
|
597
|
+
MD::unregisterPlugin(string $name): void
|
|
598
|
+
MD::getRegisteredPlugins(): string[]
|
|
599
|
+
MD::registerEmoji(string $shortcode, string $char): void // `:name:` → char
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
The callback always receives `(array $args, string $body)`:
|
|
603
|
+
|
|
604
|
+
```php
|
|
605
|
+
MD::registerPlugin('video', function (array $args, string $body): string {
|
|
606
|
+
$src = htmlspecialchars($args[0] ?? '', ENT_QUOTES, 'UTF-8');
|
|
607
|
+
return "<video src=\"{$src}\" controls></video>";
|
|
608
|
+
});
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
Then in any Markdown content (including inside `<markdown>` tags):
|
|
612
|
+
|
|
613
|
+
```
|
|
614
|
+
{% video /videos/intro.mp4 %}
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
---
|
|
618
|
+
|
|
619
|
+
### HTML
|
|
620
|
+
|
|
621
|
+
Pretty-printer for the final HTML output. Used automatically when `format: true` is set in the config.
|
|
622
|
+
|
|
623
|
+
```php
|
|
624
|
+
$formatted = HTML::format(string $html): string;
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
Uses PHP 8.4's `Dom\HTMLDocument` (Lexbor engine) to parse the input and re-serialize it with consistent 4-space indentation. Inline elements, `<script>`, and `<style>` blocks are handled correctly — their content is indented but not reformatted. Boolean HTML5 attributes (`muted`, `autoplay`, `noopener`, etc.) are written without a value.
|
|
628
|
+
|
|
629
|
+
---
|
|
630
|
+
|
|
631
|
+
### YAML
|
|
632
|
+
|
|
633
|
+
A lightweight, zero-dependency YAML parser. Covers the full subset used in static site projects.
|
|
634
|
+
|
|
635
|
+
```php
|
|
636
|
+
$data = YAML::parse(string $yaml, bool $assoc = false): mixed;
|
|
637
|
+
$data = YAML::parseFile(string $path, bool $assoc = false): mixed;
|
|
638
|
+
$data = YAML::loadFile(string $path, bool $assoc = false): mixed;
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
Supported features:
|
|
642
|
+
|
|
643
|
+
- Scalars: strings (quoted and unquoted), integers, floats, booleans, null
|
|
644
|
+
- Single and double quoted strings with escape sequences
|
|
645
|
+
- Literal block scalars (`|`, `|-`, `|+`)
|
|
646
|
+
- Folded block scalars (`>`, `>-`, `>+`)
|
|
647
|
+
- Plain scalars spanning multiple lines
|
|
648
|
+
- Nested mappings and sequences
|
|
649
|
+
- Inline collections (`[a, b]` and `{k: v}`)
|
|
650
|
+
- Comments (`#`)
|
|
651
|
+
- Multiple documents separated by `---`
|
|
652
|
+
|
|
653
|
+
By default, YAML mappings are returned as `stdClass` objects. Pass `true` as the second argument to get associative arrays instead.
|
|
654
|
+
|
|
655
|
+
`YAML::loadFile()` behaves like `YAML::parseFile()`, then walks the result recursively: any string value ending in `.yaml`, `.yml`, or `.json` that resolves to an existing file (relative to *its own* file's directory) is replaced by that file's parsed content, and so on, recursively. Values that don't match an existing file are left untouched. Circular references (`A → B → A`) throw a `RuntimeException`.
|
|
656
|
+
|
|
657
|
+
```yaml
|
|
658
|
+
# team.yaml
|
|
659
|
+
lead: people/jane.yaml # resolved and inlined automatically
|
|
660
|
+
members:
|
|
661
|
+
- people/jane.yaml
|
|
662
|
+
- people/john.yaml
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
```php
|
|
666
|
+
$team = YAML::loadFile('/project/data/team.yaml');
|
|
667
|
+
// $team->lead is now the fully parsed content of people/jane.yaml, not a string
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
---
|
|
671
|
+
|
|
672
|
+
### SCHEMA
|
|
673
|
+
|
|
674
|
+
A pure-PHP, dependency-free JSON Schema validator — Draft-7 style, with an
|
|
675
|
+
Ajv-like API. Used internally to validate structured data, but available to your
|
|
676
|
+
own code and plugins.
|
|
677
|
+
|
|
678
|
+
```php
|
|
679
|
+
$validator = new SCHEMA(array $schema);
|
|
680
|
+
|
|
681
|
+
$validator->isValid(mixed $data): bool // true / false
|
|
682
|
+
$validator->validate(mixed $data): bool // alias of isValid()
|
|
683
|
+
$validator->getErrors(): string[] // "path: message" strings from the last run
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
Supported keywords: `type`, `required`, `properties`, `patternProperties`,
|
|
687
|
+
`additionalProperties`, `items`, `minItems`, `maxItems`, `uniqueItems`,
|
|
688
|
+
`minLength`, `maxLength`, `pattern`, `minimum`, `maximum`, `exclusiveMinimum`,
|
|
689
|
+
`exclusiveMaximum`, `minProperties`, `maxProperties`, `enum`, `const`,
|
|
690
|
+
`anyOf`, `allOf`, `oneOf`, `not`, `format`, and local `$ref` pointers.
|
|
691
|
+
|
|
692
|
+
```php
|
|
693
|
+
$validator = new SCHEMA([
|
|
694
|
+
'type' => 'object',
|
|
695
|
+
'required' => ['name', 'age'],
|
|
696
|
+
'properties' => [
|
|
697
|
+
'name' => ['type' => 'string', 'minLength' => 1],
|
|
698
|
+
'age' => ['type' => 'integer', 'minimum' => 0],
|
|
699
|
+
],
|
|
700
|
+
'additionalProperties' => false,
|
|
701
|
+
]);
|
|
702
|
+
|
|
703
|
+
if (!$validator->isValid($data)) {
|
|
704
|
+
foreach ($validator->getErrors() as $err) echo $err, PHP_EOL;
|
|
705
|
+
}
|
|
706
|
+
```
|
|
707
|
+
|
|
708
|
+
---
|
|
709
|
+
|
|
710
|
+
### CACHE
|
|
711
|
+
|
|
712
|
+
Persistent SQLite-backed key-value cache. Survives across incremental builds via `.cache.db` at the project root.
|
|
713
|
+
|
|
714
|
+
```php
|
|
715
|
+
CACHE::get(string $key): mixed
|
|
716
|
+
CACHE::set(string $key, mixed $val, int $ttl = 0): bool
|
|
717
|
+
CACHE::delete(string $key): bool
|
|
718
|
+
CACHE::purge(): bool // removes expired entries
|
|
719
|
+
```
|
|
720
|
+
|
|
721
|
+
The `$ttl` is in seconds. `0` means the entry never expires. Typical use case: caching the result of network fetches in custom hooks or plugins — it is what powers [`SCRAPER`](#scraper) and `CURL`'s cookie persistence internally.
|
|
722
|
+
|
|
723
|
+
```php
|
|
724
|
+
$data = CACHE::get('my-remote-data');
|
|
725
|
+
if ($data === null) {
|
|
726
|
+
$data = json_decode(file_get_contents('https://api.example.com/data.json'));
|
|
727
|
+
CACHE::set('my-remote-data', $data, 3600); // cache for 1 hour
|
|
728
|
+
}
|
|
729
|
+
```
|
|
730
|
+
|
|
731
|
+
---
|
|
732
|
+
|
|
733
|
+
### IMG
|
|
734
|
+
|
|
735
|
+
Image manipulation helper. GD handles JPEG, PNG, GIF, WebP and AVIF directly;
|
|
736
|
+
anything GD can't decode (HEIC, TIFF, BMP, and the vector formats SVG, EPS, AI,
|
|
737
|
+
PDF) falls back to Imagick, which rasterizes it to a GD image in memory. Vector
|
|
738
|
+
files with no intrinsic pixel size are rasterized at 2000 px on the longest
|
|
739
|
+
side, preserving the aspect ratio.
|
|
740
|
+
|
|
741
|
+
```php
|
|
742
|
+
$img = new IMG(string $file);
|
|
743
|
+
|
|
744
|
+
// Properties
|
|
745
|
+
$img->width // int
|
|
746
|
+
$img->height // int
|
|
747
|
+
|
|
748
|
+
// Instance methods (resize/save are chainable)
|
|
749
|
+
$img->resize(int $width, int $height = 0, bool $cover = false): self
|
|
750
|
+
$img->save(string $dest): self
|
|
751
|
+
$img->getRepresentativeColors(int $count = 5): string[] // ['#rrggbb', …]
|
|
752
|
+
|
|
753
|
+
// Static helpers
|
|
754
|
+
IMG::asset(string $path, int $width = 0, int $height = 0, bool $cover = false): string
|
|
755
|
+
IMG::palette(string $path, int $colors = 5): string[]
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
`resize()` operates in *contain* mode by default (scales to fit within the target box while preserving aspect ratio). Pass `$cover = true` to crop and fill the exact target dimensions.
|
|
759
|
+
|
|
760
|
+
`save()` infers the output format from the file extension (`.jpg`, `.jpeg`, `.png`, `.gif`, `.webp`, `.avif`) and marks the file as a build output.
|
|
761
|
+
|
|
762
|
+
```php
|
|
763
|
+
(new IMG('/project/src/images/hero.jpg'))
|
|
764
|
+
->resize(1200, 630, true)
|
|
765
|
+
->save('/project/src/images/hero-og.jpg');
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
`IMG::asset()` and `IMG::palette()` are what back kirigami-core's `img-asset()`
|
|
769
|
+
and `colors()` Sass functions: they resolve `$path` against `image.source` from
|
|
770
|
+
`kirigami.yaml`, generate a resized/re-encoded file under `image.dest` (only
|
|
771
|
+
when missing or stale), or return a `CACHE`-backed list of representative
|
|
772
|
+
colours. Both are equally usable from your own PHP.
|
|
773
|
+
|
|
774
|
+
---
|
|
775
|
+
|
|
776
|
+
### FS
|
|
777
|
+
|
|
778
|
+
Filesystem utilities.
|
|
779
|
+
|
|
780
|
+
```php
|
|
781
|
+
FS::dig(string $glob): iterable // recursive glob, yields file paths
|
|
782
|
+
FS::getRelativePath(string $from, string $to): string
|
|
783
|
+
FS::phpFileInfo(string $file): object|false // parse PHPDOC annotations
|
|
784
|
+
FS::rmdir(string $dir, bool $removeSelf = true): bool
|
|
785
|
+
FS::pathJoin(string ...$parts): string // URL-aware path join with .. resolution
|
|
786
|
+
```
|
|
787
|
+
|
|
788
|
+
`FS::dig()` is the workhorse of directory-wide builds — it recursively walks a glob pattern and yields every matching file path.
|
|
789
|
+
|
|
790
|
+
`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.
|
|
791
|
+
|
|
792
|
+
---
|
|
793
|
+
|
|
794
|
+
### STR
|
|
795
|
+
|
|
796
|
+
String utilities used internally by the tag-processing pipeline, and available for your own templates and plugins.
|
|
797
|
+
|
|
798
|
+
```php
|
|
799
|
+
STR::htmlesc(string $str): string
|
|
800
|
+
STR::replaceTags(string $tag, string $html, callable $callback): string
|
|
801
|
+
STR::parseHtmlAttributes(string $attrString): array
|
|
802
|
+
STR::trimIndent(string $str): string
|
|
803
|
+
STR::is_url(string $str): bool
|
|
804
|
+
STR::html_entities_decode(string $str): string
|
|
805
|
+
STR::shorthash(string $str): string
|
|
806
|
+
STR::normalize(string $str): string
|
|
807
|
+
STR::slug(string $str, string $sep = ''): string
|
|
808
|
+
```
|
|
809
|
+
|
|
810
|
+
`STR::replaceTags()` is the engine behind `PREPROS::registerTag()`. It finds all occurrences of `<tagname ...>...</tagname>` in an HTML string and replaces each with the return value of `$callback($fullMatch, $attrs, $body)`.
|
|
811
|
+
|
|
812
|
+
`STR::trimIndent()` strips the common leading whitespace from a multi-line string — handy when pulling content out of indented `<markdown>` blocks.
|
|
813
|
+
|
|
814
|
+
`STR::is_url()` checks whether a string parses as a URL with a recognized scheme (`http`, `https`, `ftp`, `ftps`, `ssh`, `ssl`, `sftp`, `itunes`).
|
|
815
|
+
|
|
816
|
+
`STR::html_entities_decode()` trims a string and decodes its HTML entities — handy when normalizing text scraped from a third-party page.
|
|
817
|
+
|
|
818
|
+
`STR::shorthash()` returns the first 12 characters of a string's SHA-256 hash — used internally as a stable, filename-safe cache key (see `SCRAPER`).
|
|
819
|
+
|
|
820
|
+
`STR::normalize()` applies Unicode NFD decomposition and strips combining marks (`é` → `e`) — the accent-folding step used by `slug()`.
|
|
821
|
+
|
|
822
|
+
`STR::slug()` normalizes, transliterates to ASCII, lowercases, and replaces every run of non-`[a-z0-9]` characters with `$sep` (empty by default → a compact identifier; pass `'-'` for a conventional hyphenated slug).
|
|
823
|
+
|
|
824
|
+
---
|
|
825
|
+
|
|
826
|
+
### ARR
|
|
827
|
+
|
|
828
|
+
Recursive lookup helper for nested arrays and objects.
|
|
829
|
+
|
|
830
|
+
```php
|
|
831
|
+
ARR::find_key(mixed $data, string $key): mixed
|
|
832
|
+
```
|
|
833
|
+
|
|
834
|
+
Walks an array or object (including mixed nested `stdClass`/array structures, as produced by `YAML::parse()` or `json_decode()`) depth-first and returns the value of the **first** matching key found, at any depth, or `null` if none matches.
|
|
835
|
+
|
|
836
|
+
```php
|
|
837
|
+
$config = YAML::parseFile('team.yaml');
|
|
838
|
+
$email = ARR::find_key($config, 'email'); // finds `email` however deep it's nested
|
|
839
|
+
```
|
|
840
|
+
|
|
841
|
+
---
|
|
842
|
+
|
|
843
|
+
### CURL
|
|
844
|
+
|
|
845
|
+
Low-level HTTP client built on PHP's cURL extension, used internally by `SCRAPER`. Ships with a realistic browser `User-Agent`/header set and a cookie jar persisted at `.cookie.txt` (auto-registered via `PREPROS::exportFile()`).
|
|
846
|
+
|
|
847
|
+
```php
|
|
848
|
+
CURL::urlExists(string $url, ?string $mimereg = null): bool
|
|
849
|
+
CURL::getInfo(string $url): array|false // HEAD request, returns curl_getinfo()
|
|
850
|
+
CURL::getContents(string $file, ?string $dest = null, ?callable $clb = null): string|bool
|
|
851
|
+
```
|
|
852
|
+
|
|
853
|
+
`CURL::urlExists()` issues a `HEAD` request and returns `true` for any `2xx`/`3xx` response.
|
|
854
|
+
|
|
855
|
+
`CURL::getContents()` downloads a URL. Without `$dest`, it returns the body as a string; with `$dest`, it streams the download to that file path and returns a boolean. Pass `$clb` to receive download progress as a float between `0` and `1`.
|
|
856
|
+
|
|
857
|
+
```php
|
|
858
|
+
CURL::getContents('https://example.com/report.pdf', '/project/src/downloads/report.pdf', function (float $progress) {
|
|
859
|
+
error_log(sprintf('%.0f%%', $progress * 100));
|
|
860
|
+
});
|
|
861
|
+
```
|
|
862
|
+
|
|
863
|
+
---
|
|
864
|
+
|
|
865
|
+
### SCRAPER
|
|
866
|
+
|
|
867
|
+
Fetches a URL and extracts page metadata (`title`, `description`, `image`, `label`) from its JSON-LD (`schema.org`), Open Graph, and standard `<meta>` tags — the kind of data you'd want for a rich link preview. Results are cached indefinitely via `CACHE`, keyed on the URL.
|
|
868
|
+
|
|
869
|
+
```php
|
|
870
|
+
$metas = SCRAPER::get(string $url): object|false;
|
|
871
|
+
```
|
|
872
|
+
|
|
873
|
+
```php
|
|
874
|
+
$metas = SCRAPER::get('https://example.com/blog/some-article');
|
|
875
|
+
if ($metas) {
|
|
876
|
+
echo $metas->title; // string
|
|
877
|
+
echo $metas->description; // string
|
|
878
|
+
echo $metas->image; // string (absolute URL, may be empty)
|
|
879
|
+
echo $metas->label; // string — site/publisher name, may be empty
|
|
880
|
+
echo $metas->url; // string — the URL that was scraped
|
|
881
|
+
}
|
|
882
|
+
```
|
|
883
|
+
|
|
884
|
+
Returns `false` if the page can't be reached, can't be parsed, or has no discoverable title. Throws an `Exception` on invalid URLs. Uses `CURL::getContents()` under the hood, so it benefits from the same shared cookie jar and browser-like headers.
|
|
885
|
+
|
|
886
|
+
---
|
|
887
|
+
|
|
888
|
+
### OBF
|
|
889
|
+
|
|
890
|
+
Simple reversible obfuscation for values you want to embed in HTML without making them trivially readable (e.g., contact data, API tokens in templates).
|
|
891
|
+
|
|
892
|
+
```php
|
|
893
|
+
$encoded = OBF::encode(mixed $obj): string;
|
|
894
|
+
$decoded = OBF::decode(string $str): mixed;
|
|
895
|
+
```
|
|
896
|
+
|
|
897
|
+
Applies JSON encoding → base64 → ROT-13 → gzip. Not cryptographically secure; intended for light obfuscation only.
|
|
898
|
+
|
|
899
|
+
---
|
|
900
|
+
|
|
901
|
+
### STD
|
|
902
|
+
|
|
903
|
+
Output helpers used by the PHP runtime to communicate back to Node.js over stdout/stderr.
|
|
904
|
+
|
|
905
|
+
```php
|
|
906
|
+
STD::succeed(array|string $props = []): void // exits 0, writes JSON to stdout
|
|
907
|
+
STD::error(array|string $props = []): void // exits 1, writes JSON to stderr
|
|
908
|
+
```
|
|
909
|
+
|
|
910
|
+
These are internal to the build runner (`render()`, `sitemap()`, and `runenv()` all rely on them). You generally do not need to call them in page templates, but they are available if a script run via `runenv()` needs to terminate early with a custom result.
|
|
911
|
+
|
|
912
|
+
---
|
|
913
|
+
|
|
914
|
+
### Bundled polyfills
|
|
915
|
+
|
|
916
|
+
The WASM PHP build ships without `ext-intl`, so `@kirigami/php-prepros` bundles a
|
|
917
|
+
`Normalizer` polyfill (autoloaded like every other class). It provides the
|
|
918
|
+
standard `Normalizer::normalize()` / `Normalizer::isNormalized()` API and the
|
|
919
|
+
`Normalizer::NFC` / `NFD` / `NFKC` / `NFKD` (and `FORM_*`) constants — enough for
|
|
920
|
+
`STR::normalize()` and `STR::slug()` to fold accents. Prefer the `STR` helpers in
|
|
921
|
+
your own code; the polyfill is there so third-party snippets that call
|
|
922
|
+
`Normalizer` directly keep working.
|
|
923
|
+
|
|
924
|
+
---
|
|
925
|
+
|
|
926
|
+
## Plugin system
|
|
927
|
+
|
|
928
|
+
`@kirigami/php-prepros` has two complementary plugin layers: **PREPROS** (HTML-tag level, operates on the assembled page) and **MD** (shortcode level, operates inside Markdown content).
|
|
929
|
+
|
|
930
|
+
---
|
|
931
|
+
|
|
932
|
+
### PREPROS tags
|
|
933
|
+
|
|
934
|
+
Register a custom HTML tag that is processed **after** PHP execution, on the fully assembled HTML string:
|
|
935
|
+
|
|
936
|
+
```php
|
|
937
|
+
// In a file listed under prepros.includes in kirigami.yaml, or in before.php:
|
|
938
|
+
|
|
939
|
+
PREPROS::registerTag('gallery', function (string $fullTag, array $attrs, string $body): string {
|
|
940
|
+
$id = $attrs['id'] ?? '';
|
|
941
|
+
$imgs = glob("/project/src/images/gallery/{$id}/*.webp");
|
|
942
|
+
$html = '<div class="gallery">';
|
|
943
|
+
foreach ($imgs as $img) {
|
|
944
|
+
$src = str_replace('/project/src', '', $img);
|
|
945
|
+
$html .= "<img src=\"{$src}\" loading=\"lazy\">";
|
|
946
|
+
}
|
|
947
|
+
return $html . '</div>';
|
|
948
|
+
});
|
|
949
|
+
```
|
|
950
|
+
|
|
951
|
+
Then in any page template:
|
|
952
|
+
|
|
953
|
+
```html
|
|
954
|
+
<gallery id="summer-2025"></gallery>
|
|
955
|
+
```
|
|
956
|
+
|
|
957
|
+
The callback receives:
|
|
958
|
+
|
|
959
|
+
| Parameter | Type | Description |
|
|
960
|
+
|-----------|------|-------------|
|
|
961
|
+
| `$fullTag` | `string` | The complete matched tag string |
|
|
962
|
+
| `$attrs` | `array` | Parsed HTML attributes as an associative array |
|
|
963
|
+
| `$body` | `string` | Inner content between opening and closing tags |
|
|
964
|
+
|
|
965
|
+
The built-in `<markdown>` tag is registered this way (see below).
|
|
966
|
+
|
|
967
|
+
---
|
|
968
|
+
|
|
969
|
+
### PREPROS hooks
|
|
970
|
+
|
|
971
|
+
Hooks let you intercept and transform data at key points in the rendering pipeline:
|
|
972
|
+
|
|
973
|
+
```php
|
|
974
|
+
PREPROS::registerHook(string $hookName, callable $callback): void
|
|
975
|
+
```
|
|
976
|
+
|
|
977
|
+
| Hook | When it fires | `$data` type | Expected return |
|
|
978
|
+
|------|---------------|--------------|-----------------|
|
|
979
|
+
| `page_info` | After PHPDOC parsing, before rendering (auto-loads `.yaml`/`.json`/`.md` annotations) | `[$filePath, $pageObject]` | `$pageObject` (modified) |
|
|
980
|
+
| `pre_render` | Before PHP execution | Raw file contents as `string` | `string` |
|
|
981
|
+
| `post_render` | After tag processing, before `HTML::format()` | Assembled HTML `string` | `string` |
|
|
982
|
+
|
|
983
|
+
Multiple callbacks can be registered for the same hook — they are executed in registration order, each receiving the return value of the previous one.
|
|
984
|
+
|
|
985
|
+
```php
|
|
986
|
+
// Example: inject a last-modified date into every page
|
|
987
|
+
PREPROS::registerHook('post_render', function (string $html): string {
|
|
988
|
+
$date = date('Y-m-d');
|
|
989
|
+
return str_replace('{{build_date}}', $date, $html);
|
|
990
|
+
});
|
|
991
|
+
```
|
|
992
|
+
|
|
993
|
+
---
|
|
994
|
+
|
|
995
|
+
### MD plugins
|
|
996
|
+
|
|
997
|
+
MD plugins add custom shortcode tags inside Markdown content. They work inside `<markdown>` blocks, in `.md` data files, and anywhere `MD::toHtml()` is called.
|
|
998
|
+
|
|
999
|
+
**Inline syntax** (all on one line):
|
|
1000
|
+
|
|
1001
|
+
```
|
|
1002
|
+
{% tagname arg1 "argument with spaces" %}
|
|
1003
|
+
```
|
|
1004
|
+
|
|
1005
|
+
**Block syntax** (body on subsequent lines):
|
|
1006
|
+
|
|
1007
|
+
```
|
|
1008
|
+
{% tagname optional-arg
|
|
1009
|
+
Line one of the body.
|
|
1010
|
+
Line two of the body.
|
|
1011
|
+
%}
|
|
1012
|
+
```
|
|
1013
|
+
|
|
1014
|
+
```php
|
|
1015
|
+
MD::registerPlugin(string $name, callable $callback): void
|
|
1016
|
+
```
|
|
1017
|
+
|
|
1018
|
+
The callback signature is always `(array $args, string $body): string`. `$args` contains arguments parsed from the opening line; `$body` is the trimmed multi-line body (empty string for inline tags).
|
|
1019
|
+
|
|
1020
|
+
---
|
|
1021
|
+
|
|
1022
|
+
### Built-in plugins
|
|
1023
|
+
|
|
1024
|
+
The following MD plugins are registered out of the box in `md.plugins.php`:
|
|
1025
|
+
|
|
1026
|
+
#### `{% callout type ["Title"] content %}`
|
|
1027
|
+
|
|
1028
|
+
Renders a styled callout block. `type` is one of `info`, `success`, `warning`, `danger`.
|
|
1029
|
+
|
|
1030
|
+
```
|
|
1031
|
+
{% callout warning "Heads up" This section is outdated. %}
|
|
1032
|
+
|
|
1033
|
+
{% callout danger "Critical"
|
|
1034
|
+
Line one of a longer warning.
|
|
1035
|
+
|
|
1036
|
+
Line two after a blank line.
|
|
1037
|
+
%}
|
|
1038
|
+
```
|
|
1039
|
+
|
|
1040
|
+
#### `{% youtube id [width height] %}`
|
|
1041
|
+
|
|
1042
|
+
Embeds a responsive YouTube player via `<iframe>`. `width`/`height` default to `560`/`315`.
|
|
1043
|
+
|
|
1044
|
+
```
|
|
1045
|
+
{% youtube dQw4w9WgXcQ %}
|
|
1046
|
+
{% youtube dQw4w9WgXcQ 800 450 %}
|
|
1047
|
+
```
|
|
1048
|
+
|
|
1049
|
+
#### `{% codepen id [user height] %}`
|
|
1050
|
+
|
|
1051
|
+
Embeds a CodePen result via `<iframe>`. `user` defaults to `anonymous`, `height` defaults to `400`.
|
|
1052
|
+
|
|
1053
|
+
```
|
|
1054
|
+
{% codepen abcXYZ %}
|
|
1055
|
+
{% codepen abcXYZ jsmith 500 %}
|
|
1056
|
+
```
|
|
1057
|
+
|
|
1058
|
+
#### `{% checklist ["Title"] items %}`
|
|
1059
|
+
|
|
1060
|
+
Renders a block-syntax list of checkbox items, one per line, with an optional title.
|
|
1061
|
+
|
|
1062
|
+
```
|
|
1063
|
+
{% checklist "Today"
|
|
1064
|
+
Do the dishes
|
|
1065
|
+
Walk the dog
|
|
1066
|
+
Read a book
|
|
1067
|
+
%}
|
|
1068
|
+
```
|
|
1069
|
+
|
|
1070
|
+
---
|
|
1071
|
+
|
|
1072
|
+
## Extending the `<markdown>` tag
|
|
1073
|
+
|
|
1074
|
+
The `<markdown>` tag is registered as a PREPROS tag out of the box. It converts its inner content from Markdown to HTML and strips common leading indentation so you can write cleanly inside your PHP templates:
|
|
1075
|
+
|
|
1076
|
+
```html
|
|
1077
|
+
<section class="about">
|
|
1078
|
+
<div>
|
|
1079
|
+
<markdown>
|
|
1080
|
+
## Who we are
|
|
1081
|
+
|
|
1082
|
+
We are a **student organization** from Québec.
|
|
1083
|
+
|
|
1084
|
+
{% youtube dQw4w9WgXcQ %}
|
|
1085
|
+
</markdown>
|
|
1086
|
+
</div>
|
|
1087
|
+
</section>
|
|
1088
|
+
```
|
|
1089
|
+
|
|
1090
|
+
All registered MD plugins are available inside `<markdown>` blocks. You can extend the tag's behaviour by registering additional MD plugins (see above) or by overriding the tag itself:
|
|
1091
|
+
|
|
1092
|
+
```php
|
|
1093
|
+
PREPROS::registerTag('markdown', function (string $tag, array $attrs, string $body): string {
|
|
1094
|
+
$body = STR::trimIndent($body);
|
|
1095
|
+
$html = MD::toHtml($body);
|
|
1096
|
+
// wrap in a container, add a class, etc.
|
|
1097
|
+
$class = $attrs['class'] ?? 'prose';
|
|
1098
|
+
return "<div class=\"{$class}\">{$html}</div>";
|
|
1099
|
+
});
|
|
1100
|
+
```
|
|
1101
|
+
|
|
1102
|
+
---
|
|
1103
|
+
|
|
1104
|
+
## Requirements
|
|
1105
|
+
|
|
1106
|
+
- Node.js `>= 24.0.0`
|
|
1107
|
+
- npm `>= 10.2.3`
|
|
1108
|
+
- ESM only (`"type": "module"`)
|
|
1109
|
+
|
|
1110
|
+
---
|
|
1111
|
+
|
|
1112
|
+
## License
|
|
1113
|
+
|
|
1114
|
+
MIT © Maxime Larrivée-Roy, 2026
|