joys 0.1.0 → 0.1.2

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.
data/README.md CHANGED
@@ -1,5 +1,1014 @@
1
- # Joys
1
+ # Joys - HTML Templates at the Speed of Light
2
2
 
3
- In-depth README coming soon!
3
+ **Write HTML like Ruby, render it at the speed of C.**
4
4
 
5
- Watch this space 👀
5
+ Joys is a pure-Ruby HTML templating engine with built-in CSS and ludicrous-speed rendering.
6
+
7
+ ## Why Joys?
8
+
9
+ - **Blazing Fast** - 3x faster on cold render. Orders of magnitude faster after caching.
10
+ - **Pure Ruby** - No new syntax to learn. It's just methods and blocks.
11
+ - **Testable** - Components are just functions. Test them like any Ruby code.
12
+ - **Fully Integrated Styling** - Deduplicated, sorted css right inside your components
13
+ - **Framework Agnostic** - Use it in Rails, Roda, Sinatra, Hanami, Syro, etc.
14
+
15
+ ---
16
+
17
+ ## Installation
18
+
19
+ ```ruby
20
+ gem 'joys'
21
+ ```
22
+
23
+ ## Quick Start: The Power of Joys DSL
24
+
25
+ ```ruby
26
+ html = Joys.html do
27
+ div(cs: "card") do
28
+ h1 "Hello"
29
+ p "World", cs: "text-sm"
30
+ txt "It's me!"
31
+ end
32
+ end
33
+
34
+ puts html
35
+
36
+ # => <div class="card"><h1>Hello</h1><p class="text-sm">World</p>It's me!</div>
37
+ ```
38
+
39
+ ### Security & HTML Escaping
40
+
41
+ Joys ships with sensible defaults to prevent XSS attacks.
42
+
43
+ All text nodes must be rendered explicitly via the `txt()` or `raw()` helpers
44
+
45
+ ```ruby
46
+ # txt escapes HTML (safe by default)
47
+ p { txt user_input } # "<script>alert('xss')</script>" becomes safe text
48
+
49
+ # raw outputs HTML (use with caution)
50
+ div { raw markdown_to_html(content) }
51
+
52
+ # Pass raw: true to any tag
53
+ div(content, raw: true) # content renders as HTML
54
+ ```
55
+ Following Ruby conventions, adding a bang `!` to the elemtn indicates a method with a more 'potent' or 'dangerous' effect—in this case, disabling HTML escaping.
56
+
57
+ ```
58
+ # Escaped HTML
59
+ div("hello<br>world") # <div>Hello&lt;br&gt;world</div>
60
+
61
+ # Raw Unescaped HTML with bang (div!)
62
+ div!("hello<br>world") # <div>Hello<br>world</div>
63
+ ```
64
+
65
+ ### Smart Class Handling
66
+
67
+ Of course, you can use `class` attribute, but Joys provides `cs` for brevity given how often it is used.
68
+
69
+ You also get some extra benefits from `cs`:
70
+
71
+ ```ruby
72
+ # String, Array, or conditional classes - all just work
73
+ div(cs: "card")
74
+ div(cs: ["card", "active"])
75
+ div(cs: ["card", user.premium? && "premium"])
76
+ div(cs: ["base", ("active" if active?)])
77
+ ```
78
+
79
+ ### Boolean Attributes That Don't Suck
80
+
81
+ Elegant handling with underscore-to-dash conversion and proper boolean semantics:
82
+
83
+ ```ruby
84
+ input(type: "text", auto_focus: true, form_novalidate: true)
85
+ # => <input type="text" autofocus formnovalidate>
86
+
87
+ input(type: "text", disabled: false)
88
+ # => <input type="text">
89
+ ```
90
+
91
+ Write attributes naturally with underscores - Joys outputs the correct HTML boolean names. Works automatically for all boolean attributes, including: `autofocus`, `autoplay`, `checked`, `controls`, `disabled`, `default`, `formnovalidate`, `hidden`, `inert`, `itemscope`, `loop`, `multiple`, `muted`, `novalidate`, `open`, `readonly`, `required`, `reversed`, `scoped`, `seamless` and `selected`.
92
+
93
+ ### Smart Data and ARIA Attributes
94
+
95
+ Nested hashes auto-expand with underscore-to-dash conversion and intelligent false handling:
96
+
97
+ ```ruby
98
+ button(
99
+ "Save",
100
+ cs: "btn",
101
+ data: {
102
+ controller: "form_handler",
103
+ user_id: 123,
104
+ confirm_message: "Are you sure?"
105
+ },
106
+ aria: {
107
+ label: "Save changes",
108
+ expanded: false, # Boolean false - omitted
109
+ disabled: "false" # String false - rendered
110
+ }
111
+ )
112
+ # => <button class="btn" data-controller="form_handler" data-user-id="123"
113
+ # data-confirm-message="Are you sure?" aria-label="Save changes"
114
+ # aria-disabled="false">Save</button>
115
+ ```
116
+
117
+ **Key behaviors:**
118
+ - Underscores convert to dashes: `user_id` → `data-user-id`
119
+ - Boolean `false` values get omitted entirely
120
+ - String `"false"` values render normally
121
+ - All other values render with proper escaping
122
+
123
+ ### Every HTML Tag, Zero Boilerplate
124
+
125
+ ```ruby
126
+ # All standard HTML tags just work
127
+ article {
128
+ header { h1("Post Title") }
129
+ section { p("Content...") }
130
+ footer { time(datetime: "2024-01-01") { txt "Jan 1" } }
131
+ }
132
+
133
+ # Boolean attributes are smart
134
+ input(type: "checkbox", checked: user.subscribed?)
135
+ # => <input type="checkbox" checked> (if subscribed)
136
+ # => <input type="checkbox"> (if not)
137
+
138
+ # Void tags handled correctly
139
+ img(src: "/logo.png", alt: "Logo")
140
+ input(type: "text", name: "email", required: true)
141
+ ```
142
+
143
+ ---
144
+
145
+ ## Building Components
146
+
147
+ Components are reusable pieces of UI. Define once, use everywhere:
148
+
149
+ ```ruby
150
+ Joys.define(:comp, :user_card) do |user|
151
+ div(cs: "card") {
152
+ img(src: user[:avatar], alt: user[:name])
153
+ h3(user[:name])
154
+ p { txt user[:bio] } # txt escapes HTML
155
+ }
156
+ end
157
+
158
+ # Render standalone
159
+ html = Joys.comp(:user_card, {name: "Alice", bio: "Developer"})
160
+
161
+ # Or use inside other components
162
+ Joys.define(:comp, :user_list) do |users|
163
+ div(cs: "grid") {
164
+ users.each { |user| comp(:user_card, user) }
165
+ }
166
+ end
167
+ ```
168
+
169
+ #### A Note on Naming Conventions
170
+
171
+ **Joys** uses a simple global registry for components, pages, and layouts.
172
+
173
+ For larger applications, we recommend namespacing your definitions to avoid collisions.
174
+
175
+ Symbols and strings both work, so you can adopt a convention that fits your project.
176
+
177
+ ```ruby
178
+ # Define with a path-like string
179
+ Joys.define(:comp, 'forms/button') do |text|
180
+ # ...
181
+ end
182
+ Joys.define(:comp, 'cards/user_profile') do |user|
183
+ # ...
184
+ end
185
+
186
+ # Use them elsewhere
187
+
188
+ Joys.html do
189
+ div {
190
+ comp 'cards/user_profile', @user
191
+ comp 'forms/button', "Submit"
192
+ }
193
+ end
194
+ ```
195
+
196
+ This approach keeps your component library organized as it grows.
197
+
198
+ ### Pages
199
+
200
+ Pages are top-level templates, typically rendered by controllers. Unlike other template solutions, Joys expects each page to explicitly define its layout.
201
+
202
+ ```ruby
203
+ Joys.define(:page, :dashboard) do
204
+ layout(:main) {
205
+ push(:title) { txt "Dashboard" }
206
+
207
+ push(:main) {
208
+ h1("Welcome back!")
209
+ comp(:stats_panel, @stats)
210
+
211
+ div(cs: "users-grid") {
212
+ @users.each { |u| comp(:user_card, u) }
213
+ }
214
+ }
215
+ }
216
+ end
217
+
218
+ # Render with locals
219
+ html = Joys.page(:dashboard, users: User.all, stats: calc_stats)
220
+ ```
221
+
222
+ Similar to `content_for` in Rails, HTML may be sent to the layout via the `push(:content_name) {...}` method. The layout establishes placement of this content using `pull(:content_name)`
223
+
224
+ Note: There is no default `yield`. The `pull` method always requires a symbol. It's up to the user to be explicit in defining where to push content. `push(:main)` or `push(:body)` are sensible defualts, but feel free to use whatever you want.
225
+
226
+ ### Layouts
227
+
228
+ Layouts provide consistent structure with content slots via the `pull()` method:
229
+
230
+ ```ruby
231
+ Joys.define(:layout, :main) do
232
+ html(lang: "en") {
233
+ head {
234
+ title { pull(:title) }
235
+ link(href: "/app.css", rel: "stylesheet")
236
+ }
237
+ body {
238
+ nav { comp(:navbar) }
239
+ main(cs: "container") {
240
+ pull(:main)
241
+ }
242
+ }
243
+ }
244
+ end
245
+ ```
246
+
247
+ ---
248
+
249
+ ## Rails Integration
250
+
251
+ ### Setup
252
+
253
+ ```ruby
254
+ # config/initializers/joys.rb
255
+ require 'joys/rails'
256
+ ```
257
+
258
+ ### File Structure
259
+
260
+ ```
261
+ app/views/joys/
262
+ ├── layouts/
263
+ │ └── application.rb
264
+ ├── comps/
265
+ │ ├── navbar.rb
266
+ │ └── user_card.rb
267
+ └── pages/
268
+ ├── dashboard.rb
269
+ └── users_show.rb
270
+ ```
271
+
272
+ ### Rails Helpers Work Seamlessly
273
+
274
+ Joys is fully compatible with Rails `ActiveSupport::SafeBuffer`, so you can use all Rails helpers seamlessly.
275
+
276
+ ```ruby
277
+ Joys.define(:page, :users_edit) do
278
+ layout(:main) {
279
+ push(:main) {
280
+ # Rails form helpers just work
281
+ form_with(model: @user) do |f|
282
+ div(cs: "field") {
283
+ f.label(:name)
284
+ f.text_field(:name, class: "input")
285
+ }
286
+ f.submit("Save", class: "btn btn-primary")
287
+ end
288
+ # As do route helpers
289
+ a("Back", href: users_path, cs: "link")
290
+ }
291
+ }
292
+ end
293
+ ```
294
+
295
+ Rendering from controllers could not be more simple!
296
+
297
+ ```
298
+ class UsersController < ApplicationController
299
+ def show
300
+ @user = User.find(params[:id])
301
+ render_joys :users_show # renders from #{Rails.root}/app/views/joys/users_show.rb
302
+ end
303
+ end
304
+ ```
305
+
306
+ ---
307
+
308
+ ## Real-World Example
309
+
310
+ Here's a more complex nested component with conditions, iterations and all the usual good stuff we expect from templates.
311
+
312
+ Since Joys is just ruby you have unlimited power to present the content as you wish
313
+
314
+ ```ruby
315
+ Joys.define(:comp, :pricing_card) do |plan|
316
+ div(cs: ["card", plan[:featured] && "featured"]) {
317
+ if plan[:featured]
318
+ div("Most Popular", cs: "badge")
319
+ end
320
+
321
+ h3(plan[:name])
322
+
323
+ div(cs: "price") {
324
+ span("$", cs: "currency")
325
+ span(plan[:price].to_s, cs: "amount")
326
+ span("/mo", cs: "period")
327
+ }
328
+
329
+ ul(cs: "features") {
330
+ plan[:features].each do |feature|
331
+ li { raw "&check; #{feature}"}
332
+ end
333
+ }
334
+
335
+ button(
336
+ plan[:featured] ? "Start Free Trial" : "Choose Plan",
337
+ cs: ["btn", "w-full", (plan[:featured] ? "btn-primary" : "btn-secondary")],
338
+ data: {
339
+ plan: plan[:id],
340
+ price: plan[:price]
341
+ }
342
+ )
343
+ }
344
+ end
345
+ ```
346
+ ---
347
+ ## A Word on Performance
348
+
349
+ Using simple components and views (only 3 layers of nesting) we can see how Joy stacks up:
350
+
351
+ ### Simple Templates (385 chars)
352
+
353
+ #### ❄️ COLD RENDER (new object per call)
354
+ | | user | system | total | real. |
355
+ |------|--------|--------|--------|--------|
356
+ |Joys: |0.060535|0.000200|0.060735|0.060736|
357
+ |Slim: |0.048344|0.000183|0.048527|0.048530|
358
+ |ERB: |0.301811|0.000808|0.302619|0.302625|
359
+ |Phlex:|0.069636|0.000470|0.070106|0.071157|
360
+
361
+ #### 🔥 HOT RENDER (cached objects)
362
+ | | user | system | total | real |
363
+ |------|--------|---------|--------|--------|
364
+ |Joys: |0.065255| 0.000257|0.065512|0.065512|
365
+ |Slim: |0.049323| 0.000295|0.049618|0.049695|
366
+ |ERB: |0.309757| 0.001167|0.310924|0.310929|
367
+ |Phlex:|0.069663| 0.000141|0.069804|0.069805|
368
+
369
+ #### 💾 MEMORY USAGE
370
+ Joys memory: 532356 bytes
371
+ Slim memory: 40503436 bytes
372
+ Phlex memory: 8000 bytes
373
+ ERB memory: 1669256 bytes
374
+
375
+ At smaller scales performance is on par with Phlex, which has excellent speed and superior
376
+
377
+ ### Complex Templates (8,000+ chars)
378
+
379
+ As template complexity grows, Joys starts to really show off its optimizations. This benchmark tests a full dashboard page with 5 components and 2 loops yields:
380
+
381
+ * Data parsing
382
+ * Layouts
383
+ * Multiple Content Slots
384
+ * Multiple Components
385
+ * Several Iterations
386
+ * Conditions
387
+
388
+ ### ❄️ COLD RENDER (new object per call)
389
+
390
+ This is the bare bones benchmark, no "cheating" allowed.
391
+
392
+ | | user | system | total | real |
393
+ |------|--------|--------|--------|--------|
394
+ | Joys |0.051715|0.000364|0.052079|0.052080|
395
+ | ERB |0.520495|0.003696|0.524191|0.524187|
396
+ | Slim |6.001650|0.019418|6.021068|6.021013|
397
+ | Phlex|0.169567|0.000373|0.169940|0.169938|
398
+
399
+ Note: Joys achieves its 'cold render' speed by eliminating object overhead and using simple string operations with smart memory management.
400
+
401
+ ### 🔥 HOT RENDER (cached objects)
402
+
403
+ | | user | system | total | real |
404
+ |------|---------|--------|---------|--------|
405
+ |JOYS: | 0.000463|0.000010| 0.000473|0.000473|
406
+ |SLIM: | 0.045881|0.000358| 0.046239|0.046243|
407
+ |PHLEX:| 0.167631|0.000760| 0.168391|0.168661|
408
+ |ERB: | 0.394509|0.004084| 0.398593|0.398614|
409
+
410
+ Note: Joys achieves its 'hot render' speed by compiling a template's structure into a highly optimized render function the first time it's called. Subsequent calls with the same component structure reuse this function, bypassing compilation and object allocation, and only interpolating the dynamic data.
411
+
412
+
413
+ ### 💾 MEMORY USAGE
414
+
415
+ Joys memory: 7587400 bytes
416
+ Slim memory: 217778600 bytes
417
+ Phlex memory: 9956000 bytes
418
+ ERB memory: 7264240 bytes
419
+
420
+ Even without caching, Joys is rendering at around 50 milliseconds.
421
+
422
+ The real performance comes after caching at half a millisecond.
423
+
424
+ That's not just fast, its _ludicrous speed_ 🚀 All thanks to Joys's one-time compilation and caching of the template structure.
425
+
426
+ But don't take our word for it:
427
+
428
+ ```
429
+ gem install erb slim phlex joys
430
+
431
+ # Simple benchmark
432
+ ruby text/bench/simple.rb
433
+
434
+ # Complex benchmark
435
+ ruby text/bench/deep.rb
436
+ ```
437
+
438
+ Note: If you have any additions, critiques or input on these benchmarks please submit a pull request.
439
+
440
+ ### Performance Philosophy
441
+
442
+ Joys attains its incredible speed through:
443
+
444
+ 1. **Precompiled layouts** - Templates compile once, render many
445
+ 2. **Direct string building** - No intermediate AST or object allocation
446
+ 3. **Smart caching** - Components cache by arguments automatically
447
+ 4. **Zero abstraction penalty** - It's just method calls and string concatenation
448
+
449
+ All achieved without C or Rust. Just plain old Ruby, with a core of under 500 lines of code.
450
+
451
+ ---
452
+
453
+
454
+ ## Joy Styling – Scoped, Deduped, Responsive CSS Without the Hassle
455
+
456
+ Joys doesn’t just generate HTML—it handles CSS like a **pure Ruby, React-level, zero-Node, blazing-fast styling engine**.
457
+
458
+ With **`styles`**, you can write component CSS **inline**, **scoped**, **deduped**, and **responsive**—all automatically, without ever touching a separate asset pipeline.
459
+
460
+ ---
461
+
462
+ ### Basic Usage
463
+
464
+ ```ruby
465
+
466
+ # Make sure your layout has a style tag with pull_styles
467
+
468
+ Joys.define(:layout, :main) do
469
+ doctype
470
+ html {
471
+ head {
472
+ style { pull_styles } # all css will be rendered here
473
+ }
474
+ }
475
+ end
476
+
477
+ # Works when registering coomponents and pages
478
+
479
+ Joys.define(:comp, :card) do
480
+ div(cs: "card") do
481
+ h1("Hello", cs: "head")
482
+ p("Welcome to Joys!")
483
+ end
484
+
485
+ styles do
486
+ css ".card p { color:#333; font-size:0.875rem }"
487
+ end
488
+ end
489
+
490
+ html = Joys.comp(:card)
491
+ puts html
492
+ ```
493
+
494
+ Note: For multiline css statements you may use Ruby's `%()` method as follows
495
+
496
+ ```ruby
497
+ styles do
498
+ css %w(
499
+ .card p { color:#333; font-size:0.875rem }
500
+ .card { background:#fff; border-radius:6px; padding:1rem }
501
+ )
502
+ end
503
+ ```
504
+
505
+ **Output HTML:**
506
+
507
+ ```html
508
+ <div class="card">
509
+ <h1 class="head">Hello</h1>
510
+ <p>Welcome to Joys!</p>
511
+ </div>
512
+ ```
513
+
514
+ **Generated CSS (automatically appended):**
515
+
516
+ ```css
517
+ .card { background:#fff; border-radius:6px; padding:1rem }
518
+ .card p { color:#333; font-size:0.875rem }
519
+ ```
520
+
521
+ ---
522
+
523
+ ### Automatic CSS Scoping
524
+
525
+ Want your component styles **prefixed automatically** to avoid collisions? Just opt in with `scoped: true`:
526
+
527
+ ```ruby
528
+ Joys.define(:comp, :user) do
529
+ styles(scoped: true) do
530
+ css %w(
531
+ .card { background:#fff }
532
+ .head { font-weight:bold }
533
+ )
534
+ end
535
+ end
536
+ ```
537
+
538
+ **Generated CSS (prefixed with component name):**
539
+
540
+ ```css
541
+ .user .card { background:#fff }
542
+ .user .head { font-weight:bold }
543
+ ```
544
+
545
+ > No more worrying about collisions with other components, layouts, or third-party CSS.
546
+
547
+ ---
548
+
549
+ ### Responsive Media Queries
550
+
551
+ Joys supports **`min_media`**, **`max_media`**, and **`minmax_media`** right inside your styles. Just pass the width and CSS string.
552
+
553
+ ```ruby
554
+ styles do
555
+ css %w(
556
+ .card { padding:1rem }
557
+ )
558
+
559
+ media_min "768px", %(
560
+ .card { padding:2rem }
561
+ .head { padding:3rem }
562
+ )
563
+
564
+ media_max "480px", ".card { padding:0.5rem }"
565
+ media_minmax "481px", "767px", ".card { padding:1rem }"
566
+ end
567
+ ```
568
+
569
+ **Generated CSS:**
570
+
571
+ ```css
572
+ .card { padding:1rem }
573
+
574
+ @media (min-width: 768px) {
575
+ .card { padding:2rem }
576
+ .head { padding:3rem }
577
+ }
578
+
579
+ @media (max-width: 480px) {
580
+ .card { padding:0.5rem }
581
+ }
582
+
583
+ @media (min-width: 481px) and (max-width: 767px) {
584
+ .card { padding:1rem }
585
+ }
586
+ ```
587
+
588
+ * ✅ **Merged automatically**
589
+ * ✅ **Deduped**
590
+ * ✅ **Sorted by breakpoint**
591
+ * ✅ **Scoped if opted-in**
592
+
593
+ And yes, we also have container queries!
594
+
595
+ ```ruby
596
+ styles do
597
+ container_min "768px", ".card {font-size:1.2em}"
598
+ named_container_min "sidebar", "768px", ".card {font-size:2em}"
599
+ end
600
+ ```
601
+
602
+ **Generated CSS:**
603
+
604
+ ```css
605
+ @container (min-width:768px) {
606
+ .card {font-size:1.2em}
607
+ }
608
+ @container sidebar (min-width:768px) {
609
+ .card {font-size:1.2em}
610
+ }
611
+ ```
612
+
613
+ ---
614
+
615
+ ### Deduplication & Ordering – Built In
616
+
617
+ Write your component styles wherever you like. Joys will:
618
+
619
+ * Remove **duplicate selectors** automatically
620
+ * Preserve **correct CSS order across nested components**
621
+ * Merge **global, page-level, and component-level styles** seamlessly
622
+
623
+ ```ruby
624
+ # Even if .card appears in multiple components:
625
+ styles do
626
+ css ".card { margin:0 }"
627
+ end
628
+
629
+ # Another component:
630
+ styles do
631
+ css ".card { margin:0 }" # Joys dedupes automatically
632
+ end
633
+ ```
634
+
635
+ ---
636
+
637
+ ### Rendering External CSS
638
+
639
+ For teams that prefer external CSS files over inline styling, Joys can automatically generate and serve CSS files while maintaining all the performance benefits of consolidated, deduped styles.
640
+
641
+ #### Basic Usage
642
+
643
+ Replace `pull_styles` with `pull_external_styles` in your layout:
644
+
645
+ ```ruby
646
+ Joys.define(:layout, :main) do
647
+ html(lang: "en") {
648
+ head {
649
+ title { pull(:title) }
650
+ pull_external_styles # Generates <link> tags instead of inline styles
651
+ }
652
+ body {
653
+ pull(:main)
654
+ }
655
+ }
656
+ end
657
+ ```
658
+
659
+ #### How It Works
660
+
661
+ When you render a page, Joys:
662
+
663
+ 1. **Analyzes** which components are actually used
664
+ 2. **Consolidates** all their styles (base CSS + media queries + container queries)
665
+ 3. **Generates** a CSS file named after the component combination
666
+ 4. **Caches** the file to disk (only creates once per unique component set)
667
+ 5. **Returns** the appropriate `<link>` tag
668
+
669
+ ```ruby
670
+ # Page uses: navbar, user_card, button components
671
+ # Generates: /css/comp_navbar,comp_user_card,comp_button.css
672
+ # Returns: <link rel="stylesheet" href="/css/comp_navbar,comp_user_card,comp_button.css">
673
+ ```
674
+
675
+ #### Configuration
676
+
677
+ Set your CSS output directory (defaults to `"public/css"`):
678
+
679
+ ```ruby
680
+ # config/initializers/joys.rb (Rails)
681
+ Joys.css_path = Rails.root.join("public", "assets", "css")
682
+
683
+ # Or for other frameworks
684
+ Joys.css_path = "public/stylesheets"
685
+ ```
686
+
687
+ #### Automatic File Management
688
+
689
+ Joys handles the file lifecycle automatically:
690
+
691
+ - **Smart naming** - Files named by component combination ensure perfect caching
692
+ - **No duplication** - Same component set = same file, served from cache
693
+ - **Directory creation** - Creates nested directories as needed
694
+ - **Production ready** - Files persist across deployments
695
+
696
+ #### Example Generated CSS
697
+
698
+ ```ruby
699
+ Joys.define(:comp, :card) do
700
+ div(cs: "card") { yield }
701
+
702
+ styles do
703
+ css ".card { padding: 1rem; background: white; }"
704
+ media_min(768, ".card { padding: 2rem; }")
705
+ end
706
+ end
707
+
708
+ Joys.define(:comp, :button) do
709
+ button(cs: "btn") { yield }
710
+
711
+ styles(scoped: true) do
712
+ css ".btn { padding: 0.5rem 1rem; }"
713
+ container_min(300, ".btn { width: 100%; }")
714
+ end
715
+ end
716
+ ```
717
+
718
+ **Generated CSS file:**
719
+ ```css
720
+ .card { padding: 1rem; background: white; }
721
+ .button .btn { padding: 0.5rem 1rem; }
722
+ @media (min-width: 768px){
723
+ .card { padding: 2rem; }
724
+ }
725
+ @container (min-width: 300px){
726
+ .button .btn { width: 100%; }
727
+ }
728
+ ```
729
+
730
+ #### Performance Characteristics
731
+
732
+ External CSS provides:
733
+
734
+ - **Browser caching** - CSS files cached separately from HTML
735
+ - **Parallel loading** - CSS downloads while HTML processes
736
+ - **CDN friendly** - Static files easily cached at edge locations
737
+ - **Same deduplication** - All Joys optimizations still apply
738
+
739
+ Trade-offs vs inline:
740
+
741
+ - **Extra HTTP request** - One additional round trip
742
+ - **Render blocking** - CSS must load before styled rendering
743
+ - **Cache complexity** - More moving parts in deployment
744
+
745
+ #### When to Use External CSS
746
+
747
+ Choose external CSS when you have:
748
+
749
+ - **Large stylesheets** (>20KB) where caching outweighs request overhead
750
+ - **Strict CSP policies** that prohibit inline styles
751
+ - **Team preferences** for traditional CSS file organization
752
+ - **CDN optimization** requirements
753
+
754
+ For most applications under 30-40KB of CSS, inline styles offer better performance and simpler deployment.
755
+
756
+ #### Rails Integration
757
+
758
+ External CSS works seamlessly with Rails asset pipeline:
759
+
760
+ ```ruby
761
+ # config/initializers/joys.rb
762
+ Joys.css_path = Rails.root.join("public", "assets", "joys")
763
+
764
+ # Your layout
765
+ Joys.define(:layout, :application) do
766
+ html {
767
+ head {
768
+ stylesheet_link_tag "application", "data-turbo-track": "reload"
769
+ pull_external_styles # Joys-generated styles
770
+ }
771
+ }
772
+ end
773
+ ```
774
+
775
+ #### Deployment Considerations
776
+
777
+ Since CSS files are generated at runtime:
778
+
779
+ - Ensure write permissions on your CSS directory
780
+ - Consider warming the cache on deployment
781
+ - Add `*.css` to your `.gitignore` if files are in your repo path
782
+ - For containerized deployments, mount a persistent volume for the CSS directory
783
+
784
+ #### Mixing with Traditional Assets
785
+
786
+ External Joys CSS plays nicely with existing stylesheets:
787
+
788
+ ```ruby
789
+ head {
790
+ # Your existing global styles
791
+ stylesheet_link_tag "application"
792
+
793
+ # Component-specific Joys styles
794
+ pull_external_styles
795
+
796
+ # Page-specific overrides
797
+ stylesheet_link_tag "admin" if admin_page?
798
+ }
799
+ ```
800
+
801
+ ---
802
+
803
+ ### TL;DR – Why You’ll Never Write CSS the Old Way Again
804
+
805
+ * **Scoped**: Optional prefixing prevents collisions
806
+ * **Deduped**: Automatic removal of duplicate rules
807
+ * **Ordered**: Nested components and responsive queries just work
808
+ * **Responsive**: min/max/minmax media queries built-in
809
+ * **Inline or exportable**: Works in rendering or as a static CSS file
810
+
811
+ > Write CSS where it belongs: next to your component, in pure Ruby, with **React-level convenience and performance**—all without leaving Ruby.
812
+
813
+ NOTE: This feature is completely optional. If you are using Tailwind or similar asset compiler, Joys will work just fine with that. However, the integrated css handling greatly simplifies the generation and compilation of styles, using a pure, Node-free process which has zero impact on rendering speed.
814
+
815
+ ## Functional at Heart, But Ready for OOP
816
+
817
+ Joys is designed to stay out of your way. At its core, it leans functional — you define small, composable render functions instead of verbose boilerplate. But if your project already leans heavily on OOP, Joys can fold right in, allowing you to organize views as classes with inheritance and overrides.
818
+
819
+ The DSL is the same either way — Joys just adapts.
820
+
821
+
822
+ ```ruby
823
+ class BaseLayout
824
+ include Joys::Helpers
825
+
826
+ def render(&block)
827
+ html {
828
+ head { title("My App") }
829
+ body { block.call }
830
+ }
831
+ end
832
+ end
833
+
834
+ class Dashboard < BaseLayout
835
+ def render
836
+ super {
837
+ div(cs: "dashboard") {
838
+ h1("Dashboard")
839
+ p("Rendered through OOP inheritance.")
840
+ a("Logout", cs:"btn",href:"/logout")
841
+ }
842
+ }
843
+ end
844
+ end
845
+
846
+ puts Dashboard.new.render
847
+ # => <html><head><title>My App</title></head><body><div class="dashboard"><h1>Dashboard</h1><p>Rendered through OOP inheritance.</p><a class="btn" href="/logout">Logout</a></div></body></html>
848
+ ```
849
+
850
+ You can pick the style that fits your project, or even mix them.
851
+
852
+ ---
853
+ ## 🧩 Bespoke Page Architecture: Content-Driven Flexibility
854
+
855
+ **Perfect for landing pages, marketing campaigns, and any layout requiring easy reorganization.**
856
+
857
+ Modular Page Architecture lets you build pages as Ruby modules with clean data separation, allowing rapid section reorganization without touching template code.
858
+
859
+ ### The Pattern: Data + Methods + Flexible Assembly
860
+
861
+ ```ruby
862
+ # pages/product_launch.rb
863
+ module ProductLaunch
864
+ DATA = {
865
+ hero: {
866
+ title: "Revolutionary SaaS Platform",
867
+ cta: "Start Free Trial"
868
+ },
869
+ features: {
870
+ items: [
871
+ { title: "Lightning Fast", desc: "10x faster" },
872
+ { title: "Secure", desc: "Bank-level security" }
873
+ ]
874
+ }
875
+ }.freeze
876
+
877
+ def self.render
878
+ Joys.define(:page, :product_launch) do
879
+ layout(:main) {
880
+ push(:main) {
881
+ raw hero(DATA[:hero])
882
+ raw features(DATA[:features])
883
+
884
+ # Custom sections between data-driven ones
885
+ div(cs: "demo") {
886
+ h2("See It In Action")
887
+ comp(:demo_video)
888
+ }
889
+
890
+ comp(:testimonials) # Reusable global component
891
+ }
892
+ }
893
+ end
894
+ end
895
+
896
+ private
897
+
898
+ def self.hero(data)
899
+ Joys.html {
900
+ styles { css ".hero { background: #667eea; padding: 4rem; }" }
901
+ # All css rendered and deduplicated within page scope
902
+ section(cs: "hero") {
903
+ h1(data[:title])
904
+ a(data[:cta], href: "/signup", cs: "btn")
905
+ }
906
+ }
907
+ end
908
+
909
+ def self.features(data)
910
+ Joys.html {
911
+ section(cs: "features") {
912
+ data[:items].each { |item|
913
+ div {
914
+ h3(item[:title])
915
+ p(item[:desc])
916
+ }
917
+ }
918
+ }
919
+ }
920
+ end
921
+ end
922
+ ```
923
+
924
+ ### Why This Pattern Works
925
+
926
+ **📊 Content Management Made Simple**
927
+ - All page content in one DATA hash
928
+ - Easy updates without touching templates
929
+
930
+ **🎯 Effortless Reorganization**
931
+ - Move sections by reordering method calls
932
+ - Client requests become 30-second changes
933
+
934
+ **🧩 Best of All Worlds**
935
+ - Data-driven sections for consistency
936
+ - Custom HTML where needed
937
+ - Reusable global components
938
+ - Page-specific anonymous components
939
+
940
+ **⚡ Performance Benefits**
941
+ - No global registry pollution
942
+ - Page-scoped CSS compilation
943
+ - Leverages existing Joys optimizations
944
+
945
+ This pattern transforms tedious page layout management into an agile, data-driven workflow that scales beautifully.
946
+
947
+ ---
948
+
949
+ ## The Final Assessment: When to Use Joys
950
+
951
+ - You want **high-throughput** Ruby APIs serving HTML.
952
+ - You want **memory-tight rendering** in constrained environments.
953
+ - You want to **replace ERB** without giving up **developer happiness**.
954
+ - You want to build functional, composable user interfaces
955
+
956
+ ## When Not to Use Joys
957
+
958
+ - You need **streaming HTML** (use ERB, Slim & Phlex instead)
959
+ - You want maturity and community. This library is quite new!
960
+ - You prefer HTML markup over a Ruby DSL
961
+
962
+ Read on for the full scope of features and then decide if it's worth giving Joys a try!
963
+
964
+ ## Frequently Asked Questions
965
+
966
+ ### What versions of Ruby are supported?
967
+
968
+ Joys is compatible with modern Ruby versions (≥2.6) and tested on recent releases for performance and feature stability.
969
+
970
+ ### How does Joys handle escaping and injection attacks?
971
+
972
+ Joys defaults to HTML-escaping text, making output safe unless explicitly marked as raw. Use txt for safe user content, and raw only for trusted HTML fragments.
973
+
974
+ ### Can Joys be used outside Rails?
975
+
976
+ Yes, Joys is a pure Ruby templating engine and works in any Ruby project, not just Rails. Rails integration is completely optional.
977
+
978
+ ### Do Rails helpers and SafeBuffer work with Joys?
979
+
980
+ All Rails helpers, SafeBuffer, and routing helpers work natively within Joys components and pages, so migration is straightforward and full-featured.
981
+
982
+ ### How are components and layouts organized?
983
+
984
+ Joys uses a global registry for components, pages, and layouts. Namespacing and convention (symbols or strings) are supported for large codebases to avoid naming collisions.
985
+
986
+ ### What should I do if I see an error?
987
+
988
+ Any runtime errors or misuse are clearly displayed in the stack trace, making debugging direct and transparent. There are very few edge case errors, and all are intentionally explicit to ease troubleshooting.
989
+
990
+ ### Is it really that fast, or is this just hype?
991
+
992
+ Joys is exactly as fast as advertised after much battle testing, benching and profiling it to oblivion. If you find any flaw in our testing methodology please let us know how we can improve or level the playing field.
993
+
994
+ ### Why Not Just Use Phlex instead?
995
+
996
+ Truthfully, there is no compelling reason not to use Phlex. It's mature, blazing fast and smart on memory consumption. Joys is very much inspired by this framework. The APIs are strikingly similar, however Joys adopts a more functional/minimal paradigm for composition. It also comes with UI styling baked in (something not seen in any other ruby templating library).
997
+
998
+ ---
999
+
1000
+ ## License
1001
+
1002
+ MIT
1003
+
1004
+ ---
1005
+
1006
+ Joys isn’t just a templating engine. It’s a new definition of what rendering in Ruby should feel like: fast, safe, and joyful.
1007
+
1008
+ *Built with ❤️ and a relentless focus on performance.*
1009
+
1010
+ ---
1011
+
1012
+ ## Thanks and Gratitude
1013
+
1014
+ Massive shout-out to the Masatoshi Seki (ERB), Daniel Mendler (Slim), Daniel Mendler (Phlex/P2), and Jonathan Gillette (Markaby). All of you have made countless lives so much easier. The ruby community thanks you for your service!