Files
Reservair/TEMPLATING_PROMPT.md
T
Martin Slachta 7b3d9f0ece (#3) - templating
2026-06-14 07:16:13 +02:00

11 KiB

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/<Name>/ folder, namespace Reservair\<Name>, 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 <html>/<body>, 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:
    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 <some-element attr="x">…</some-element>, 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 (<el attr="test"> 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.:
    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):

$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 <div class="rsv-success-summary"> 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.