coffeehaml 0.1.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/LICENSE +21 -0
- package/README.md +175 -0
- package/dist/ast.d.ts +107 -0
- package/dist/ast.d.ts.map +1 -0
- package/dist/ast.js +125 -0
- package/dist/ast.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +53 -0
- package/dist/cli.js.map +1 -0
- package/dist/compiler.d.ts +10 -0
- package/dist/compiler.d.ts.map +1 -0
- package/dist/compiler.js +83 -0
- package/dist/compiler.js.map +1 -0
- package/dist/emitter.d.ts +4 -0
- package/dist/emitter.d.ts.map +1 -0
- package/dist/emitter.js +464 -0
- package/dist/emitter.js.map +1 -0
- package/dist/errors.d.ts +3 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +2 -0
- package/dist/errors.js.map +1 -0
- package/dist/expressions.d.ts +10 -0
- package/dist/expressions.d.ts.map +1 -0
- package/dist/expressions.js +70 -0
- package/dist/expressions.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/lexer.d.ts +29 -0
- package/dist/lexer.d.ts.map +1 -0
- package/dist/lexer.js +315 -0
- package/dist/lexer.js.map +1 -0
- package/dist/parser.d.ts +4 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +408 -0
- package/dist/parser.js.map +1 -0
- package/dist/types.d.ts +65 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +12 -0
- package/dist/types.js.map +1 -0
- package/dist/vite-plugin.d.ts +7 -0
- package/dist/vite-plugin.d.ts.map +1 -0
- package/dist/vite-plugin.js +44 -0
- package/dist/vite-plugin.js.map +1 -0
- package/docs/README.md +110 -0
- package/docs/architecture.md +619 -0
- package/docs/ast.md +413 -0
- package/docs/examples.md +719 -0
- package/docs/grammar.md +432 -0
- package/docs/index.html +129 -0
- package/package.json +61 -0
package/docs/ast.md
ADDED
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
# CoffeeHaml AST Design
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The CoffeeHaml AST is a **renderer-agnostic** intermediate representation.
|
|
6
|
+
It models the structure of a CoffeeHaml template without committing to any
|
|
7
|
+
specific output runtime. The first (and primary) backend targets React's
|
|
8
|
+
`jsx-runtime`, but the AST is designed so that emitters for Solid, Vue,
|
|
9
|
+
Mithril, or Web Components can be added later.
|
|
10
|
+
|
|
11
|
+
All AST nodes carry **source location** information for diagnostics and
|
|
12
|
+
source map generation.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Node Interface (TypeScript)
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
interface SourceLocation {
|
|
20
|
+
line: number; // 1-based
|
|
21
|
+
column: number; // 1-based
|
|
22
|
+
offset: number; // 0-based byte offset
|
|
23
|
+
endLine: number;
|
|
24
|
+
endColumn: number;
|
|
25
|
+
endOffset: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface BaseNode {
|
|
29
|
+
type: string;
|
|
30
|
+
loc: SourceLocation;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type Node =
|
|
34
|
+
| Document
|
|
35
|
+
| Element
|
|
36
|
+
| ImplicitDiv
|
|
37
|
+
| Text
|
|
38
|
+
| Output
|
|
39
|
+
| ControlFlow
|
|
40
|
+
| Comment
|
|
41
|
+
| Filter
|
|
42
|
+
| Doctype
|
|
43
|
+
| Fragment;
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Node Types
|
|
49
|
+
|
|
50
|
+
### `Document`
|
|
51
|
+
|
|
52
|
+
The root of every CoffeeHaml AST. Wraps all top-level nodes.
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
interface Document extends BaseNode {
|
|
56
|
+
type: "Document";
|
|
57
|
+
children: Node[];
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
A Document may contain zero or more children. There is no implicit
|
|
62
|
+
wrapper — the emitter decides whether to wrap in a Fragment.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
### `Element`
|
|
67
|
+
|
|
68
|
+
The primary structural node. Represents `%tag` with optional modifiers,
|
|
69
|
+
attributes, and children.
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
interface Element extends BaseNode {
|
|
73
|
+
type: "Element";
|
|
74
|
+
tag: string; // e.g. "div", "svg:circle", "MyComponent"
|
|
75
|
+
classes: string[]; // from .class modifiers
|
|
76
|
+
id: string | null; // from #id modifier
|
|
77
|
+
attributes: Attribute[]; // from {...} or (...)
|
|
78
|
+
inlineText: string | null; // inline text content
|
|
79
|
+
inlineOutput: Expression | null; // trailing = expr on same line
|
|
80
|
+
selfClose: boolean; // %br/
|
|
81
|
+
children: Node[]; // indented children
|
|
82
|
+
isComponent: boolean; // derived: tag[0] is uppercase
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**`isComponent` derivation**: If `tag[0]` matches `[A-Z]`, the element
|
|
87
|
+
refers to a React component (or similar in other frameworks). The emitter
|
|
88
|
+
uses this to decide whether to emit `jsx("div", ...)` or
|
|
89
|
+
`jsx(MyComponent, ...)`.
|
|
90
|
+
|
|
91
|
+
**`classes` and `id`**: Extracted from `.class` and `#id` modifiers.
|
|
92
|
+
These are merged into the `attributes` during emission (as `className`
|
|
93
|
+
and `id` respectively).
|
|
94
|
+
|
|
95
|
+
### `Attribute`
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
type AttributeValue =
|
|
99
|
+
| { kind: "expression"; source: string; parsed: CoffeeAstNode }
|
|
100
|
+
| { kind: "string"; value: string }
|
|
101
|
+
| { kind: "boolean"; value: boolean }
|
|
102
|
+
| { kind: "number"; value: number }
|
|
103
|
+
| { kind: "null" }
|
|
104
|
+
| { kind: "undefined" }
|
|
105
|
+
| { kind: "splat"; source: string }; // for ...spread
|
|
106
|
+
|
|
107
|
+
interface Attribute extends BaseNode {
|
|
108
|
+
type: "Attribute";
|
|
109
|
+
name: string; // property name
|
|
110
|
+
value: AttributeValue;
|
|
111
|
+
shorthand: boolean; // {disabled} → {disabled: true}
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Attributes are parsed from the CoffeeScript object literal inside
|
|
116
|
+
`{...}` or `(...)`. The CoffeeScript compiler provides the parsed
|
|
117
|
+
expression AST for each value.
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
### `ImplicitDiv`
|
|
122
|
+
|
|
123
|
+
Created when a line starts with `.class` or `#id` without a `%tag`.
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
interface ImplicitDiv extends BaseNode {
|
|
127
|
+
type: "ImplicitDiv";
|
|
128
|
+
tag: "div"; // always "div"
|
|
129
|
+
classes: string[];
|
|
130
|
+
id: string | null;
|
|
131
|
+
attributes: Attribute[];
|
|
132
|
+
inlineText: string | null;
|
|
133
|
+
inlineOutput: Expression | null;
|
|
134
|
+
selfClose: false;
|
|
135
|
+
children: Node[];
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
This is effectively an `Element` with `tag: "div"`. It exists as a
|
|
140
|
+
separate node type only for source-accurate diagnostics (so the compiler
|
|
141
|
+
can say "implicit div at line 3" rather than lying about the tag).
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
### `Text`
|
|
146
|
+
|
|
147
|
+
Literal text content. Produced from inline text on elements or from
|
|
148
|
+
raw text lines.
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
interface Text extends BaseNode {
|
|
152
|
+
type: "Text";
|
|
153
|
+
value: string; // the text content
|
|
154
|
+
htmlSafe: boolean; // true if from ==, false if from =
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
### `Output`
|
|
161
|
+
|
|
162
|
+
Represents `= expression` (escaped) or `== expression` (raw).
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
interface Output extends BaseNode {
|
|
166
|
+
type: "Output";
|
|
167
|
+
expression: Expression; // parsed CoffeeScript
|
|
168
|
+
escape: boolean; // true for =, false for ==
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
### `Expression`
|
|
175
|
+
|
|
176
|
+
Wraps a CoffeeScript expression with its raw source and parsed AST.
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
interface Expression extends BaseNode {
|
|
180
|
+
type: "Expression";
|
|
181
|
+
source: string; // raw CoffeeScript source
|
|
182
|
+
parsed: CoffeeAstNode; // CoffeeScript AST (foreign)
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
The `parsed` field holds the CoffeeScript compiler's AST for the
|
|
187
|
+
expression. The emitter compiles this to JavaScript and embeds it
|
|
188
|
+
in the output.
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
### `ControlFlow`
|
|
193
|
+
|
|
194
|
+
Represents structural control flow: `- if`, `- for`, `- while`, `- else`,
|
|
195
|
+
and arbitrary CoffeeScript statements.
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
type ControlKind =
|
|
199
|
+
| "if"
|
|
200
|
+
| "unless"
|
|
201
|
+
| "else"
|
|
202
|
+
| "else_if"
|
|
203
|
+
| "for"
|
|
204
|
+
| "while"
|
|
205
|
+
| "statement"; // arbitrary CoffeeScript
|
|
206
|
+
|
|
207
|
+
interface ControlFlow extends BaseNode {
|
|
208
|
+
type: "ControlFlow";
|
|
209
|
+
kind: ControlKind;
|
|
210
|
+
expression: Expression | null; // null for `else`
|
|
211
|
+
pattern: Pattern | null; // for `for`: destructuring pattern
|
|
212
|
+
iterator: Expression | null; // for `for`: the collection
|
|
213
|
+
body: Node[]; // indented children
|
|
214
|
+
alternate: ControlFlow | null; // chained else/else if
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
**Chaining**: `- if` / `- else if` / `- else` form a chain via `alternate`:
|
|
219
|
+
|
|
220
|
+
```
|
|
221
|
+
- if a ControlFlow { kind: "if", expr: a, body: [...],
|
|
222
|
+
%p A alternate: ControlFlow { kind: "else", body: [...] } }
|
|
223
|
+
- else
|
|
224
|
+
%p B
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
**For loops**: The `pattern` and `iterator` fields capture the
|
|
228
|
+
CoffeeScript for-comprehension structure. The `body` elements become
|
|
229
|
+
the comprehension body.
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
### `Comment`
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
interface Comment extends BaseNode {
|
|
237
|
+
type: "Comment";
|
|
238
|
+
kind: "haml" | "html";
|
|
239
|
+
text: string;
|
|
240
|
+
children: Node[]; // nested content (rare)
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
### `Filter`
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
interface Filter extends BaseNode {
|
|
250
|
+
type: "Filter";
|
|
251
|
+
name: string; // e.g. "css", "javascript", "markdown"
|
|
252
|
+
content: string; // raw filter content (dedented)
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
### `Doctype`
|
|
259
|
+
|
|
260
|
+
```ts
|
|
261
|
+
interface Doctype extends BaseNode {
|
|
262
|
+
type: "Doctype";
|
|
263
|
+
value: string; // e.g. "html", "5", ""
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
### `Fragment`
|
|
270
|
+
|
|
271
|
+
Represents an explicit fragment wrapper (for cases where multiple
|
|
272
|
+
siblings need a key or where the user explicitly groups nodes). Not
|
|
273
|
+
yet in the grammar; reserved for future use.
|
|
274
|
+
|
|
275
|
+
```ts
|
|
276
|
+
interface Fragment extends BaseNode {
|
|
277
|
+
type: "Fragment";
|
|
278
|
+
children: Node[];
|
|
279
|
+
key: Expression | null;
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## AST Construction Example
|
|
286
|
+
|
|
287
|
+
Given:
|
|
288
|
+
|
|
289
|
+
```haml
|
|
290
|
+
%div.container#main{style: {color: "red"}}
|
|
291
|
+
%h1
|
|
292
|
+
= pageTitle
|
|
293
|
+
- for item in items
|
|
294
|
+
%ItemCard{item: item}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Produces:
|
|
298
|
+
|
|
299
|
+
```json
|
|
300
|
+
{
|
|
301
|
+
"type": "Document",
|
|
302
|
+
"children": [
|
|
303
|
+
{
|
|
304
|
+
"type": "Element",
|
|
305
|
+
"tag": "div",
|
|
306
|
+
"classes": ["container"],
|
|
307
|
+
"id": "main",
|
|
308
|
+
"attributes": [
|
|
309
|
+
{
|
|
310
|
+
"type": "Attribute",
|
|
311
|
+
"name": "style",
|
|
312
|
+
"value": { "kind": "expression", "source": "{color: \"red\"}", "parsed": null }
|
|
313
|
+
}
|
|
314
|
+
],
|
|
315
|
+
"inlineText": null,
|
|
316
|
+
"inlineOutput": null,
|
|
317
|
+
"selfClose": false,
|
|
318
|
+
"isComponent": false,
|
|
319
|
+
"children": [
|
|
320
|
+
{
|
|
321
|
+
"type": "Element",
|
|
322
|
+
"tag": "h1",
|
|
323
|
+
"classes": [],
|
|
324
|
+
"id": null,
|
|
325
|
+
"attributes": [],
|
|
326
|
+
"inlineText": null,
|
|
327
|
+
"inlineOutput": null,
|
|
328
|
+
"selfClose": false,
|
|
329
|
+
"isComponent": false,
|
|
330
|
+
"children": [
|
|
331
|
+
{
|
|
332
|
+
"type": "Output",
|
|
333
|
+
"expression": { "type": "Expression", "source": "pageTitle", "parsed": null },
|
|
334
|
+
"escape": true
|
|
335
|
+
}
|
|
336
|
+
]
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
"type": "ControlFlow",
|
|
340
|
+
"kind": "for",
|
|
341
|
+
"expression": null,
|
|
342
|
+
"pattern": { "type": "Pattern", "names": ["item"] },
|
|
343
|
+
"iterator": { "type": "Expression", "source": "items", "parsed": null },
|
|
344
|
+
"body": [
|
|
345
|
+
{
|
|
346
|
+
"type": "Element",
|
|
347
|
+
"tag": "ItemCard",
|
|
348
|
+
"classes": [],
|
|
349
|
+
"id": null,
|
|
350
|
+
"attributes": [
|
|
351
|
+
{
|
|
352
|
+
"type": "Attribute",
|
|
353
|
+
"name": "item",
|
|
354
|
+
"value": { "kind": "expression", "source": "item", "parsed": null }
|
|
355
|
+
}
|
|
356
|
+
],
|
|
357
|
+
"inlineText": null,
|
|
358
|
+
"inlineOutput": null,
|
|
359
|
+
"selfClose": false,
|
|
360
|
+
"isComponent": true,
|
|
361
|
+
"children": []
|
|
362
|
+
}
|
|
363
|
+
],
|
|
364
|
+
"alternate": null
|
|
365
|
+
}
|
|
366
|
+
]
|
|
367
|
+
}
|
|
368
|
+
]
|
|
369
|
+
}
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
---
|
|
373
|
+
|
|
374
|
+
## Traversal
|
|
375
|
+
|
|
376
|
+
The AST supports a generic visitor pattern:
|
|
377
|
+
|
|
378
|
+
```ts
|
|
379
|
+
interface Visitor<T = void> {
|
|
380
|
+
Document?: (node: Document) => T;
|
|
381
|
+
Element?: (node: Element) => T;
|
|
382
|
+
ImplicitDiv?: (node: ImplicitDiv) => T;
|
|
383
|
+
Text?: (node: Text) => T;
|
|
384
|
+
Output?: (node: Output) => T;
|
|
385
|
+
ControlFlow?: (node: ControlFlow) => T;
|
|
386
|
+
Comment?: (node: Comment) => T;
|
|
387
|
+
Filter?: (node: Filter) => T;
|
|
388
|
+
Doctype?: (node: Doctype) => T;
|
|
389
|
+
Fragment?: (node: Fragment) => T;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function walk<T>(node: Node, visitor: Visitor<T>): T;
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
---
|
|
396
|
+
|
|
397
|
+
## Immutability
|
|
398
|
+
|
|
399
|
+
The AST is **immutable** after construction. Transformations (if any)
|
|
400
|
+
produce new AST nodes. This simplifies source mapping and incremental
|
|
401
|
+
compilation.
|
|
402
|
+
|
|
403
|
+
---
|
|
404
|
+
|
|
405
|
+
## Source Mapping
|
|
406
|
+
|
|
407
|
+
Every node carries its `SourceLocation`. During emission, the emitter
|
|
408
|
+
records mappings from generated JS positions back to CoffeeHaml source
|
|
409
|
+
positions using the `loc` fields. This enables:
|
|
410
|
+
|
|
411
|
+
- Accurate error messages pointing to CoffeeHaml source
|
|
412
|
+
- Debugger breakpoints in CoffeeHaml source
|
|
413
|
+
- Stack traces referencing CoffeeHaml lines
|