trellis 0.2.1 copy "trellis: ^0.2.1" to clipboard
trellis: ^0.2.1 copied to clipboard

Template engine for Dart, using natural HTML templates. Fragment-first for hypermedia-driven frameworks like HTMX. Inspired by Thymeleaf.

trellis #

pub package package publisher

A natural HTML template engine for Dart — templates are valid HTML that browsers render as prototypes without a server. Fragment-first design built for hypermedia-driven web frameworks like HTMX. Inspired by Thymeleaf.

Features #

  • Natural templates -- valid HTML that browsers render as prototypes without a server
  • Fragment-first -- tl:fragment + renderFragment() maps directly to HTMX partial responses
  • Full expression language -- variables, arithmetic, literal substitution, selection, URL, ternary, Elvis, comparisons, boolean
  • Switch/case, block, remove, inline -- multi-branch conditionals, virtual elements, output control, inline JS/CSS processing
  • Parameterized fragments -- tl:fragment="card(title, body)" with argument passing at inclusion
  • CSS selector targeting -- tl:insert="~{file :: #id}" and tl:insert="~{file :: .class}"
  • Sync-first API -- render() is synchronous; renderFile() is async only for I/O
  • LRU DOM cache -- configurable size, CacheStats for hit/miss metrics
  • Strict mode -- undefined variables/members throw ExpressionException
  • Secure -- FileSystemLoader enforces path traversal and symlink escape protection
  • AOT-compatible -- context is Map<String, dynamic>, no reflection

Installation #

dart pub add trellis

Quick Start #

import 'package:trellis/trellis.dart';

final engine = Trellis();

final html = engine.render(
  '<h1 tl:text="${title}">Default Title</h1>',
  {'title': 'Hello, Trellis!'},
);
// <h1>Hello, Trellis!</h1>

Template Syntax Reference #

Text Substitution #

<!-- Escaped text (safe from XSS) -->
<p tl:text="${message}">placeholder</p>

<!-- Unescaped HTML (use with trusted content only) -->
<div tl:utext="${richContent}">placeholder</div>

Conditionals #

<div tl:if="${user}">Welcome back!</div>
<div tl:unless="${loggedIn}">Please log in.</div>

Truthy: non-null, non-false, non-zero, not "false"/"off"/"no". Empty strings and empty lists are truthy.

Switch / Case

<div tl:switch="${role}">
  <p tl:case="admin">Admin view</p>
  <p tl:case="user">User view</p>
  <p tl:case="*">Guest view</p>
</div>

Iteration #

<li tl:each="item : ${items}" tl:text="${item}">placeholder</li>

Status variables are available as ${itemStat} (or custom name via item, stat : ${items}):

Variable Description
index 0-based index
count 1-based count
size Total number of items
first true for first item
last true for last item
odd true for 0-based odd indices
even true for 0-based even indices
current Current item value

Fragments #

<!-- Define a fragment (parameterized) -->
<div tl:fragment="card(title, body)">
  <h2 tl:text="${title}">Title</h2>
  <p tl:text="${body}">Body</p>
</div>

<!-- Include a fragment (keeps host element) -->
<div tl:insert="card('Hello', 'World')">replaced by fragment</div>

<!-- Replace with fragment (replaces host element) -->
<div tl:replace="userCard">replaced entirely</div>

<!-- Cross-file inclusion -->
<div tl:insert="~{components :: header}">loads header from components.html</div>

<!-- CSS selector targeting -->
<div tl:insert="~{components :: #main-nav}">loads by id</div>

Circular fragment inclusions are detected and reported with the full cycle path.

Local Variables #

<div tl:with="fullName=${first} + ' ' + ${last}">
  <span tl:text="${fullName}">Name</span>
</div>

Object / Selection #

<!-- Set object context; *{} accesses fields directly -->
<div tl:object="${user}">
  <span tl:text="*{name}">Name</span>
  <span tl:text="*{email}">Email</span>
</div>

Objects are accessed via toMap() or toJson() if present, otherwise as Map<String, dynamic>.

Attribute Setting #

<!-- Shorthand attributes -->
<a tl:href="${url}">link</a>
<img tl:src="${imageUrl}">
<input tl:value="${val}">
<div tl:class="${className}">styled</div>
<div tl:id="${elementId}">identified</div>

<!-- Append to existing class/style -->
<div class="card" tl:classappend="${active} ? 'active' : ''">content</div>
<div style="color:red" tl:styleappend="font-weight:bold">content</div>

<!-- Generic attribute setting -->
<div tl:attr="data-id=${item.id},title=${item.name}">content</div>

tl:class replaces the existing class (not appends). tl:classappend/tl:styleappend append. Null values remove the attribute. Boolean HTML attributes (disabled, checked, etc.): true renders valueless, false removes.

Block (Virtual Element) #

<!-- tl:block renders only its children — the host element is not emitted -->
<tl:block tl:each="item : ${items}">
  <dt tl:text="${item.key}">key</dt>
  <dd tl:text="${item.value}">value</dd>
</tl:block>

<!-- Self-closing form also supported -->
<tl:block tl:utext="${bodyHtml}"/>

Remove #

<div tl:remove="all">removed entirely from output</div>
<div tl:remove="body">tag kept, children removed</div>
<div tl:remove="tag">children kept, tag removed</div>
<ul tl:remove="all-but-first"><li>kept</li><li tl:remove="all">removed</li></ul>
<div tl:remove="none">kept as-is (prototype marker)</div>

