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.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +175 -0
  3. package/dist/ast.d.ts +107 -0
  4. package/dist/ast.d.ts.map +1 -0
  5. package/dist/ast.js +125 -0
  6. package/dist/ast.js.map +1 -0
  7. package/dist/cli.d.ts +3 -0
  8. package/dist/cli.d.ts.map +1 -0
  9. package/dist/cli.js +53 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/compiler.d.ts +10 -0
  12. package/dist/compiler.d.ts.map +1 -0
  13. package/dist/compiler.js +83 -0
  14. package/dist/compiler.js.map +1 -0
  15. package/dist/emitter.d.ts +4 -0
  16. package/dist/emitter.d.ts.map +1 -0
  17. package/dist/emitter.js +464 -0
  18. package/dist/emitter.js.map +1 -0
  19. package/dist/errors.d.ts +3 -0
  20. package/dist/errors.d.ts.map +1 -0
  21. package/dist/errors.js +2 -0
  22. package/dist/errors.js.map +1 -0
  23. package/dist/expressions.d.ts +10 -0
  24. package/dist/expressions.d.ts.map +1 -0
  25. package/dist/expressions.js +70 -0
  26. package/dist/expressions.js.map +1 -0
  27. package/dist/index.d.ts +4 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +3 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/lexer.d.ts +29 -0
  32. package/dist/lexer.d.ts.map +1 -0
  33. package/dist/lexer.js +315 -0
  34. package/dist/lexer.js.map +1 -0
  35. package/dist/parser.d.ts +4 -0
  36. package/dist/parser.d.ts.map +1 -0
  37. package/dist/parser.js +408 -0
  38. package/dist/parser.js.map +1 -0
  39. package/dist/types.d.ts +65 -0
  40. package/dist/types.d.ts.map +1 -0
  41. package/dist/types.js +12 -0
  42. package/dist/types.js.map +1 -0
  43. package/dist/vite-plugin.d.ts +7 -0
  44. package/dist/vite-plugin.d.ts.map +1 -0
  45. package/dist/vite-plugin.js +44 -0
  46. package/dist/vite-plugin.js.map +1 -0
  47. package/docs/README.md +110 -0
  48. package/docs/architecture.md +619 -0
  49. package/docs/ast.md +413 -0
  50. package/docs/examples.md +719 -0
  51. package/docs/grammar.md +432 -0
  52. package/docs/index.html +129 -0
  53. 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