# Implementation prompt — Templating module > Hand this whole file to a Claude Sonnet model as its task. It is written to be > self-contained but assumes the model can read the repository. --- ## Mission Implement the templating engine specified in `TEMPLATING.md` as a **new, self-contained module** in this WordPress plugin (Reservair). Today the plugin has two ad-hoc, regex-based templaters; your job is to replace them with one properly-designed module and wire the existing call sites into it. Build the smallest thing that satisfies the spec. `TEMPLATING.md` is explicit that the language must stay simple ("Helm being the example of what not to do") — do **not** add conditionals, loops, or expression evaluation. The only language features are `{{ jsonpath }}` interpolation and custom elements. ## Read these first (in order) 1. `TEMPLATING.md` — the requirements you are implementing. Treat it as the spec. 2. `modules/Database/README.md` and `modules/Logger/README.md` — the **module template** to imitate: a `modules//` folder, namespace `Reservair\`, a `README.md` with *Why / Usage / API reference* sections, `Rsv`-prefixed classes, snake_case method names, a module-specific exception. 3. `modules/Database/Db.php` and `modules/Logger/Logger.php` — the coding style to match (PHP 8.0 union types, typed properties, named args, `Logger` for errors, a `\RuntimeException` subclass for failures). 4. The two things you are replacing: - `includes/Services/Emails/RsvEmailTemplater.php` — `{{ word }}` → `$data[word]`. - `includes/Services/Forms/RsvFormHtmlRenderer.php::draw_success_template()` — swaps `{{ reservation_summary }}` for a client-filled placeholder. 5. `reservair.php::rsv_bootstrap()` — where runtime services and registries are wired on `plugins_loaded`. New registration hooks fire from here. 6. `includes/Events/RsvEventDispatcher.php` — the `do_action`/`add_action` hook convention used in this codebase. ## The module to create - Path: `modules/Templating/` - Namespace: `Reservair\Templating` (PSR-4 is already configured in `composer.json`: `"Reservair\\": "modules/"` — no autoload changes needed). - Must include `modules/Templating/README.md` following the Database/Logger README shape. ## Architecture (a tiny compiler) `TEMPLATING.md` describes a compiler with a swappable **frontend**. Honour that: ``` source string ──[ frontend ]──▶ internal node tree ──[ renderer ]──▶ output string RsvHtmlTemplateParser (frontend-agnostic) ``` - **Frontend** `RsvHtmlTemplateParser`: parses HTML into an internal node tree (the "internal structure" from the spec). The renderer must depend only on the node tree, never on HTML — so a future Markdown/other frontend can be dropped in by producing the same tree. Define the parser behind a small interface (e.g. `RsvTemplateParser` with `parse(string $source): RsvTemplateNode`) so the engine takes a frontend by constructor injection and defaults to the HTML one. - **Internal node tree**: a minimal set of node types is enough — e.g. a text node, an interpolation node (holds a JSON Path), and a custom-element node (holds tag name, attribute map, children). Keep it small and explicit. - **Renderer/evaluator**: walks the tree with (a) the submitted-data symbol table and (b) the custom-element registry, and concatenates strings. You may use PHP's `DOMDocument` to do the actual HTML tokenizing inside the frontend, but convert its output into your own node tree — do not leak DOM objects into the renderer. Watch the usual `DOMDocument::loadHTML` pitfalls (wrapping `/`, entity handling, UTF-8); strip the synthetic wrappers. ## Language semantics ### `{{ jsonpath }}` interpolation - Resolves a JSON Path (RFC 9535) expression against the submitted data array and substitutes the **atomic value** (string/number/bool) as text. Non-scalar results (objects/arrays) resolve to empty string. - Keep the resolver to the minimal subset the use cases need: child access by dot and by bracket, and array index — e.g. `name`, `$.name`, `$.guest.email`, `$.items[0]`. Do **not** implement wildcards, filters, slices, or recursion. - **Use a simple token-walk implementation**, not a real JSON Path parser. Split the path on `.`/`[`/`]` and walk the data array one key at a time: ```php function resolve(string $path, array $data): mixed { $tokens = preg_split('/[\.\[\]]+/', $path, -1, PREG_SPLIT_NO_EMPTY); $current = $data; foreach ($tokens as $token) { if (!isset($current[$token])) return null; $current = $current[$token]; } return $current; } ``` This already handles bare keys, dotted access, and `[index]` in one pass. Make two small additions so the `$`-rooted and quoted-bracket forms resolve: drop a leading `$` token, and strip surrounding `'`/`"` from bracket keys (so `$['guest']` works). A `null` return means "no such path" — render it as the empty string. - **Backward compatibility (required):** existing templates use a bare key, `{{ reservation_summary }}` and email bodies like `{{ name }}`. A bare identifier must resolve as a top-level key, identically to today. - **Security:** the output target is HTML, so HTML-escape interpolated scalar values by default (`esc_html`). This lets you delete the manual `RsvEmailListener::escape_values()` pre-escaping once the engine owns escaping — do that. Custom elements (below) are trusted and return their own markup. ### Custom elements - A custom element is a registered handler keyed by tag name. The renderer, on meeting ``, builds a **symbol table** and asks the handler to render a string. - Per the spec: the symbol table contains the submitted-data symbols **plus** one symbol per attribute (`` adds `attr` => `"test"`). Decide and document how children/inner content are exposed (a captured inner string is the simplest; do the simplest thing that the reservation-summary case needs). - Define a small interface, e.g.: ```php interface RsvTemplateElement { /** Render this element to a string given its symbol table. */ public function render(RsvTemplateSymbols $symbols): string; /** Symbols/attributes this element understands — used by validation. */ public function symbols(): array; } ``` (Adjust names to fit, but keep "has a symbol table, outputs a string".) ### Registration hook - The module fires `do_action('rsv-template-register-custom-elements', $registry)` so the plugin and any extension can register elements in the handler. Register the core element(s) the same way the plugin registers everything else (from a callback wired in `rsv_bootstrap()`), not by hard-coding them inside the module. ### Validation - Provide a `validate(string $source): array` (or a small result object) that returns the problems in a template without rendering it: unknown/invalid JSON Path symbols and unregistered custom elements. The spec calls this out as a first-class capability ("The language can be easily validated for symbols existence and validity"). ## Public API (the facade) Expose one obvious entry point — e.g. `RsvTemplateEngine` (or `RsvTemplate`): ```php $engine = new RsvTemplateEngine(); // defaults to the HTML frontend + registry $html = $engine->render($source, $data); // data = submitted values array $errors = $engine->validate($source); // [] when valid ``` Keep method names snake_case to match the other modules. Throw a module exception (e.g. `RsvTemplateException extends \RuntimeException`) for malformed templates, and log via `Reservair\Logger\Logger` where the existing modules do. ## Integration (wire the new module into the live call sites) 1. **Email bodies** — replace `RsvEmailTemplater` with the engine. `RsvEmailListener` currently calls `(new RsvEmailTemplater())->render(...)` and pre-escapes values; switch it to the engine and drop the manual escaping (the engine escapes interpolations). Keep `RsvEmailTemplater` only if something else needs it; otherwise remove it and update references. 2. **Form success message** — replace the regex in `RsvFormHtmlRenderer::draw_success_template()` with the engine. **Critical caveat:** `reservation_summary` in the success message is **not** server data — it is the visitor's live selection, filled in the browser by `assets/js/forms/RsvFormSender.js`, which looks for the `
` placeholder. So model `reservation-summary` as a registered custom element that, here, emits that exact placeholder div, so the existing client JS keeps working unchanged. Do not try to compute the summary server-side. The admin HTML is still sanitized with `wp_kses_post` before/around templating, as today. 3. **Bootstrap** — in `rsv_bootstrap()`, after the form registry block, fire the custom-element registration (or instantiate the engine's registry and register core elements) consistent with how the form element registry is built there. ## Conventions & constraints (do not skip) - **Match module style**, not `includes/` style: `Rsv` class prefix, snake_case methods, typed properties, PHP 8.0 features. Files namespaced `Reservair\Templating`. - **Doc comments state intent, not mechanics** — say *what/why*, never narrate the loop. (This is a standing rule in this repo.) - **No new Composer dependencies** unless genuinely unavoidable; the JSON Path subset and HTML parsing are small enough to implement in-module. If you believe a dependency is warranted, stop and justify it instead of adding it silently. - **No database, no migrations** — this module is pure text transformation. - **Security:** never emit unescaped submitted data; escape interpolations, sanitize admin HTML with `wp_kses_post` at the boundary as today, and treat custom-element output as trusted-by-registration. - Ship `modules/Templating/README.md` (Why / Usage / API reference / Notes), and delete or update `TEMPLATING.md`-superseded code so there is exactly one templating path. ## Definition of done - [ ] `modules/Templating/` exists with the engine, HTML frontend, node tree, custom-element registry + interface, JSON Path resolver, validation, a module exception, and `README.md`. - [ ] `rsv-template-register-custom-elements` action is fired by the module and consumed from `rsv_bootstrap()`; `reservation-summary` is registered there. - [ ] Email bodies and form success messages render through the engine; existing templates (`{{ name }}`, `{{ reservation_summary }}`) behave exactly as before, including client-side summary fill. - [ ] `RsvEmailTemplater` and the success-message regex are gone (or justified). - [ ] `composer lint` (phpcs, phpstan, psalm — see `composer.json` scripts) passes clean on the new and changed files. - [ ] README includes at least one worked example for each use case (email, success message) and a custom-element registration example. If anything in `TEMPLATING.md` is ambiguous, prefer the **simpler** reading and note the decision in the README rather than expanding the language.