@kirigami/php-prepros 1.2.0 → 1.6.1

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.
@@ -0,0 +1,836 @@
1
+ <?php
2
+
3
+ /**
4
+ * Procedural shortcuts for the class library in this folder.
5
+ *
6
+ * Every helper is a thin wrapper over a static method, named
7
+ * `<lowercase class>_<snake_case method>()`:
8
+ *
9
+ * CURL::getContents(...) => curl_get_contents(...)
10
+ * IMG::asset(...) => img_asset(...)
11
+ * MD::toHtml(...) => md_to_html(...)
12
+ *
13
+ * Handy inside page templates and `kiri run` scripts, where the terse
14
+ * function form reads better than the fully-qualified static call. The
15
+ * classes stay the canonical API; these only forward their arguments.
16
+ *
17
+ * Loaded from utils.inc.php, right after the autoloader is registered.
18
+ */
19
+
20
+
21
+ // ===========================================================================
22
+ // PREPROS — the engine
23
+ // ===========================================================================
24
+
25
+ /**
26
+ * Compiles a page template (`_name.php`) to its `.html` sibling: runs the
27
+ * `before`/`after` includes, processes registered tags, fires the
28
+ * `pre_render` / `post_render` hooks, applies `HTML::format()` when
29
+ * `format` is on, then marks the result as a build output.
30
+ *
31
+ * @param string $file Absolute (or cwd-relative) path to the template.
32
+ * @return string|false Absolute path of the generated HTML, or false
33
+ * if `$file` could not be resolved.
34
+ * @see PREPROS::render()
35
+ */
36
+ function prepros_render(string $file) { return PREPROS::render($file); }
37
+
38
+ /**
39
+ * Generates `sitemap.xml` and `robots.txt` at the project root from the
40
+ * tree of `_index.php` pages, and marks both as build outputs.
41
+ *
42
+ * @return string|false Absolute path of the generated `sitemap.xml`.
43
+ * @see PREPROS::sitemap()
44
+ */
45
+ function prepros_sitemap() { return PREPROS::sitemap(); }
46
+
47
+ /**
48
+ * Mounts extra local files (any extension) into the WASM filesystem so PHP
49
+ * can read them during the build.
50
+ *
51
+ * @param string|string[] $patterns One or more glob patterns, relative to cwd.
52
+ * @return string[]|false Virtual paths of the mounted files, or
53
+ * false on failure.
54
+ * @see PREPROS::mount()
55
+ */
56
+ function prepros_mount(string|array $patterns) { return PREPROS::mount($patterns); }
57
+
58
+ /**
59
+ * Stats a file inside the WASM filesystem.
60
+ *
61
+ * @param string $path Virtual path to inspect.
62
+ * @return object|false `stdClass` with `->exists`, `->modifiedAt`, …,
63
+ * or false when the file does not exist.
64
+ * @see PREPROS::fstat()
65
+ */
66
+ function prepros_fstat(string $path) { return PREPROS::fstat($path); }
67
+
68
+ /**
69
+ * Marks one or more files as build outputs (they show up in the result and
70
+ * get copied back to the host).
71
+ *
72
+ * @param string|string[] $file Absolute path(s) to the generated file(s).
73
+ * @return void
74
+ * @see PREPROS::exportFile()
75
+ */
76
+ function prepros_export_file(string|array $file): void { PREPROS::exportFile($file); }
77
+
78
+ /**
79
+ * Returns the list of files marked as build outputs so far (de-duplicated).
80
+ *
81
+ * @return string[] Absolute paths.
82
+ * @see PREPROS::getExportedFiles()
83
+ */
84
+ function prepros_get_exported_files(): array { return PREPROS::getExportedFiles(); }
85
+
86
+ /**
87
+ * Path of the page template currently being rendered.
88
+ *
89
+ * @return string|false The template's path, or false outside of a render.
90
+ * @see PREPROS::backtraceFile()
91
+ */
92
+ function prepros_backtrace_file() { return PREPROS::backtraceFile(); }
93
+
94
+ /**
95
+ * Registers a custom HTML tag handler, invoked during `post_render`.
96
+ *
97
+ * The callback receives `($fullTag, array $attrs, string $body)` and returns
98
+ * the replacement string, e.g.:
99
+ *
100
+ * prepros_register_tag('gallery', fn($tag, $attrs, $body) => '<div class="gallery">…</div>');
101
+ *
102
+ * @param string $tag Tag name, without angle brackets (`gallery`).
103
+ * @param callable $clb `function(string $fullTag, array $attrs, string $body): string`
104
+ * @return void
105
+ * @see PREPROS::registerTag()
106
+ */
107
+ function prepros_register_tag(string $tag, callable $clb) { return PREPROS::registerTag($tag, $clb); }
108
+
109
+ /**
110
+ * Registers a build hook.
111
+ *
112
+ * Known hooks: `boot` (`stdClass $config`, once per process after bootstrap,
113
+ * before any render — return value ignored), `page_info` (fires with
114
+ * `[$file, stdClass $info]`; each callback returns the `$info` object it wants
115
+ * to pass on, so a callback registered later receives that bare object — accept
116
+ * both shapes, e.g. `$page = is_array($p) ? $p[1] : $p;`), `pre_render` (raw
117
+ * template source), `pre_before` / `pre_after`
118
+ * (the `before` / `after` config path, just before that include — echo here to
119
+ * prepend output, return value ignored), `post_before` / `post_after` (the
120
+ * captured header / footer string, right after the include), `post_render`
121
+ * (assembled HTML). The callback receives the hook payload and must return it
122
+ * (possibly modified).
123
+ *
124
+ * @param string $hook Hook name.
125
+ * @param callable $clb `function(mixed $payload): mixed`
126
+ * @return void
127
+ * @see PREPROS::registerHook()
128
+ */
129
+ function prepros_register_hook(string $hook, callable $clb) { return PREPROS::registerHook($hook, $clb); }
130
+
131
+ /**
132
+ * Fires a hook and returns the payload after every registered callback has
133
+ * piped it through. Works for the built-in hooks and for any custom hook name
134
+ * a plugin defines.
135
+ *
136
+ * @param string $hook Hook name.
137
+ * @param mixed $data Initial payload.
138
+ * @return mixed The payload after the last callback.
139
+ * @see PREPROS::runHook()
140
+ */
141
+ function prepros_run_hook(string $hook, mixed $data = null): mixed { return PREPROS::runHook($hook, $data); }
142
+
143
+ /**
144
+ * Alias of {@see prepros_register_tag()} (historical unprefixed spelling).
145
+ *
146
+ * @param string $tag
147
+ * @param callable $clb `function(string $fullTag, array $attrs, string $body): string`
148
+ * @return void
149
+ */
150
+ function register_tag(string $tag, callable $clb) { return PREPROS::registerTag($tag, $clb); }
151
+
152
+ /**
153
+ * Alias of {@see prepros_register_hook()} (historical unprefixed spelling).
154
+ *
155
+ * @param string $hook
156
+ * @param callable $clb `function(mixed $payload): mixed`
157
+ * @return void
158
+ */
159
+ function register_hook(string $hook, callable $clb) { return PREPROS::registerHook($hook, $clb); }
160
+
161
+
162
+ // ===========================================================================
163
+ // MD — Markdown -> HTML
164
+ // ===========================================================================
165
+
166
+ /**
167
+ * Converts Markdown to HTML (GitHub-flavored: tables, task lists, GFM
168
+ * alerts, footnotes, reference links, `:emoji:` shortcodes, a safe raw-HTML
169
+ * subset, and `{% plugin %}` tags).
170
+ *
171
+ * @param string $markdown Markdown source.
172
+ * @return string HTML fragment.
173
+ * @see MD::toHtml()
174
+ */
175
+ function md_to_html(string $markdown): string { return MD::toHtml($markdown); }
176
+
177
+ /**
178
+ * Registers a Markdown plugin, usable as `{% name arg "arg 2" %}` (inline)
179
+ * or as a multi-line `{% name … %}` block.
180
+ *
181
+ * md_register_plugin('video', fn(array $args, string $body) => '<iframe …></iframe>');
182
+ *
183
+ * @param string $name Tag name (case-insensitive).
184
+ * @param callable $clb `function(array $args, string $body): string`
185
+ * @return void
186
+ * @see MD::registerPlugin()
187
+ */
188
+ function md_register_plugin(string $name, callable $clb): void { MD::registerPlugin($name, $clb); }
189
+
190
+ /**
191
+ * Removes a previously registered Markdown plugin.
192
+ *
193
+ * @param string $name Tag name.
194
+ * @return void
195
+ * @see MD::unregisterPlugin()
196
+ */
197
+ function md_unregister_plugin(string $name): void { MD::unregisterPlugin($name); }
198
+
199
+ /**
200
+ * Names of all currently registered Markdown plugins.
201
+ *
202
+ * @return string[]
203
+ * @see MD::getRegisteredPlugins()
204
+ */
205
+ function md_get_registered_plugins(): array { return MD::getRegisteredPlugins(); }
206
+
207
+ /**
208
+ * Registers (or overrides) a `:shortcode:` → character emoji mapping.
209
+ *
210
+ * @param string $shortcode Shortcode, with or without the surrounding colons.
211
+ * @param string $char Replacement character(s).
212
+ * @return void
213
+ * @see MD::registerEmoji()
214
+ */
215
+ function md_register_emoji(string $shortcode, string $char): void { MD::registerEmoji($shortcode, $char); }
216
+
217
+
218
+ // ===========================================================================
219
+ // HTML — formatter
220
+ // ===========================================================================
221
+
222
+ /**
223
+ * Re-indents an HTML string (PHP 8.4 `Dom\HTMLDocument` / Lexbor, 4-space
224
+ * indent). `<script>` / `<style>` bodies are re-indented but not reformatted.
225
+ *
226
+ * @param string $html Source HTML.
227
+ * @return string Pretty-printed HTML, trailing newline included.
228
+ * @see HTML::format()
229
+ */
230
+ function html_format(string $html): string { return HTML::format($html); }
231
+
232
+
233
+ // ===========================================================================
234
+ // YAML — parser
235
+ //
236
+ // Guarded: yaml_parse() / yaml_parse_file() are also the names of the PECL
237
+ // yaml extension's functions.
238
+ // ===========================================================================
239
+
240
+ if (!function_exists('yaml_parse')) {
241
+ /**
242
+ * Parses a YAML string.
243
+ *
244
+ * @param string $yaml YAML source.
245
+ * @param bool $assoc `true` → mappings as arrays, `false` → `stdClass`.
246
+ * @return mixed
247
+ * @see YAML::parse()
248
+ */
249
+ function yaml_parse(string $yaml, bool $assoc = false): mixed { return YAML::parse($yaml, $assoc); }
250
+ }
251
+ if (!function_exists('yaml_parse_file')) {
252
+ /**
253
+ * Parses a YAML file.
254
+ *
255
+ * @param string $path Path to the `.yaml` / `.yml` file.
256
+ * @param bool $assoc `true` → arrays, `false` → `stdClass`.
257
+ * @return mixed
258
+ * @throws \RuntimeException When the file cannot be read.
259
+ * @see YAML::parseFile()
260
+ */
261
+ function yaml_parse_file(string $path, bool $assoc = false): mixed { return YAML::parseFile($path, $assoc); }
262
+ }
263
+
264
+ /**
265
+ * Parses a YAML/JSON file, then recursively inlines any string value that
266
+ * points to an existing relative `.yaml` / `.yml` / `.json` file.
267
+ *
268
+ * @param string $path Path to the root file.
269
+ * @param bool $assoc `true` → arrays, `false` → `stdClass`.
270
+ * @return mixed
271
+ * @throws \RuntimeException On an unreadable file or a circular reference.
272
+ * @see YAML::loadFile()
273
+ */
274
+ function yaml_load_file(string $path, bool $assoc = false): mixed { return YAML::loadFile($path, $assoc); }
275
+
276
+
277
+ // ===========================================================================
278
+ // SCHEMA — pure-PHP JSON Schema validator (instance class)
279
+ // ===========================================================================
280
+
281
+ /**
282
+ * Builds a JSON Schema validator (Draft-7-ish, Ajv-like API).
283
+ *
284
+ * $v = schema($schema);
285
+ * if (!$v->isValid($data)) print_r($v->getErrors());
286
+ *
287
+ * @param array<string,mixed> $schema The root schema.
288
+ * @return SCHEMA
289
+ * @see SCHEMA::__construct()
290
+ */
291
+ function schema(array $schema): SCHEMA { return new SCHEMA($schema); }
292
+
293
+ /**
294
+ * One-shot validation. The error list (if any) comes back through `$errors`.
295
+ *
296
+ * if (!schema_validate($schema, $data, $errors)) { … }
297
+ *
298
+ * @param array<string,mixed> $schema The root schema.
299
+ * @param mixed $data Value to validate.
300
+ * @param string[]|null $errors Filled with `"path: message"` strings.
301
+ * @return bool `true` when `$data` is valid.
302
+ * @see SCHEMA::isValid()
303
+ */
304
+ function schema_validate(array $schema, mixed $data, ?array &$errors = null): bool
305
+ {
306
+ $v = new SCHEMA($schema);
307
+ $ok = $v->isValid($data);
308
+ $errors = $v->getErrors();
309
+ return $ok;
310
+ }
311
+
312
+
313
+ // ===========================================================================
314
+ // LD — schema.org JSON-LD generator
315
+ // ===========================================================================
316
+
317
+ /**
318
+ * Builds a node and registers it in the page's JSON-LD `@graph`. Any
319
+ * schema.org type is also reachable as `LD::typeName([...])`.
320
+ *
321
+ * ld_add('Recipe', ['name' => 'Tarte', 'recipeYield' => '6']);
322
+ *
323
+ * @param string|string[] $type
324
+ * @param array<string,mixed> $props
325
+ * @param string|null $id Stable `@id` (repeat calls merge).
326
+ * @return array<string,mixed> The stored node.
327
+ * @see LD::add()
328
+ */
329
+ function ld_add(string|array $type, array $props = [], ?string $id = null): array { return LD::add($type, $props, $id); }
330
+
331
+ /**
332
+ * Builds a bare node (`@type` + pruned `$props`) without touching the graph.
333
+ *
334
+ * @param string|string[] $type
335
+ * @param array<string,mixed> $props
336
+ * @return array<string,mixed>
337
+ * @see LD::node()
338
+ */
339
+ function ld_node(string|array $type, array $props = []): array { return LD::node($type, $props); }
340
+
341
+ /**
342
+ * `['@id' => …]` reference to another node (`#organization`, `#website`, …).
343
+ *
344
+ * @param string $id Fragment or absolute URL.
345
+ * @return array{@id:string}
346
+ * @see LD::ref()
347
+ */
348
+ function ld_ref(string $id): array { return LD::ref($id); }
349
+
350
+ /**
351
+ * Adds the site's main entity (`Organization`, or `jsonld.type`) to the graph.
352
+ *
353
+ * @param array<string,mixed> $overrides
354
+ * @return array<string,mixed>
355
+ * @see LD::organization()
356
+ */
357
+ function ld_organization(array $overrides = []): array { return LD::organization($overrides); }
358
+
359
+ /**
360
+ * Adds the `Person` behind the site, linked to the `Organization`.
361
+ *
362
+ * @param array<string,mixed> $overrides
363
+ * @return array<string,mixed>
364
+ * @see LD::person()
365
+ */
366
+ function ld_person(array $overrides = []): array { return LD::person($overrides); }
367
+
368
+ /**
369
+ * Adds the `WebSite` node.
370
+ *
371
+ * @param array<string,mixed> $overrides
372
+ * @return array<string,mixed>
373
+ * @see LD::website()
374
+ */
375
+ function ld_website(array $overrides = []): array { return LD::website($overrides); }
376
+
377
+ /**
378
+ * Adds the current page's `WebPage` node, built from its PHPDOC.
379
+ *
380
+ * @param array<string,mixed> $overrides
381
+ * @return array<string,mixed>
382
+ * @see LD::webPage()
383
+ */
384
+ function ld_web_page(array $overrides = []): array { return LD::webPage($overrides); }
385
+
386
+ /**
387
+ * Adds a `BreadcrumbList`. With no `$items` it is derived from the page's
388
+ * ancestor trail (needs `@breadcrumb true`).
389
+ *
390
+ * @param array<int,array{name?:string,url?:string}>|null $items
391
+ * @param array<string,mixed> $overrides
392
+ * @return array<string,mixed>
393
+ * @see LD::breadcrumb()
394
+ */
395
+ function ld_breadcrumb(?array $items = null, array $overrides = []): array { return LD::breadcrumb($items, $overrides); }
396
+
397
+ /**
398
+ * Adds an `FAQPage` from a `question => answer` map.
399
+ *
400
+ * @param array<string,string|array<string,mixed>> $qa
401
+ * @param array<string,mixed> $overrides
402
+ * @return array<string,mixed>
403
+ * @see LD::faqPage()
404
+ */
405
+ function ld_faq_page(array $qa, array $overrides = []): array { return LD::faqPage($qa, $overrides); }
406
+
407
+ /**
408
+ * The `<script type="application/ld+json">…</script>` block for the current
409
+ * graph, or `''`. Calling this from a template places the block by hand and
410
+ * disables the automatic injection.
411
+ *
412
+ * @param bool $pretty
413
+ * @return string
414
+ * @see LD::script()
415
+ */
416
+ function ld_script(bool $pretty = true): string { return LD::script($pretty); }
417
+
418
+ /**
419
+ * The JSON-LD document as a string (no `<script>` wrapper).
420
+ *
421
+ * @param bool $pretty
422
+ * @return string
423
+ * @see LD::json()
424
+ */
425
+ function ld_json(bool $pretty = true): string { return LD::json($pretty); }
426
+
427
+
428
+ // ===========================================================================
429
+ // CACHE — persistent key/value (SQLite, `.cache.db` at the project root)
430
+ // ===========================================================================
431
+
432
+ /**
433
+ * Reads a cached value.
434
+ *
435
+ * @param string $key Cache key.
436
+ * @return mixed The stored value, or `null` when missing or expired.
437
+ * @see CACHE::get()
438
+ */
439
+ function cache_get(string $key): mixed { return CACHE::get($key); }
440
+
441
+ /**
442
+ * Writes a cached value.
443
+ *
444
+ * @param string $key Cache key.
445
+ * @param mixed $val Value to store (serialized).
446
+ * @param int $ttl Lifetime in seconds; `0` = forever.
447
+ * @return bool `true` on success.
448
+ * @see CACHE::set()
449
+ */
450
+ function cache_set(string $key, mixed $val, int $ttl = 0): bool { return CACHE::set($key, $val, $ttl); }
451
+
452
+ /**
453
+ * Deletes a single cache entry.
454
+ *
455
+ * @param string $key Cache key.
456
+ * @return bool `true` on success.
457
+ * @see CACHE::delete()
458
+ */
459
+ function cache_delete(string $key): bool { return CACHE::delete($key); }
460
+
461
+ /**
462
+ * Drops every expired entry from the cache.
463
+ *
464
+ * @return bool `true` on success.
465
+ * @see CACHE::purge()
466
+ */
467
+ function cache_purge(): bool { return CACHE::purge(); }
468
+
469
+
470
+ // ===========================================================================
471
+ // IMG — image autogenerator
472
+ // ===========================================================================
473
+
474
+ /**
475
+ * Resolves `$path` against `image.source`, generates a resized copy in
476
+ * `image.dest` (encoded as `image.format`), and returns its URL relative to
477
+ * the file that called this function. Re-uses the existing file when it is
478
+ * newer than the source.
479
+ *
480
+ * <img src="<?= img_asset('hero.jpg', 800) ?>">
481
+ *
482
+ * @param string $path Source image path, relative to `image.source`.
483
+ * @param int $width Target width in px; `0` keeps the aspect ratio.
484
+ * @param int $height Target height in px; `0` keeps the aspect ratio.
485
+ * @param bool $cover `true` crops to fill `width`×`height` (center),
486
+ * `false` fits inside.
487
+ * @return string URL of the generated image, relative to the caller.
488
+ * @see IMG::asset()
489
+ */
490
+ function img_asset(string $path, int $width = 0, int $height = 0, bool $cover = false): string
491
+ {
492
+ // Forward the real caller's path so IMG::asset() builds the URL
493
+ // relative to the template, not to this wrapper file.
494
+ $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1);
495
+ return IMG::asset($path, $width, $height, $cover, $trace[0]['file'] ?? '');
496
+ }
497
+
498
+ /**
499
+ * Extracts the most representative colors from an image (median-cut in Lab
500
+ * space, near-white/near-black excluded). CACHE-backed.
501
+ *
502
+ * @param string $path Source image path, relative to `image.source`.
503
+ * @param int $colors Number of colors to return.
504
+ * @return string[] Colors as `#rrggbb`, most frequent first.
505
+ * @see IMG::palette()
506
+ */
507
+ function img_palette(string $path, int $colors = 5): array { return IMG::palette($path, $colors); }
508
+
509
+
510
+ // ===========================================================================
511
+ // FS — filesystem helpers
512
+ // ===========================================================================
513
+
514
+ /**
515
+ * Recursively walks a directory, yielding every file whose name matches the
516
+ * glob pattern in the last path segment.
517
+ *
518
+ * foreach (fs_dig('src/*.php') as $file) { … }
519
+ *
520
+ * @param string $path Directory + glob pattern (e.g. `src/_index.php`).
521
+ * @return iterable<string> File paths.
522
+ * @see FS::dig()
523
+ */
524
+ function fs_dig(string $path): iterable { return FS::dig($path); }
525
+
526
+ /**
527
+ * Computes the relative path from one location to another (directories are
528
+ * detected and treated as such). Both accept `/` or `\` separators.
529
+ *
530
+ * @param string $from Source path.
531
+ * @param string $to Target path.
532
+ * @return string Relative path, `/`-separated.
533
+ * @see FS::getRelativePath()
534
+ */
535
+ function fs_get_relative_path(string $from, string $to): string { return FS::getRelativePath($from, $to); }
536
+
537
+ /**
538
+ * Parses the first PHPDoc block of a PHP file into an object of `@tag`
539
+ * values.
540
+ *
541
+ * @param string $file Path to the PHP file.
542
+ * @return object|false `stdClass` of tag → value, or false when the
543
+ * file cannot be resolved.
544
+ * @see FS::phpFileInfo()
545
+ */
546
+ function fs_php_file_info(string $file): object|bool { return FS::phpFileInfo($file); }
547
+
548
+ /**
549
+ * Immediate child pages of the page being rendered, ordered by `@position`
550
+ * ascending (a missing `@position` counts as 999999), then by folder name.
551
+ *
552
+ * Scans the folders directly below the current page's directory, keeps the
553
+ * ones holding an `_index.php`, and returns one `stdClass` per child: the
554
+ * parsed PHPDOC of that `_index.php`, plus a `->file` key with its absolute
555
+ * path. Works the same from a template, a layout partial or a helper.
556
+ * Only usable during a render.
557
+ *
558
+ * foreach (fs_get_children() as $page) { echo $page->title, $page->file; }
559
+ *
560
+ * @return object[]
561
+ * @throws \Exception When called outside of a render.
562
+ * @see FS::getChildren()
563
+ */
564
+ function fs_get_children(): array
565
+ {
566
+ // Anchored on the page under render (PREPROS::$file), so it behaves the
567
+ // same whether it's called from the template, a layout partial, or a
568
+ // helper function.
569
+ return FS::getChildren();
570
+ }
571
+
572
+ /**
573
+ * Breadcrumb trail of the page being rendered: one `stdClass` per ancestor
574
+ * page, ordered from the top-most ancestor down to the nearest parent (the
575
+ * parsed PHPDOC of each `_index.php`, plus a `->file` key with its absolute
576
+ * path).
577
+ *
578
+ * Returns `[]` unless the current page opts in with `@breadcrumb true` (or
579
+ * `1`). The current page is never part of its own trail; the walk climbs the
580
+ * parent folders and stops at the source root, or at the first ancestor
581
+ * `_index.php` with no active `@breadcrumb` tag (that separator page is left
582
+ * out). Works the same from a template, a layout partial or a helper.
583
+ * Only usable during a render.
584
+ *
585
+ * foreach (fs_get_breadcrumb() as $crumb):
586
+ * <a href="<?= FS::getRelativePath(__DIR__, dirname($crumb->file)) ?>/"><?= $crumb->title ?></a>
587
+ * endforeach
588
+ *
589
+ * @return object[]
590
+ * @throws \Exception When called outside of a render.
591
+ * @see FS::getBreadcrumb()
592
+ */
593
+ function fs_get_breadcrumb(): array
594
+ {
595
+ // Anchored on the page under render (PREPROS::$file), so it behaves the
596
+ // same whether it's called from the template, a layout partial, or a
597
+ // helper function.
598
+ return FS::getBreadcrumb();
599
+ }
600
+
601
+ /**
602
+ * Recursively deletes a directory.
603
+ *
604
+ * @param string $dir Directory to empty.
605
+ * @param bool $removeSelf `true` also removes `$dir` itself; `false`
606
+ * leaves it empty.
607
+ * @return bool `true` on success.
608
+ * @see FS::rmdir()
609
+ */
610
+ function fs_rmdir(string $dir, bool $removeSelf = true): bool { return FS::rmdir($dir, $removeSelf); }
611
+
612
+ /**
613
+ * Joins path segments with `/`, resolving `.` and `..`. URL-aware: a
614
+ * leading scheme/host is preserved.
615
+ *
616
+ * @param string ...$parts Path segments.
617
+ * @return string The joined path.
618
+ * @see FS::pathJoin()
619
+ */
620
+ function fs_path_join(string ...$parts): string { return FS::pathJoin(...$parts); }
621
+
622
+
623
+ // ===========================================================================
624
+ // STR — string helpers
625
+ // ===========================================================================
626
+
627
+ /**
628
+ * Escapes a string for HTML output (`htmlspecialchars`, `ENT_QUOTES`, UTF-8).
629
+ *
630
+ * @param string $str Raw string.
631
+ * @return string HTML-safe string.
632
+ * @see STR::htmlesc()
633
+ */
634
+ function str_htmlesc(string $str): string { return STR::htmlesc($str); }
635
+
636
+ /**
637
+ * Replaces every `<$tag …>…</$tag>` (paired, self-closing or opening-only)
638
+ * in `$contents` with the callback's return value. This is the engine
639
+ * behind {@see prepros_register_tag()}.
640
+ *
641
+ * @param string $tag Tag name, without angle brackets.
642
+ * @param string $contents Haystack.
643
+ * @param callable $clb `function(string $fullMatch, array $attrs, string $inner): string`
644
+ * @return string `$contents` with every match replaced.
645
+ * @see STR::replaceTags()
646
+ */
647
+ function str_replace_tags(string $tag, string $contents, callable $clb): string { return STR::replaceTags($tag, $contents, $clb); }
648
+
649
+ /**
650
+ * Parses an HTML attribute string into an associative array (boolean
651
+ * attributes map to `true`).
652
+ *
653
+ * @param string $attributes e.g. `href="/x" target="_blank" hidden`
654
+ * @return array<string,string|true>
655
+ * @see STR::parseHtmlAttributes()
656
+ */
657
+ function str_parse_html_attributes(string $attributes): array { return STR::parseHtmlAttributes($attributes); }
658
+
659
+ /**
660
+ * Removes the common leading whitespace shared by every non-blank line.
661
+ *
662
+ * @param string $str Indented text.
663
+ * @return string De-indented text.
664
+ * @see STR::trimIndent()
665
+ */
666
+ function str_trim_indent(string $str): string { return STR::trimIndent($str); }
667
+
668
+ /**
669
+ * Tells whether a string is an URL with a recognized scheme
670
+ * (http, https, ftp, ftps, ssh, ssl, sftp, itunes).
671
+ *
672
+ * @param string $str Candidate string.
673
+ * @return bool
674
+ * @see STR::is_url()
675
+ */
676
+ function str_is_url(string $str): bool { return STR::is_url($str); }
677
+
678
+ /**
679
+ * Trims, then decodes HTML entities (`ENT_QUOTES`, UTF-8).
680
+ *
681
+ * @param string $str String possibly containing entities.
682
+ * @return string Decoded string.
683
+ * @see STR::html_entities_decode()
684
+ */
685
+ function str_html_entities_decode(string $str): string { return STR::html_entities_decode($str); }
686
+
687
+ /**
688
+ * Short, stable digest of a string: the first 12 hex chars of its SHA-256.
689
+ *
690
+ * @param string $str Input.
691
+ * @return string 12-character hex string.
692
+ * @see STR::shorthash()
693
+ */
694
+ function str_shorthash(string $str): string { return STR::shorthash($str); }
695
+
696
+ /**
697
+ * Unicode-normalizes a string to NFD and strips combining marks
698
+ * (`é` → `e`, `ü` → `u`).
699
+ *
700
+ * @param string $str Input.
701
+ * @return string ASCII-folded string.
702
+ * @see STR::normalize()
703
+ */
704
+ function str_normalize(string $str): string { return STR::normalize($str); }
705
+
706
+ /**
707
+ * Turns a string into a slug: normalized, lowercased, non-alphanumerics
708
+ * replaced by `$sep`.
709
+ *
710
+ * @param string $str Input.
711
+ * @param string $sep Separator; `''` → compact id, `'-'` → hyphenated slug.
712
+ * @return string
713
+ * @see STR::slug()
714
+ */
715
+ function str_slug(string $str, string $sep = ''): string { return STR::slug($str, $sep); }
716
+
717
+
718
+ // ===========================================================================
719
+ // ARR — array helpers
720
+ // ===========================================================================
721
+
722
+ /**
723
+ * Depth-first search through a nested array/object structure; returns the
724
+ * value of the first entry keyed `$key` at any depth, or `null`.
725
+ *
726
+ * @param mixed $data Array or object to search.
727
+ * @param string $key Key to look for.
728
+ * @return mixed The matched value, or `null`.
729
+ * @see ARR::find_key()
730
+ */
731
+ function arr_find_key(mixed $data, string $key): mixed { return ARR::find_key($data, $key); }
732
+
733
+
734
+ // ===========================================================================
735
+ // CURL — network (only usable when kirigami.yaml `network: true`)
736
+ // ===========================================================================
737
+
738
+ /**
739
+ * Checks whether an URL is reachable (HEAD request, follows redirects).
740
+ *
741
+ * @param string $url URL to probe.
742
+ * @param string|null $mimereg Reserved (currently unused).
743
+ * @return bool `true` on a 2xx/3xx response.
744
+ * @see CURL::urlExists()
745
+ */
746
+ function curl_url_exists(string $url, $mimereg = null) { return CURL::urlExists($url, $mimereg); }
747
+
748
+ /**
749
+ * Returns `curl_getinfo()` for an URL (HEAD request).
750
+ *
751
+ * @param string $url URL to probe.
752
+ * @return array|false The info array, or false for a non-URL.
753
+ * @see CURL::getInfo()
754
+ */
755
+ function curl_get_info(string $url) { return CURL::getInfo($url); }
756
+
757
+ /**
758
+ * Fetches an URL, browser-like headers and a shared cookie jar included.
759
+ * A drop-in replacement for `file_get_contents()` on remote URLs.
760
+ *
761
+ * @param string $file URL to fetch.
762
+ * @param string|null $dest Optional local path to stream the body into;
763
+ * when set, the function returns a bool instead
764
+ * of the body.
765
+ * @param callable|null $clb Optional progress callback `function(float $ratio): void`
766
+ * (`$ratio` between 0 and 1).
767
+ * @return string|bool Response body, or (with `$dest`) `true`/`false`.
768
+ * @see CURL::getContents()
769
+ */
770
+ function curl_get_contents(string $file, $dest = null, $clb = null) { return CURL::getContents($file, $dest, $clb); }
771
+
772
+
773
+ // ===========================================================================
774
+ // SCRAPER — page metadata (CACHE-backed)
775
+ // ===========================================================================
776
+
777
+ /**
778
+ * Scrapes an URL for its social/meta card.
779
+ *
780
+ * @param string $url Page URL.
781
+ * @return object|false `stdClass` with `->title ->description ->image
782
+ * ->label ->url ->key`, or false when the page has
783
+ * no usable title.
784
+ * @throws \Exception When the page cannot be crawled.
785
+ * @see SCRAPER::get()
786
+ */
787
+ function scraper_get(string $url) { return SCRAPER::get($url); }
788
+
789
+
790
+ // ===========================================================================
791
+ // OBF — reversible payload obfuscation
792
+ // ===========================================================================
793
+
794
+ /**
795
+ * Obfuscates a value: JSON → base64 → ROT-13 → gzip (2-byte header dropped).
796
+ * Not encryption — just makes an embedded payload non-obvious.
797
+ *
798
+ * @param mixed $obj Any JSON-serializable value.
799
+ * @return string Obfuscated blob.
800
+ * @see OBF::encode()
801
+ */
802
+ function obf_encode(mixed $obj): string { return OBF::encode($obj); }
803
+
804
+ /**
805
+ * Reverses {@see obf_encode()}.
806
+ *
807
+ * @param string $str Blob produced by `obf_encode()`.
808
+ * @return mixed The original value.
809
+ * @see OBF::decode()
810
+ */
811
+ function obf_decode(string $str): mixed { return OBF::decode($str); }
812
+
813
+
814
+ // ===========================================================================
815
+ // STD — build-result output (these terminate the process)
816
+ // ===========================================================================
817
+
818
+ /**
819
+ * Writes a success result (exported files + `$props`) as JSON and exits 0.
820
+ *
821
+ * @param array<string,mixed>|string $props Extra fields, or a plain message
822
+ * string (stored as `message`).
823
+ * @return never
824
+ * @see STD::succeed()
825
+ */
826
+ function std_succeed($props = []): never { STD::succeed($props); exit(0); }
827
+
828
+ /**
829
+ * Writes an error result as JSON and exits 1.
830
+ *
831
+ * @param array<string,mixed>|string $props Extra fields, or a plain message
832
+ * string (stored as `error`).
833
+ * @return never
834
+ * @see STD::error()
835
+ */
836
+ function std_error($props = []): never { STD::error($props); exit(1); }