Inline Processing #

<!-- Enable inline expressions in element text content -->
<p tl:inline="text">Hello, [[${name}]]! Today is [(${rawHtml})].</p>

<!-- Inline in script blocks -->
<script tl:inline="javascript">
  var user = [[${user.name}]];
</script>

<!-- Inline in style blocks -->
<style tl:inline="css">
  .alert { color: [[${alertColor}]]; }
</style>

[[${expr}]] — escaped output. [(${expr})] — unescaped output.

Filters #

Filters transform expression values using pipe syntax:

<span tl:text="${name | upper}">NAME</span>
<span tl:text="${input | trim | lower}">cleaned</span>

Built-in filters: upper, lower, trim, length.

Custom filters via constructor:

Trellis(filters: {
  'currency': (v) => '\$${(v as num).toStringAsFixed(2)}',
})

Expression Syntax #

Expression Example Description
Variable ${user.name} Dot-notation, null-safe traversal
Selection *{field} Field access on tl:object context
URL @{/users(id=${userId})} URL with query params
String literal 'hello' Single-quoted string
Literal substitution |Hello, ${name}!| Pipe-delimited template string
Arithmetic ${a} + ${b}, - * / % Numeric arithmetic
Dynamic index ${list[index]} List/map access with expression index
Ternary ${active} ? 'yes' : 'no' Conditional expression
Elvis ${val} ?: 'default' Null-coalescing
Comparison ${a} == ${b}, !=, <, >, <=, >= Value comparison
Comparison alias gt, lt, ge, le, eq, ne Word-form comparison operators
Boolean ${a} and ${b}, or, not / ! Logical operators
Concat ${first} + ' ' + ${last} String concatenation
Filter ${name | upper} Pipe-based value transformation
No-op _ Explicitly do nothing (prototype preservation)

HTMX Fragment Example #

import 'package:trellis/trellis.dart';
// Also add shelf: dart pub add shelf shelf_io

final engine = Trellis(loader: FileSystemLoader('templates/'));

// Full page render
final page = engine.render(pageTemplate, {'items': items});

// HTMX partial -- render only the fragment
final fragment = engine.renderFragment(
  pageTemplate,
  fragment: 'itemList',
  context: {'items': items},
);

// Render multiple fragments in one call
final fragments = engine.renderFragments(
  pageTemplate,
  fragments: ['header', 'itemList'],
  context: {'items': items},
);

Configuration #

Trellis(
  loader: FileSystemLoader('templates/'), // Default
  cache: true,         // DOM caching with deep-clone (default: true)
  maxCacheSize: 100,   // LRU eviction threshold (default: 256)
  prefix: 'tl',        // Attribute prefix (default: 'tl')
  strict: false,       // Throw on undefined variables/members (default: false)
)

TrellisContext is a fluent builder for constructing rendering context maps:

final context = TrellisContext()
  .set('title', 'Hello')
  .set('user', {'name': 'Alice'})
  .setAll({'items': ['a', 'b', 'c']})
  .build();

final html = engine.render(template, context);

Template Loaders #

  • FileSystemLoader(basePath) -- loads from filesystem with security boundaries
  • MapLoader(templates) -- in-memory templates, useful for testing

HTML5-Valid Attribute Names #

Use data-tl-* attributes to pass HTML5 validation:

Trellis(prefix: 'data-tl')
<p data-tl-text="${message}">placeholder</p>

Public API #

Method / Property Returns Description
render(source, context) String Render template string
renderFile(name, context) Future<String> Load and render template file
renderFragment(source, fragment:, context:) String Render named fragment from string
renderFileFragment(name, fragment:, context:) Future<String> Load file and render named fragment
renderFragments(source, fragments:, context:) String Render multiple fragments concatenated
renderFileFragments(name, fragments:, context:) Future<String> Load file and render multiple fragments
clearCache() void Clear DOM cache and reset statistics
cacheStats CacheStats Hit/miss/size metrics for the DOM cache

ExpressionEvaluator can be used standalone for expression evaluation without templates:

final evaluator = ExpressionEvaluator(strict: true);
final result = evaluator.evaluate(r'${a} + ${b}', {'a': 1, 'b': 2}); // 3

Error Handling #

Exception When
TemplateException Base class for all template errors
ExpressionException Malformed or unevaluable expression (also thrown in strict mode for undefined variables)
FragmentNotFoundException Named fragment not found in template
TemplateNotFoundException Template file not found by loader
TemplateSecurityException Path traversal or symlink escape attempt

Security #

  • tl:utext renders unescaped HTML -- only use with trusted content to avoid XSS
  • tl:inline in javascript/css mode escapes output including </script> and </style> closing tags
  • FileSystemLoader rejects absolute paths, .. traversal, and symlink escapes outside the base directory
  • tl:text always HTML-escapes output

Contributing #

Trellis is in early development and we're not accepting pull requests at this time. That said, we'd love to hear from you — bug reports, feature ideas, and general feedback are all very welcome! Please feel free to open an issue.

License #

MIT

2
likes
0
points
482
downloads

Publisher

verified publisherleafnode.se

Weekly Downloads

Template engine for Dart, using natural HTML templates. Fragment-first for hypermedia-driven frameworks like HTMX. Inspired by Thymeleaf.

Repository (GitHub)
View/report issues

Topics

#template #html #htmx #server-side-rendering

License

unknown (license)

Dependencies

html, string_scanner

More

Packages that depend on trellis