HTMLispπ
HTMLisp is a light extension of HTML meant for templating. In other words, HTMLisp accepts a context, allows binding it, and supports basic features, such as iteration. All the features have been implemented using as HTML attribute extensions meaning HTML-oriented tooling works well with HTMLisp out of the box. HTMLisp is the default option for templating in Gustwind although technically you could bring your own solution.
I've listed the main features below:
- Support for data binding from global context or props
- Scoped shorthand lookup inside expression bindings
- Named components - these allow reuse of markup and building your own abstractions that compile to HTML
- Function components in addition to string components
- Iteration - to allow expanding arrays into more complex markup, such as a list, iteration is supported
- Visibility control - hiding entire portions of markup is possible
fragmentandnoophelpers - for cases where you want logic or composition without emitting a parent element- Structured attributes through
&attrs - Optional escaped-by-default output with explicit raw HTML
evalfree - to support execution in environments without support for JavaScriptevalthe solution does not rely oneval
Expressionsπ
If you understand how Lisp evaluation works, then you understand how Gustwind templating works. To give a concrete example, consider the component example below:
<div>
<SiteLink
&children="title"
&href="(urlJoin blog slug)"
/>
</div>
Note the & before the attribute name as that signifies an expression binding.
HTMLisp supports two styles inside those bindings:
Lisp-style utility calls
<a &href="(urlJoin blog (get props slug))"></a>
Scoped shorthand lookup
<a &href="post.slug" &children="message"></a>
Shorthand lookup resolves in this order:
localpropscontext
That means message will read from local loop or noop scope first, then from component props, and finally from global rendering context.
The Lisp-style form remains fully supported and is still useful when you want utility calls or explicit lookups.
Escaping And Raw HTMLπ
HTMLisp can now run in an escaped-by-default mode:
import { htmlispToHTML, raw } from "htmlisp";
await htmlispToHTML({
htmlInput: `<div &children="message"></div>`,
props: { message: "<em>unsafe</em>" },
renderOptions: { escapeByDefault: true },
});
That renders:
<div><em>unsafe</em></div>
When you want to inject trusted HTML explicitly, use raw(...):
<div &children="(raw summaryHtml)"></div>
or pass a raw value from TypeScript:
raw("<strong>trusted</strong>");
This keeps the dangerous path explicit while allowing normal strings to remain safe.
Iterationπ
To allow iteration over data, there is a specific &foreach syntax as shown below:
<ul &foreach="blogPosts">
<li class="inline">
<SiteLink
&children="title"
&href="(urlJoin blog slug)"
/>
</li>
</ul>
The expression given to &foreach should produce an array. For each item, object fields are merged into the current props scope. In case the array contains pure values such as strings or numbers, those are exposed through value.
<ul &foreach="blogPosts">
<li &children="value"></li>
</ul>
If you want an explicit alias for the current loop item, use as:
<dl>
<noop &foreach="readonlyFields as field">
<dd &children="field.text"></dd>
</noop>
</dl>
Alias behavior is additive:
- object fields are still exposed directly for compatibility
valuestill points to the current item- the alias points to the full current item
That means both of the following remain valid:
<ul &foreach="blogPosts as post">
<li &title="value" &children="post"></li>
</ul>
Visibilityπ
Given there are times when you might want to remove a part of the DOM structure based on an expression, there is &visibleIf helper that works as below:
<body>
<MainNavigation />
<aside
&visibleIf="showToc"
class="fixed top-16 pl-4 hidden lg:inline"
>
<TableOfContents />
</aside>
<main &children="content"></main>
<MainFooter />
<Scripts />
</body>
When showToc evaluates as false, aside element is removed completely from the structure.
Fragment And Noopπ
Use fragment when you want composition without wrapper markup:
<fragment>
<Button />
<Button />
</fragment>
or:
<fragment &children="submitButton"></fragment>
Given there are also cases where you want to perform an operation but not generate markup directly, noop still exists. It remains useful for advanced cases such as local bindings and dynamic tag replacement.
Do nothing
<noop />
Iterate without generating a parent element
<noop &foreach="scripts">
<script &type="type" &src="src"></script>
</noop>
Replace type based on a given prop
<noop
&type="type"
&class="class"
&children="(processMarkdown children)"
></noop>
As a rule of thumb:
- use
fragmentfor plain composition - use
noopwhen you need its local-binding or dynamic-type behavior
Commentsπ
There is a commenting syntax that allows documenting and gets removed through processing:
<div __reference="https://gustwind.js.org/">Site creator</div>
Componentsπ
Within components, props field is available within the context. On a high level it is comparable to how React and other libraries work so that components can encapsulate specific functionality and may be reused across projects easily.
HTMLisp supports both string components and function components.
String components
const components = {
Button: `<button &children="children"></button>`,
};
Function components
const components = {
Button: (props) => `<button>${props.children}</button>`,
};
Function components are useful when you want TypeScript-level authoring and typing instead of large inline template strings.
The async renderer accepts async component functions. The sync renderer accepts sync component functions only.
Structured Attributesπ
For helper components that need to pass through normal HTML attributes, use &attrs:
<button
&type="type"
&class="className"
&attrs="extraAttributes"
&children="label"
></button>
Attribute map behavior is:
- string values become escaped attributes
truebecomes a boolean attributefalse,null, andundefinedare omitted- explicit attributes win over values coming from
&attrs
Slotsπ
To make it convenient to construct complex components that accept structures from the consumer, there is support for slots as below:
<BaseLayout>
<slot name="content">Main content goes here</slot>
<slot name="aside"><TableOfContents /></slot>
</BaseLayout>
Internally slots map to props. The main benefit is that they allow expression of complex element structures without having to go through an attribute.
Playgroundπ
Use the playground below to experiment with the syntax and see how it converts to HTML:
Note that this playground works only on Gustwind website.
Licenseπ
MIT.