@chilts/weave 0.5.0 → 0.7.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 ADDED
@@ -0,0 +1,178 @@
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.7.0] - 2026-10-20
12
+
13
+ ### Added
14
+
15
+ - **Escaped dots in class and id shorthand.** `.gap-1\.5` is the single class
16
+ `gap-1.5`, so Tailwind's fractional spacing classes work as shorthand.
17
+ - **Multi-line parameter lists.** The parentheses after `template`, `mixin`,
18
+ `extends`, `for` and a mixin call may now wrap over several lines, staying
19
+ open until they balance, so a long destructured signature can be written
20
+ the way TypeScript is normally wrapped. Continuation lines must be indented
21
+ under the opening line (a closing line such as `}: Props)` may sit level with
22
+ it). A string inside must close on its own line, and `//` comments are
23
+ dropped from the output. The compiled code keeps the wrapped layout, adding
24
+ `attrs`/`blocks` on a line of their own when the `)` was on its own line.
25
+ - **Emacs mode understands context.** `weave-mode` now follows multi-line
26
+ attribute and parameter lists and raw (`tag!`) blocks when highlighting and
27
+ indenting, highlights attribute names, aligns `else` with its `if`, and
28
+ cycles outwards on repeated TAB. An apostrophe in text no longer opens a
29
+ string, `//` in a URL is no longer a comment, and `.`/`#` are only coloured
30
+ as a class or id where they really are one.
31
+
32
+ ### Fixed
33
+
34
+ - A dotted class such as `.gap-1.5` was silently split into `gap-1` plus a
35
+ stray class `5`. A shorthand class that is a bare number is now a
36
+ `LexerError` that names the split and suggests escaping the dot.
37
+ - `include ./partials` imported a binding named after the file (`partials`)
38
+ instead of the mixins the template calls, so a file defining several
39
+ mixins, or one not named after its file, compiled to code that failed in
40
+ `tsc`. The compiler now reads the included file and imports only the mixins
41
+ called (`import { badge, chip } from './partials.js'`). Calling a mixin that
42
+ no include or local definition provides is a `CodegenError`. For API users,
43
+ `compile()`, `compileWithSourceMap()` and `generate()` take an optional
44
+ `resolveInclude`. Without it, a lone include provides every called mixin
45
+ that isn't defined locally, and several includes are an error.
46
+ - Shorthand classes combined with a non-literal `class=` value leaked the
47
+ value's string form: `div.pt-4(class=undefined)` rendered
48
+ `class="pt-4 undefined"`, as did the usual conditional-class pattern
49
+ `class=(on ? "x" : undefined)` whenever it was off. Non-literal values are
50
+ now merged at render time by a new `mergeClass()` runtime helper, which
51
+ drops `undefined`, `null`, `false` and `""`. An attribute whose value is
52
+ `null` is now omitted instead of rendering as `"null"`.
53
+ - A template literal with a `${...}` interpolation inside a mixin call's
54
+ arguments, or a `template`/`mixin` parameter list, failed with
55
+ "Unterminated parentheses". A string containing a brace inside the
56
+ interpolation is also handled.
57
+ - Error locations and source maps now point at the start of a token rather
58
+ than just past its end — for example, a mixin call is located at its `+`
59
+ instead of its argument list.
60
+ - In watch mode, changing an included file now recompiles the templates that
61
+ include it, so a newly added mixin is picked up without touching each page.
62
+ The start-up reconcile likewise recompiles a template whose include is
63
+ newer than its output.
64
+ - Rebuilding with `--no-sourcemap` now removes a `.ts.map` left over from an
65
+ earlier build instead of leaving a map that no longer matches its `.ts`.
66
+
67
+ ## [0.6.0] - 2026-10-19
68
+
69
+ ### Added
70
+
71
+ - **Multi-line attribute lists.** End a line with the opening `(` to spread an
72
+ element's attributes over several lines — one attribute per line, indented
73
+ under the element, closed by a `)` on a line of its own. Works for mixin call
74
+ attributes too. Anything after the closing `)` is read as trailing content.
75
+ Attribute values follow exactly the same rules as the inline form.
76
+
77
+ ### Changed
78
+
79
+ - **Breaking: every attribute must have a value.** The Pug-style valueless
80
+ boolean shorthand has been removed — `input(disabled)` no longer compiles.
81
+ Boolean attributes are now written with an explicit boolean value:
82
+ `disabled=true`, `disabled=false`, or an expression `disabled=isActive`
83
+ (the runtime renders a bare attribute for `true` and omits it for `false`
84
+ or `undefined`). Do not use a quoted `disabled="false"` — in HTML any
85
+ attribute that is present counts as true.
86
+ - A bare (unquoted) attribute value must be a single token. Multi-token
87
+ expressions such as a ternary or string concatenation must be wrapped in
88
+ parentheses (`class=(wide ? "a" : "b")`) or backticks
89
+ (``class=`${wide ? "a" : "b"}` ``), because a space separates one
90
+ attribute from the next.
91
+
92
+ ### Fixed
93
+
94
+ - The lexer no longer hangs and aborts the process (SIGABRT) on a multi-token
95
+ bare attribute value; it now raises a clear `LexerError` with line and
96
+ column, pointing you to wrap the expression.
97
+ - `main(class=a b)` is rejected with a message offering both readings
98
+ (`class="a b"` as one value, or `class=a b="..."` as two attributes)
99
+ instead of silently parsing as `class="a"` plus a boolean `b`.
100
+ - An attribute list that ran off the end of its line was accepted as if it had
101
+ been closed, so the following lines were lexed as child elements — attribute
102
+ names became tags and their values rendered as visible text, and the stray
103
+ `)` could leak into the generated TypeScript. It is now a `LexerError`
104
+ anchored on the opening `(`.
105
+ - Watch mode reconciles the output tree on start so edits made while the
106
+ watcher was down are not missed.
107
+ - Watch mode on Linux stopped seeing a template after the first time an editor
108
+ saved it atomically (write a temp file, rename it over the original), so
109
+ later edits were never recompiled. Each directory now has its own watcher,
110
+ which survives atomic saves, and subdirectories created while watching are
111
+ picked up — including any templates already written into them.
112
+ - Deleting a `.weave` template now removes its compiled `.ts` from the output
113
+ directory (in one-shot builds and in watch mode). The output directory is
114
+ treated as owned by Weave: any `.ts` with no matching `.weave` is removed.
115
+ - A mixin, or a block's default content, with more than one root element
116
+ referenced `Fragment` without importing it, so the compiled file failed to
117
+ type-check. `Fragment` is now imported whenever it is used.
118
+
119
+ ### Removed
120
+
121
+ - Source maps are no longer included in the published npm bundle.
122
+
123
+ ## [0.5.0] - 2026-06-11
124
+
125
+ ### Fixed
126
+
127
+ - Multi-level `extends` and `doctype` now generate type-safe output.
128
+
129
+ ## [0.4.0] - 2026-04-05
130
+
131
+ ### Changed
132
+
133
+ - Compiled templates return strings directly instead of arrays, simplifying
134
+ the generated output and how templates compose.
135
+
136
+ ## [0.3.0] - 2026-04-05
137
+
138
+ ### Added
139
+
140
+ - Raw block syntax (`tag!`) for passing literal, unescaped content through to
141
+ the output (inline CSS, JavaScript, etc.).
142
+ - Emacs major mode with syntax highlighting, indentation, and comment support.
143
+
144
+ ### Documentation
145
+
146
+ - Added guidance on special characters and the single-pass escaping model to
147
+ the README.
148
+
149
+ ## [0.2.0] - 2026-04-05
150
+
151
+ ### Added
152
+
153
+ - A bare `|` (pipe) on its own line emits a single space, for adding
154
+ whitespace between sibling elements.
155
+
156
+ ## [0.1.0] - 2026-04-05
157
+
158
+ Initial release — the Weave compiler and runtime.
159
+
160
+ ### Added
161
+
162
+ - Template syntax compiling `.weave` files to typed TypeScript functions:
163
+ templates, mixins, `extends`/`block` layouts, conditionals, iteration,
164
+ includes, interpolation, buffered expressions, and silent/pass-through
165
+ comments.
166
+ - Runtime helpers: `h`, `doctype`, `Fragment`, `raw`, and single-pass escaping.
167
+ - CLI `compile` command and a `--watch` mode for automatic recompilation.
168
+ - Error messages with source code frames.
169
+ - Source map generation.
170
+
171
+ [Unreleased]: https://codeberg.org/chilts/weave/compare/v0.7.0...HEAD
172
+ [0.7.0]: https://codeberg.org/chilts/weave/compare/v0.6.0...v0.7.0
173
+ [0.6.0]: https://codeberg.org/chilts/weave/compare/v0.5.0...v0.6.0
174
+ [0.5.0]: https://codeberg.org/chilts/weave/compare/v0.4.0...v0.5.0
175
+ [0.4.0]: https://codeberg.org/chilts/weave/compare/v0.3.0...v0.4.0
176
+ [0.3.0]: https://codeberg.org/chilts/weave/compare/v0.2.0...v0.3.0
177
+ [0.2.0]: https://codeberg.org/chilts/weave/compare/v0.1.0...v0.2.0
178
+ [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
 
@@ -97,6 +97,104 @@ p.intro This is a paragraph with a class
97
97
  a(href="/about" class="nav-link") About
98
98
  ```
99
99
 
100
+ 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:
101
+
102
+ ```pug
103
+ .flex.gap-1\.5.px-2\.5
104
+ ```
105
+
106
+ 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.
107
+
108
+ ### Attributes
109
+
110
+ Attributes live inside parentheses and are separated by spaces. **Every attribute must have a value** — there is no valueless shorthand:
111
+
112
+ ```
113
+ a(href="/about" class="nav-link") About
114
+ input(type="email" name="email")
115
+ ```
116
+
117
+ A value can be a quoted string, a template literal, or a bare TypeScript expression:
118
+
119
+ ```
120
+ a(href=post.author.url)= post.author.title
121
+ button(class=`btn-${variant}`)= label
122
+ ```
123
+
124
+ #### Boolean attributes
125
+
126
+ 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`:
127
+
128
+ ```
129
+ input(type="checkbox" checked=true) → <input type="checkbox" checked>
130
+ input(type="checkbox" checked=false) → <input type="checkbox">
131
+ input(type="checkbox" checked=isActive) → omitted when isActive is false
132
+ ```
133
+
134
+ Avoid a quoted `checked="false"` — in HTML any attribute that is present counts as true, so that renders as checked.
135
+
136
+ #### Combining shorthand classes with `class=`
137
+
138
+ 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:
139
+
140
+ ```
141
+ div.pt-4(class="mt-2") → <div class="pt-4 mt-2">
142
+ div.pt-4(class=undefined) → <div class="pt-4">
143
+ div.btn(class=(active ? "active" : undefined)) → <div class="btn"> when active is false
144
+ ```
145
+
146
+ #### Multi-token values must be wrapped
147
+
148
+ 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:
149
+
150
+ ```
151
+ main(class=(wide ? "px-6" : "mx-auto px-6"))
152
+ main(class=`${wide ? "" : "mx-auto "}px-6`)
153
+ ```
154
+
155
+ An unwrapped multi-token value is a compile error rather than a silent mis-parse. This is also why `main(class=a b)` is rejected: `b` could be part of the value or a separate attribute, so Weave asks you to say which you meant — `class="a b"` or `class=a b="..."`.
156
+
157
+ #### Long attribute lists
158
+
159
+ An attribute list normally lives on one line. To split it over several lines, end
160
+ the line with the opening `(`, put **one attribute per line**, and close with a `)`
161
+ on a line of its own:
162
+
163
+ ```
164
+ button.btn(
165
+ type="button"
166
+ class="rounded px-2"
167
+ aria-pressed=isActive
168
+ ...attrs
169
+ )
170
+ | Toggle theme
171
+ ```
172
+
173
+ The continuation lines have to be indented further than the element, and the
174
+ closing `)` has to start its own line — `aria-pressed=isActive)` is an error. That
175
+ strictness is what makes a forgotten `)` a local error instead of something that
176
+ swallows the rest of the file. Anything after the closing `)` is read as trailing
177
+ content, so `) Toggle theme` works too.
178
+
179
+ Everything else is unchanged: this is a layout choice, not a different grammar.
180
+ Bare values still have to be a single token here, so a ternary still needs its
181
+ wrapping parentheses.
182
+
183
+ A `(` that is *not* at the end of its line must still close on that line:
184
+
185
+ ```
186
+ a(href="/about"
187
+ class="nav-link") ← error: unterminated attribute list
188
+ ```
189
+
190
+ #### Spread
191
+
192
+ `...attrs` forwards a whole set of attributes, most often a mixin's implicit `attrs`:
193
+
194
+ ```
195
+ button.btn(class=`btn-${variant}` ...attrs)= label
196
+ ```
197
+
100
198
  ### Conditionals
101
199
 
102
200
  ```
@@ -164,6 +262,32 @@ template home({ title, welcomeMessage }: HomeProps)
164
262
 
165
263
  Blocks can have default content in the layout. If a page doesn't override a block, the default renders.
166
264
 
265
+ ### Long parameter lists
266
+
267
+ The parentheses after `template`, `mixin`, `extends`, `for` and a mixin call hold
268
+ TypeScript, which Weave copies through untouched. They can wrap over several lines
269
+ however TypeScript usually wraps: the group simply stays open until its parentheses
270
+ balance.
271
+
272
+ ```
273
+ template dashboard({
274
+ title,
275
+ user,
276
+ }: DashboardProps)
277
+ h1= title
278
+
279
+ +card(
280
+ post.title,
281
+ post.summary,
282
+ )(class="mt-4")
283
+ ```
284
+
285
+ Unlike a long attribute list there is no required shape, but continuation lines must
286
+ be indented under the line that opens the group. The closing line may sit level with
287
+ it if it starts with a closing bracket, as `}: DashboardProps)` does. So a forgotten
288
+ `)` is reported where the indentation drops back. A string must close on the line it
289
+ starts on, and `//` comments are allowed but left out of the compiled output.
290
+
167
291
  ### Includes
