Treebark accepts input in the TreebarkInput format:
interface TreebarkInput {
template: TemplateElement | TemplateElement[];
data?: Data;
}Rendering functions also accept optional RenderOptions:
interface RenderOptions {
indent?: string | number | boolean; // Indentation for string renderer
logger?: Logger; // Custom logger for error/warning messages
}
interface Logger {
error(message: string): void;
warn(message: string): void;
log(message: string): void;
}Simple template:
{
template: { div: "Hello world" }
}Template with data:
{
template: { div: "Hello {{name}}" },
data: { name: "Alice" }
}With custom logger:
renderToString(
{ template: { div: "Hello" } },
{ logger: customLogger }
)Treebark follows a no-throw policy. Instead of throwing exceptions, errors and warnings are sent to a logger (defaults to console).
Behavior when errors occur:
- Invalid tags are skipped and an error is logged
- Invalid attributes are skipped and a warning is logged
- Nested comments are skipped and an error is logged
- Invalid conditional syntax is logged as an error and the element is skipped
Treebark renders as much valid content as possible, only skipping problematic elements.
- Tag Node:
{ "div": { ... } } - Array (fragment):
[ node, node, ... ]→ renders siblings with no wrapper - String (text leaf):
"Hello world"
$children→ array of child nodes (strings, nodes, or arrays)$bind→ bind current node to an array or object property in data
For nodes without attributes, you can use a shorthand array syntax instead of $children:
div:
- h2: "Title"
- p: "Content"This is equivalent to:
div:
$children:
- h2: "Title"
- p: "Content"Rules:
- Only works when the node has no attributes
- If you need attributes (class, id, etc.), use explicit
$childrensyntax - Mixing shorthand and attributes is not allowed
{{prop}}→ resolves against current context- Dot access allowed:
{{price.sale}} - Array element access:
{{items.0.name}}→ access array elements using numeric indices (no square brackets) - Parent access:
{{..parentProp}}→ access parent binding context - Multi-level parent access:
{{../..grandparentProp}}→ access multiple levels up - Escaping:
{{…}}→ binding{{{…}}}→ literal{{…}}{{{{…}}}}→ literal{{{…}}}
Array element access examples:
{{items.0}}→ first item in array{{data.1.name}}→nameproperty of second item{{matrix.0.1.value}}→ nested array access (first array, second element,valueproperty)
Note: Numeric indices work because JavaScript allows both array[0] and array["0"]. The implementation splits the path by . and uses each segment as a property key.
-
$childrencan contain strings + nodes:div: $children: - "Hello " - span: "World" - "!"
→
<div>Hello <span>World</span>!</div> -
Shorthand array syntax also supports mixed content:
div: - "Hello " - span: "World" - "!"
→
<div>Hello <span>World</span>!</div> -
Arrays act as fragments:
- h1: "Hello" - p: "World"
→
<h1>Hello</h1><p>World</p>
- Attributes are plain key/value pairs.
- Values may contain interpolations.
- Allowed:
- Global:
id,class,style,title,aria-*,data-*,role a:href,target,relimg:src,alt,width,heighttable:summaryth/td:scope,colspan,rowspanblockquote:cite
- Global:
- Blocked: event handlers (
on*attributes likeonclick,onload) - See Security section for comprehensive security details
For complex array scenarios where you need a wrapper element or nested structure, use $bind:
{
template: {
ul: {
class: "product-list",
$bind: "products",
$children: [
{ li: "{{name}} — {{price}}" }
]
}
},
data: {
products: [
{ name: "Laptop", price: "$999" },
{ name: "Phone", price: "$499" }
]
}
}$bind supports property access patterns:
- Literal property:
$bind: "products" - Nested property:
$bind: "catalog.products"(single dots for nested object access)
Note: $bind uses literal property paths only - no interpolation or parent context access. For parent property access, use interpolation {{..prop}} in content/attributes instead.
Common use cases for parent property access:
- Cross-referencing data: Access IDs or metadata from outer scopes
- Shared resources: Use common lookup tables or configuration from parent contexts
- Hierarchical navigation: Build breadcrumbs or nested navigation with parent context
- Conditional rendering: Access parent flags or settings to control child rendering
Example - Customer orders with product links:
{
template: {
div: {
$bind: "customers",
$children: [
{ h2: "{{name}}" },
{
ul: {
$bind: "orders",
$children: [
{
li: {
$children: [
"Order #{{orderId}}: ",
{
ul: {
$bind: "products",
$children: [
{
li: {
$children: [
{
a: {
href: "/customer/{{../../..customerId}}/order/{{..orderId}}/product/{{productId}}",
$children: ["{{name}}"]
}
}
]
}
}
]
}
}
]
}
}
]
}
}
]
}
}
}Individual array elements can be accessed using numeric indices in dot notation without square brackets:
Basic syntax:
{{arrayName.0}} // First element
{{arrayName.1}} // Second element
{{items.2.property}} // Property of third elementExample:
{
template: {
div: {
$children: [
{ p: "First: {{items.0.name}}" },
{ p: "Second: {{items.1.name}}" }
]
}
},
data: {
items: [
{ name: "Laptop", price: "$999" },
{ name: "Mouse", price: "$25" }
]
}
}Output:
<div>
<p>First: Laptop</p>
<p>Second: Mouse</p>
</div>Multi-level array access:
{
template: { div: "{{matrix.0.1.value}}" },
data: {
matrix: [
[{ value: "A1" }, { value: "A2" }],
[{ value: "B1" }, { value: "B2" }]
]
}
}
// Output: <div>A2</div>How it works:
JavaScript allows both array[0] and array["0"] syntax. Since the path is split by . and each segment is used as a property key, numeric string indices work seamlessly for array access.
When to use:
- Accessing specific array positions by index
- Extracting individual elements from small, fixed-size arrays
- Referencing array elements in templates where the index is known
When to use $bind instead:
- Iterating over all elements in an array
- Dynamic arrays where the length is unknown
- Building lists or repeated elements
Standard HTML tags:
div, span, p, header, footer, main, section, article,
h1–h6, strong, em, blockquote, code, pre,
ul, ol, li,
table, thead, tbody, tr, th, td,
a, img
Special tags:
comment, if
Blocked tags:
script, iframe, embed, object, applet,
form, input, button, select,
video, audio,
style, link, meta, base
HTML comments are generated using the $comment tag:
$comment: "This is a comment"→ <!--This is a comment-->
Features:
- Support interpolation:
$comment: "Generated on {{date}}" - Support mixed content with
$children - Cannot be nested (attempting to place a
$commentinside another$commentlogs an error and skips rendering the nested comment)
Examples:
Basic comment:
$comment: "This is a comment"Comment with interpolation:
$comment: "User: {{name}}"Comment with mixed content:
$comment:
$children:
- "Start: "
- span: "highlighted text"
- " :End"The $if tag provides advanced conditional rendering based on data properties. It acts as a transparent container that renders its children only when specified conditions are met.
Key Features:
- Uses
$checkto specify the property to check - Supports comparison operators:
$<,$>,$<=,$>=,$=,$in - Operators can be stacked (multiple operators)
- Supports
$notto invert the final result - Uses AND logic by default, can switch to OR logic with
$join: "OR" - Supports
$thenChildrenand$elseChildrenfor explicit if/else branching - Does not render itself as an HTML element
- Cannot have regular HTML attributes (only special operators and modifiers)
The $if tag supports explicit if/else branching using $thenChildren and $elseChildren:
{
template: {
div: {
$children: [
{
$if: {
$check: 'isLoggedIn',
$thenChildren: [
{ p: 'Welcome back!' }
],
$elseChildren: [
{ p: 'Please log in' }
]
}
}
]
}
},
data: { isLoggedIn: true }
}→ <div><p>Welcome back!</p></div> when isLoggedIn is true
→ <div><p>Please log in</p></div> when isLoggedIn is false
With operators:
{
template: {
$if: {
$check: 'score',
'$>': 90,
$thenChildren: [
{ p: { class: 'excellent', $children: ['Excellent!'] } }
],
$elseChildren: [
{ p: { class: 'good', $children: ['Good effort!'] } }
]
}
},
data: { score: 95 }
}→ <p class="excellent">Excellent!</p>
Backward compatibility: $children still works and is equivalent to $thenChildren (no else branch).
When no operators are provided, performs a standard JavaScript truthiness check:
{
template: {
div: {
$children: [
{
$if: {
$check: 'showMessage',
$children: [
{ p: 'This message is conditionally shown' }
]
}
}
]
}
},
data: { showMessage: true }
}Truthiness rules:
- Truthy values:
true, non-empty strings, non-zero numbers, objects, arrays - Falsy values:
false,null,undefined,0,""(empty string),NaN
{
template: {
$if: {
$check: 'age',
'$<': 18,
$children: [
{ p: 'Minor' }
]
}
},
data: { age: 15 }
}→ <p>Minor</p>
{
template: {
$if: {
$check: 'score',
'$>': 90,
$children: [
{ p: 'Excellent!' }
]
}
},
data: { score: 95 }
}→ <p>Excellent!</p>
{
template: {
$if: {
$check: 'status',
'$=': 'active',
$children: [
{ p: 'User is active' }
]
}
},
data: { status: 'active' }
}→ <p>User is active</p>
{
template: {
$if: {
$check: 'role',
$in: ['admin', 'moderator', 'editor'],
$children: [
{ p: 'Has special privileges' }
]
}
},
data: { role: 'admin' }
}→ <p>Has special privileges</p>
{
template: {
$if: {
$check: 'age',
'$<=': 18,
$children: [
{ p: 'Youth (18 or under)' }
]
}
},
data: { age: 18 }
}→ <p>Youth (18 or under)</p>
{
template: {
$if: {
$check: 'score',
'$>=': 90,
$children: [
{ p: 'Excellent performance!' }
]
}
},
data: { score: 90 }
}→ <p>Excellent performance!</p>
Multiple operators can be used together. By default, they use AND logic (all must be true):
Using exclusive bounds ($> and $<):
{
template: {
$if: {
$check: 'age',
'$>': 18,
'$<': 65,
$children: [
{ p: 'Working age adult (19-64)' }
]
}
},
data: { age: 30 }
}→ <p>Working age adult (19-64)</p> (renders because age > 18 AND age < 65)
Using inclusive bounds ($>= and $<=):
{
template: {
$if: {
$check: 'age',
'$>=': 18,
'$<=': 65,
$children: [
{ p: 'Working age adult (18-65 inclusive)' }
]
}
},
data: { age: 18 }
}→ <p>Working age adult (18-65 inclusive)</p> (renders because age >= 18 AND age <= 65)
Use $join: "OR" to change from AND to OR logic (at least one must be true):
{
template: {
$if: {
$check: 'age',
'$<': 18,
'$>': 65,
$join: 'OR',
$children: [
{ p: 'Non-working age' }
]
}
},
data: { age: 70 }
}→ <p>Non-working age</p> (renders because age > 65, even though age is not < 18)
The $not modifier inverts the entire result after all operators are evaluated:
{
template: {
$if: {
$check: 'age',
'$<': 18,
$not: true,
$children: [
{ p: 'Adult' }
]
}
},
data: { age: 25 }
}→ <p>Adult</p> (renders because NOT(age < 18) = true)
{
template: {
div: {
$children: [
{
$if: {
$check: 'user.status',
'$=': 'pending',
$in: ['error', 'failed'],
$join: 'OR',
$not: true,
$children: [
{ p: 'Valid user status' }
]
}
}
]
}
},
data: { user: { status: 'active' } }
}→ Renders because status is NOT ('pending' OR in ['error', 'failed'])
{
template: {
div: {
$children: [
{
$if: {
$check: 'user.isAdmin',
$children: [
{ p: 'Admin panel access' }
]
}
}
]
}
},
data: { user: { isAdmin: true } }
}→ <div><p>Admin panel access</p></div>
When condition is falsy, nothing is rendered:
{
template: {
$if: {
$check: 'showBanner',
$children: [
{ div: 'Banner content' }
]
}
},
data: { showBanner: false }
}→ `` (empty string)
Nested $if tags for complex conditions:
{
template: {
div: {
$children: [
{
$if: {
$check: 'hasPermissions',
$children: [
{ h2: 'Protected content' },
{
$if: {
$check: 'isVerified',
$children: [
{ p: 'Verified user content' }
]
}
}
]
}
}
]
}
},
data: { hasPermissions: true, isVerified: true }
}→ <div><h2>Protected content</h2><p>Verified user content</p></div>
Using $not for "unless" behavior:
{
template: {
div: {
class: 'status',
$children: [
{
$if: {
$check: 'count',
$not: true,
$children: [
{ p: 'No items available' }
]
}
},
{
if: {
$bind: 'count',
$children: [
{ p: 'Items found: {{count}}' }
]
}
}
]
}
},
data: { count: 0 }
}
},
{
$if: {
$check: 'count',
$children: [
{ p: 'Items found: {{count}}' }
]
}
}
]
}
},
data: { count: 0 }
}→ <div class="status"><p>No items available</p></div>
Attribute values can be conditional objects that use the same operator system as $if tags. This allows dynamic attribute values based on data conditions.
Basic conditional attribute:
{
template: {
div: {
class: {
$check: 'isActive',
$then: 'active',
$else: 'inactive'
},
$children: ['User status']
}
},
data: { isActive: true }
}→ <div class="active">User status</div> when isActive is true
→ <div class="inactive">User status</div> when isActive is false
With operators:
{
template: {
div: {
class: {
$check: 'score',
'$>': 90,
$then: 'excellent',
$else: 'good'
},
$children: ['Score display']
}
},
data: { score: 95 }
}→ <div class="excellent">Score display</div>
With $in operator:
{
template: {
div: {
class: {
$check: 'role',
$in: ['admin', 'moderator'],
$then: 'privileged',
$else: 'regular'
},
$children: ['User panel']
}
},
data: { role: 'admin' }
}→ <div class="privileged">User panel</div>
Multiple conditional attributes:
{
template: {
div: {
class: {
$check: 'theme',
'$=': 'dark',
$then: 'dark-mode',
$else: 'light-mode'
},
'data-theme': {
$check: 'theme',
$then: '{{theme}}',
$else: 'default'
},
$children: ['Themed content']
}
},
data: { theme: 'dark' }
}→ <div class="dark-mode" data-theme="dark">Themed content</div>
With modifiers:
Conditional attributes support all the same modifiers and operators as $if tags:
$not: Invert the condition$join: Combine multiple operators with "AND" or "OR"$<,$>,$<=,$>=,$=,$in: Comparison operators
{
template: {
div: {
class: {
$check: 'isGuest',
$not: true,
$then: 'member',
$else: 'guest'
},
$children: ['User']
}
},
data: { isGuest: false }
}→ <div class="member">User</div>
Restrictions:
- The
$iftag requires a$checkattribute - The
$iftag cannot have regular HTML attributes (likeclass,id, etc.) - Only special operators (
$<,$>,$<=,$>=,$=,$in) and modifiers ($not,$join) are allowed - If you need a wrapper element with attributes, use a regular tag inside the
$iftag's children
Treebark implements multiple layers of security to prevent XSS attacks and other vulnerabilities.
Only safe HTML tags are permitted. Dangerous tags are blocked and logged as errors:
Blocked tags:
script,iframe,object,embed,applet- XSS vectorsform,input,button,select,textarea- Form hijackingstyle,link,meta,base- Style/metadata injectionvideo,audio,canvas- Media-based attackssvg,math- Vector-based attacks
Case variations blocked: Tag names are case-sensitive. ScRiPt, IFRAME, etc. are also blocked.
Example:
{ script: 'alert("xss")' }
// Logs error: Tag "script" is not allowed
// Renders: (nothing)Only safe attributes are permitted per tag. Event handlers are blocked:
Blocked attributes:
onclick,onload,onerror,onmouseover, etc. - Allon*event handlers- Case variations:
onClick,ONCLICK, etc. are also blocked
Allowed attributes per tag:
- Global:
id,class,style,title,aria-*,data-*,role a:href,target,relimg:src,alt,width,heighttable:summaryth/td:scope,colspan,rowspanblockquote:cite
Example:
{ div: { onclick: 'alert(1)', $children: ['text'] } }
// Logs warning: Attribute "onclick" is not allowed on tag "div"
// Renders: <div>text</div> (onclick omitted)All content and attribute values are automatically HTML-escaped to prevent injection:
Example:
{ div: '<script>alert(1)</script>' }
// Renders: <div><script>alert(1)</script></div>The style attribute uses a structured object format that blocks multiple attack vectors:
Dangerous CSS patterns blocked:
url()with external URLs (data: URIs allowed for inline images)expression()- IE expression injectionjavascript:protocol in CSS values@import- CSS imports
Dangerous CSS properties blocked:
behavior- IE behavior property (can execute code)-moz-binding- Firefox XBL binding (can execute code)
Property name validation:
- Must be kebab-case format (lowercase letters and hyphens)
- Invalid formats are skipped with a warning
Semicolon injection prevention: Only the first CSS value before a semicolon is used:
{
div: {
style: {
color: 'red; position: absolute; z-index: 999'
},
$children: ['text']
}
}
// Logs warning: CSS value contained semicolon - using only first part
// Renders: <div style="color: red">text</div>The href and src attributes validate URL protocols to prevent XSS attacks:
Safe protocols allowed:
http:,https:- Standard web protocolsmailto:,tel:,sms:- Communication protocolsftp:,ftps:- File transfer protocols- Relative URLs:
/path,#anchor,?query,page.html
Dangerous protocols blocked:
javascript:- JavaScript executiondata:- Data URIs (can contain HTML/scripts)vbscript:- VBScript executionfile:- Local file access- Any other unlisted protocols
Example:
{ a: { href: 'javascript:alert(1)', $children: ['Click'] } }
// Logs warning: Attribute "href" contains blocked protocol "javascript:"
// Renders: <a>Click</a> (href omitted)
{ a: { href: 'https://example.com', $children: ['Safe'] } }
// Renders: <a href="https://example.com">Safe</a>Access to JavaScript prototype chain properties is blocked in template interpolation:
Blocked properties:
constructor- Object constructor access__proto__- Prototype chain accessprototype- Prototype property access
Example:
{ div: '{{constructor}}' }
// Logs warning: Access to property "constructor" is blocked for security reasons
// Renders: <div></div>
{ div: '{{name}}' }
// Renders: <div>Alice</div> (normal properties work fine)Treebark implements multiple overlapping security layers:
- Tag allowlist - Only safe HTML tags permitted
- Attribute allowlist - Only safe attributes permitted per tag
- HTML escaping - All content and attribute values escaped
- Structured style objects - Prevents CSS string injection
- CSS pattern blocking - Blocks dangerous CSS patterns and properties
- URL protocol validation - Blocks dangerous protocols in href/src
- Prototype chain blocking - Prevents access to internal object properties
This defense-in-depth approach ensures that even if one layer is bypassed, others remain to protect against attacks.