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/examples.md
ADDED
|
@@ -0,0 +1,719 @@
|
|
|
1
|
+
# CoffeeHaml Syntax Examples
|
|
2
|
+
|
|
3
|
+
## 1. Basic Elements
|
|
4
|
+
|
|
5
|
+
### HTML elements
|
|
6
|
+
|
|
7
|
+
```haml
|
|
8
|
+
%div
|
|
9
|
+
%span
|
|
10
|
+
%button
|
|
11
|
+
%input/
|
|
12
|
+
%br/
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The `/` suffix produces a self-closing element. In React, this means
|
|
16
|
+
`jsx("br", null)` — no children.
|
|
17
|
+
|
|
18
|
+
### Component elements
|
|
19
|
+
|
|
20
|
+
```haml
|
|
21
|
+
%MyComponent
|
|
22
|
+
%UserProfile
|
|
23
|
+
%Router.Link
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Capitalized tags become component references. Dotted names like
|
|
27
|
+
`%Router.Link` work — the tag is emitted as `Router.Link`.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 2. Class and ID Shorthand
|
|
32
|
+
|
|
33
|
+
```haml
|
|
34
|
+
%div.container
|
|
35
|
+
%span#main-content
|
|
36
|
+
%button.primary.large#submit-btn
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Multiple classes and IDs chain:
|
|
40
|
+
|
|
41
|
+
```haml
|
|
42
|
+
%div.container.fluid#main
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Equivalent to writing `{className: "container fluid", id: "main"}`.
|
|
46
|
+
|
|
47
|
+
### Implicit div
|
|
48
|
+
|
|
49
|
+
```haml
|
|
50
|
+
.container
|
|
51
|
+
%p Content
|
|
52
|
+
|
|
53
|
+
#sidebar
|
|
54
|
+
%nav Links
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
No `%div` needed — `.class` or `#id` alone creates a `<div>`.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 3. Attributes
|
|
62
|
+
|
|
63
|
+
### Braces syntax `{...}`
|
|
64
|
+
|
|
65
|
+
```haml
|
|
66
|
+
%div{class: "container", id: "main"}
|
|
67
|
+
%input{type: "text", placeholder: "Search", disabled: true}
|
|
68
|
+
%Button{onClick: handleClick, variant: "primary"}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Parens syntax `(...)`
|
|
72
|
+
|
|
73
|
+
```haml
|
|
74
|
+
%div(class: "container", id: "main")
|
|
75
|
+
%Button(onClick: handleClick, variant: "primary")
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Both are equivalent. Braces match Haml convention; parens feel like
|
|
79
|
+
function calls — natural for React components.
|
|
80
|
+
|
|
81
|
+
### CoffeeScript expressions in attribute values
|
|
82
|
+
|
|
83
|
+
```haml
|
|
84
|
+
%div{
|
|
85
|
+
className: if active then "active" else "inactive"
|
|
86
|
+
style: {color: theme.primary, fontSize: 14}
|
|
87
|
+
onClick: (e) -> handleClick(e, item.id)
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Any CoffeeScript expression is valid.
|
|
92
|
+
|
|
93
|
+
### Shorthand boolean attributes
|
|
94
|
+
|
|
95
|
+
```haml
|
|
96
|
+
%input{required, disabled}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
CoffeeScript shorthand `{required}` → `{required: true}`.
|
|
100
|
+
|
|
101
|
+
### Splat attributes
|
|
102
|
+
|
|
103
|
+
```haml
|
|
104
|
+
%div{props..., className: "extra"}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Merges `props` object with additional overrides.
|
|
108
|
+
|
|
109
|
+
### String keys
|
|
110
|
+
|
|
111
|
+
```haml
|
|
112
|
+
%div{"data-x": value, "aria-label": "Close"}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Quoted keys for non-identifier property names.
|
|
116
|
+
|
|
117
|
+
### No attributes
|
|
118
|
+
|
|
119
|
+
```haml
|
|
120
|
+
%div
|
|
121
|
+
%p
|
|
122
|
+
%Component
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Empty attribute block is optional.
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## 4. Inline Content
|
|
130
|
+
|
|
131
|
+
### Inline text
|
|
132
|
+
|
|
133
|
+
```haml
|
|
134
|
+
%h1 Welcome to CoffeeHaml
|
|
135
|
+
%p This is a paragraph.
|
|
136
|
+
%span Click
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Text after the tag (and optional attributes) becomes a child text node.
|
|
140
|
+
|
|
141
|
+
### Inline output
|
|
142
|
+
|
|
143
|
+
```haml
|
|
144
|
+
%h1 = pageTitle
|
|
145
|
+
%p Hello, = user.name
|
|
146
|
+
%span Total: = items.length
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
`=` inserts the expression result. Mixed text and output on the same
|
|
150
|
+
line concatenates.
|
|
151
|
+
|
|
152
|
+
### Inline text with dynamic class
|
|
153
|
+
|
|
154
|
+
```haml
|
|
155
|
+
%span.status{class: status} = statusText
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Attribute block before the text/output.
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## 5. Nested Children
|
|
163
|
+
|
|
164
|
+
```haml
|
|
165
|
+
%div.container
|
|
166
|
+
%header
|
|
167
|
+
%h1 = title
|
|
168
|
+
%main
|
|
169
|
+
%p Welcome
|
|
170
|
+
%Button{onClick: start} Get Started
|
|
171
|
+
%footer
|
|
172
|
+
%p © = year
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Indentation creates nesting. This is the heart of Haml's readability.
|
|
176
|
+
|
|
177
|
+
### Deep nesting
|
|
178
|
+
|
|
179
|
+
```haml
|
|
180
|
+
%ul
|
|
181
|
+
%li
|
|
182
|
+
%a{href: url}
|
|
183
|
+
%span.icon
|
|
184
|
+
= label
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## 6. Output
|
|
190
|
+
|
|
191
|
+
### Escaped output (`=`)
|
|
192
|
+
|
|
193
|
+
```haml
|
|
194
|
+
%p = user.bio
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Emits escaped text (React handles this by default with string children).
|
|
198
|
+
|
|
199
|
+
### Unescaped output (`==`)
|
|
200
|
+
|
|
201
|
+
```haml
|
|
202
|
+
%div == user.richBioHTML
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Passes raw HTML. In React, this would need `dangerouslySetInnerHTML` or
|
|
206
|
+
similar — the emitter wraps accordingly.
|
|
207
|
+
|
|
208
|
+
### Standalone output
|
|
209
|
+
|
|
210
|
+
```haml
|
|
211
|
+
= greeting
|
|
212
|
+
= formatDate(today)
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Output at the top level becomes a text node in the parent (or a fragment
|
|
216
|
+
child).
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## 7. Control Flow
|
|
221
|
+
|
|
222
|
+
### If / else
|
|
223
|
+
|
|
224
|
+
```haml
|
|
225
|
+
- if user
|
|
226
|
+
%WelcomeBanner{user: user}
|
|
227
|
+
- else
|
|
228
|
+
%LoginButton
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Unless
|
|
232
|
+
|
|
233
|
+
```haml
|
|
234
|
+
- unless items.length
|
|
235
|
+
%EmptyState
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### If / else if / else chain
|
|
239
|
+
|
|
240
|
+
```haml
|
|
241
|
+
- if status == "loading"
|
|
242
|
+
%Spinner
|
|
243
|
+
- else if status == "error"
|
|
244
|
+
%ErrorMessage{message: error}
|
|
245
|
+
- else
|
|
246
|
+
%Content{data: data}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### For loop
|
|
250
|
+
|
|
251
|
+
```haml
|
|
252
|
+
- for item in items
|
|
253
|
+
%ItemCard{item: item}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### For loop with index
|
|
257
|
+
|
|
258
|
+
```haml
|
|
259
|
+
- for item, index in items
|
|
260
|
+
%Row{item: item, key: index}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### For loop with destructuring
|
|
264
|
+
|
|
265
|
+
```haml
|
|
266
|
+
- for {name, age} in users
|
|
267
|
+
%UserRow{name: name, age: age}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### For loop with `when` filter
|
|
271
|
+
|
|
272
|
+
```haml
|
|
273
|
+
- for item in items when item.active
|
|
274
|
+
%ActiveItem{item: item}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
CoffeeScript comprehensions fully supported.
|
|
278
|
+
|
|
279
|
+
### For-of loop
|
|
280
|
+
|
|
281
|
+
```haml
|
|
282
|
+
- for item of iterable
|
|
283
|
+
%Item{item: item}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### While loop
|
|
287
|
+
|
|
288
|
+
```haml
|
|
289
|
+
- while hasMore
|
|
290
|
+
%LoadMore{onLoad: fetchNext}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### Nested control flow
|
|
294
|
+
|
|
295
|
+
```haml
|
|
296
|
+
- if user
|
|
297
|
+
%Dashboard
|
|
298
|
+
- for project in user.projects
|
|
299
|
+
%ProjectCard{project: project}
|
|
300
|
+
- if project.urgent
|
|
301
|
+
%Badge{kind: "urgent"}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### Arbitrary CoffeeScript
|
|
305
|
+
|
|
306
|
+
```haml
|
|
307
|
+
- console.log("rendering dashboard")
|
|
308
|
+
- setupSubscriptions()
|
|
309
|
+
|
|
310
|
+
%Dashboard
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
Statements that are not recognized control keywords are emitted verbatim
|
|
314
|
+
as JavaScript.
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
## 8. Comments
|
|
319
|
+
|
|
320
|
+
### Haml comments (removed)
|
|
321
|
+
|
|
322
|
+
```haml
|
|
323
|
+
-# This is a comment, not in output
|
|
324
|
+
-# TODO: implement pagination
|
|
325
|
+
%div
|
|
326
|
+
-# This child is commented out
|
|
327
|
+
-# %p Not rendered
|
|
328
|
+
%p Rendered
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
Haml comments and their nested children are stripped entirely.
|
|
332
|
+
|
|
333
|
+
### HTML comments
|
|
334
|
+
|
|
335
|
+
```haml
|
|
336
|
+
/ This appears in output
|
|
337
|
+
/ <!-- visible in devtools -->
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
---
|
|
341
|
+
|
|
342
|
+
## 9. Filters
|
|
343
|
+
|
|
344
|
+
### CSS
|
|
345
|
+
|
|
346
|
+
```haml
|
|
347
|
+
%head
|
|
348
|
+
:css
|
|
349
|
+
body {
|
|
350
|
+
margin: 0;
|
|
351
|
+
font-family: sans-serif;
|
|
352
|
+
}
|
|
353
|
+
.container {
|
|
354
|
+
max-width: 1200px;
|
|
355
|
+
}
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### JavaScript
|
|
359
|
+
|
|
360
|
+
```haml
|
|
361
|
+
:javascript
|
|
362
|
+
window.APP_CONFIG = {
|
|
363
|
+
version: "1.0.0"
|
|
364
|
+
};
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### CoffeeScript
|
|
368
|
+
|
|
369
|
+
```haml
|
|
370
|
+
:coffeescript
|
|
371
|
+
init = ->
|
|
372
|
+
console.log "ready"
|
|
373
|
+
init()
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
### Markdown (build-time)
|
|
377
|
+
|
|
378
|
+
```haml
|
|
379
|
+
%article
|
|
380
|
+
:markdown
|
|
381
|
+
# Title
|
|
382
|
+
|
|
383
|
+
This is **markdown** content.
|
|
384
|
+
|
|
385
|
+
- Item 1
|
|
386
|
+
- Item 2
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
---
|
|
390
|
+
|
|
391
|
+
## 10. Complete Component Examples
|
|
392
|
+
|
|
393
|
+
### Simple component
|
|
394
|
+
|
|
395
|
+
```haml
|
|
396
|
+
%Button{onClick: save, disabled: !valid}
|
|
397
|
+
Save Changes
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
Compiles to:
|
|
401
|
+
|
|
402
|
+
```js
|
|
403
|
+
jsx(Button, { onClick: save, disabled: !valid }, "Save Changes")
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
### List component
|
|
407
|
+
|
|
408
|
+
```haml
|
|
409
|
+
%ul.todo-list
|
|
410
|
+
- for todo in todos
|
|
411
|
+
%TodoItem{
|
|
412
|
+
todo: todo
|
|
413
|
+
key: todo.id
|
|
414
|
+
onToggle: -> toggleTodo(todo.id)
|
|
415
|
+
onDelete: -> deleteTodo(todo.id)
|
|
416
|
+
}
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
### Layout component
|
|
420
|
+
|
|
421
|
+
```haml
|
|
422
|
+
%div.app-layout
|
|
423
|
+
%Header{user: currentUser}
|
|
424
|
+
%main.content
|
|
425
|
+
= children
|
|
426
|
+
%Footer
|
|
427
|
+
%p © = new Date().getFullYear()
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
### Form component
|
|
431
|
+
|
|
432
|
+
```haml
|
|
433
|
+
%form{onSubmit: handleSubmit}
|
|
434
|
+
%fieldset
|
|
435
|
+
%legend Contact Info
|
|
436
|
+
|
|
437
|
+
%label{htmlFor: "name"} Name
|
|
438
|
+
%input#name{
|
|
439
|
+
type: "text"
|
|
440
|
+
value: name
|
|
441
|
+
onChange: (e) -> setName(e.target.value)
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
%label{htmlFor: "email"} Email
|
|
445
|
+
%input#email{
|
|
446
|
+
type: "email"
|
|
447
|
+
value: email
|
|
448
|
+
onChange: (e) -> setEmail(e.target.value)
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
%Button{type: "submit", disabled: !valid}
|
|
452
|
+
Submit
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
### Conditional rendering
|
|
456
|
+
|
|
457
|
+
```haml
|
|
458
|
+
%div.dashboard
|
|
459
|
+
- if loading
|
|
460
|
+
%Spinner
|
|
461
|
+
- else if error
|
|
462
|
+
%ErrorBanner{message: error}
|
|
463
|
+
- else
|
|
464
|
+
- if data.length
|
|
465
|
+
- for item in data
|
|
466
|
+
%DataCard{item: item, key: item.id}
|
|
467
|
+
- else
|
|
468
|
+
%EmptyState{
|
|
469
|
+
message: "No data yet"
|
|
470
|
+
onAction: fetchData
|
|
471
|
+
}
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
### SVG element
|
|
475
|
+
|
|
476
|
+
```haml
|
|
477
|
+
%svg{
|
|
478
|
+
xmlns: "http://www.w3.org/2000/svg"
|
|
479
|
+
viewBox: "0 0 100 100"
|
|
480
|
+
width: 100
|
|
481
|
+
height: 100
|
|
482
|
+
}
|
|
483
|
+
%circle{
|
|
484
|
+
cx: 50
|
|
485
|
+
cy: 50
|
|
486
|
+
r: 40
|
|
487
|
+
fill: color
|
|
488
|
+
stroke: "black"
|
|
489
|
+
strokeWidth: 2
|
|
490
|
+
}
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
---
|
|
494
|
+
|
|
495
|
+
## 11. Comparison: JSX vs CoffeeHaml
|
|
496
|
+
|
|
497
|
+
### JSX
|
|
498
|
+
|
|
499
|
+
```jsx
|
|
500
|
+
<div className="container">
|
|
501
|
+
<header>
|
|
502
|
+
<h1>{pageTitle}</h1>
|
|
503
|
+
<nav>
|
|
504
|
+
{links.map(link => (
|
|
505
|
+
<NavLink key={link.id} href={link.url} active={link.active}>
|
|
506
|
+
{link.label}
|
|
507
|
+
</NavLink>
|
|
508
|
+
))}
|
|
509
|
+
</nav>
|
|
510
|
+
</header>
|
|
511
|
+
<main>
|
|
512
|
+
{loading ? (
|
|
513
|
+
<Spinner />
|
|
514
|
+
) : error ? (
|
|
515
|
+
<ErrorBanner message={error} />
|
|
516
|
+
) : (
|
|
517
|
+
<Content data={data} />
|
|
518
|
+
)}
|
|
519
|
+
</main>
|
|
520
|
+
</div>
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
### CoffeeHaml
|
|
524
|
+
|
|
525
|
+
```haml
|
|
526
|
+
%div.container
|
|
527
|
+
%header
|
|
528
|
+
%h1 = pageTitle
|
|
529
|
+
%nav
|
|
530
|
+
- for link in links
|
|
531
|
+
%NavLink{
|
|
532
|
+
key: link.id
|
|
533
|
+
href: link.url
|
|
534
|
+
active: link.active
|
|
535
|
+
}
|
|
536
|
+
= link.label
|
|
537
|
+
%main
|
|
538
|
+
- if loading
|
|
539
|
+
%Spinner
|
|
540
|
+
- else if error
|
|
541
|
+
%ErrorBanner{message: error}
|
|
542
|
+
- else
|
|
543
|
+
%Content{data: data}
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
The CoffeeHaml version has **no closing tags**, **no curly braces for
|
|
547
|
+
children**, **no ternary operator noise**, and **fewer lines**.
|
|
548
|
+
|
|
549
|
+
---
|
|
550
|
+
|
|
551
|
+
## 12. CoffeeScript Features Used
|
|
552
|
+
|
|
553
|
+
### Arrow functions
|
|
554
|
+
|
|
555
|
+
```haml
|
|
556
|
+
%Button{onClick: -> handleClick()}
|
|
557
|
+
%Button{onClick: (e) -> handleClick(e)}
|
|
558
|
+
%Button{onClick: (e) => handleClick(e)} // bound
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
### String interpolation
|
|
562
|
+
|
|
563
|
+
```haml
|
|
564
|
+
%p = "Hello, #{user.name}!"
|
|
565
|
+
%img{alt: "Photo of #{user.name}"}
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
### Existential operator
|
|
569
|
+
|
|
570
|
+
```haml
|
|
571
|
+
%div = user?.profile?.bio ?: "No bio"
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
### Destructuring
|
|
575
|
+
|
|
576
|
+
```haml
|
|
577
|
+
- for {name, email} in users
|
|
578
|
+
%UserCard{name: name, email: email}
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
### Splats in attributes
|
|
582
|
+
|
|
583
|
+
```haml
|
|
584
|
+
%div{commonProps..., className: "special"}
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
### Chained comparisons
|
|
588
|
+
|
|
589
|
+
```haml
|
|
590
|
+
- if 0 < count < 10
|
|
591
|
+
%Badge = count
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
---
|
|
595
|
+
|
|
596
|
+
## 13. Complete Application Page
|
|
597
|
+
|
|
598
|
+
```haml
|
|
599
|
+
%div.app#main-app
|
|
600
|
+
%Navigation{
|
|
601
|
+
user: currentUser
|
|
602
|
+
routes: appRoutes
|
|
603
|
+
onLogout: handleLogout
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
%main.content
|
|
607
|
+
- if loading
|
|
608
|
+
%LoadingScreen
|
|
609
|
+
- else if !currentUser
|
|
610
|
+
%LoginPage{onLogin: handleLogin}
|
|
611
|
+
- else
|
|
612
|
+
%Dashboard{user: currentUser}
|
|
613
|
+
%StatsPanel{stats: dashboardStats}
|
|
614
|
+
|
|
615
|
+
%section.recent-activity
|
|
616
|
+
%h2 Recent Activity
|
|
617
|
+
- for activity in recentActivities
|
|
618
|
+
%ActivityCard{activity: activity, key: activity.id}
|
|
619
|
+
|
|
620
|
+
%section.projects
|
|
621
|
+
%h2 Projects
|
|
622
|
+
- if projects.length
|
|
623
|
+
%ProjectGrid
|
|
624
|
+
- for project in projects
|
|
625
|
+
%ProjectCard{
|
|
626
|
+
project: project
|
|
627
|
+
key: project.id
|
|
628
|
+
onClick: -> navigateTo(project.url)
|
|
629
|
+
}
|
|
630
|
+
- else
|
|
631
|
+
%EmptyState{
|
|
632
|
+
icon: "folder"
|
|
633
|
+
message: "No projects yet"
|
|
634
|
+
onAction: -> createProject()
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
%Footer
|
|
638
|
+
%p = "© #{new Date().getFullYear()} #{companyName}"
|
|
639
|
+
%nav
|
|
640
|
+
- for link in footerLinks
|
|
641
|
+
%a{key: link.id, href: link.url} = link.text
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
---
|
|
645
|
+
|
|
646
|
+
## 14. Edge Cases & FAQ
|
|
647
|
+
|
|
648
|
+
### Multi-line attributes
|
|
649
|
+
|
|
650
|
+
Attributes can span multiple lines within `{...}`:
|
|
651
|
+
|
|
652
|
+
```haml
|
|
653
|
+
%ComplexComponent{
|
|
654
|
+
data: fetchData()
|
|
655
|
+
columns: [
|
|
656
|
+
{key: "name", title: "Name"}
|
|
657
|
+
{key: "age", title: "Age"}
|
|
658
|
+
]
|
|
659
|
+
onSort: (col) -> sortBy(col.key)
|
|
660
|
+
onFilter: (query) -> filterData(query)
|
|
661
|
+
}
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
### Empty elements
|
|
665
|
+
|
|
666
|
+
```haml
|
|
667
|
+
%div
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
Emits `jsx("div", null)` — a div with no children and no props.
|
|
671
|
+
|
|
672
|
+
### Text starting with special characters
|
|
673
|
+
|
|
674
|
+
Use `\` to escape:
|
|
675
|
+
|
|
676
|
+
```haml
|
|
677
|
+
\% Not a tag
|
|
678
|
+
\= Not output
|
|
679
|
+
\- Not control
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
Or use a plain text region:
|
|
683
|
+
|
|
684
|
+
```haml
|
|
685
|
+
%p
|
|
686
|
+
This line starts with % but it's fine inside an element.
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
### Dynamic tag name
|
|
690
|
+
|
|
691
|
+
```haml
|
|
692
|
+
%{dynamicTag}{props} content
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
Actually, this is tricky. For dynamic tags, use a component:
|
|
696
|
+
|
|
697
|
+
```haml
|
|
698
|
+
%DynamicComponent{tag: dynamicTag, props: props}
|
|
699
|
+
content
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
Or use a raw CoffeeScript expression:
|
|
703
|
+
|
|
704
|
+
```haml
|
|
705
|
+
- output = jsx(dynamicTag, props, "content")
|
|
706
|
+
= output
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
(But this loses Haml structure. A future version may support `%{expr}`.)
|
|
710
|
+
|
|
711
|
+
### React.Suspense / React.Fragment
|
|
712
|
+
|
|
713
|
+
```haml
|
|
714
|
+
%Suspense{fallback: %Spinner}
|
|
715
|
+
%LazyComponent
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
Fragments are implicit — multiple top-level nodes in a Document emit
|
|
719
|
+
a React Fragment automatically.
|