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)
TEMPLATING.md— the requirements you are implementing. Treat it as the spec.modules/Database/README.mdandmodules/Logger/README.md— the module template to imitate: amodules/<Name>/folder, namespaceReservair\<Name>, aREADME.mdwith Why / Usage / API reference sections,Rsv-prefixed classes, snake_case method names, a module-specific exception.modules/Database/Db.phpandmodules/Logger/Logger.php— the coding style to match (PHP 8.0 union types, typed properties, named args,Loggerfor errors, a\RuntimeExceptionsubclass for failures).- 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.
reservair.php::rsv_bootstrap()— where runtime services and registries are wired onplugins_loaded. New registration hooks fire from here.includes/Events/RsvEventDispatcher.php— thedo_action/add_actionhook convention used in this codebase.
The module to create
- Path:
modules/Templating/ - Namespace:
Reservair\Templating(PSR-4 is already configured incomposer.json:"Reservair\\": "modules/"— no autoload changes needed). - Must include
modules/Templating/README.mdfollowing 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.RsvTemplateParserwithparse(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:This already handles bare keys, dotted access, andfunction 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; }[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). Anullreturn 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 manualRsvEmailListener::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">addsattr=>"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.:
(Adjust names to fit, but keep "has a symbol table, outputs a string".)
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; }
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 inrsv_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)
- Email bodies — replace
RsvEmailTemplaterwith the engine.RsvEmailListenercurrently calls(new RsvEmailTemplater())->render(...)and pre-escapes values; switch it to the engine and drop the manual escaping (the engine escapes interpolations). KeepRsvEmailTemplateronly if something else needs it; otherwise remove it and update references. - Form success message — replace the regex in
RsvFormHtmlRenderer::draw_success_template()with the engine. Critical caveat:reservation_summaryin the success message is not server data — it is the visitor's live selection, filled in the browser byassets/js/forms/RsvFormSender.js, which looks for the<div class="rsv-success-summary">placeholder. So modelreservation-summaryas 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 withwp_kses_postbefore/around templating, as today. - 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:Rsvclass prefix, snake_case methods, typed properties, PHP 8.0 features. Files namespacedReservair\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_postat 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 updateTEMPLATING.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, andREADME.md.rsv-template-register-custom-elementsaction is fired by the module and consumed fromrsv_bootstrap();reservation-summaryis 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. RsvEmailTemplaterand the success-message regex are gone (or justified).composer lint(phpcs, phpstan, psalm — seecomposer.jsonscripts) 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.