joys 0.1.0 → 0.1.1

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 (6) hide show
  1. checksums.yaml +4 -4
  2. data/.DS_Store +0 -0
  3. data/CHANGELOG.md +4 -0
  4. data/README.md +1008 -3
  5. data/lib/joys/version.rb +1 -1
  6. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c4d2699d78bb4996d566545b6732a6827b3cc69598e5d7022ce19af9ae5ed094
4
- data.tar.gz: 5d873a049ad5b90ebf7619a0217c9c279761e29a8bfb89ee1a0a33f273a23b10
3
+ metadata.gz: 23119ec444e28d23bfa38b26001df0ad2fad672021083fc3371fd5dd8957bea6
4
+ data.tar.gz: e4735595c70399f92886a28b7faeb1f75ac80041d5231cf048800b85ea68d12d
5
5
  SHA512:
6
- metadata.gz: 41e4ae91ebc5db41f3f3fc31f2161e242cb59e2c0080108520b5f063e67025e1c4648ebce8da87ef0d794bef90c6d45cc780d25601f8d2d1b655682c4737885b
7
- data.tar.gz: e36523d18caa7751241a550b39dbd0c0ba96e8fdb3a710ecd4458345edeae77805ad457ce401e0b4916e713b17037039d677154051ad11bfeda49d282084c94a
6
+ metadata.gz: 05faad732f1786b7138283f616f76afe6fca52c4d57d46085250c12c18ea9ce367e024443e3e45da742dca5fe057416030b6663cee402019d4ef9cef2209d1b5
7
+ data.tar.gz: 05c5543a53c6540f3330ca365f0f0dbfc52fa40f028aac7407f7ed48c4cfde8a3e8dd8f451912d7b608242963107fc3b18c8ef968f8474a9a22f73a3a220fb15
data/.DS_Store CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -3,3 +3,7 @@
3
3
  ## [0.1.0] - 2025-09-16
4
4
 
5
5
  - Initial release
