xPulse
πŸ‡¬πŸ‡§ EN

@xpulse/template – Component Spec

Status: CONCEPT Β· Created March 2026 Parent: GLOBAL_concept-ecosystem.md, GLOBAL_concept-web.md


Overview

Template engine for the xPulse ecosystem. Renders .tpl.html files into finished HTML.

One responsibility: render templates – nothing more.

Has no knowledge of HTTP, controllers, or routing. Accepts a template path and a data object – returns HTML.


Dependencies

@xpulse/template
└── @xpulse/event ← emit template:render:before/after, template:loaded etc.

File Structure in an xPart

No separate views/ folder – everything lives in src/templates/. The controller references templates directly via their path.

src/
templates/
base.tpl.html ← base layout
platform.tpl.html ← {% extends 'base' %}
index.tpl.html ← {% extends 'platform' %}
info/
index.tpl.html ← {% extends 'platform' %}
bla.tpl.html ← {% extends 'platform' %}
contact/
index.tpl.html ← {% extends 'platform' %}
partials/
card.tpl.html ← partial – no extends
nav.tpl.html ← partial – no extends
template-methods/ ← custom method registrations
custom.js

Template Syntax

Two clearly distinct syntax forms:

{{ variable }} ← output – outputs a value
{% directive %} ← logic – control flow, inheritance, includes

Template Inheritance: extends + slots

A template can extend exactly one other template – stackable to any depth. Slots are filled from the inside out.

<!-- src/templates/base.tpl.html -->
<!DOCTYPE html>
<html lang="de">
<head>
<title>{% slot 'title' %}xPulse{% endslot %}</title>
</head>
<body>
{% slot 'body' %}{% endslot %}
</body>
</html>
<!-- src/templates/platform.tpl.html -->
{% extends 'base' %}
{% slot 'title' %}{{ title }} – xPulse{% endslot %}
{% slot 'body' %}
<nav>
<a href="/" class="{% if _route.path === '/' %}active{% endif %}">Home</a>
<a href="/info" class="{% if _route.path === '/info' %}active{% endif %}">Info</a>
</nav>
<main>
{% slot 'content' %}{% endslot %}
</main>
{% endslot %}
<!-- src/templates/info/bla.tpl.html -->
{% extends 'platform' %}
{% slot 'content' %}
<h1>{{ title }}</h1>
<ul>
{% each items as item %}
<li>{{ item }}</li>
{% endeach %}
</ul>
{% endslot %}

Variables

Output

{{ title }}
{{ user.name }}
{{ _route.path }}

Setting and Overriding

<!-- Set with fallback – only if not already present -->
{% set lang = lang ?? 'de' %}
<!-- Set with a fixed value -->
{% set label = 'Willkommen' %}
<!-- Override -->
{% set title = title ?? 'Standardtitel' %}
<!-- Output -->
{{ lang }}
{{ label }}

Methods

Methods extend variables with callable transformations. Two call styles – both are equivalent:

{{ variable.methodName }} ← dot notation
{% methodName(variable) %} ← directive notation

Built-in Methods (String)

Method Example Description
upper {{ title.upper }} Uppercase
lower {{ title.lower }} Lowercase
trim {{ text.trim }} Remove whitespace
length {{ text.length }} Length
reverse {{ text.reverse }} Reverse
slice(a, b) {{ text.slice(0, 50) }} Substring
replace(a, b) {{ text.replace('foo', 'bar') }} Replace
includes(a) {{ text.includes('foo') }} Contains – returns true/false
startsWith(a) {{ text.startsWith('http') }} Starts with
endsWith(a) {{ text.endsWith('.de') }} Ends with
split(sep) {{ text.split(',') }} Split

Built-in Methods (Array)

Method Example Description
length {{ items.length }} Number of elements
first {{ items.first }} First element
last {{ items.last }} Last element
contains(v) {{ items.contains('foo') }} Contains value
join(sep) {{ items.join(', ') }} Join
reverse {{ items.reverse }} Reverse

Built-in Methods (Number)

Method Example Description
toFixed(n) {{ price.toFixed(2) }} Decimal places
round {{ score.round }} Round
floor {{ score.floor }} Round down
ceil {{ score.ceil }} Round up
abs {{ diff.abs }} Absolute value

Method Discovery

@xpulse/template discovers methods in three stages – analogous to controller discovery:

1. @xpulse/template/src/template-methods/**/*.js ← built-ins (always loaded)
2. node_modules/@xpulse/*/src/template-methods/**/*.js ← from other xPulse packages
3. src/template-methods/**/*.js ← from the app itself (overrides)

Local overrides package – with a note in the debug log.

Method Base Class

