@chilts/weave
v0.4.0
Published
A Pug-inspired templating engine that compiles to TypeScript.
Maintainers
Readme
Weave
A Pug-inspired templating engine that compiles to TypeScript. Write indentation-based templates, get typed functions that return HTML.
views/*.weave → weave compiler → lib/views/*.ts → tscWhy Weave?
Pug is great. Weave takes the same indentation-based syntax and makes it TypeScript-native — your templates become typed functions, checked by tsc like any other code.
- Type-safe templates — template parameters are real TypeScript types
- Type-safe mixins — positional args with TS type annotations
- Tailwind-friendly — class names with colons (
.md:flex,.hover:bg-blue-500) just work - Single-pass escaping — values are escaped once at render time, never pre-escaped
- No runtime template engine — compiled output is plain TypeScript
Example
A type definition:
export interface SimpleProps {
title: string
content: string
}A .weave template:
import type { SimpleProps } from '../types/views.js'
template simple({ title, content }: SimpleProps)
doctype html
html(lang="en")
head
title= title
body
h1= title
p= contentCompiles to a .ts file:
import { h, doctype } from "@chilts/weave"
import type { SimpleProps } from '../types/views.js'
export function simple({ title, content }: SimpleProps) {
return doctype()
+ h("html", { lang: "en" },
h("head", {},
h("title", {}, title)
),
h("body", {},
h("h1", {}, title),
h("p", {}, content)
)
)
}Now tsc catches type errors, your IDE gives you autocomplete, and calling simple({ title: "Hello", content: "World" }) returns HTML.
Usage
Templates are just functions that return strings, so they work with any framework:
import express from 'express'
import { simple } from './lib/views/simple.js'
const app = express()
app.get('/', (req, res) => {
res.send(simple({ title: "Hello", content: "World" }))
})
app.listen(3000)Syntax
Weave's syntax will be familiar if you've used Pug.
Elements, classes, IDs, and attributes
h1 Hello World
p.intro This is a paragraph with a class
.container
span#main.text-lg.md:text-xl Content here
a(href="/about" class="nav-link") AboutConditionals
if role === "admin"
p.badge You are an administrator
else if role === "editor"
p.badge You are an editor
else
p Welcome, visitorIteration
for ( const post of posts )
h2= post.title
p= post.contentMixins
Mixins use positional args. An implicit attrs parameter captures any extra attributes passed at the call site.
Definition:
mixin button(label: string, variant: "primary" | "secondary" = "primary")
button.btn(class=`btn-${variant}` ...attrs)= labelUsage:
+button("Save", "primary")(class="mr-2")
+button("Cancel", "secondary")
+button("Delete", "secondary")(class="ml-4" data-confirm="true")Layouts with extends and blocks
A layout defines named blocks as content slots:
template layout({ title }: LayoutProps)
doctype html
html(lang="en")
head
title= title
body
main
block content
footer
p © 2024A page extends the layout, filling in the blocks. Data is passed explicitly — nothing is implicitly inherited:
template home({ title, welcomeMessage }: HomeProps)
extends layout({ title })
block content
h1= welcomeMessage
p Welcome to our site.Blocks can have default content in the layout. If a page doesn't override a block, the default renders.
Includes
Include another .weave file to use its mixins:
include ./components/button
template actionsPage({ title }: ActionsPageProps)
doctype html
html(lang="en")
body
+button("Save", "primary")Text content
Use | (pipe) to add literal text as a child of an element:
p
| This is plain text inside a paragraph.A bare | on its own line (with no text after it) emits a single space. This is useful for adding whitespace between sibling elements:
p Hello
|
strong= email
| .This renders as: Hello <strong>[email protected]</strong>. — with a space between "Hello" and the email address.
Inline elements
Weave does not support inline element syntax (e.g. Pug's #[strong bold] tag interpolation). To mix elements within text, place each element on its own indented line and use | for spacing and text fragments.
Raw blocks
Use ! after a tag to write literal content that won't be escaped or parsed as Weave syntax. This is useful for inline CSS, JavaScript, or any other content that should be passed through as-is:
style!
body { font-family: sans-serif; max-width: 640px; margin: 0 auto; }
h1 { border-bottom: 2px solid #eee; }script!
console.log("hello")The indented content is compiled to a raw() call, so the output is not HTML-escaped. Works with classes and IDs too: div.content!.
For inline raw expressions, use raw() directly: div.prose= raw(html).
Special characters
Weave escapes all text content by default. HTML entities like « will be escaped and rendered literally in the browser rather than interpreted. Use the actual Unicode character instead:
a(href="/back") « BackThis keeps the escaping model simple and predictable — all text is escaped, no exceptions.
Imports
Standard TypeScript imports pass through to the compiled output:
import type { BlogPost } from '../types/models.js'Development
npm run build # compile TypeScript to dist/
npm test # run tests (vitest)
npm run lint # type-check without emitting (tsc --noEmit)Status
Weave is in early design. The examples/ directory contains paired .weave and .ts files that define the target syntax and expected compiler output. The compiler is under active development.
Editor Support
- Emacs —
editors/emacs/— major mode with syntax highlighting, indentation, and comment support
License
ISC