168
292
 
169
293
  Include another `.weave` file to use its mixins:
@@ -178,6 +302,12 @@ template actionsPage({ title }: ActionsPageProps)
178
302
  +button("Save", "primary")
179
303
  ```
180
304
 
305
+ An included file can define any number of mixins. Weave reads it and imports
306
+ only the ones the template calls, so `include ./partials` followed by
307
+ `+badge(...)` and `+chip(...)` compiles to
308
+ `import { badge, chip } from './partials.js'`. Calling a mixin that is neither
309
+ defined in the file nor provided by an include is a compile error.
310
+
181
311
  ### Text content
182
312
 
183
313
  Use `|` (pipe) to add literal text as a child of an element:
@@ -255,6 +385,20 @@ Weave is in early design. The `examples/` directory contains paired `.weave` and
255
385
 
256
386
  - **Emacs** — [`editors/emacs/`](editors/emacs/) — major mode with syntax highlighting, indentation, and comment support
257
387
 
388
+ ## Releases
389
+
390
+ See [CHANGELOG.md](CHANGELOG.md) for the full details of each release.
391
+
392
+ | Version | Date | Highlights |
393
+ |---------|------------|------------|
394
+ | 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 |
395
+ | 0.6.0 | 2026-10-19 | Multi-line attribute lists; every attribute requires a value (breaking); watch-mode fixes |
396
+ | 0.5.0 | 2026-06-11 | Type-safe multi-level `extends` and `doctype` |
397
+ | 0.4.0 | 2026-04-05 | Templates return strings directly |
398
+ | 0.3.0 | 2026-04-05 | Raw blocks (`tag!`); Emacs mode |
399
+ | 0.2.0 | 2026-04-05 | Bare `\|` emits a space |
400
+ | 0.1.0 | 2026-04-05 | Initial release |
401
+
258
402
  ## License
259
403
 
260
404
  ISC
@@ -1,3 +1,4 @@
1
+ import type { IncludeResolver } from '../codegen/index.js';
1
2
  interface CompileArgs {
2
3
  mode: 'compile';
3
4
  inputPath: string;
@@ -20,12 +21,29 @@ export declare class UsageError extends Error {
20
21
  export declare function parseArgs(argv: string[]): CliArgs;
21
22
  export declare function usage(): string;
22
23
  export declare function discoverFiles(inputDir: string): string[];
24
+ export declare function pruneOrphans(inputDir: string, outputDir: string): void;
23
25
  export declare function formatElapsed(ms: number): string;
24
26
  export declare function formatCodeFrame(source: string, line: number, col: number): string;
25
27
  export declare function formatError(filePath: string, err: unknown, source?: string): string;
28
+ /** Resolves `include` paths against the including file and reads their mixins. */
29
+ export declare function includeResolver(inputFile: string): IncludeResolver;
30
+ /**
31
+ * Absolute paths of the .weave files a template includes. A template that does
32
+ * not lex or parse includes nothing as far as the watcher is concerned — its
33
+ * own compile reports the error.
34
+ */
35
+ export declare function includedFiles(inputFile: string, source: string): string[];
36
+ /**
37
+ * Templates under inputDir (relative paths) that include relPath. An includer's
38
+ * import line is built from the mixins its includes define, so it must be
39
+ * recompiled when one of them changes. Includes do not re-export, so only
40
+ * direct includers are affected.
41
+ */
42
+ export declare function findIncluders(inputDir: string, relPath: string): string[];
26
43
  export declare function compileFile(inputFile: string, outputFile: string, sourcemap?: boolean): void;
27
44
  export declare function compileDir(inputDir: string, outputDir: string, sourcemap?: boolean): boolean;
28
- export declare function watchDir(inputDir: string, outputDir: string, sourcemap?: boolean): void;
45
+ export declare function watchDir(inputDir: string, outputDir: string, sourcemap?: boolean): () => void;
46
+ export declare function reconcile(inputDir: string, outputDir: string, sourcemap: boolean, pending: Map<string, ReturnType<typeof setTimeout>>): void;
29
47
  export declare function main(argv: string[]): void;
30
48
  export {};
31
49
  //# sourceMappingURL=index.d.ts.map