// src/template-methods/custom.js
import { Method } from '@xpulse/template';
export default class CustomMethods extends Method {
register() {
this.add('cool', (data) => data.split('').reverse().join(''));
this.add('currency', (data) => `€ ${Number(data).toFixed(2)}`);
this.add('slug', (data) => data.toLowerCase().replace(/\s+/g, '-'));
}
}

In templates:

{{ title.cool }}
{{ price.currency }}
{{ title.slug }}
{% cool(title) %}
{% currency(price) %}

Include with Optional Parameters

<!-- Simple include -->
{% include 'partials/card' %}
<!-- Include with custom parameters -->
{% include 'partials/card' { title: 'Foo', active: true } %}
<!-- Include in an iteration – each iteration gets its own params -->
{% each tools as tool %}
{% include 'partials/card' { title: tool.name, id: tool.id, type: tool.type } %}
{% endeach %}

Parameters are only available within the partial's scope – no leak into the parent scope.

<!-- src/templates/partials/card.tpl.html -->
<div class="card">
<h2>{{ title }}</h2>
<span class="type">{{ type }}</span>
</div>

Directives Overview

Directive Description
{% extends 'name' %} This template extends another
{% slot 'name' %}...{% endslot %} Define or fill a slot
{% set var = value %} Set or override a variable
{% if condition %}...{% endif %} Conditional output
{% if condition %}...{% else %}...{% endif %} Conditional output with fallback
{% each items as item %}...{% endeach %} Iterate over an array
{% each items as item, index %}...{% endeach %} Iterate with index
{% include 'path/partial' %} Include a partial
{% include 'path/partial' { key: val } %} Include a partial with custom parameters
{% methodName(variable) %} Call a custom or built-in method

API

Rendering

import template from '@xpulse/template';
const html = await template.render('info/bla', {
title: 'Bla',
items: ['a', 'b', 'c'],
});

template.render() automatically looks in src/templates/ – the path is relative to it:

template.render('info/bla')
// β†’ loads src/templates/info/bla.tpl.html

_route is not passed manually – @xpulse/controller injects it automatically via the template:render:before event (see below).


Emitted Events

Event Payload When
template:render:before { view, data } before rendering – data is still mutable
template:render:after { view, html, duration } after rendering – html is still mutable
template:loaded { path, cached } when a .tpl.html file is loaded
template:methods:discovered { count, sources } after method discovery
template:method:registered { name, source } when an individual method is registered

Middleware via Events

import event from '@xpulse/event';
// Inject global variables – always available in every template
event.on('template:render:before', ({ view, data }) => {
data.appVersion = config.get('release.current');
data.env = config.get('app.env');
});
// Post-process HTML – e.g. minify, remove comments
event.on('template:render:after', ({ view, html }) => {
// mutate html
});

Render Order

1. emit template:render:before ← data still mutable
2. load src/templates/info/bla.tpl.html
3. emit template:loaded
4. {% extends 'platform' %} β†’ load src/templates/platform.tpl.html + template:loaded
5. {% extends 'base' %} β†’ load src/templates/base.tpl.html + template:loaded
6. fill slots from the inside out
7. resolve {% set %} variables
8. embed {% include %} partials (with their own scope)
9. resolve {% if %} / {% each %} directives
10. resolve methods – {{ var.method }} + {% method(var) %}
11. replace {{ variables }}
12. emit template:render:after ← html still mutable
13. return finished HTML

Debug Integration

When @xpulse/debug is installed as a devDependency, @xpulse/template automatically provides a DataCollector. @xpulse/debug discovers it via node_modules/@xpulse/template/src/datacollectors/ – no configuration needed.

"optionalDependencies": {
"@xpulse/debug": "^1.0.0"
}

The TemplateCollector (name: 'template', icon: 'πŸ“‹') shows in the Web Profiler:

Main areas:

Sub-panels – one per rendered template:

For each template in the extends chain there is an additional sub-panel with path info and a note that duration and data are measured at the child template.

The TemplateCollector also writes a Waterfall Span per render (kind: 'template', duration from payload.duration).

Badge in the toolbar: number of renders (e.g. 3)


Package Structure

@xpulse/template/
src/
index.js ← default export: template object
loader.js ← load + cache .tpl.html files
parser.js ← parse template syntax
renderer.js ← resolve slots, variables, directives
resolver.js ← resolve extends chain
method.js ← Method base class + discovery
template-methods/
string.js ← built-in String methods
array.js ← built-in Array methods
number.js ← built-in Number methods
test/
parser.test.js
renderer.test.js
resolver.test.js
method.test.js
docs/
index.md
api.md
_meta.json
Dockerfile
Makefile
README.md
package.json
xpulse.json
en/spec.md 2026-03-27