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
@@ -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.