@kirigami/php-prepros 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Maxime Larrivée-Roy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,622 @@
1
+ # @kirigami/php-prepros
2
+
3
+ > PHP preprocessor for the **Kirigami** static site generator.
4
+
5
+ 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.
6
+
7
+ Part of the **Kirigami** project ecosystem. Other packages are coming soon.
8
+
9
+ [![npm version](https://img.shields.io/npm/v/@kirigami/php-prepros)](https://www.npmjs.com/package/@kirigami/php-wasm)
10
+ [![License: MIT](https://img.shields.io/badge/MIT-blue)](./LICENSE)
11
+ [![Node.js >=20.10.0](https://img.shields.io/badge/node-%3E%3D20.10.0-brightgreen)](https://nodejs.org)
12
+
13
+ ---
14
+
15
+ ## Table of contents
16
+
17
+ - [@kirigami/php-prepros](#kirigamiphp-prepros)
18
+ - [Table of contents](#table-of-contents)
19
+ - [How it works](#how-it-works)
20
+ - [Installation](#installation)
21
+ - [Configuration — `kirigami.yaml`](#configuration--kirigamiyaml)
22
+ - [Writing pages](#writing-pages)
23
+ - [PHPDOC header](#phpdoc-header)
24
+ - [Auto-loading data files](#auto-loading-data-files)
25
+ - [JavaScript API](#javascript-api)
26
+ - [`render(file?)`](#renderfile)
27
+ - [`sitemap()`](#sitemap)
28
+ - [PHP classes reference](#php-classes-reference)
29
+ - [PREPROS](#prepros)
30
+ - [`PREPROS::render(string $file)`](#preprosrenderstring-file)
31
+ - [`PREPROS::sitemap()`](#preprossitemap)
32
+ - [`PREPROS::exportFile(string $file)`](#preprosexportfilestring-file)
33
+ - [MD](#md)
34
+ - [Plugin API](#plugin-api)
35
+ - [HTML](#html)
36
+ - [YAML](#yaml)
37
+ - [CACHE](#cache)
38
+ - [IMG](#img)
39
+ - [FS](#fs)
40
+ - [STR](#str)
41
+ - [OBF](#obf)
42
+ - [STD](#std)
43
+ - [Plugin system](#plugin-system)
44
+ - [PREPROS tags](#prepros-tags)
45
+ - [PREPROS hooks](#prepros-hooks)
46
+ - [MD plugins](#md-plugins)
47
+ - [Built-in plugins](#built-in-plugins)
48
+ - [`{% callout type ["Title"] content %}`](#-callout-type-title-content-)
49
+ - [Extending the `<markdown>` tag](#extending-the-markdown-tag)
50
+ - [License](#license)
51
+
52
+ ---
53
+
54
+ ## How it works
55
+
56
+ `@kirigami/php-prepros` runs your PHP source files inside a **WebAssembly PHP 8.x runtime** ([`@kirigami/php-wasm`](https://github.com/kirigami/php-wasm)), entirely in Node.js — no PHP installation required on the host machine.
57
+
58
+ The lifecycle of a page build looks like this:
59
+
60
+ ```
61
+ _index.php ──▶ PHP (wasm) ──▶ processTags() ──▶ HTML::format() ──▶ index.html
62
+ │
63
+ ├── before.php (optional layout header)
64
+ ├── after.php (optional layout footer)
65
+ └── PHPDOC annotations resolved (yaml / json / md / url)
66
+ ```
67
+
68
+ 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.
69
+
70
+ ---
71
+
72
+ ## Installation
73
+
74
+ ```bash
75
+ npm install @kirigami/php-prepros
76
+ ```
77
+
78
+ > **Node.js ≥ 20** is required (ESM-only package).
79
+
80
+ ---
81
+
82
+ ## Configuration — `kirigami.yaml`
83
+
84
+ Every project **must** have a `kirigami.yaml` at its root. The preprocessor reads it at startup and throws if it is absent or invalid.
85
+
86
+ ```yaml
87
+ kirigami:
88
+ root: src/ # Required. Source directory containing your _*.php pages.
89
+ baseurl: https://example.com # Used by sitemap generation.
90
+ sitename: My Website # Arbitrary key/value pairs injected as PHP variables.
91
+ author: Jane Doe
92
+
93
+ prepros:
94
+ before: _layout/header.php # Included before every page body.
95
+ after: _layout/footer.php # Included after every page body.
96
+ format: true # Pretty-print the HTML output (default: false).
97
+ network: false # Allow HTTP fetches in PHPDOC @tag annotations.
98
+ mountext: # Extra file extensions to mount into the wasm fs.
99
+ - .svg
100
+ - .txt
101
+ includes: # PHP files auto-included before page rendering.
102
+ - _lib/helpers.php
103
+ ```
104
+
105
+ 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.
106
+
107
+ ---
108
+
109
+ ## Writing pages
110
+
111
+ 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.
112
+
113
+ ```
114
+ src/
115
+ ├── _index.php → src/index.html
116
+ ├── about/
117
+ │ └── _index.php → src/about/index.html
118
+ └── blog/
119
+ ├── _index.php → src/blog/index.html
120
+ └── _articles.yaml (data file, not compiled)
121
+ ```
122
+
123
+ Directories whose name starts with `_` (e.g. `_layout/`, `_lib/`) are skipped entirely during directory-wide builds.
124
+
125
+ ### PHPDOC header
126
+
127
+ Every page starts with a PHP docblock that drives metadata and data loading:
128
+
129
+ ```php
130
+ <?php
131
+ /**
132
+ * @name about
133
+ * @title About us
134
+ * @abstract A short description of this page.
135
+ */
136
+ ?>
137
+ <section>
138
+ <h1><?php echo $title; ?></h1>
139
+ <p><?php echo $abstract; ?></p>
140
+ </section>
141
+ ```
142
+
143
+ All annotations are injected as PHP variables (`$name`, `$title`, `$abstract`, …). You can define any custom annotation you need.
144
+
145
+ Anotations are also avaiables as variables in `before` and `header` php included files so you can write proper metas in the HTML header.
146
+
147
+ ### Auto-loading data files
148
+
149
+ 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.
150
+
151
+ ```php
152
+ <?php
153
+ /**
154
+ * @name medias
155
+ * @articles _articles.yaml
156
+ */
157
+ ?>
158
+ <?php foreach ($articles as $article): ?>
159
+ <a href="<?php echo $article->lien; ?>">
160
+ <?php echo $article->titre; ?>
161
+ </a>
162
+ <?php endforeach; ?>
163
+ ```
164
+
165
+ | Extension | Parsed as |
166
+ |-----------|-----------|
167
+ | `.yaml` / `.yml` | `stdClass` object (or array of objects for sequences) |
168
+ | `.json` | Result of `json_decode()` |
169
+ | `.md` | HTML string via `MD::toHtml()` |
170
+
171
+ 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:
172
+
173
+ ```php
174
+ /**
175
+ * @posts https://api.example.com/posts.json
176
+ */
177
+ ```
178
+
179
+ ---
180
+
181
+ ## JavaScript API
182
+
183
+ ```js
184
+ import { render, sitemap } from '@kirigami/php-prepros';
185
+ ```
186
+
187
+ ### `render(file?)`
188
+
189
+ Compile a single PHP page or a whole directory.
190
+
191
+ ```js
192
+ // Compile one page
193
+ const result = await render('about/_index.php');
194
+
195
+ // Compile everything under src/
196
+ const result = await render('.');
197
+
198
+ // Compile everything (uses kirigami.root from config)
199
+ const result = await render();
200
+ ```
201
+ > Path use by `render()` are all relative to `kirigami.root` configuration.
202
+
203
+
204
+ **Returns** `Promise<PreprosResult>`:
205
+
206
+ ```ts
207
+ interface PreprosResult {
208
+ success: boolean;
209
+ files: string[]; // relative paths of every file written
210
+ error?: string; // present only on failure
211
+ }
212
+ ```
213
+
214
+ ### `sitemap()`
215
+
216
+ Generate `sitemap.xml` at the source root.
217
+
218
+ ```js
219
+ const result = await sitemap();
220
+ // result.files === ['src/sitemap.xml']
221
+ ```
222
+
223
+ ---
224
+
225
+ ## PHP classes reference
226
+
227
+ All classes are autoloaded — no manual `require` needed inside your page files.
228
+
229
+ ---
230
+
231
+ ### PREPROS
232
+
233
+ The core engine. Manages the rendering pipeline, tag processing, hooks, and file export.
234
+
235
+ ```php
236
+ // Available inside page templates and included files.
237
+ PREPROS::$config // stdClass — full resolved config (prepros section of kirigami.yaml)
238
+ PREPROS::registerTag(string $tag, callable $callback)
239
+ PREPROS::registerHook(string $hook, callable $callback)
240
+ PREPROS::exportFile(string $absolutePath)
241
+ PREPROS::getExportedFiles(): string[]
242
+ ```
243
+
244
+ #### `PREPROS::render(string $file)`
245
+
246
+ Internal method called once per source file. Orchestrates the full pipeline:
247
+
248
+ 1. Resolves PHPDOC metadata and auto-loads data files.
249
+ 2. Fires the `pre_render` hook with the raw source contents.
250
+ 3. Includes `before.php`, the page body, and `after.php` into a single string.
251
+ 4. Processes all registered custom HTML tags.
252
+ 5. Fires the `post_render` hook on the assembled HTML.
253
+ 6. Optionally pretty-prints via `HTML::format()` (when `format: true`).
254
+ 7. Writes the output `.html` file.
255
+
256
+ #### `PREPROS::sitemap()`
257
+
258
+ Scans the source tree for `_index.php` files and generates a standards-compliant `sitemap.xml` (Sitemaps 0.9).
259
+
260
+ #### `PREPROS::exportFile(string $file)`
261
+
262
+ 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.
263
+
264
+ ---
265
+
266
+ ### MD
267
+
268
+ Markdown-to-HTML converter with a plugin system for custom shortcodes.
269
+
270
+ ```php
271
+ $html = MD::toHtml(string $markdown): string;
272
+ ```
273
+
274
+ Supports the full GitHub Flavored Markdown subset:
275
+
276
+ - ATX headings (`#` through `######`) with auto-generated `id` attributes
277
+ - Ordered and unordered lists, including nested
278
+ - GFM task lists (`- [ ]` / `- [x]`)
279
+ - GFM tables with column alignment
280
+ - GFM alerts (`> [!NOTE]`, `> [!WARNING]`, etc.)
281
+ - Blockquotes (recursive)
282
+ - Fenced code blocks with language class
283
+ - Inline code
284
+ - Bold, italic, bold+italic, strikethrough
285
+ - Links with automatic `target="_blank" rel="noopener noreferrer"` for external URLs
286
+ - Images with `loading="lazy"`
287
+ - Auto-linked bare URLs
288
+ - Horizontal rules
289
+ - Hard line breaks (trailing double space → `<br>`)
290
+
291
+ #### Plugin API
292
+
293
+ Extend Markdown with custom shortcode tags:
294
+
295
+ ```php
296
+ // Inline tag {% tagname arg1 "arg with spaces" %}
297
+ // Block tag {% tagname arg1
298
+ // body content
299
+ // %}
300
+
301
+ MD::registerPlugin(string $name, callable $callback): void
302
+ MD::unregisterPlugin(string $name): void
303
+ MD::getRegisteredPlugins(): string[]
304
+ ```
305
+
306
+ The callback always receives `(array $args, string $body)`:
307
+
308
+ ```php
309
+ MD::registerPlugin('video', function (array $args, string $body): string {
310
+ $src = htmlspecialchars($args[0] ?? '', ENT_QUOTES, 'UTF-8');
311
+ return "<video src=\"{$src}\" controls></video>";
312
+ });
313
+ ```
314
+
315
+ Then in any Markdown content (including inside `<markdown>` tags):
316
+
317
+ ```
318
+ {% video /videos/intro.mp4 %}
319
+ ```
320
+
321
+ ---
322
+
323
+ ### HTML
324
+
325
+ Pretty-printer for the final HTML output. Used automatically when `format: true` is set in the config.
326
+
327
+ ```php
328
+ $formatted = HTML::format(string $html): string;
329
+ ```
330
+
331
+ 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.
332
+
333
+ ---
334
+
335
+ ### YAML
336
+
337
+ A lightweight, zero-dependency YAML parser. Covers the full subset used in static site projects.
338
+
339
+ ```php
340
+ $data = YAML::parse(string $yaml, bool $assoc = false): mixed;
341
+ $data = YAML::parseFile(string $path, bool $assoc = false): mixed;
342
+ ```
343
+
344
+ Supported features:
345
+
346
+ - Scalars: strings (quoted and unquoted), integers, floats, booleans, null
347
+ - Single and double quoted strings with escape sequences
348
+ - Literal block scalars (`|`, `|-`, `|+`)
349
+ - Folded block scalars (`>`, `>-`, `>+`)
350
+ - Plain scalars spanning multiple lines
351
+ - Nested mappings and sequences
352
+ - Inline collections (`[a, b]` and `{k: v}`)
353
+ - Comments (`#`)
354
+ - Multiple documents separated by `---`
355
+
356
+ By default, YAML mappings are returned as `stdClass` objects. Pass `true` as the second argument to get associative arrays instead.
357
+
358
+ ---
359
+
360
+ ### CACHE
361
+
362
+ Persistent SQLite-backed key-value cache. Survives across incremental builds via `.cache.db` at the project root.
363
+
364
+ ```php
365
+ CACHE::get(string $key): mixed
366
+ CACHE::set(string $key, mixed $val, int $ttl = 0): bool
367
+ CACHE::delete(string $key): bool
368
+ CACHE::purge(): bool // removes expired entries
369
+ ```
370
+
371
+ 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.
372
+
373
+ ```php
374
+ $data = CACHE::get('my-remote-data');
375
+ if ($data === null) {
376
+ $data = json_decode(file_get_contents('https://api.example.com/data.json'));
377
+ CACHE::set('my-remote-data', $data, 3600); // cache for 1 hour
378
+ }
379
+ ```
380
+
381
+ ---
382
+
383
+ ### IMG
384
+
385
+ Image manipulation helper built on PHP GD. Supports JPEG, PNG, GIF, and WebP.
386
+
387
+ ```php
388
+ $img = new IMG(string $file);
389
+
390
+ // Properties
391
+ $img->width // int
392
+ $img->height // int
393
+
394
+ // Methods (chainable)
395
+ $img->resize(int $width, int $height = 0, bool $cover = false): self
396
+ $img->save(string $dest): self
397
+ ```
398
+
399
+ `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.
400
+
401
+ `save()` infers the output format from the file extension (`.jpg`, `.jpeg`, `.png`, `.gif`, `.webp`). The saved file is automatically registered via `PREPROS::exportFile()`.
402
+
403
+ ```php
404
+ (new IMG('/project/src/images/hero.jpg'))
405
+ ->resize(1200, 630, true)
406
+ ->save('/project/src/images/hero-og.jpg');
407
+ ```
408
+
409
+ ---
410
+
411
+ ### FS
412
+
413
+ Filesystem utilities.
414
+
415
+ ```php
416
+ FS::dig(string $glob): iterable // recursive glob, yields file paths
417
+ FS::getRelativePath(string $from, string $to): string
418
+ FS::phpFileInfo(string $file): object|false // parse PHPDOC annotations
419
+ FS::rmdir(string $dir, bool $removeSelf = true): bool
420
+ FS::pathJoin(string ...$parts): string // URL-aware path join with .. resolution
421
+ ```
422
+
423
+ `FS::dig()` is the workhorse of directory-wide builds — it recursively walks a glob pattern and yields every matching file path.
424
+
425
+ `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.
426
+
427
+ ---
428
+
429
+ ### STR
430
+
431
+ String utilities used internally by the tag-processing pipeline.
432
+
433
+ ```php
434
+ STR::htmlesc(string $str): string
435
+ STR::replaceTags(string $tag, string $html, callable $callback): string
436
+ STR::parseHtmlAttributes(string $attrString): array
437
+ STR::trimIndent(string $str): string
438
+ ```
439
+
440
+ `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)`.
441
+
442
+ `STR::trimIndent()` strips the common leading whitespace from a multi-line string — handy when pulling content out of indented `<markdown>` blocks.
443
+
444
+ ---
445
+
446
+ ### OBF
447
+
448
+ Simple reversible obfuscation for values you want to embed in HTML without making them trivially readable (e.g., contact data, API tokens in templates).
449
+
450
+ ```php
451
+ $encoded = OBF::encode(mixed $obj): string;
452
+ $decoded = OBF::decode(string $str): mixed;
453
+ ```
454
+
455
+ Applies JSON encoding → base64 → ROT-13 → gzip. Not cryptographically secure; intended for light obfuscation only.
456
+
457
+ ---
458
+
459
+ ### STD
460
+
461
+ Output helpers used by the PHP runtime to communicate back to Node.js over stdout/stderr.
462
+
463
+ ```php
464
+ STD::succeed(array|string $props = []): void // exits 0, writes JSON to stdout
465
+ STD::error(array|string $props = []): void // exits 1, writes JSON to stderr
466
+ ```
467
+
468
+ These are internal to the build runner. You generally do not need to call them in page templates.
469
+
470
+ ---
471
+
472
+ ## Plugin system
473
+
474
+ `@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).
475
+
476
+ ---
477
+
478
+ ### PREPROS tags
479
+
480
+ Register a custom HTML tag that is processed **after** PHP execution, on the fully assembled HTML string:
481
+
482
+ ```php
483
+ // In a file listed under prepros.includes in kirigami.yaml, or in before.php:
484
+
485
+ PREPROS::registerTag('gallery', function (string $fullTag, array $attrs, string $body): string {
486
+ $id = $attrs['id'] ?? '';
487
+ $imgs = glob("/project/src/images/gallery/{$id}/*.webp");
488
+ $html = '<div class="gallery">';
489
+ foreach ($imgs as $img) {
490
+ $src = str_replace('/project/src', '', $img);
491
+ $html .= "<img src=\"{$src}\" loading=\"lazy\">";
492
+ }
493
+ return $html . '</div>';
494
+ });
495
+ ```
496
+
497
+ Then in any page template:
498
+
499
+ ```html
500
+ <gallery id="summer-2025"></gallery>
501
+ ```
502
+
503
+ The callback receives:
504
+
505
+ | Parameter | Type | Description |
506
+ |-----------|------|-------------|
507
+ | `$fullTag` | `string` | The complete matched tag string |
508
+ | `$attrs` | `array` | Parsed HTML attributes as an associative array |
509
+ | `$body` | `string` | Inner content between opening and closing tags |
510
+
511
+ The built-in `<markdown>` tag is registered this way (see below).
512
+
513
+ ---
514
+
515
+ ### PREPROS hooks
516
+
517
+ Hooks let you intercept and transform data at key points in the rendering pipeline:
518
+
519
+ ```php
520
+ PREPROS::registerHook(string $hookName, callable $callback): void
521
+ ```
522
+
523
+ | Hook | When it fires | `$data` type | Expected return |
524
+ |------|---------------|--------------|-----------------|
525
+ | `page_info` | After PHPDOC parsing, before rendering | `[$filePath, $pageObject]` | `$pageObject` (modified) |
526
+ | `pre_render` | Before PHP execution | Raw file contents as `string` | `string` |
527
+ | `post_render` | After tag processing, before `HTML::format()` | Assembled HTML `string` | `string` |
528
+
529
+ Multiple callbacks can be registered for the same hook — they are executed in registration order, each receiving the return value of the previous one.
530
+
531
+ ```php
532
+ // Example: inject a last-modified date into every page
533
+ PREPROS::registerHook('post_render', function (string $html): string {
534
+ $date = date('Y-m-d');
535
+ return str_replace('{{build_date}}', $date, $html);
536
+ });
537
+ ```
538
+
539
+ ---
540
+
541
+ ### MD plugins
542
+
543
+ MD plugins add custom shortcode tags inside Markdown content. They work inside `<markdown>` blocks, in `.md` data files, and anywhere `MD::toHtml()` is called.
544
+
545
+ **Inline syntax** (all on one line):
546
+
547
+ ```
548
+ {% tagname arg1 "argument with spaces" %}
549
+ ```
550
+
551
+ **Block syntax** (body on subsequent lines):
552
+
553
+ ```
554
+ {% tagname optional-arg
555
+ Line one of the body.
556
+ Line two of the body.
557
+ %}
558
+ ```
559
+
560
+ ```php
561
+ MD::registerPlugin(string $name, callable $callback): void
562
+ ```
563
+
564
+ 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).
565
+
566
+ ---
567
+
568
+ ### Built-in plugins
569
+
570
+ The following MD plugins are registered out of the box in `md.plugins.php`:
571
+
572
+ #### `{% callout type ["Title"] content %}`
573
+
574
+ Renders a styled callout block. `type` is one of `info`, `success`, `warning`, `danger`.
575
+
576
+ ```
577
+ {% callout warning "Heads up" This section is outdated. %}
578
+
579
+ {% callout danger "Critical"
580
+ Line one of a longer warning.
581
+
582
+ Line two after a blank line.
583
+ %}
584
+ ```
585
+
586
+ ---
587
+
588
+ ## Extending the `<markdown>` tag
589
+
590
+ 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:
591
+
592
+ ```html
593
+ <section class="about">
594
+ <div>
595
+ <markdown>
596
+ ## Who we are
597
+
598
+ We are a **student organization** from Québec.
599
+
600
+ {% youtube dQw4w9WgXcQ %}
601
+ </markdown>
602
+ </div>
603
+ </section>
604
+ ```
605
+
606
+ 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:
607
+
608
+ ```php
609
+ PREPROS::registerTag('markdown', function (string $tag, array $attrs, string $body): string {
610
+ $body = STR::trimIndent($body);
611
+ $html = MD::toHtml($body);
612
+ // wrap in a container, add a class, etc.
613
+ $class = $attrs['class'] ?? 'prose';
614
+ return "<div class=\"{$class}\">{$html}</div>";
615
+ });
616
+ ```
617
+
618
+ ---
619
+
620
+ ## License
621
+
622
+ MIT © Maxime Larrivée-Roy, 2026