@chilts/weave 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +303 -0
- package/ReadMe.md +261 -8
- package/dist/cli/check.d.ts +14 -0
- package/dist/cli/check.js +133 -0
- package/dist/cli/index.d.ts +42 -4
- package/dist/cli/index.js +190 -36
- package/dist/codegen/index.d.ts +17 -3
- package/dist/codegen/index.js +436 -238
- package/dist/codegen/sourcemap.d.ts +49 -1
- package/dist/codegen/sourcemap.js +154 -2
- package/dist/diagnostics/index.d.ts +25 -0
- package/dist/diagnostics/index.js +68 -0
- package/dist/index.d.ts +8 -4
- package/dist/index.js +4 -2
- package/dist/lexer/index.d.ts +5 -1
- package/dist/lexer/index.js +340 -92
- package/dist/parser/index.d.ts +1 -1
- package/dist/parser/index.js +74 -9
- package/dist/parser/types.d.ts +13 -0
- package/dist/runtime/index.d.ts +45 -1
- package/dist/runtime/index.js +67 -2
- package/package.json +23 -5
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@chilts/weave` are documented here.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
While the version is below `1.0.0`, breaking changes may land in minor releases.
|
|
8
|
+
|
|
9
|
+
## [Unreleased]
|
|
10
|
+
|
|
11
|
+
## [0.8.0] - 2026-10-22
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **`unless`.** `unless cond` renders its block when `cond` is falsy,
|
|
16
|
+
compiling to the same ternary as `if !(cond)`. It is a guard only: an
|
|
17
|
+
`else` or `else if` after it is a compile error asking for an `if` with the
|
|
18
|
+
branches swapped. Previously `unless` was a compile error pointing at
|
|
19
|
+
`if !(cond)`.
|
|
20
|
+
|
|
21
|
+
- **Class arrays/objects and style objects.** `class=` accepts an array
|
|
22
|
+
(nested, with `null`/`undefined`/`false`/`""` dropped) or an object whose
|
|
23
|
+
truthy keys are kept, also when merged with shorthand classes:
|
|
24
|
+
`li.item(class=({ active: isActive }))` renders `class="item active"`.
|
|
25
|
+
`style=` accepts an object: `div(style=({ marginTop: 0 }))` renders
|
|
26
|
+
`style="margin-top:0"`. Previously these rendered `class="a,b"` or
|
|
27
|
+
`[object Object]`. The `Attrs` type now types `class` and `style` with the
|
|
28
|
+
new exported `ClassValue` and `StyleValue` types, and `mergeClass()` accepts
|
|
29
|
+
any `ClassValue`.
|
|
30
|
+
|
|
31
|
+
- **htmx example and docs.** A new `examples/htmx/` shows a page and the
|
|
32
|
+
fragment endpoints that reuse its mixin, and the README describes using
|
|
33
|
+
Weave for server-rendered apps.
|
|
34
|
+
|
|
35
|
+
- **`for ... else`.** An `else` after a `for` renders when the collection is
|
|
36
|
+
empty.
|
|
37
|
+
|
|
38
|
+
- **Mixin call-site content.** A bare `block` inside a mixin renders the
|
|
39
|
+
content indented under the call (`+card("Hi")` followed by indented lines),
|
|
40
|
+
with indented lines under `block` as its default. The mixin gains a
|
|
41
|
+
`block?: () => unknown` parameter after `attrs`; a call with content passes
|
|
42
|
+
`{}` for `attrs` when it has none. One line of content can also go on the
|
|
43
|
+
call's own line, as for an element: `+btn("save") Save` or
|
|
44
|
+
`+btn("save")(class="wide")= label`. A call can't have both inline and
|
|
45
|
+
indented content (a `ParserError`). Previously text after a mixin call was
|
|
46
|
+
dropped without a word, so the button rendered empty.
|
|
47
|
+
|
|
48
|
+
- **Type errors mapped back to `.weave`.** `mapDiagnostic()` takes a
|
|
49
|
+
diagnostic reported by `tsc` against a compiled `.ts` file and uses the
|
|
50
|
+
`.ts.map` written beside it to translate it to the `.weave` file, line and
|
|
51
|
+
column; `formatDiagnostic()` prints it like a `LexerError`, with a code
|
|
52
|
+
frame. An error inside a TS expression points at the exact token: in
|
|
53
|
+
`h1= user.nmae` it lands on `nmae`, not `h1`. This covers buffered code,
|
|
54
|
+
`#{}` interpolations, attribute values, `if`/`else if` conditions, `for`
|
|
55
|
+
headers, `template`/`mixin` params, and mixin call and `extends` args,
|
|
56
|
+
including every line of a multi-line param list. An error in code weave
|
|
57
|
+
generated around them points at the start of the node.
|
|
58
|
+
`decodeMappings()`, `originalPosition()` and
|
|
59
|
+
`formatCodeFrame()` are exported too. The API takes plain
|
|
60
|
+
`{ file, line, col, message }` objects, so it does not depend on the
|
|
61
|
+
`typescript` package.
|
|
62
|
+
|
|
63
|
+
- **`weave check`.** `weave check views/ lib/views/` compiles the templates,
|
|
64
|
+
type-checks the whole project against its `tsconfig.json` (or
|
|
65
|
+
`--project <path>`), and prints every error — those in a compiled view at
|
|
66
|
+
their `.weave` file, line and column with a code frame, the rest as-is —
|
|
67
|
+
followed by `Found N errors.` It exits 1 on any compile or type error, so it
|
|
68
|
+
can stand in for `tsc --noEmit` in CI. It loads the project's own
|
|
69
|
+
`typescript`, now an optional peer dependency. `--watch` is not supported
|
|
70
|
+
yet.
|
|
71
|
+
|
|
72
|
+
### Changed
|
|
73
|
+
|
|
74
|
+
- **Layouts declare their own `<Name>Blocks` type.** The compiler now emits
|
|
75
|
+
an exported `<Name>Blocks` interface (one optional `() => unknown` member
|
|
76
|
+
per `block`) above each template with slots, instead of splicing the name
|
|
77
|
+
into the first `import type` and requiring you to write the interface.
|
|
78
|
+
This works with no type import and with several layouts in one file.
|
|
79
|
+
**Breaking:** delete any hand-written `<Name>Blocks` interface, or `tsc`
|
|
80
|
+
reports a duplicate identifier.
|
|
81
|
+
|
|
82
|
+
- `for` loops now compile to the new `each()` runtime helper instead of
|
|
83
|
+
`.map()`, so compiled templates import `each` from `@chilts/weave`.
|
|
84
|
+
|
|
85
|
+
- Text with `#{}` interpolation now compiles to the new `interpolate` tagged
|
|
86
|
+
template (``h("h1", {}, interpolate`Hello ${name}`)``), so compiled
|
|
87
|
+
templates import `interpolate` from `@chilts/weave`. An interpolated `null`
|
|
88
|
+
or `undefined` now renders nothing, the same as `p= value`, where it used
|
|
89
|
+
to print `null` or `undefined`.
|
|
90
|
+
|
|
91
|
+
### Fixed
|
|
92
|
+
|
|
93
|
+
- `raw()` and mixin output inside `#{}` were escaped: `p Hi #{raw(html)}`
|
|
94
|
+
rendered `Hi <b>...` and `p By #{userLink(author, {})}` showed the
|
|
95
|
+
link's markup as text. Interpolated values now render exactly as element
|
|
96
|
+
children do, so both come out as HTML.
|
|
97
|
+
- A bare `block` inside a mixin compiled to invalid TypeScript
|
|
98
|
+
(`blocks.?.()`) and content indented under a mixin call was silently
|
|
99
|
+
dropped. A bare `block` outside a mixin, or a named block inside one, is now
|
|
100
|
+
a `CodegenError`.
|
|
101
|
+
- `for` over a non-array iterable (`xs.entries()`, `Set`, `Map`, a generator)
|
|
102
|
+
rendered `[object Iterator Helper]` or failed to type-check; any iterable
|
|
103
|
+
now works.
|
|
104
|
+
- The `Attrs` type now accepts `null`, so `a(href=x)` with `x: string | null`
|
|
105
|
+
type-checks; the runtime already omitted `null` attributes.
|
|
106
|
+
- Templates return `SafeHtml`, not a string, despite 0.4.0's note that they
|
|
107
|
+
return strings directly: only doctype-led templates ever did, by accident,
|
|
108
|
+
and 0.5.0 ended that. `SafeHtml` is now the documented contract for templates
|
|
109
|
+
and mixins alike, so nested output is never escaped twice; call `String()` on
|
|
110
|
+
it for the HTML. The README's Express example passed the result straight to
|
|
111
|
+
`res.send()`, which sends it as JSON; it now calls `String()` first.
|
|
112
|
+
- A mixin that never uses `attrs` compiled to an unused `attrs` parameter,
|
|
113
|
+
failing `tsc` under `noUnusedParameters`; it is now named `_attrs`. The
|
|
114
|
+
parameter stays, so call sites may still pass extra attributes.
|
|
115
|
+
- A template with a single root element indented that element's children one
|
|
116
|
+
level too deep in the compiled `.ts`. Cosmetic only; the output is unchanged.
|
|
117
|
+
- Pug syntax that Weave doesn't support no longer mis-parses silently; each
|
|
118
|
+
form is now a `LexerError` that names the Weave alternative. `p!= x` used to
|
|
119
|
+
drop `x`; block expansion (`li: a Home`) and anything else run onto an
|
|
120
|
+
element (`a(href="/")Home`, `br/`) dropped the rest of the line; `p.` emitted
|
|
121
|
+
`class=""` and turned its text into elements; `- const x = 1` became a `-`
|
|
122
|
+
element; and `case`, `when`, `default`, `each`, `while`, `unless`, `append`
|
|
123
|
+
and `prepend` became elements of those names. Tag interpolation
|
|
124
|
+
(`p Hello #[strong world]`) and unescaped interpolation (`p Hi !{html}`)
|
|
125
|
+
rendered their markup characters as text; `!{html}` now points at
|
|
126
|
+
`#{raw(html)}`. Empty `.` / `#` shorthands and tag names that don't start
|
|
127
|
+
with a letter are rejected too.
|
|
128
|
+
- `block` took the rest of its line as the slot name, so Pug's
|
|
129
|
+
`block append scripts` compiled to invalid TypeScript
|
|
130
|
+
(`blocks.append scripts?.()`). A block name must now be a single identifier,
|
|
131
|
+
or it is a `LexerError`; `block append` / `block prepend` (and bare
|
|
132
|
+
`append` / `prepend`) point at adding a second, empty slot to the layout
|
|
133
|
+
(`block pageScripts` after `block scripts`) and filling that instead.
|
|
134
|
+
|
|
135
|
+
## [0.7.0] - 2026-10-20
|
|
136
|
+
|
|
137
|
+
### Added
|
|
138
|
+
|
|
139
|
+
- **Escaped dots in class and id shorthand.** `.gap-1\.5` is the single class
|
|
140
|
+
`gap-1.5`, so Tailwind's fractional spacing classes work as shorthand.
|
|
141
|
+
- **Multi-line parameter lists.** The parentheses after `template`, `mixin`,
|
|
142
|
+
`extends`, `for` and a mixin call may now wrap over several lines, staying
|
|
143
|
+
open until they balance, so a long destructured signature can be written
|
|
144
|
+
the way TypeScript is normally wrapped. Continuation lines must be indented
|
|
145
|
+
under the opening line (a closing line such as `}: Props)` may sit level with
|
|
146
|
+
it). A string inside must close on its own line, and `//` comments are
|
|
147
|
+
dropped from the output. The compiled code keeps the wrapped layout, adding
|
|
148
|
+
`attrs`/`blocks` on a line of their own when the `)` was on its own line.
|
|
149
|
+
- **Emacs mode understands context.** `weave-mode` now follows multi-line
|
|
150
|
+
attribute and parameter lists and raw (`tag!`) blocks when highlighting and
|
|
151
|
+
indenting, highlights attribute names, aligns `else` with its `if`, and
|
|
152
|
+
cycles outwards on repeated TAB. An apostrophe in text no longer opens a
|
|
153
|
+
string, `//` in a URL is no longer a comment, and `.`/`#` are only coloured
|
|
154
|
+
as a class or id where they really are one.
|
|
155
|
+
|
|
156
|
+
### Fixed
|
|
157
|
+
|
|
158
|
+
- A dotted class such as `.gap-1.5` was silently split into `gap-1` plus a
|
|
159
|
+
stray class `5`. A shorthand class that is a bare number is now a
|
|
160
|
+
`LexerError` that names the split and suggests escaping the dot.
|
|
161
|
+
- `include ./partials` imported a binding named after the file (`partials`)
|
|
162
|
+
instead of the mixins the template calls, so a file defining several
|
|
163
|
+
mixins, or one not named after its file, compiled to code that failed in
|
|
164
|
+
`tsc`. The compiler now reads the included file and imports only the mixins
|
|
165
|
+
called (`import { badge, chip } from './partials.js'`). Calling a mixin that
|
|
166
|
+
no include or local definition provides is a `CodegenError`. For API users,
|
|
167
|
+
`compile()`, `compileWithSourceMap()` and `generate()` take an optional
|
|
168
|
+
`resolveInclude`. Without it, a lone include provides every called mixin
|
|
169
|
+
that isn't defined locally, and several includes are an error.
|
|
170
|
+
- Shorthand classes combined with a non-literal `class=` value leaked the
|
|
171
|
+
value's string form: `div.pt-4(class=undefined)` rendered
|
|
172
|
+
`class="pt-4 undefined"`, as did the usual conditional-class pattern
|
|
173
|
+
`class=(on ? "x" : undefined)` whenever it was off. Non-literal values are
|
|
174
|
+
now merged at render time by a new `mergeClass()` runtime helper, which
|
|
175
|
+
drops `undefined`, `null`, `false` and `""`. An attribute whose value is
|
|
176
|
+
`null` is now omitted instead of rendering as `"null"`.
|
|
177
|
+
- A template literal with a `${...}` interpolation inside a mixin call's
|
|
178
|
+
arguments, or a `template`/`mixin` parameter list, failed with
|
|
179
|
+
"Unterminated parentheses". A string containing a brace inside the
|
|
180
|
+
interpolation is also handled.
|
|
181
|
+
- Error locations and source maps now point at the start of a token rather
|
|
182
|
+
than just past its end — for example, a mixin call is located at its `+`
|
|
183
|
+
instead of its argument list.
|
|
184
|
+
- In watch mode, changing an included file now recompiles the templates that
|
|
185
|
+
include it, so a newly added mixin is picked up without touching each page.
|
|
186
|
+
The start-up reconcile likewise recompiles a template whose include is
|
|
187
|
+
newer than its output.
|
|
188
|
+
- Rebuilding with `--no-sourcemap` now removes a `.ts.map` left over from an
|
|
189
|
+
earlier build instead of leaving a map that no longer matches its `.ts`.
|
|
190
|
+
|
|
191
|
+
## [0.6.0] - 2026-10-19
|
|
192
|
+
|
|
193
|
+
### Added
|
|
194
|
+
|
|
195
|
+
- **Multi-line attribute lists.** End a line with the opening `(` to spread an
|
|
196
|
+
element's attributes over several lines — one attribute per line, indented
|
|
197
|
+
under the element, closed by a `)` on a line of its own. Works for mixin call
|
|
198
|
+
attributes too. Anything after the closing `)` is read as trailing content.
|
|
199
|
+
Attribute values follow exactly the same rules as the inline form.
|
|
200
|
+
|
|
201
|
+
### Changed
|
|
202
|
+
|
|
203
|
+
- **Breaking: every attribute must have a value.** The Pug-style valueless
|
|
204
|
+
boolean shorthand has been removed — `input(disabled)` no longer compiles.
|
|
205
|
+
Boolean attributes are now written with an explicit boolean value:
|
|
206
|
+
`disabled=true`, `disabled=false`, or an expression `disabled=isActive`
|
|
207
|
+
(the runtime renders a bare attribute for `true` and omits it for `false`
|
|
208
|
+
or `undefined`). Do not use a quoted `disabled="false"` — in HTML any
|
|
209
|
+
attribute that is present counts as true.
|
|
210
|
+
- A bare (unquoted) attribute value must be a single token. Multi-token
|
|
211
|
+
expressions such as a ternary or string concatenation must be wrapped in
|
|
212
|
+
parentheses (`class=(wide ? "a" : "b")`) or backticks
|
|
213
|
+
(``class=`${wide ? "a" : "b"}` ``), because a space separates one
|
|
214
|
+
attribute from the next.
|
|
215
|
+
|
|
216
|
+
### Fixed
|
|
217
|
+
|
|
218
|
+
- The lexer no longer hangs and aborts the process (SIGABRT) on a multi-token
|
|
219
|
+
bare attribute value; it now raises a clear `LexerError` with line and
|
|
220
|
+
column, pointing you to wrap the expression.
|
|
221
|
+
- `main(class=a b)` is rejected with a message offering both readings
|
|
222
|
+
(`class="a b"` as one value, or `class=a b="..."` as two attributes)
|
|
223
|
+
instead of silently parsing as `class="a"` plus a boolean `b`.
|
|
224
|
+
- An attribute list that ran off the end of its line was accepted as if it had
|
|
225
|
+
been closed, so the following lines were lexed as child elements — attribute
|
|
226
|
+
names became tags and their values rendered as visible text, and the stray
|
|
227
|
+
`)` could leak into the generated TypeScript. It is now a `LexerError`
|
|
228
|
+
anchored on the opening `(`.
|
|
229
|
+
- Watch mode reconciles the output tree on start so edits made while the
|
|
230
|
+
watcher was down are not missed.
|
|
231
|
+
- Watch mode on Linux stopped seeing a template after the first time an editor
|
|
232
|
+
saved it atomically (write a temp file, rename it over the original), so
|
|
233
|
+
later edits were never recompiled. Each directory now has its own watcher,
|
|
234
|
+
which survives atomic saves, and subdirectories created while watching are
|
|
235
|
+
picked up — including any templates already written into them.
|
|
236
|
+
- Deleting a `.weave` template now removes its compiled `.ts` from the output
|
|
237
|
+
directory (in one-shot builds and in watch mode). The output directory is
|
|
238
|
+
treated as owned by Weave: any `.ts` with no matching `.weave` is removed.
|
|
239
|
+
- A mixin, or a block's default content, with more than one root element
|
|
240
|
+
referenced `Fragment` without importing it, so the compiled file failed to
|
|
241
|
+
type-check. `Fragment` is now imported whenever it is used.
|
|
242
|
+
|
|
243
|
+
### Removed
|
|
244
|
+
|
|
245
|
+
- Source maps are no longer included in the published npm bundle.
|
|
246
|
+
|
|
247
|
+
## [0.5.0] - 2026-06-11
|
|
248
|
+
|
|
249
|
+
### Fixed
|
|
250
|
+
|
|
251
|
+
- Multi-level `extends` and `doctype` now generate type-safe output.
|
|
252
|
+
|
|
253
|
+
## [0.4.0] - 2026-04-05
|
|
254
|
+
|
|
255
|
+
### Changed
|
|
256
|
+
|
|
257
|
+
- Compiled templates return strings directly instead of arrays, simplifying
|
|
258
|
+
the generated output and how templates compose.
|
|
259
|
+
|
|
260
|
+
## [0.3.0] - 2026-04-05
|
|
261
|
+
|
|
262
|
+
### Added
|
|
263
|
+
|
|
264
|
+
- Raw block syntax (`tag!`) for passing literal, unescaped content through to
|
|
265
|
+
the output (inline CSS, JavaScript, etc.).
|
|
266
|
+
- Emacs major mode with syntax highlighting, indentation, and comment support.
|
|
267
|
+
|
|
268
|
+
### Documentation
|
|
269
|
+
|
|
270
|
+
- Added guidance on special characters and the single-pass escaping model to
|
|
271
|
+
the README.
|
|
272
|
+
|
|
273
|
+
## [0.2.0] - 2026-04-05
|
|
274
|
+
|
|
275
|
+
### Added
|
|
276
|
+
|
|
277
|
+
- A bare `|` (pipe) on its own line emits a single space, for adding
|
|
278
|
+
whitespace between sibling elements.
|
|
279
|
+
|
|
280
|
+
## [0.1.0] - 2026-04-05
|
|
281
|
+
|
|
282
|
+
Initial release — the Weave compiler and runtime.
|
|
283
|
+
|
|
284
|
+
### Added
|
|
285
|
+
|
|
286
|
+
- Template syntax compiling `.weave` files to typed TypeScript functions:
|
|
287
|
+
templates, mixins, `extends`/`block` layouts, conditionals, iteration,
|
|
288
|
+
includes, interpolation, buffered expressions, and silent/pass-through
|
|
289
|
+
comments.
|
|
290
|
+
- Runtime helpers: `h`, `doctype`, `Fragment`, `raw`, and single-pass escaping.
|
|
291
|
+
- CLI `compile` command and a `--watch` mode for automatic recompilation.
|
|
292
|
+
- Error messages with source code frames.
|
|
293
|
+
- Source map generation.
|
|
294
|
+
|
|
295
|
+
[Unreleased]: https://codeberg.org/chilts/weave/compare/v0.8.0...HEAD
|
|
296
|
+
[0.8.0]: https://codeberg.org/chilts/weave/compare/v0.7.0...v0.8.0
|
|
297
|
+
[0.7.0]: https://codeberg.org/chilts/weave/compare/v0.6.0...v0.7.0
|
|
298
|
+
[0.6.0]: https://codeberg.org/chilts/weave/compare/v0.5.0...v0.6.0
|
|
299
|
+
[0.5.0]: https://codeberg.org/chilts/weave/compare/v0.4.0...v0.5.0
|
|
300
|
+
[0.4.0]: https://codeberg.org/chilts/weave/compare/v0.3.0...v0.4.0
|
|
301
|
+
[0.3.0]: https://codeberg.org/chilts/weave/compare/v0.2.0...v0.3.0
|
|
302
|
+
[0.2.0]: https://codeberg.org/chilts/weave/compare/v0.1.0...v0.2.0
|
|
303
|
+
[0.1.0]: https://codeberg.org/chilts/weave/src/tag/v0.1.0
|
package/ReadMe.md
CHANGED
|
@@ -12,7 +12,7 @@ Pug is great. Weave takes the same indentation-based syntax and makes it TypeScr
|
|
|
12
12
|
|
|
13
13
|
- **Type-safe templates** — template parameters are real TypeScript types
|
|
14
14
|
- **Type-safe mixins** — positional args with TS type annotations
|
|
15
|
-
- **Tailwind-friendly** — class names with colons (`.md:flex`, `.hover:bg-blue-500`) just work
|
|
15
|
+
- **Tailwind-friendly** — class names with colons (`.md:flex`, `.hover:bg-blue-500`) just work, and dots escape (`.gap-1\.5`)
|
|
16
16
|
- **Single-pass escaping** — values are escaped once at render time, never pre-escaped
|
|
17
17
|
- **No runtime template engine** — compiled output is plain TypeScript
|
|
18
18
|
|
|
@@ -66,9 +66,40 @@ export function simple({ title, content }: SimpleProps) {
|
|
|
66
66
|
|
|
67
67
|
Now `tsc` catches type errors, your IDE gives you autocomplete, and calling `simple({ title: "Hello", content: "World" })` returns HTML.
|
|
68
68
|
|
|
69
|
+
### Type-checking templates
|
|
70
|
+
|
|
71
|
+
`tsc` reports errors against the compiled `.ts`, which you never edit.
|
|
72
|
+
`weave check` compiles the templates, type-checks your whole project with your
|
|
73
|
+
`tsconfig.json`, and reports each error in a view at its `.weave` line:
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
$ npx weave check views/ lib/views/
|
|
77
|
+
weave: views/page.weave:3:5: Property 'nmae' does not exist on type '{ name: string; }'. TS2339
|
|
78
|
+
3 | h1= user.nmae
|
|
79
|
+
| ^
|
|
80
|
+
|
|
81
|
+
Found 1 error.
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
It exits 1 on any error, so it can replace `tsc --noEmit` in CI, and the
|
|
85
|
+
`file:line:col:` format works with Emacs `compile`. It uses your project's
|
|
86
|
+
`typescript` package (install it as a dev dependency) and the nearest
|
|
87
|
+
`tsconfig.json`, or the one given with `--project <path>`. Errors point at the
|
|
88
|
+
exact token inside an expression, attribute value, condition, parameter or
|
|
89
|
+
argument, and at the start of the node for code Weave generated around them.
|
|
90
|
+
|
|
91
|
+
To map diagnostics yourself (from another tool or build step), use
|
|
92
|
+
`mapDiagnostic({ file, line, col, message })`, which translates a position in a
|
|
93
|
+
compiled `.ts` file to its `.weave` source via the `.ts.map` beside it, and
|
|
94
|
+
`formatDiagnostic()`, which prints the result with a code frame. Neither
|
|
95
|
+
depends on the `typescript` package.
|
|
96
|
+
|
|
69
97
|
## Usage
|
|
70
98
|
|
|
71
|
-
Templates are just functions that return
|
|
99
|
+
Templates are just functions that return HTML, so they work with any framework.
|
|
100
|
+
Every template and mixin returns a `SafeHtml`, so its output isn't escaped
|
|
101
|
+
again when nested in another template. Call `String()` on it to get the HTML
|
|
102
|
+
string:
|
|
72
103
|
|
|
73
104
|
```typescript
|
|
74
105
|
import express from 'express'
|
|
@@ -77,12 +108,56 @@ import { simple } from './lib/views/simple.js'
|
|
|
77
108
|
const app = express()
|
|
78
109
|
|
|
79
110
|
app.get('/', (req, res) => {
|
|
80
|
-
res.send(simple({ title: "Hello", content: "World" }))
|
|
111
|
+
res.send(String(simple({ title: "Hello", content: "World" })))
|
|
81
112
|
})
|
|
82
113
|
|
|
83
114
|
app.listen(3000)
|
|
84
115
|
```
|
|
85
116
|
|
|
117
|
+
### Server-rendered apps and htmx
|
|
118
|
+
|
|
119
|
+
Weave is a good fit for apps that render HTML on the server and swap pieces of
|
|
120
|
+
it into the page — [htmx](https://htmx.org), Turbo, Unpoly, or plain `fetch`.
|
|
121
|
+
|
|
122
|
+
- **Every template and mixin is an exported function.** Rendering a fragment
|
|
123
|
+
for a swap is just calling the mixin; there is no partial-rendering API.
|
|
124
|
+
- **The page and its fragments share one definition.** The page loops over
|
|
125
|
+
`+todoItem(todo)`, and the endpoint returns `todoItem(todo)`, so the markup
|
|
126
|
+
can't drift apart.
|
|
127
|
+
- **No runtime engine, zero dependencies.** The runtime is a few small helpers
|
|
128
|
+
with no Node-specific imports, so compiled views run on Node, Deno, Bun and
|
|
129
|
+
Cloudflare Workers alike.
|
|
130
|
+
|
|
131
|
+
A mixin for one row:
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
mixin todoItem(todo: Todo)
|
|
135
|
+
li.todo(id=`todo-${todo.id}` ...attrs)
|
|
136
|
+
input(
|
|
137
|
+
type="checkbox"
|
|
138
|
+
checked=todo.done
|
|
139
|
+
hx-post=`/todos/${todo.id}/toggle`
|
|
140
|
+
hx-target=`#todo-${todo.id}`
|
|
141
|
+
hx-swap="outerHTML"
|
|
142
|
+
)
|
|
143
|
+
span= todo.text
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
The page uses it in a loop, and the toggle endpoint returns just that row:
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
import { todoItem } from './lib/views/components/todo.js'
|
|
150
|
+
|
|
151
|
+
// POST /todos/:id/toggle
|
|
152
|
+
todo.done = !todo.done
|
|
153
|
+
return new Response(String(todoItem(todo)), {
|
|
154
|
+
headers: { 'content-type': 'text/html; charset=utf-8' },
|
|
155
|
+
})
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
See [`examples/htmx/`](examples/htmx/) for the full page, the mixin, and a
|
|
159
|
+
web-standard `fetch` handler serving both.
|
|
160
|
+
|
|
86
161
|
## Syntax
|
|
87
162
|
|
|
88
163
|
Weave's syntax will be familiar if you've used Pug.
|
|
@@ -97,6 +172,14 @@ p.intro This is a paragraph with a class
|
|
|
97
172
|
a(href="/about" class="nav-link") About
|
|
98
173
|
```
|
|
99
174
|
|
|
175
|
+
A `.` always starts a new class, so a class name that contains a dot — like Tailwind's `gap-1.5` — must escape it with a backslash:
|
|
176
|
+
|
|
177
|
+
```pug
|
|
178
|
+
.flex.gap-1\.5.px-2\.5
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
An unescaped `.gap-1.5` would split into `gap-1` and a stray `5`, so a class that is a bare number is rejected with an error pointing at the fix.
|
|
182
|
+
|
|
100
183
|
### Attributes
|
|
101
184
|
|
|
102
185
|
Attributes live inside parentheses and are separated by spaces. **Every attribute must have a value** — there is no valueless shorthand:
|
|
@@ -115,7 +198,7 @@ button(class=`btn-${variant}`)= label
|
|
|
115
198
|
|
|
116
199
|
#### Boolean attributes
|
|
117
200
|
|
|
118
|
-
Write the boolean explicitly. The runtime renders `true` as a bare attribute and omits the attribute entirely when the value is `false` or `
|
|
201
|
+
Write the boolean explicitly. The runtime renders `true` as a bare attribute and omits the attribute entirely when the value is `false`, `undefined` or `null`:
|
|
119
202
|
|
|
120
203
|
```
|
|
121
204
|
input(type="checkbox" checked=true) → <input type="checkbox" checked>
|
|
@@ -125,6 +208,34 @@ input(type="checkbox" checked=isActive) → omitted when isActive is false
|
|
|
125
208
|
|
|
126
209
|
Avoid a quoted `checked="false"` — in HTML any attribute that is present counts as true, so that renders as checked.
|
|
127
210
|
|
|
211
|
+
#### Combining shorthand classes with `class=`
|
|
212
|
+
|
|
213
|
+
Shorthand classes and a `class=` value are merged into one attribute. The shorthand classes are always kept; a value of `undefined`, `null`, `false` or `""` just adds nothing:
|
|
214
|
+
|
|
215
|
+
```
|
|
216
|
+
div.pt-4(class="mt-2") → <div class="pt-4 mt-2">
|
|
217
|
+
div.pt-4(class=undefined) → <div class="pt-4">
|
|
218
|
+
div.btn(class=(active ? "active" : undefined)) → <div class="btn"> when active is false
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
#### Class arrays and objects, style objects
|
|
222
|
+
|
|
223
|
+
`class=` also takes an array or an object, which suits conditional Tailwind classes. Arrays may nest and drop `null`, `undefined`, `false` and `""`; an object keeps each key whose value is truthy. Both merge with shorthand classes:
|
|
224
|
+
|
|
225
|
+
```
|
|
226
|
+
div(class=(["a", "b"])) → <div class="a b">
|
|
227
|
+
li.item(class=({ active: isActive })) → <li class="item active"> when isActive is true
|
|
228
|
+
a(class=(["link", { "md:flex": wide }])) → <a class="link md:flex"> when wide is true
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
`style=` takes an object of properties. camelCase keys become kebab-case, `--custom` properties are kept as written, and `null`, `undefined`, `false` and `""` values are dropped:
|
|
232
|
+
|
|
233
|
+
```
|
|
234
|
+
div(style=({ marginTop: 0, "--gap": gap })) → <div style="margin-top:0;--gap:1rem">
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
An array or object that yields nothing omits the attribute entirely. Numbers are not given units, so write `"4px"` rather than `4` where CSS needs one.
|
|
238
|
+
|
|
128
239
|
#### Multi-token values must be wrapped
|
|
129
240
|
|
|
130
241
|
Because a space separates one attribute from the next, a bare (unquoted) value has to be a single token. Wrap anything longer in parentheses or backticks:
|
|
@@ -188,14 +299,28 @@ else
|
|
|
188
299
|
p Welcome, visitor
|
|
189
300
|
```
|
|
190
301
|
|
|
302
|
+
`unless cond` is a guard: its block renders when `cond` is falsy, the same as
|
|
303
|
+
`if !(cond)`. It takes no `else` or `else if` — `unless … else` is a double
|
|
304
|
+
negative, so write an `if` with the branches swapped instead:
|
|
305
|
+
|
|
306
|
+
```
|
|
307
|
+
unless user.verified
|
|
308
|
+
p Please confirm your email address.
|
|
309
|
+
```
|
|
310
|
+
|
|
191
311
|
### Iteration
|
|
192
312
|
|
|
193
313
|
```
|
|
194
314
|
for ( const post of posts )
|
|
195
315
|
h2= post.title
|
|
196
316
|
p= post.content
|
|
317
|
+
else
|
|
318
|
+
p No posts yet.
|
|
197
319
|
```
|
|
198
320
|
|
|
321
|
+
The optional `else` renders when the collection is empty. Any iterable works —
|
|
322
|
+
arrays, `Set`, `Map`, generators, or `xs.entries()` — and it is consumed once.
|
|
323
|
+
|
|
199
324
|
### Mixins
|
|
200
325
|
|
|
201
326
|
Mixins use positional args. An implicit `attrs` parameter captures any extra attributes passed at the call site.
|
|
@@ -215,6 +340,42 @@ Usage:
|
|
|
215
340
|
+button("Delete", "secondary")(class="ml-4" data-confirm="true")
|
|
216
341
|
```
|
|
217
342
|
|
|
343
|
+
#### Call-site content
|
|
344
|
+
|
|
345
|
+
A bare `block` inside a mixin renders whatever is indented under the call,
|
|
346
|
+
so one mixin can wrap different content each time. Lines indented under
|
|
347
|
+
`block` are the default when the call has none, and `if block` tests for it:
|
|
348
|
+
|
|
349
|
+
```
|
|
350
|
+
mixin card(title: string)
|
|
351
|
+
.card(...attrs)
|
|
352
|
+
h2= title
|
|
353
|
+
block
|
|
354
|
+
p.muted Nothing to show.
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
```
|
|
358
|
+
+card("Profile")(class="wide")
|
|
359
|
+
p= user.name
|
|
360
|
+
p= user.email
|
|
361
|
+
+card("Recent activity")
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
The content is passed as a callback after `attrs`, so the call above compiles
|
|
365
|
+
to `card("Profile", { class: "wide" }, () => ...)`. Named blocks (`block
|
|
366
|
+
content`) are layout slots and are not allowed inside a mixin. See
|
|
367
|
+
[`examples/mixin-block/`](examples/mixin-block/).
|
|
368
|
+
|
|
369
|
+
One line of content can go on the call's own line instead, just as an
|
|
370
|
+
element takes inline text or `= expr`:
|
|
371
|
+
|
|
372
|
+
```
|
|
373
|
+
+btn("save") Save
|
|
374
|
+
+btn("save")(class="wide")= label
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
A call takes its content inline or indented, not both.
|
|
378
|
+
|
|
218
379
|
### Layouts with extends and blocks
|
|
219
380
|
|
|
220
381
|
A layout defines named blocks as content slots:
|
|
@@ -232,6 +393,17 @@ template layout({ title }: LayoutProps)
|
|
|
232
393
|
p © 2024
|
|
233
394
|
```
|
|
234
395
|
|
|
396
|
+
The compiled layout takes its slots as a second `blocks` argument, typed by an
|
|
397
|
+
interface the compiler generates from the `block` names — you don't write it:
|
|
398
|
+
|
|
399
|
+
```ts
|
|
400
|
+
export interface LayoutBlocks {
|
|
401
|
+
content?: () => unknown
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export function layout({ title }: LayoutProps, blocks: LayoutBlocks = {}) {
|
|
405
|
+
```
|
|
406
|
+
|
|
235
407
|
A page extends the layout, filling in the blocks. Data is passed explicitly — nothing is implicitly inherited:
|
|
236
408
|
|
|
237
409
|
```
|
|
@@ -244,6 +416,32 @@ template home({ title, welcomeMessage }: HomeProps)
|
|
|
244
416
|
|
|
245
417
|
Blocks can have default content in the layout. If a page doesn't override a block, the default renders.
|
|
246
418
|
|
|
419
|
+
### Long parameter lists
|
|
420
|
+
|
|
421
|
+
The parentheses after `template`, `mixin`, `extends`, `for` and a mixin call hold
|
|
422
|
+
TypeScript, which Weave copies through untouched. They can wrap over several lines
|
|
423
|
+
however TypeScript usually wraps: the group simply stays open until its parentheses
|
|
424
|
+
balance.
|
|
425
|
+
|
|
426
|
+
```
|
|
427
|
+
template dashboard({
|
|
428
|
+
title,
|
|
429
|
+
user,
|
|
430
|
+
}: DashboardProps)
|
|
431
|
+
h1= title
|
|
432
|
+
|
|
433
|
+
+card(
|
|
434
|
+
post.title,
|
|
435
|
+
post.summary,
|
|
436
|
+
)(class="mt-4")
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
Unlike a long attribute list there is no required shape, but continuation lines must
|
|
440
|
+
be indented under the line that opens the group. The closing line may sit level with
|
|
441
|
+
it if it starts with a closing bracket, as `}: DashboardProps)` does. So a forgotten
|
|
442
|
+
`)` is reported where the indentation drops back. A string must close on the line it
|
|
443
|
+
starts on, and `//` comments are allowed but left out of the compiled output.
|
|
444
|
+
|
|
247
445
|
### Includes
|
|
248
446
|
|
|
249
447
|
Include another `.weave` file to use its mixins:
|
|
@@ -258,6 +456,12 @@ template actionsPage({ title }: ActionsPageProps)
|
|
|
258
456
|
+button("Save", "primary")
|
|
259
457
|
```
|
|
260
458
|
|
|
459
|
+
An included file can define any number of mixins. Weave reads it and imports
|
|
460
|
+
only the ones the template calls, so `include ./partials` followed by
|
|
461
|
+
`+badge(...)` and `+chip(...)` compiles to
|
|
462
|
+
`import { badge, chip } from './partials.js'`. Calling a mixin that is neither
|
|
463
|
+
defined in the file nor provided by an include is a compile error.
|
|
464
|
+
|
|
261
465
|
### Text content
|
|
262
466
|
|
|
263
467
|
Use `|` (pipe) to add literal text as a child of an element:
|
|
@@ -280,7 +484,7 @@ This renders as: `Hello <strong>user@example.com</strong>.` — with a space bet
|
|
|
280
484
|
|
|
281
485
|
### Inline elements
|
|
282
486
|
|
|
283
|
-
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.
|
|
487
|
+
Weave does not support inline element syntax (e.g. Pug's `#[strong bold]` tag interpolation), and `#[` in text is a compile error. To mix elements within text, place each element on its own indented line and use `|` for spacing and text fragments. To show a literal `#[` (or `!{`), use a string expression: `p= "Use #[0] for the first item"`.
|
|
284
488
|
|
|
285
489
|
### Raw blocks
|
|
286
490
|
|
|
@@ -319,17 +523,65 @@ Standard TypeScript imports pass through to the compiled output:
|
|
|
319
523
|
import type { BlogPost } from '../types/models.js'
|
|
320
524
|
```
|
|
321
525
|
|
|
526
|
+
## Coming from Pug
|
|
527
|
+
|
|
528
|
+
Most Pug templates read the same in Weave, but some Pug features are left out
|
|
529
|
+
on purpose. Each one below is a compile error that names the Weave way to
|
|
530
|
+
write it, rather than being silently mis-rendered.
|
|
531
|
+
|
|
532
|
+
| Pug | Weave |
|
|
533
|
+
|---|---|
|
|
534
|
+
| `- const x = 1` (unbuffered code) | Templates hold no statements. Compute values in TypeScript before rendering, or use `tag= expr` |
|
|
535
|
+
| `case` / `when` / `default` | `if` / `else if` / `else` |
|
|
536
|
+
| `each x in xs`, `while` | `for ( const x of xs )`; for keys and values, `for ( const [k, v] of Object.entries(obj) )` |
|
|
537
|
+
| `p!= html` | `p= raw(html)` |
|
|
538
|
+
| `p Hi !{html}` | `p Hi #{raw(html)}` |
|
|
539
|
+
| `a(href!=url)` | Attribute values are always escaped |
|
|
540
|
+
| `p.` (block text) | `p!` for raw text, or `\|` lines for escaped text |
|
|
541
|
+
| `li: a Home` (block expansion) | Put the child on its own indented line |
|
|
542
|
+
| `#[strong bold]` (tag interpolation) | Put each element on its own line; see [Inline elements](#inline-elements) |
|
|
543
|
+
| `foo/` (explicit self-closing) | Void elements like `br` and `img` close themselves |
|
|
544
|
+
| `div&attributes(obj)` | `div(...obj)` |
|
|
545
|
+
| `input(disabled)` | `input(disabled=true)`; see [Boolean attributes](#boolean-attributes) |
|
|
546
|
+
| `extends ./layout` | `extends layout({ data })` calls the layout's template function and passes its data |
|
|
547
|
+
|
|
548
|
+
**Filters** (`:markdown-it`, `include:markdown ./post.md`) are not supported
|
|
549
|
+
and are not planned. Render the content in TypeScript and pass it in as a
|
|
550
|
+
`raw()` value.
|
|
551
|
+
|
|
552
|
+
**`block append` / `block prepend`** are not supported. A page replaces a
|
|
553
|
+
layout's block completely. When pages need to add to what the layout already
|
|
554
|
+
renders, give the layout a second, empty slot:
|
|
555
|
+
|
|
556
|
+
```
|
|
557
|
+
head
|
|
558
|
+
block scripts
|
|
559
|
+
script(src="/app.js")
|
|
560
|
+
block pageScripts
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
**Dynamic mixin calls** (`+#{name}()`) are not supported. Mixins are ordinary
|
|
564
|
+
functions, so you can choose one at runtime with a buffered expression and
|
|
565
|
+
pass `{}` for its `attrs`: `div= mixins[kind](item, {})`.
|
|
566
|
+
|
|
567
|
+
**Implicit scope.** In Pug, included and extended templates can see their
|
|
568
|
+
caller's variables. In Weave, data only travels explicitly: through template
|
|
569
|
+
params, `extends layout({ ... })` and mixin args.
|
|
570
|
+
|
|
322
571
|
## Development
|
|
323
572
|
|
|
324
573
|
```bash
|
|
325
574
|
npm run build # compile TypeScript to dist/
|
|
326
|
-
npm test # run tests (vitest)
|
|
327
|
-
npm run lint #
|
|
575
|
+
npm test # lint, then type-check, then run tests (vitest)
|
|
576
|
+
npm run lint # lint with ESLint
|
|
577
|
+
npm run typecheck # type-check without emitting (tsc --noEmit)
|
|
328
578
|
```
|
|
329
579
|
|
|
330
580
|
## Status
|
|
331
581
|
|
|
332
|
-
Weave is
|
|
582
|
+
Weave is usable today: the compiler, the `weave` CLI (with watch mode and source maps) and the runtime helpers are published on npm. It is still pre-1.0, so the syntax may change in a minor release — breaking changes are called out in [CHANGELOG.md](CHANGELOG.md).
|
|
583
|
+
|
|
584
|
+
The `examples/` directory pairs each `.weave` template with its compiled `.ts`. Every example is compiled in the test suite and compared byte-for-byte against that file, so they double as a reference for what the compiler emits.
|
|
333
585
|
|
|
334
586
|
## Editor Support
|
|
335
587
|
|
|
@@ -341,6 +593,7 @@ See [CHANGELOG.md](CHANGELOG.md) for the full details of each release.
|
|
|
341
593
|
|
|
342
594
|
| Version | Date | Highlights |
|
|
343
595
|
|---------|------------|------------|
|
|
596
|
+
| 0.7.0 | 2026-10-20 | Multi-line parameter lists; escaped dots in class shorthand; `include` imports the mixins it provides; context-aware Emacs mode |
|
|
344
597
|
| 0.6.0 | 2026-10-19 | Multi-line attribute lists; every attribute requires a value (breaking); watch-mode fixes |
|
|
345
598
|
| 0.5.0 | 2026-06-11 | Type-safe multi-level `extends` and `doctype` |
|
|
346
599
|
| 0.4.0 | 2026-04-05 | Templates return strings directly |
|