6
+
7
+ ## [0.1.1] - 2025-09-16
8
+
9
+ - Actual templating and CSS functionality added
data/README.md CHANGED
@@ -1,5 +1,1010 @@
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
+ |Joys: |0.060535|0.000200|0.060735|0.060736|
356
+ |Slim: |0.048344|0.000183|0.048527|0.048530|
357
+ |ERB: |0.301811|0.000808|0.302619|0.302625|
358
+ |Phlex:|0.069636|0.000470|0.070106|0.071157|
359
+
360
+ #### 🔥 HOT RENDER (cached objects)
361
+ | | user | system | total | real |
362
+ |Joys: |0.065255| 0.000257|0.065512|0.065512|
363
+ |Slim: |0.049323| 0.000295|0.049618|0.049695|
364
+ |ERB: |0.309757| 0.001167|0.310924|0.310929|
365
+ |Phlex: |0.069663| 0.000141|0.069804|0.069805|
366
+
367
+ #### 💾 MEMORY USAGE
368
+ Joys memory: 532356 bytes
369
+ Slim memory: 40503436 bytes
370
+ Phlex memory: 8000 bytes
371
+ ERB memory: 1669256 bytes
372
+
373
+ At smaller scales performance is on par with Phlex, which has excellent speed and superior
374
+
375
+ ### Complex Templates (8,000+ chars)
376
+
377
+ 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:
378
+
379
+ * Data parsing
380
+ * Layouts
381
+ * Multiple Content Slots
382
+ * Multiple Components
383
+ * Several Iterations
384
+ * Conditions
385
+
386
+ ### ❄️ COLD RENDER (new object per call)
387
+
388
+ This is the bare bones benchmark, no "cheating" allowed.
389
+
390
+ | | user | system | total | real |
391
+ | Joys (cold)|0.051715|0.000364|0.052079|0.052080|
392
+ | ERB (cold)|0.520495|0.003696|0.524191|0.524187|
393
+ | Slim (cold)|6.001650|0.019418|6.021068|6.021013|
394
+ | Phlex(cold)|0.169567|0.000373|0.169940|0.169938|
395
+
396
+ Note: Joys achieves its 'cold render' speed by eliminating object overhead and using simple string operations with smart memory management.
397
+
398
+ ### 🔥 HOT RENDER (cached objects)
399
+
400
+ | | user | system | total | real |
401
+ |JOYS: | 0.000463|0.000010| 0.000473|0.000473|
402
+ |SLIM: | 0.045881|0.000358| 0.046239|0.046243|
403
+ |PHLEX:| 0.167631|0.000760| 0.168391|0.168661|
404
+ |ERB: | 0.394509|0.004084| 0.398593|0.398614|
405
+
406
+ 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.
407
+
408
+
409
+ ### 💾 MEMORY USAGE
410
+
411
+ Joys memory: 7587400 bytes
412
+ Slim memory: 217778600 bytes
413
+ Phlex memory: 9956000 bytes
414
+ ERB memory: 7264240 bytes
415
+
416
+ Even without caching, Joys is rendering at around 50 milliseconds.
417
+
418
+ The real performance comes after caching at half a millisecond.
419
+
420
+ That's not just fast, its _ludicrous speed_ 🚀 All thanks to Joys's one-time compilation and caching of the template structure.
421
+
422
+ But don't take our word for it:
423
+
424
+ ```
425
+ gem install erb slim phlex joys
426
+
427
+ # Simple benchmark
428
+ ruby text/bench/simple.rb
429
+
430
+ # Complex benchmark
431
+ ruby text/bench/deep.rb
432
+ ```
433
+
434
+ Note: If you have any additions, critiques or input on these benchmarks please submit a pull request.
435
+
436
+ ### Performance Philosophy
437
+
438
+ Joys attains its incredible speed through:
439
+
440
+ 1. **Precompiled layouts** - Templates compile once, render many
441
+ 2. **Direct string building** - No intermediate AST or object allocation
442
+ 3. **Smart caching** - Components cache by arguments automatically
443
+ 4. **Zero abstraction penalty** - It's just method calls and string concatenation
444
+
445
+ All achieved without C or Rust. Just plain old Ruby, with a core of under 500 lines of code.
446
+
447
+ ---
448
+
449
+
450
+ ## Joy Styling – Scoped, Deduped, Responsive CSS Without the Hassle
451
+
452
+ Joys doesn’t just generate HTML—it handles CSS like a **pure Ruby, React-level, zero-Node, blazing-fast styling engine**.
453
+
454
+ With **`styles`**, you can write component CSS **inline**, **scoped**, **deduped**, and **responsive**—all automatically, without ever touching a separate asset pipeline.
455
+
456
+ ---
457
+
458
+ ### Basic Usage
459
+
460
+ ```ruby
461
+
462
+ # Make sure your layout has a style tag with pull_styles
463
+
464
+ Joys.define(:layout, :main) do
465
+ doctype
466
+ html {
467
+ head {
468
+ style { pull_styles } # all css will be rendered here
469
+ }
470
+ }
471
+ end
472
+
473
+ # Works when registering coomponents and pages
474
+
475
+ Joys.define(:comp, :card) do
476
+ div(cs: "card") do
477
+ h1("Hello", cs: "head")
478
+ p("Welcome to Joys!")
479
+ end
480
+
481
+ styles do
482
+ css ".card p { color:#333; font-size:0.875rem }"
483
+ end
484
+ end
485
+
486
+ html = Joys.comp(:card)
487
+ puts html
488
+ ```
489
+
490
+ Note: For multiline css statements you may use Ruby's `%()` method as follows
491
+
492
+ ```ruby
493
+ styles do
494
+ css %w(
495
+ .card p { color:#333; font-size:0.875rem }
496
+ .card { background:#fff; border-radius:6px; padding:1rem }
497
+ )
498
+ end
499
+ ```
500
+
501
+ **Output HTML:**
502
+
503
+ ```html
504
+ <div class="card">
505
+ <h1 class="head">Hello</h1>
506
+ <p>Welcome to Joys!</p>
507
+ </div>
508
+ ```
509
+
510
+ **Generated CSS (automatically appended):**
511
+
512
+ ```css
513
+ .card { background:#fff; border-radius:6px; padding:1rem }
514
+ .card p { color:#333; font-size:0.875rem }
515
+ ```
516
+
517
+ ---
518
+
519
+ ### Automatic CSS Scoping
520
+
521
+ Want your component styles **prefixed automatically** to avoid collisions? Just opt in with `scoped: true`:
522
+
523
+ ```ruby
524
+ Joys.define(:comp, :user) do
525
+ styles(scoped: true) do
526
+ css %w(
527
+ .card { background:#fff }
528
+ .head { font-weight:bold }
529
+ )
530
+ end
531
+ end
532
+ ```
533
+
534
+ **Generated CSS (prefixed with component name):**
535
+
536
+ ```css
537
+ .user .card { background:#fff }
538
+ .user .head { font-weight:bold }
539
+ ```
540
+
541
+ > No more worrying about collisions with other components, layouts, or third-party CSS.
542
+
543
+ ---
544
+
545
+ ### Responsive Media Queries
546
+
547
+ Joys supports **`min_media`**, **`max_media`**, and **`minmax_media`** right inside your styles. Just pass the width and CSS string.
548
+
549
+ ```ruby
550
+ styles do
551
+ css %w(
552
+ .card { padding:1rem }
553
+ )
554
+
555
+ media_min "768px", %(
556
+ .card { padding:2rem }
557
+ .head { padding:3rem }
558
+ )
559
+
560
+ media_max "480px", ".card { padding:0.5rem }"
561
+ media_minmax "481px", "767px", ".card { padding:1rem }"
562
+ end
563
+ ```
564
+
565
+ **Generated CSS:**
566
+
567
+ ```css
568
+ .card { padding:1rem }
569
+
570
+ @media (min-width: 768px) {
571
+ .card { padding:2rem }
572
+ .head { padding:3rem }
573
+ }
574
+
575
+ @media (max-width: 480px) {
576
+ .card { padding:0.5rem }
577
+ }
578
+
579
+ @media (min-width: 481px) and (max-width: 767px) {
580
+ .card { padding:1rem }
581
+ }
582
+ ```
583
+
584
+ * ✅ **Merged automatically**
585
+ * ✅ **Deduped**
586
+ * ✅ **Sorted by breakpoint**
587
+ * ✅ **Scoped if opted-in**
588
+
589
+ And yes, we also have container queries!
590
+
591
+ ```ruby
592
+ styles do
593
+ container_min "768px", ".card {font-size:1.2em}"
594
+ named_container_min "sidebar", "768px", ".card {font-size:2em}"
595
+ end
596
+ ```
597
+
598
+ **Generated CSS:**
599
+
600
+ ```css
601
+ @container (min-width:768px) {
602
+ .card {font-size:1.2em}
603
+ }
604
+ @container sidebar (min-width:768px) {
605
+ .card {font-size:1.2em}
606
+ }
607
+ ```
608
+
609
+ ---
610
+
611
+ ### Deduplication & Ordering – Built In
612
+
613
+ Write your component styles wherever you like. Joys will:
614
+
615
+ * Remove **duplicate selectors** automatically
616
+ * Preserve **correct CSS order across nested components**
617
+ * Merge **global, page-level, and component-level styles** seamlessly
618
+
619
+ ```ruby
620
+ # Even if .card appears in multiple components:
621
+ styles do
622
+ css ".card { margin:0 }"
623
+ end
624
+
625
+ # Another component:
626
+ styles do
627
+ css ".card { margin:0 }" # Joys dedupes automatically
628
+ end
629
+ ```
630
+
631
+ ---
632
+
633
+ ### Rendering External CSS
634
+
635
+ 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.
636
+
637
+ #### Basic Usage
638
+
639
+ Replace `pull_styles` with `pull_external_styles` in your layout:
640
+
641
+ ```ruby
642
+ Joys.define(:layout, :main) do
643
+ html(lang: "en") {
644
+ head {
645
+ title { pull(:title) }
646
+ pull_external_styles # Generates <link> tags instead of inline styles
647
+ }
648
+ body {
649
+ pull(:main)
650
+ }
651
+ }
652
+ end
653
+ ```
654
+
655
+ #### How It Works
656
+
657
+ When you render a page, Joys:
658
+
659
+ 1. **Analyzes** which components are actually used
660
+ 2. **Consolidates** all their styles (base CSS + media queries + container queries)
661
+ 3. **Generates** a CSS file named after the component combination
662
+ 4. **Caches** the file to disk (only creates once per unique component set)
663
+ 5. **Returns** the appropriate `<link>` tag
664
+
665
+ ```ruby
666
+ # Page uses: navbar, user_card, button components
667
+ # Generates: /css/comp_navbar,comp_user_card,comp_button.css
668
+ # Returns: <link rel="stylesheet" href="/css/comp_navbar,comp_user_card,comp_button.css">
669
+ ```
670
+
671
+ #### Configuration
672
+
673
+ Set your CSS output directory (defaults to `"public/css"`):
674
+
675
+ ```ruby
676
+ # config/initializers/joys.rb (Rails)
677
+ Joys.css_path = Rails.root.join("public", "assets", "css")
678
+
679
+ # Or for other frameworks
680
+ Joys.css_path = "public/stylesheets"
681
+ ```
682
+
683
+ #### Automatic File Management
684
+
685
+ Joys handles the file lifecycle automatically:
686
+
687
+ - **Smart naming** - Files named by component combination ensure perfect caching
688
+ - **No duplication** - Same component set = same file, served from cache
689
+ - **Directory creation** - Creates nested directories as needed
690
+ - **Production ready** - Files persist across deployments
691
+
692
+ #### Example Generated CSS
693
+
694
+ ```ruby
695
+ Joys.define(:comp, :card) do
696
+ div(cs: "card") { yield }
697
+
698
+ styles do
699
+ css ".card { padding: 1rem; background: white; }"
700
+ media_min(768, ".card { padding: 2rem; }")
701
+ end
702
+ end
703
+
704
+ Joys.define(:comp, :button) do
705
+ button(cs: "btn") { yield }
706
+
707
+ styles(scoped: true) do
708
+ css ".btn { padding: 0.5rem 1rem; }"
709
+ container_min(300, ".btn { width: 100%; }")
710
+ end
711
+ end
712
+ ```
713
+
714
+ **Generated CSS file:**
715
+ ```css
716
+ .card { padding: 1rem; background: white; }
717
+ .button .btn { padding: 0.5rem 1rem; }
718
+ @media (min-width: 768px){
719
+ .card { padding: 2rem; }
720
+ }
721
+ @container (min-width: 300px){
722
+ .button .btn { width: 100%; }
723
+ }
724
+ ```
725
+
726
+ #### Performance Characteristics
727
+
728
+ External CSS provides:
729
+
730
+ - **Browser caching** - CSS files cached separately from HTML
731
+ - **Parallel loading** - CSS downloads while HTML processes
732
+ - **CDN friendly** - Static files easily cached at edge locations
733
+ - **Same deduplication** - All Joys optimizations still apply
734
+
735
+ Trade-offs vs inline:
736
+
737
+ - **Extra HTTP request** - One additional round trip
738
+ - **Render blocking** - CSS must load before styled rendering
739
+ - **Cache complexity** - More moving parts in deployment
740
+
741
+ #### When to Use External CSS
742
+
743
+ Choose external CSS when you have:
744
+
745
+ - **Large stylesheets** (>20KB) where caching outweighs request overhead
746
+ - **Strict CSP policies** that prohibit inline styles
747
+ - **Team preferences** for traditional CSS file organization
748
+ - **CDN optimization** requirements
749
+
750
+ For most applications under 30-40KB of CSS, inline styles offer better performance and simpler deployment.
751
+
752
+ #### Rails Integration
753
+
754
+ External CSS works seamlessly with Rails asset pipeline:
755
+
756
+ ```ruby
757
+ # config/initializers/joys.rb
758
+ Joys.css_path = Rails.root.join("public", "assets", "joys")
759
+
760
+ # Your layout
761
+ Joys.define(:layout, :application) do
762
+ html {
763
+ head {
764
+ stylesheet_link_tag "application", "data-turbo-track": "reload"
765
+ pull_external_styles # Joys-generated styles
766
+ }
767
+ }
768
+ end
769
+ ```
770
+
771
+ #### Deployment Considerations
772
+
773
+ Since CSS files are generated at runtime:
774
+
775
+ - Ensure write permissions on your CSS directory
776
+ - Consider warming the cache on deployment
777
+ - Add `*.css` to your `.gitignore` if files are in your repo path
778
+ - For containerized deployments, mount a persistent volume for the CSS directory
779
+
780
+ #### Mixing with Traditional Assets
781
+
782
+ External Joys CSS plays nicely with existing stylesheets:
783
+
784
+ ```ruby
785
+ head {
786
+ # Your existing global styles
787
+ stylesheet_link_tag "application"
788
+
789
+ # Component-specific Joys styles
790
+ pull_external_styles
791
+
792
+ # Page-specific overrides
793
+ stylesheet_link_tag "admin" if admin_page?
794
+ }
795
+ ```
796
+
797
+ ---
798
+
799
+ ### TL;DR – Why You’ll Never Write CSS the Old Way Again
800
+
801
+ * **Scoped**: Optional prefixing prevents collisions
802
+ * **Deduped**: Automatic removal of duplicate rules
803
+ * **Ordered**: Nested components and responsive queries just work
804
+ * **Responsive**: min/max/minmax media queries built-in
805
+ * **Inline or exportable**: Works in rendering or as a static CSS file
806
+
807
+ > Write CSS where it belongs: next to your component, in pure Ruby, with **React-level convenience and performance**—all without leaving Ruby.
808
+
809
+ 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.
810
+
811
+ ## Functional at Heart, But Ready for OOP
812
+
813
+ 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.
814
+
815
+ The DSL is the same either way — Joys just adapts.
816
+
817
+
818
+ ```ruby
819
+ class BaseLayout
820
+ include Joys::Helpers
821
+
822
+ def render(&block)
823
+ html {
824
+ head { title("My App") }
825
+ body { block.call }
826
+ }
827
+ end
828
+ end
829
+
830
+ class Dashboard < BaseLayout
831
+ def render
832
+ super {
833
+ div(cs: "dashboard") {
834
+ h1("Dashboard")
835
+ p("Rendered through OOP inheritance.")
836
+ a("Logout", cs:"btn",href:"/logout")
837
+ }
838
+ }
839
+ end
840
+ end
841
+
842
+ puts Dashboard.new.render
843
+ # => <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>
844
+ ```
845
+
846
+ You can pick the style that fits your project, or even mix them.
847
+
848
+ ---
849
+ ## 🧩 Bespoke Page Architecture: Content-Driven Flexibility
850
+
851
+ **Perfect for landing pages, marketing campaigns, and any layout requiring easy reorganization.**
852
+
853
+ Modular Page Architecture lets you build pages as Ruby modules with clean data separation, allowing rapid section reorganization without touching template code.
854
+
855
+ ### The Pattern: Data + Methods + Flexible Assembly
856
+
857
+ ```ruby
858
+ # pages/product_launch.rb
859
+ module ProductLaunch
860
+ DATA = {
861
+ hero: {
862
+ title: "Revolutionary SaaS Platform",
863
+ cta: "Start Free Trial"
864
+ },
865
+ features: {
866
+ items: [
867
+ { title: "Lightning Fast", desc: "10x faster" },
868
+ { title: "Secure", desc: "Bank-level security" }
869
+ ]
870
+ }
871
+ }.freeze
872
+
873
+ def self.render
874
+ Joys.define(:page, :product_launch) do
875
+ layout(:main) {
876
+ push(:main) {
877
+ raw hero(DATA[:hero])
878
+ raw features(DATA[:features])
879
+
880
+ # Custom sections between data-driven ones
881
+ div(cs: "demo") {
882
+ h2("See It In Action")
883
+ comp(:demo_video)
884
+ }
885
+
886
+ comp(:testimonials) # Reusable global component
887
+ }
888
+ }
889
+ end
890
+ end
891
+
892
+ private
893
+
894
+ def self.hero(data)
895
+ Joys.html {
896
+ styles { css ".hero { background: #667eea; padding: 4rem; }" }
897
+ # All css rendered and deduplicated within page scope
898
+ section(cs: "hero") {
899
+ h1(data[:title])
900
+ a(data[:cta], href: "/signup", cs: "btn")
901
+ }
902
+ }
903
+ end
904
+
905
+ def self.features(data)
906
+ Joys.html {
907
+ section(cs: "features") {
908
+ data[:items].each { |item|
909
+ div {
910
+ h3(item[:title])
911
+ p(item[:desc])
912
+ }
913
+ }
914
+ }
915
+ }
916
+ end
917
+ end
918
+ ```
919
+
920
+ ### Why This Pattern Works
921
+
922
+ **📊 Content Management Made Simple**
923
+ - All page content in one DATA hash
924
+ - Easy updates without touching templates
925
+
926
+ **🎯 Effortless Reorganization**
927
+ - Move sections by reordering method calls
928
+ - Client requests become 30-second changes
929
+
930
+ **🧩 Best of All Worlds**
931
+ - Data-driven sections for consistency
932
+ - Custom HTML where needed
933
+ - Reusable global components
934
+ - Page-specific anonymous components
935
+
936
+ **⚡ Performance Benefits**
937
+ - No global registry pollution
938
+ - Page-scoped CSS compilation
939
+ - Leverages existing Joys optimizations
940
+
941
+ This pattern transforms tedious page layout management into an agile, data-driven workflow that scales beautifully.
942
+
943
+ ---
944
+
945
+ ## The Final Assessment: When to Use Joys
946
+
947
+ - You want **high-throughput** Ruby APIs serving HTML.
948
+ - You want **memory-tight rendering** in constrained environments.
949
+ - You want to **replace ERB** without giving up **developer happiness**.
950
+ - You want to build functional, composable user interfaces
951
+
952
+ ## When Not to Use Joys
953
+
954
+ - You need **streaming HTML** (use ERB, Slim & Phlex instead)
955
+ - You want maturity and community. This library is quite new!
956
+ - You prefer HTML markup over a Ruby DSL
957
+
958
+ Read on for the full scope of features and then decide if it's worth giving Joys a try!
959
+
960
+ ## Frequently Asked Questions
961
+
962
+ ### What versions of Ruby are supported?
963
+
964
+ Joys is compatible with modern Ruby versions (≥2.6) and tested on recent releases for performance and feature stability.
965
+
966
+ ### How does Joys handle escaping and injection attacks?
967
+
968
+ 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.
969
+
970
+ ### Can Joys be used outside Rails?
971
+
972
+ Yes, Joys is a pure Ruby templating engine and works in any Ruby project, not just Rails. Rails integration is completely optional.
973
+
974
+ ### Do Rails helpers and SafeBuffer work with Joys?
975
+
976
+ All Rails helpers, SafeBuffer, and routing helpers work natively within Joys components and pages, so migration is straightforward and full-featured.
977
+
978
+ ### How are components and layouts organized?
979
+
980
+ 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.
981
+
982
+ ### What should I do if I see an error?
983
+
984
+ 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.
985
+
986
+ ### Is it really that fast, or is this just hype?
987
+
988
+ 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.
989
+
990
+ ### Why Not Just Use Phlex instead?
991
+
992
+ 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).
993
+
994
+ ---
995
+
996
+ ## License
997
+
998
+ MIT
999
+
1000
+ ---
1001
+
1002
+ Joys isn’t just a templating engine. It’s a new definition of what rendering in Ruby should feel like: fast, safe, and joyful.
1003
+
1004
+ *Built with ❤️ and a relentless focus on performance.*
1005
+
1006
+ ---
1007
+
1008
+ ## Thanks and Gratitude
1009
+
1010
+ 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!
data/lib/joys/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Joys
4
- VERSION = "0.1.0"
4
+ VERSION = "0.1.1"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: joys
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Steven Garcia