@kirigami/php-prepros 1.0.9 → 1.1.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
@@ -17,42 +17,67 @@ Part of the **Kirigami** project ecosystem. Other packages are coming soon.
17
17
  ## Table of contents
18
18
 
19
19
  - [@kirigami/php-prepros](#kirigamiphp-prepros)
20
- - [Table of contents](#table-of-contents)
21
- - [How it works](#how-it-works)
22
- - [Installation](#installation)
23
- - [Configuration — `kirigami.yaml`](#configuration--kirigamiyaml)
24
- - [Writing pages](#writing-pages)
25
- - [PHPDOC header](#phpdoc-header)
26
- - [Auto-loading data files](#auto-loading-data-files)
27
- - [JavaScript API](#javascript-api)
28
- - [`render(file?)`](#renderfile)
29
- - [`sitemap()`](#sitemap)
30
- - [PHP classes reference](#php-classes-reference)
31
- - [PREPROS](#prepros)
32
- - [`PREPROS::render(string $file)`](#preprosrenderstring-file)
33
- - [`PREPROS::sitemap()`](#preprossitemap)
34
- - [`PREPROS::exportFile(string $file)`](#preprosexportfilestring-file)
35
- - [MD](#md)
36
- - [Plugin API](#plugin-api)
37
- - [HTML](#html)
38
- - [YAML](#yaml)
39
- - [CACHE](#cache)
40
- - [IMG](#img)
41
- - [FS](#fs)
42
- - [STR](#str)
43
- - [OBF](#obf)
44
- - [STD](#std)
45
- - [Plugin system](#plugin-system)
46
- - [PREPROS tags](#prepros-tags)
47
- - [PREPROS hooks](#prepros-hooks)
48
- - [MD plugins](#md-plugins)
49
- - [Built-in plugins](#built-in-plugins)
50
- - [`{% callout type ["Title"] content %}`](#-callout-type-title-content-)
51
- - [Extending the `<markdown>` tag](#extending-the-markdown-tag)
52
- - [License](#license)
20
+ - [Table of contents](#table-of-contents)
21
+ - [What's new in 1.1.0](#whats-new-in-110)
22
+ - [How it works](#how-it-works)
23
+ - [Installation](#installation)
24
+ - [Configuration — `kirigami.yaml`](#configuration--kirigamiyaml)
25
+ - [`kirigami` block](#kirigami-block)
26
+ - [`prepros` block](#prepros-block)
27
+ - [Writing pages](#writing-pages)
28
+ - [PHPDOC header](#phpdoc-header)
29
+ - [Auto-loading data files](#auto-loading-data-files)
30
+ - [`@content` and `@indent`](#content-and-indent)
31
+ - [JavaScript API](#javascript-api)
32
+ - [`render(file?)`](#renderfile)
33
+ - [`sitemap()`](#sitemap)
34
+ - [`runenv(script, paths?, ...args)`](#runenvscript-paths-args)
35
+ - [PHP classes reference](#php-classes-reference)
36
+ - [PREPROS](#prepros)
37
+ - [`PREPROS::render(string $file)`](#preprosrenderstring-file)
38
+ - [`PREPROS::sitemap()`](#preprossitemap)
39
+ - [`PREPROS::mount(string|array $patterns)`](#preprosmountstringarray-patterns)
40
+ - [`PREPROS::exportFile(string $file)`](#preprosexportfilestring-file)
41
+ - [MD](#md)
42
+ - [Plugin API](#plugin-api)
43
+ - [HTML](#html)
44
+ - [YAML](#yaml)
45
+ - [CACHE](#cache)
46
+ - [IMG](#img)
47
+ - [FS](#fs)
48
+ - [STR](#str)
49
+ - [ARR](#arr)
50
+ - [CURL](#curl)
51
+ - [SCRAPER](#scraper)
52
+ - [OBF](#obf)
53
+ - [STD](#std)
54
+ - [Plugin system](#plugin-system)
55
+ - [PREPROS tags](#prepros-tags)
56
+ - [PREPROS hooks](#prepros-hooks)
57
+ - [MD plugins](#md-plugins)
58
+ - [Built-in plugins](#built-in-plugins)
59
+ - [`{% callout type ["Title"] content %}`](#-callout-type-title-content-)
60
+ - [`{% youtube id [width height] %}`](#-youtube-id-width-height-)
61
+ - [`{% codepen id [user height] %}`](#-codepen-id-user-height-)
62
+ - [`{% checklist ["Title"] items %}`](#-checklist-title-items-)
63
+ - [Extending the `<markdown>` tag](#extending-the-markdown-tag)
64
+ - [License](#license)
53
65
 
54
66
  ---
55
67
 
68
+ ## What's new in 1.1.0
69
+
70
+ - **`PREPROS::mount()`** — mount extra files into the WASM filesystem on demand, from a glob pattern, at any point during rendering.
71
+ - **`runenv()`** JavaScript export — run an arbitrary PHP script (not a page template) inside the same sandboxed environment, with full access to every `php-prepros` class.
72
+ - **`SCRAPER`** class — fetch a URL and extract `title` / `description` / `image` / `label` from its Open Graph, `<meta>`, and JSON-LD data, with automatic caching.
73
+ - **`CURL`** class — low-level cURL helper used internally by `SCRAPER`, also usable directly (`urlExists()`, `getInfo()`, `getContents()`), with a shared, persisted cookie jar.
74
+ - **`ARR`** class — recursive associative array/object key lookup.
75
+ - **`YAML::loadFile()`** — like `YAML::parseFile()`, but recursively resolves any string value that points to another existing `.yaml`/`.yml`/`.json` file into its parsed content.
76
+ - New `STR` helpers: `STR::is_url()`, `STR::html_entities_decode()`, `STR::shorthash()`, `STR::slug()`.
77
+ - New built-in MD plugins: `{% youtube %}`, `{% codepen %}`, and `{% checklist %}`, alongside the existing `{% callout %}`.
78
+ - `@content` and `@indent` PHPDOC annotations, letting a page skip its own PHP body in favour of pre-rendered content, and control its indentation when nested inside a layout.
79
+
80
+ ---
56
81
 
57
82
  ## How it works
58
83
 
@@ -68,7 +93,7 @@ _index.php ──▶ PHP (wasm) ──▶ processTags() ──▶ HTML::fo
68
93
  └── PHPDOC annotations resolved (yaml / json / md / url)
69
94
  ```
70
95
 
71
- Files are mounted into the WebAssembly virtual filesystem on demand. Only `.php`, `.json`, `.yaml`, `.md` and any extra extensions listed in `prepros.mountext` are mounted, keeping memory usage low.
96
+ 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).
72
97
 
73
98
  ---
74
99
 
@@ -88,24 +113,65 @@ Every project **must** have a `kirigami.yaml` at its root. The preprocessor read
88
113
 
89
114
  ```yaml
90
115
  kirigami:
91
- root: src/ # Required. Source directory containing your _*.php pages.
92
- baseurl: https://example.com # Used by sitemap generation.
93
- sitename: My Website # Arbitrary key/value pairs injected as PHP variables.
94
- author: Jane Doe
116
+ # ── Required ──────────────────────────────────────────────────────────
117
+ root: src # Source directory containing your _*.php pages.
118
+
119
+ # ── Used internally ──────────────────────────────────────────────────
120
+ baseurl: https://example.com # Used as the base URL when generating sitemap.xml.
121
+
122
+ # ── Arbitrary project data ──────────────────────────────────────────
123
+ # Everything else under `kirigami:` is free-form. The whole block is
124
+ # extracted as PHP variables and made available in every page, in
125
+ # before.php/after.php, and anywhere PREPROS::$config->data is read.
126
+ project: My Website
127
+ author: Jane Doe
128
+ person: John Smith
129
+ jobtitle: Founder
130
+ email: hello@example.com
131
+ facebook: https://www.facebook.com/example
132
+ area: Somewhere, Country
133
+ gtag: G-XXXXXXXXXX
134
+ banner: assets/banner.txt
135
+ description: A short description of the site, useful for <meta name="description">.
136
+ knowsabout:
137
+ - Topic one
138
+ - Topic two
139
+ keywords:
140
+ - keyword one
141
+ - keyword two
95
142
 
96
143
  prepros:
97
- before: _layout/header.php # Included before every page body.
98
- after: _layout/footer.php # Included after every page body.
99
- format: true # Pretty-print the HTML output (default: false).
100
- network: false # Allow HTTP fetches in PHPDOC @tag annotations.
101
- mountext: # Extra file extensions to mount into the wasm fs.
102
- - .svg
103
- - .txt
104
- includes: # PHP files auto-included before page rendering.
144
+ before: _layout/header.php # Included before every page body.
145
+ after: _layout/footer.php # Included after every page body.
146
+ format: true # Pretty-print the HTML output (default: false).
147
+ network: false # Allow HTTP fetches in PHPDOC @tag annotations.
148
+ mountext: # Extra file extensions to auto-mount into the wasm fs,
149
+ - .svg # in addition to the defaults (.php .json .yaml .yml .md .db .txt).
150
+ - .webp
151
+ includes: # PHP files auto-included once, before any page renders.
105
152
  - _lib/helpers.php
106
153
  ```
107
154
 
108
- The entire `kirigami` block is extracted into PHP variables and made available in every page template. `$sitename`, `$author`, etc. are available without any further setup.
155
+ ### `kirigami` block
156
+
157
+ | Key | Required | Description |
158
+ |-----|----------|--------------|
159
+ | `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. |
160
+ | `baseurl` | for `sitemap()` | Root URL used to build absolute `<loc>` entries when generating `sitemap.xml`. |
161
+ | *anything else* | — | Free-form key/value pairs (strings, numbers, booleans, lists, nested maps — anything valid YAML). Every key is extracted as a PHP variable (`$project`, `$author`, `$gtag`, …) and available in page templates, `before.php`, `after.php`, and PHP files listed under `prepros.includes`. Use this to hold your site name, contact info, social links, analytics IDs, SEO keywords, banners, or any project-specific data you want available everywhere. |
162
+
163
+ ### `prepros` block
164
+
165
+ | Key | Type | Default | Description |
166
+ |-----|------|---------|--------------|
167
+ | `before` | `string` | — | Path (relative to `kirigami.root`) to a PHP file included **before** every page's body. Typically your `<head>`/layout opening. |
168
+ | `after` | `string` | — | Path (relative to `kirigami.root`) to a PHP file included **after** every page's body. Typically your layout closing. |
169
+ | `format` | `bool` | `false` | Pretty-print the compiled HTML via [`HTML::format()`](#html) before writing it to disk. |
170
+ | `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)). |
171
+ | `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`, `.txt`, `.webp`). Files with extensions not in this set are simply skipped during mounting — mount them on demand with [`PREPROS::mount()`](#preprosmountstringarray-patterns) instead. |
172
+ | `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()`. |
173
+
174
+ The entire `kirigami` block is extracted into PHP variables and made available in every page template, `before.php`, and `after.php`. `$project`, `$author`, `$gtag`, etc. are available without any further setup.
109
175
 
110
176
  ---
111
177
 
@@ -148,7 +214,7 @@ Every page starts with a PHP docblock that drives metadata and data loading:
148
214
 
149
215
  All annotations are injected as PHP variables (`$name`, `$title`, `$abstract`, …). You can define any custom annotation you need.
150
216
 
151
- Anotations are also avaiables as variables in `before` and `after` php included files so you can write proper metas in the HTML header.
217
+ Annotations are also available as variables in `before` and `after` PHP included files, so you can write proper metas in the HTML header.
152
218
 
153
219
  ### Auto-loading data files
154
220
 
@@ -182,12 +248,29 @@ When `network: true` is set in `kirigami.yaml`, annotation values that start wit
182
248
  */
183
249
  ```
184
250
 
251
+ ### `@content` and `@indent`
252
+
253
+ Two special annotation names change how a page's body is assembled:
254
+
255
+ - **`@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.
256
+ - **`@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.
257
+
258
+ ```php
259
+ <?php
260
+ /**
261
+ * @name changelog
262
+ * @title Changelog
263
+ * @content _changelog.md
264
+ * @indent 4
265
+ */
266
+ ```
267
+
185
268
  ---
186
269
 
187
270
  ## JavaScript API
188
271
 
189
272
  ```js
190
- import { render, sitemap } from '@kirigami/php-prepros';
273
+ import { render, sitemap, runenv } from '@kirigami/php-prepros';
191
274
  ```
192
275
 
193
276
  ### `render(file?)`
@@ -204,7 +287,7 @@ const result = await render('.');
204
287
  // Compile everything (uses kirigami.root from config)
205
288
  const result = await render();
206
289
  ```
207
- > Path use by `render()` are all relative to `kirigami.root` configuration.
290
+ > Paths used by `render()` are all relative to the `kirigami.root` configuration.
208
291
 
209
292
 
210
293
  **Returns** `Promise<PreprosResult>`:
@@ -226,6 +309,27 @@ const result = await sitemap();
226
309
  // result.files === ['src/sitemap.xml']
227
310
  ```
228
311
 
312
+ ### `runenv(script, paths?, ...args)`
313
+
314
+ 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.
315
+
316
+ ```js
317
+ // Run a standalone PHP script
318
+ const result = await runenv('scripts/purge-cache.php');
319
+
320
+ // Also mount extra local paths/files into the sandbox before running
321
+ const result = await runenv('scripts/build-og-images.php', ['assets/photos']);
322
+
323
+ // Extra arguments are appended and available as $argv[2], $argv[3], … in the script
324
+ const result = await runenv('scripts/import.php', [], '--force');
325
+ ```
326
+
327
+ - `script` — path to a PHP file **inside the project**, executed with `require_once`.
328
+ - `paths` — optional array of extra local paths (files or directories) to mount into the sandbox before the script runs.
329
+ - `...args` — extra string arguments appended to the script's `$argv`.
330
+
331
+ **Returns** `Promise<PreprosResult>`, following the same shape as `render()`. Inside the script, call `PREPROS::exportFile()` for any file you want listed in `result.files`.
332
+
229
333
  ---
230
334
 
231
335
  ## PHP classes reference
@@ -236,13 +340,14 @@ All classes are autoloaded — no manual `require` needed inside your page files
236
340
 
237
341
  ### PREPROS
238
342
 
239
- The core engine. Manages the rendering pipeline, tag processing, hooks, and file export.
343
+ The core engine. Manages the rendering pipeline, tag processing, hooks, mounting, and file export.
240
344
 
241
345
  ```php
242
346
  // Available inside page templates and included files.
243
347
  PREPROS::$config // stdClass — full resolved config (prepros section of kirigami.yaml)
244
348
  PREPROS::registerTag(string $tag, callable $callback)
245
349
  PREPROS::registerHook(string $hook, callable $callback)
350
+ PREPROS::mount(string|array $patterns)
246
351
  PREPROS::exportFile(string $absolutePath)
247
352
  PREPROS::getExportedFiles(): string[]
248
353
  ```
@@ -253,7 +358,7 @@ Internal method called once per source file. Orchestrates the full pipeline:
253
358
 
254
359
  1. Resolves PHPDOC metadata and auto-loads data files.
255
360
  2. Fires the `pre_render` hook with the raw source contents.
256
- 3. Includes `before.php`, the page body, and `after.php` into a single string.
361
+ 3. Includes `before.php`, the page body (or `@content`, see [above](#content-and-indent)), and `after.php` into a single string.
257
362
  4. Processes all registered custom HTML tags.
258
363
  5. Fires the `post_render` hook on the assembled HTML.
259
364
  6. Optionally pretty-prints via `HTML::format()` (when `format: true`).
@@ -261,11 +366,25 @@ Internal method called once per source file. Orchestrates the full pipeline:
261
366
 
262
367
  #### `PREPROS::sitemap()`
263
368
 
264
- Scans the source tree for `_index.php` files and generates a standards-compliant `sitemap.xml` (Sitemaps 0.9).
369
+ 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.
370
+
371
+ #### `PREPROS::mount(string|array $patterns)`
372
+
373
+ 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.
374
+
375
+ ```php
376
+ // Mount every .webp under assets/, wherever the page needs them
377
+ PREPROS::mount('assets/**/*.webp');
378
+
379
+ // Multiple patterns at once
380
+ PREPROS::mount(['data/**/*.csv', 'vendor/fonts/*.woff2']);
381
+ ```
382
+
383
+ Returns an array of the virtual paths (under `/project/...`) that were mounted, or `false` on failure.
265
384
 
266
385
  #### `PREPROS::exportFile(string $file)`
267
386
 
268
- Marks a file as a build output so it gets surfaced in `PreprosResult.files`. Called automatically by `render()`, `sitemap()`, `CACHE::set()`, and `IMG::save()`. Call it manually if your custom code writes additional files.
387
+ 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.
269
388
 
270
389
  ---
271
390
 
@@ -345,6 +464,7 @@ A lightweight, zero-dependency YAML parser. Covers the full subset used in stati
345
464
  ```php
346
465
  $data = YAML::parse(string $yaml, bool $assoc = false): mixed;
347
466
  $data = YAML::parseFile(string $path, bool $assoc = false): mixed;
467
+ $data = YAML::loadFile(string $path, bool $assoc = false): mixed;
348
468
  ```
349
469
 
350
470
  Supported features:
@@ -361,6 +481,21 @@ Supported features:
361
481
 
362
482
  By default, YAML mappings are returned as `stdClass` objects. Pass `true` as the second argument to get associative arrays instead.
363
483
 
484
+ `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`.
485
+
486
+ ```yaml
487
+ # team.yaml
488
+ lead: people/jane.yaml # resolved and inlined automatically
489
+ members:
490
+ - people/jane.yaml
491
+ - people/john.yaml
492
+ ```
493
+
494
+ ```php
495
+ $team = YAML::loadFile('/project/data/team.yaml');
496
+ // $team->lead is now the fully parsed content of people/jane.yaml, not a string
497
+ ```
498
+
364
499
  ---
365
500
 
366
501
  ### CACHE
@@ -374,7 +509,7 @@ CACHE::delete(string $key): bool
374
509
  CACHE::purge(): bool // removes expired entries
375
510
  ```
376
511
 
377
- 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.
512
+ 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.
378
513
 
379
514
  ```php
380
515
  $data = CACHE::get('my-remote-data');
@@ -404,7 +539,7 @@ $img->save(string $dest): self
404
539
 
405
540
  `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.
406
541
 
407
- `save()` infers the output format from the file extension (`.jpg`, `.jpeg`, `.png`, `.gif`, `.webp`). The saved file is automatically registered via `PREPROS::exportFile()`.
542
+ `save()` infers the output format from the file extension (`.jpg`, `.jpeg`, `.png`, `.gif`, `.webp`).
408
543
 
409
544
  ```php
410
545
  (new IMG('/project/src/images/hero.jpg'))
@@ -434,19 +569,95 @@ FS::pathJoin(string ...$parts): string // URL-aware path join with .. resoluti
434
569
 
435
570
  ### STR
436
571
 
437
- String utilities used internally by the tag-processing pipeline.
572
+ String utilities used internally by the tag-processing pipeline, and available for your own templates and plugins.
438
573
 
439
574
  ```php
440
575
  STR::htmlesc(string $str): string
441
576
  STR::replaceTags(string $tag, string $html, callable $callback): string
442
577
  STR::parseHtmlAttributes(string $attrString): array
443
578
  STR::trimIndent(string $str): string
579
+ STR::is_url(string $str): bool
580
+ STR::html_entities_decode(string $str): string
581
+ STR::shorthash(string $str): string
582
+ STR::slug(string $str): string
444
583
  ```
445
584
 
446
585
  `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)`.
447
586
 
448
587
  `STR::trimIndent()` strips the common leading whitespace from a multi-line string — handy when pulling content out of indented `<markdown>` blocks.
449
588
 
589
+ `STR::is_url()` checks whether a string parses as a URL with a recognized scheme (`http`, `https`, `ftp`, `ftps`, `ssh`, `ssl`, `sftp`, `itunes`).
590
+
591
+ `STR::html_entities_decode()` trims a string and decodes its HTML entities — handy when normalizing text scraped from a third-party page.
592
+
593
+ `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`).
594
+
595
+ `STR::slug()` transliterates a string to ASCII, lowercases it, and strips anything that isn't `[a-z0-9]` — a compact identifier rather than a hyphenated slug.
596
+
597
+ ---
598
+
599
+ ### ARR
600
+
601
+ Recursive lookup helper for nested arrays and objects.
602
+
603
+ ```php
604
+ ARR::find_key(mixed $data, string $key): mixed
605
+ ```
606
+
607
+ 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.
608
+
609
+ ```php
610
+ $config = YAML::parseFile('team.yaml');
611
+ $email = ARR::find_key($config, 'email'); // finds `email` however deep it's nested
612
+ ```
613
+
614
+ ---
615
+
616
+ ### CURL
617
+
618
+ 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()`).
619
+
620
+ ```php
621
+ CURL::urlExists(string $url, ?string $mimereg = null): bool
622
+ CURL::getInfo(string $url): array|false // HEAD request, returns curl_getinfo()
623
+ CURL::getContents(string $file, ?string $dest = null, ?callable $clb = null): string|bool
624
+ ```
625
+
626
+ `CURL::urlExists()` issues a `HEAD` request and returns `true` for any `2xx`/`3xx` response.
627
+
628
+ `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`.
629
+
630
+ ```php
631
+ CURL::getContents('https://example.com/report.pdf', '/project/src/downloads/report.pdf', function (float $progress) {
632
+ error_log(sprintf('%.0f%%', $progress * 100));
633
+ });
634
+ ```
635
+
636
+ ---
637
+
638
+ ### SCRAPER
639
+
640
+ 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.
641
+
642
+ ```php
643
+ $metas = SCRAPER::get(string $url): object|false;
644
+ ```
645
+
646
+ ```php
647
+ $metas = SCRAPER::get('https://example.com/blog/some-article');
648
+ if ($metas) {
649
+ echo $metas->title; // string
650
+ echo $metas->description; // string
651
+ echo $metas->image; // string (absolute URL, may be empty)
652
+ echo $metas->label; // string — site/publisher name, may be empty
653
+ echo $metas->url; // string — the URL that was scraped
654
+ }
655
+ ```
656
+
657
+ 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.
658
+
659
+ A free `url_exists(string $url, ?string $mimereg = null): bool` function is also available globally (not namespaced under a class), cached the same way as `SCRAPER::get()`.
660
+
450
661
  ---
451
662
 
452
663
  ### OBF
@@ -471,7 +682,7 @@ STD::succeed(array|string $props = []): void // exits 0, writes JSON to stdout
471
682
  STD::error(array|string $props = []): void // exits 1, writes JSON to stderr
472
683
  ```
473
684
 
474
- These are internal to the build runner. You generally do not need to call them in page templates.
685
+ 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.
475
686
 
476
687
  ---
477
688
 
@@ -528,7 +739,7 @@ PREPROS::registerHook(string $hookName, callable $callback): void
528
739
 
529
740
  | Hook | When it fires | `$data` type | Expected return |
530
741
  |------|---------------|--------------|-----------------|
531
- | `page_info` | After PHPDOC parsing, before rendering | `[$filePath, $pageObject]` | `$pageObject` (modified) |
742
+ | `page_info` | After PHPDOC parsing, before rendering (auto-loads `.yaml`/`.json`/`.md` annotations) | `[$filePath, $pageObject]` | `$pageObject` (modified) |
532
743
  | `pre_render` | Before PHP execution | Raw file contents as `string` | `string` |
533
744
  | `post_render` | After tag processing, before `HTML::format()` | Assembled HTML `string` | `string` |
534
745
 
@@ -589,6 +800,36 @@ Line two after a blank line.
589
800
  %}
590
801
  ```
591
802
 
803
+ #### `{% youtube id [width height] %}`
804
+
805
+ Embeds a responsive YouTube player via `<iframe>`. `width`/`height` default to `560`/`315`.
806
+
807
+ ```
808
+ {% youtube dQw4w9WgXcQ %}
809
+ {% youtube dQw4w9WgXcQ 800 450 %}
810
+ ```
811
+
812
+ #### `{% codepen id [user height] %}`
813
+
814
+ Embeds a CodePen result via `<iframe>`. `user` defaults to `anonymous`, `height` defaults to `400`.
815
+
816
+ ```
817
+ {% codepen abcXYZ %}
818
+ {% codepen abcXYZ jsmith 500 %}
819
+ ```
820
+
821
+ #### `{% checklist ["Title"] items %}`
822
+
823
+ Renders a block-syntax list of checkbox items, one per line, with an optional title.
824
+
825
+ ```
826
+ {% checklist "Today"
827
+ Do the dishes
828
+ Walk the dog
829
+ Read a book
830
+ %}
831
+ ```
832
+
592
833
  ---
593
834
 
594
835
  ## Extending the `<markdown>` tag
package/index.js CHANGED
@@ -1 +1 @@
1
- export { render, sitemap } from "./src/prepros.js";
1
+ export { render, sitemap, runenv } from "./src/prepros.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kirigami/php-prepros",
3
- "version": "1.0.9",
3
+ "version": "1.1.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",
@@ -44,8 +44,9 @@
44
44
  "test": "echo \"Error: no test specified\" && exit 1"
45
45
  },
46
46
  "dependencies": {
47
- "@kirigami/php-wasm": "8.5.10-2",
48
- "@kirigami/struct-walker": "1.0.3"
47
+ "@kirigami/php-wasm": "8.5.10-3",
48
+ "@kirigami/struct-walker": "1.0.3",
49
+ "picomatch": "^4.0.7"
49
50
  },
50
51
  "repository": {
51
52
  "type": "git",
@@ -0,0 +1,29 @@
1
+ <?php
2
+
3
+
4
+ class ARR
5
+ {
6
+ public static function find_key(mixed $data, string $key)
7
+ {
8
+ if (is_object($data)) {
9
+ $data = (array) $data;
10
+ }
11
+
12
+ if (is_array($data)) {
13
+ foreach ($data as $k => $value) {
14
+ if ($k === $key) {
15
+ return $value;
16
+ }
17
+
18
+ if (is_array($value) || is_object($value)) {
19
+ $found = ARR::find_key($value, $key);
20
+ if ($found !== null) {
21
+ return $found;
22
+ }
23
+ }
24
+ }
25
+ }
26
+
27
+ return null;
28
+ }
29
+ }
@@ -13,10 +13,7 @@ class CACHE
13
13
  static $db = null;
14
14
  if($db !== null) return $db;
15
15
 
16
- if(!empty($_SERVER['NODE_PROJECT'])) $root = $_SERVER['NODE_PROJECT'] . '/';
17
- elseif(!empty(PREPROS::$config->root)) $root = rtrim(PREPROS::$config->root, '/') . '/';
18
- else throw new Exception("Can't find project root.");
19
-
16
+
20
17
  if(!is_file(($dbfile = self::getPath()))) $create = true;
21
18
  if(!$db = new SQLite3($dbfile)) throw new Exception("Can't open .cache.db.");
22
19
 
@@ -36,6 +33,10 @@ class CACHE
36
33
 
37
34
 
38
35
  private static function getPath() {
36
+ // if(!empty($_SERVER['NODE_PROJECT'])) $root = $_SERVER['NODE_PROJECT'] . '/';
37
+ // elseif(!empty(PREPROS::$config->root)) $root = rtrim(PREPROS::$config->root, '/') . '/';
38
+ // else throw new Exception("Can't find project root.");
39
+ // return $root . '.cache.db';
39
40
  return '/project/.cache.db';
40
41
  }
41
42