rip-lang 2.5.0 → 2.7.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.
package/docs/GUIDE.md CHANGED
@@ -2,19 +2,17 @@
2
2
 
3
3
  # Rip Language Guide
4
4
 
5
- > **The Language IS the Framework**
5
+ > **Modern CoffeeScript with Built-in Reactivity**
6
6
 
7
- This comprehensive guide covers Rip's reactive primitives, template syntax, component model, and special operators. Rip provides these features as **language-level constructs**, not library importsreactivity and UI are built into the syntax itself.
7
+ This comprehensive guide covers Rip's reactive primitives, special operators, and regex enhancements. Rip provides reactivity as a **language-level construct**, not a library importstate management is built into the syntax itself.
8
8
 
9
9
  ---
10
10
 
11
11
  ## Table of Contents
12
12
 
13
13
  1. [Reactivity](#1-reactivity)
14
- 2. [Templates](#2-templates)
15
- 3. [Components](#3-components)
16
- 4. [Special Operators](#4-special-operators)
17
- 5. [Regex+ Features](#5-regex-features)
14
+ 2. [Special Operators](#2-special-operators)
15
+ 3. [Regex+ Features](#3-regex-features)
18
16
 
19
17
  ---
20
18
 
@@ -24,72 +22,91 @@ Rip provides reactive primitives as **language-level operators**, not library im
24
22
 
25
23
  ## Reactive Operators
26
24
 
27
- | Operator | Name | Purpose |
28
- |----------|------|---------|
29
- | `:=` | Signal | Reactive state variable |
30
- | `~=` | Derived | Computed value (auto-updates when dependencies change) |
31
- | `effect` | Effect | Side effect that runs when dependencies change |
32
- | `=!` | Equal, dammit! | Constant (`const`) - not reactive, just immutable |
25
+ | Operator | Name | Read as | Purpose |
26
+ |----------|------|---------|---------|
27
+ | `=` | Assign | "gets value" | Regular assignment |
28
+ | `:=` | State | "**has state**" | Reactive state variable |
29
+ | `~=` | Computed | "**always equals**" | Computed value (auto-updates when dependencies change) |
30
+ | `~>` | Effect | "**reacts to**" | Side effect that runs when dependencies change |
31
+ | `=!` | Readonly | "equals, dammit!" | Constant (`const`) - not reactive, just immutable |
33
32
 
34
- ## Reactive State (`:=`)
33
+ ## Reactive State (`:=`) — "has state"
35
34
 
36
- The signal operator creates reactive state:
35
+ The state operator creates reactive state:
37
36
 
38
37
  ```coffee
39
- count := 0 # Reactive signal
40
- name := "world" # Another reactive signal
38
+ count := 0 # count has state 0
39
+ name := "world" # name has state "world"
41
40
  ```
42
41
 
43
- State changes automatically trigger updates in any derived values or effects that depend on them.
42
+ State changes automatically trigger updates in any computed values or effects that depend on them.
44
43
 
45
- ## Derived Values (`~=`)
44
+ ## Computed Values (`~=`) — "always equals"
46
45
 
47
- The "always equals" operator creates a value that automatically recomputes when its dependencies change:
46
+ The computed operator creates a value that automatically recomputes when its dependencies change:
48
47
 
49
48
  ```coffee
50
49
  count := 0
51
- doubled ~= count * 2 # Always equals count * 2
50
+ doubled ~= count * 2 # doubled always equals count * 2
52
51
 
53
52
  count = 5 # doubled automatically becomes 10
54
53
  count = 10 # doubled automatically becomes 20
55
54
  ```
56
55
 
57
- ## Constant Values (`=!`) - "Equal, Dammit!"
56
+ ## Side Effects (`~>`) "reacts to"
58
57
 
59
- In Rip, regular assignment (`=`) compiles to `let` for maximum flexibility. When you want an immutable constant, use the "equal, dammit!" operator (`=!`), which compiles to `const`:
58
+ The effect operator defines a side effect that runs when its dependencies change. Dependencies are auto-tracked from reactive values read in the body:
60
59
 
61
60
  ```coffee
62
- # Regular assignment → let (can reassign)
63
- host = "localhost"
64
- host = "example.com" # OK - variables are flexible by default
61
+ count := 0
65
62
 
66
- # Equal, dammit! const (can't reassign)
67
- API_URL =! "https://api.example.com"
68
- MAX_RETRIES =! 3
63
+ ~> console.log "Count changed to:", count
69
64
 
70
- API_URL = "other" # Error! const cannot be reassigned
65
+ count = 5 # Logs: "Count changed to: 5"
66
+ count = 10 # Logs: "Count changed to: 10"
71
67
  ```
72
68
 
73
- This gives you opt-in immutability when you need it, while keeping the default flexible for scripting.
69
+ Effects are useful for:
70
+ - Logging and debugging
71
+ - Syncing with external systems
72
+ - Analytics tracking
73
+ - Local storage persistence
74
74
 
75
- ## Side Effects (`effect`)
75
+ ### Controllable Effects
76
76
 
77
- The `effect` keyword defines a side effect block that runs when its dependencies change:
77
+ Assign the effect to a variable to control it:
78
78
 
79
79
  ```coffee
80
80
  count := 0
81
81
 
82
- effect -> console.log "Count changed to:", count
82
+ # Fire and forget (no assignment)
83
+ ~> console.log count
83
84
 
84
- count = 5 # Logs: "Count changed to: 5"
85
- count = 10 # Logs: "Count changed to: 10"
85
+ # Controllable (assign to variable)
86
+ logger ~> console.log count
87
+
88
+ logger.stop! # Pause reactions
89
+ logger.run! # Resume reactions
90
+ logger.cancel! # Permanent disposal
86
91
  ```
87
92
 
88
- Effects are useful for:
89
- - Logging and debugging
90
- - Syncing with external systems
91
- - Analytics tracking
92
- - Local storage persistence
93
+ ## Constant Values (`=!`) — "equals, dammit!"
94
+
95
+ In Rip, regular assignment (`=`) compiles to `let` for maximum flexibility. When you want an immutable constant, use the "equal, dammit!" operator (`=!`), which compiles to `const`:
96
+
97
+ ```coffee
98
+ # Regular assignment → let (can reassign)
99
+ host = "localhost"
100
+ host = "example.com" # OK - variables are flexible by default
101
+
102
+ # Equal, dammit! → const (can't reassign)
103
+ API_URL =! "https://api.example.com"
104
+ MAX_RETRIES =! 3
105
+
106
+ API_URL = "other" # Error! const cannot be reassigned
107
+ ```
108
+
109
+ This gives you opt-in immutability when you need it, while keeping the default flexible for scripting.
93
110
 
94
111
  ## Auto-Unwrapping
95
112
 
@@ -114,11 +131,22 @@ count.read() # Get value without tracking dependencies
114
131
  |--------|---------|
115
132
  | `x.read()` | Get value without tracking (for effects that shouldn't re-run) |
116
133
  | `x.value` | Direct access to the underlying value |
117
- | `+x` | Shorthand for `x.value` (triggers tracking in effects) |
134
+ | `+x` | Shorthand for `x.value` (triggers tracking in computed/effects) |
118
135
  | `x.lock()` | Make value readonly (can read but can't change) |
119
- | `x.free()` | Unsubscribe from all dependencies (signal still works) |
136
+ | `x.free()` | Unsubscribe from all dependencies (state still works) |
120
137
  | `x.kill()` | Clean up everything and return final value |
121
138
 
139
+ ## Effect Controller Methods
140
+
141
+ When you assign an effect to a variable, you get a controller object:
142
+
143
+ | Method | Purpose |
144
+ |--------|---------|
145
+ | `e.stop!` | Pause reactions (can resume later) |
146
+ | `e.run!` | Resume reactions |
147
+ | `e.cancel!` | Permanent disposal (cannot resume) |
148
+ | `e.active` | Boolean — is the effect running? |
149
+
122
150
  ## Dependency Tracking
123
151
 
124
152
  Understanding when dependencies are tracked is key to effective reactive programming.
@@ -133,7 +161,7 @@ Understanding when dependencies are tracked is key to effective reactive program
133
161
  | `+count` | ✅ Yes | Unary plus triggers `.valueOf()` |
134
162
  | `count.value` | ✅ Yes | Direct `.value` access |
135
163
  | `count.read()` | ❌ No | Explicit non-tracking read |
136
- | `y = count` | ❌ No | Assigns signal object, not value |
164
+ | `y = count` | ❌ No | Assigns state object, not value |
137
165
 
138
166
  ### Example: Tracking vs Non-Tracking
139
167
 
@@ -141,37 +169,17 @@ Understanding when dependencies are tracked is key to effective reactive program
141
169
  count := 10
142
170
 
143
171
  # Effect A: Subscribes to count (will re-run when count changes)
144
- effect -> console.log "A: #{count}"
172
+ ~> console.log "A: #{count}"
145
173
 
146
- # Effect B: Does NOT subscribe (won't re-run)
147
- effect -> console.log "B: #{count.read()}"
148
-
149
- count = 20
150
- # Output:
151
- # A: 20 ← Effect A re-ran
152
- # ← Effect B did NOT re-run
153
- ```
154
-
155
- ### When to Use `.read()`
156
-
157
- Use `.read()` when you need the current value but don't want to create a dependency:
158
-
159
- ```coffee
160
- count := 0
161
- lastSaved := 0
162
-
163
- effect ->
164
- # We want to log count changes, but compare against lastSaved
165
- # without re-running when lastSaved changes
166
- if count != lastSaved.read()
167
- console.log "Unsaved changes: #{count}"
174
+ # Reading without tracking (for comparisons, etc.)
175
+ currentValue = count.read() # Does not create dependency
168
176
  ```
169
177
 
170
178
  ## Lifecycle & Cleanup
171
179
 
172
- ### Locking a Signal
180
+ ### Locking a State
173
181
 
174
- Make a signal readonly (subscriptions stay active):
182
+ Make a state readonly (subscriptions stay active):
175
183
 
176
184
  ```coffee
177
185
  config := { theme: "dark" }
@@ -193,25 +201,29 @@ doubled.free() # No longer updates when count changes
193
201
  count = 10 # doubled stays at its last value
194
202
  ```
195
203
 
196
- ### Killing a Signal
204
+ ### Killing a State
197
205
 
198
206
  Clean up completely and get the final value:
199
207
 
200
208
  ```coffee
201
209
  count := 10
202
- finalValue = count.kill() # Returns 10, signal is now dead
210
+ finalValue = count.kill() # Returns 10, state is now dead
203
211
 
204
- count = 20 # Error or no-op (signal is dead)
212
+ count = 20 # Error or no-op (state is dead)
205
213
  ```
206
214
 
207
215
  ### Effect Cleanup
208
216
 
209
- Effects can return a cleanup function:
217
+ Use the effect controller to manage lifecycle:
210
218
 
211
219
  ```coffee
212
- effect ->
220
+ # Assign to a variable for control
221
+ ticker ~>
213
222
  interval = setInterval (-> tick()), 1000
214
- -> clearInterval interval # Cleanup when effect re-runs or disposes
223
+ -> clearInterval interval # Cleanup function returned
224
+
225
+ # Later, when done:
226
+ ticker.cancel! # Stops the effect and runs cleanup
215
227
  ```
216
228
 
217
229
  ## Real-World Example
@@ -219,21 +231,17 @@ effect ->
219
231
  A complete reactive counter with persistence:
220
232
 
221
233
  ```coffee
222
- # Reactive state
234
+ # Reactive state — count has state (loaded from localStorage)
223
235
  count := parseInt(localStorage.getItem("count")) or 0
224
236
 
225
- # Derived values
237
+ # Computed values — always equal to their expressions
226
238
  doubled ~= count * 2
227
239
  isEven ~= count % 2 == 0
228
240
  message ~= "Count is #{count} (#{isEven ? 'even' : 'odd'})"
229
241
 
230
- # Side effect: persist to localStorage
231
- effect ->
232
- localStorage.setItem "count", count
233
-
234
- # Side effect: log changes
235
- effect ->
236
- console.log message
242
+ # Side effects react to dependencies (auto-tracked)
243
+ ~> localStorage.setItem "count", count # Persist
244
+ ~> console.log message # Log
237
245
 
238
246
  # Usage
239
247
  count = 5
@@ -251,16 +259,16 @@ The Rip compiler transforms reactive operators into efficient JavaScript:
251
259
 
252
260
  ```coffee
253
261
  # Rip source
254
- count := 0
255
- doubled ~= count * 2
256
- effect -> console.log doubled
262
+ count := 0 # count has state 0
263
+ doubled ~= count * 2 # doubled always equals count * 2
264
+ ~> console.log count # reacts to count changes
257
265
  ```
258
266
 
259
267
  ```javascript
260
268
  // Compiled output (conceptual)
261
- const count = __signal(0);
269
+ const count = __state(0);
262
270
  const doubled = __computed(() => count.value * 2);
263
- __effect(() => console.log(doubled.value));
271
+ __effect(() => { console.log(count.value); });
264
272
  ```
265
273
 
266
274
  The runtime is **automatically inlined** - no external dependencies required.
@@ -289,843 +297,15 @@ console.log(y);
289
297
  | Concept | React | Vue | Solid | Rip |
290
298
  |---------|-------|-----|-------|-----|
291
299
  | State | `useState()` | `ref()` | `createSignal()` | `x := 0` |
292
- | Derived | `useMemo()` | `computed()` | `createMemo()` | `x ~= y * 2` |
293
- | Effect | `useEffect()` | `watch()` | `createEffect()` | `effect ->` |
300
+ | Computed | `useMemo()` | `computed()` | `createMemo()` | `x ~= y * 2` |
301
+ | Effect | `useEffect()` | `watch()` | `createEffect()` | `~> body` or `x ~> body` |
294
302
  | Constant | `const` | `const` | `const` | `x =! 0` |
295
303
 
296
304
  Rip's approach: **No imports, no hooks, no special functions. Just operators.**
297
305
 
298
306
  ---
299
307
 
300
- # 2. Templates
301
-
302
- Rip's template syntax is not a separate language—it's native Rip syntax. The `render` block uses indentation-based markup that compiles directly to efficient DOM operations.
303
-
304
- ## Quick Reference
305
-
306
- | Syntax | Purpose | Example |
307
- |--------|---------|---------|
308
- | `tag` | Element | `div`, `span`, `button` |
309
- | `.class` | CSS class | `div.card`, `button.btn.primary` |
310
- | `#id` | Element ID | `div#main`, `section#hero` |
311
- | `.()` | Dynamic classes | `div.('active', isOn && 'on')` |
312
- | `attr: val` | Attribute | `type: "text"`, `disabled: true` |
313
- | `@event: fn` | Event handler | `@click: handleClick` |
314
- | `@event.mod:` | Event modifier | `@click.prevent: submit` |
315
- | `"text"` | Text content | `span "Hello"` |
316
- | `#{expr}` | Interpolation | `"Count: #{count}"` |
317
- | `ref: var` | Element reference | `ref: inputEl` |
318
- | `key: val` | List item key | `key: item.id` |
319
- | `...props` | Spread attributes | `div ...props` |
320
- | `X <=> var` | Two-way binding | `value <=> name` |
321
- | `if`/`else` | Conditional | `div if visible` |
322
- | `for...in` | Iteration | `for item in items` |
323
-
324
- ## Tags
325
-
326
- ### Basic Tags
327
-
328
- ```coffee
329
- render
330
- div
331
- span
332
- button
333
- input
334
- MyComponent
335
- ```
336
-
337
- ### With Classes (CSS Selector Syntax)
338
-
339
- ```coffee
340
- div.card
341
- div.card.active
342
- button.btn.btn-primary
343
- span.badge.badge-success
344
- ```
345
-
346
- ### With IDs
347
-
348
- ```coffee
349
- div#main
350
- section#hero
351
- input#search-field
352
- ```
353
-
354
- ### Combined
355
-
356
- ```coffee
357
- section#hero.full-width.dark
358
- div#sidebar.panel.collapsed
359
- article#post-123.blog-post.featured
360
- ```
361
-
362
- ## Attributes
363
-
364
- ### Inline
365
-
366
- ```coffee
367
- input type: "text", placeholder: "Enter name", required: true
368
- a href: "/home", target: "_blank", "Go Home"
369
- img src: user.avatar, alt: user.name
370
- ```
371
-
372
- ### Indented (for many attributes)
373
-
374
- ```coffee
375
- input
376
- type: "email"
377
- placeholder: "you@example.com"
378
- required: true
379
- autocomplete: "email"
380
- @input: handleInput
381
- ```
382
-
383
- ### Dynamic Values
384
-
385
- ```coffee
386
- input value: searchTerm, disabled: isLoading
387
- img src: user.avatar, alt: user.name
388
- a href: "/users/#{user.id}", "View Profile"
389
- div title: tooltip, data-id: item.id
390
- ```
391
-
392
- ### Boolean Attributes
393
-
394
- Boolean attributes are present when `true`, absent when `false`:
395
-
396
- ```coffee
397
- button disabled: isLoading # <button disabled> or <button>
398
- input required: true, readonly: isLocked
399
- option selected: isDefault
400
- details open: expanded
401
- ```
402
-
403
- ### Dynamic Classes with `cx()` (clsx-compatible)
404
-
405
- Rip includes a `clsx`-compatible `cx()` helper for dynamic class composition. Use the `.()` syntax on elements:
406
-
407
- ```coffee
408
- # Basic: conditions in parens
409
- div.('card', isActive && 'active', size)
410
-
411
- # With static classes too
412
- div.card.('highlighted', isNew && 'new')
413
-
414
- # Object syntax (like clsx)
415
- div.({ active: isActive, disabled: isDisabled })
416
-
417
- # Mixed - strings, conditions, objects
418
- div.base.('extra', { selected: isSelected })
419
- ```
420
-
421
- ### Spreading Props
422
-
423
- Spread an object as attributes:
424
-
425
- ```coffee
426
- render
427
- # Basic spread
428
- div ...props
429
-
430
- # With static classes
431
- div.card ...props
432
-
433
- # With explicit attrs (override spreads)
434
- input ...inputProps, class: "extra", disabled: true
435
-
436
- # With children
437
- div.wrapper ...containerProps
438
- span "Content"
439
- ```
440
-
441
- ## Two-Way Binding
442
-
443
- Two-way binding automatically syncs an element's value with a variable using the `<=>` operator:
444
-
445
- ```coffee
446
- render
447
- # Text input - value syncs with username
448
- input value <=> username
449
-
450
- # Checkbox - checked syncs with isActive
451
- input type: "checkbox", checked <=> isActive
452
-
453
- # Select dropdown
454
- select value <=> selectedId
455
-
456
- # Textarea
457
- textarea value <=> content
458
- ```
459
-
460
- The `<=>` operator reads as "syncs with" — it's a visual representation of bidirectional data flow.
461
-
462
- **Smart event selection:**
463
- - `value` on `input`/`textarea` → `oninput` event
464
- - `value` on `select` → `onchange` event
465
- - `checked` → `onchange` event
466
-
467
- **Smart value access:**
468
- - `type="number"` inputs → `e.target.valueAsNumber`
469
- - `type="range"` inputs → `e.target.valueAsNumber`
470
- - All other inputs → `e.target.value`
471
-
472
- ## Event Handlers
473
-
474
- ### Basic Events
475
-
476
- ```coffee
477
- button @click: handleClick
478
- button @click: -> count += 1
479
- input @input: (e) -> value = e.target.value
480
- form @submit: handleSubmit
481
- ```
482
-
483
- ### Event Handler Patterns
484
-
485
- There are two common patterns for event handlers:
486
-
487
- ```coffee
488
- # Normal: define methods, reference with @
489
- inc: -> count += 1
490
- button @click: @inc, "+"
491
-
492
- # Compact: inline with fat arrow (parens required)
493
- button (@click: => @count++), "+"
494
- ```
495
-
496
- The fat arrow (`=>`) binds `this` correctly for inline handlers.
497
-
498
- ### Event Modifiers
499
-
500
- ```coffee
501
- # Prevent default
502
- form @submit.prevent: handleSubmit
503
- a @click.prevent: navigate
504
-
505
- # Stop propagation
506
- button @click.stop: handleClick
507
-
508
- # Combined
509
- a @click.prevent.stop: handleNavigation
510
-
511
- # Once (auto-removes after first call)
512
- button @click.once: initialize
513
-
514
- # Self (only if target is the element itself)
515
- div @click.self: handleDivClick
516
- ```
517
-
518
- ### Key Modifiers
519
-
520
- ```coffee
521
- input @keydown.enter: submit
522
- input @keydown.escape: cancel
523
- input @keydown.tab: handleTab
524
- input @keydown.space: togglePlay
525
- input @keydown.up: previousItem
526
- input @keydown.down: nextItem
527
- ```
528
-
529
- ### Modifier Key Combinations
530
-
531
- ```coffee
532
- input @keydown.ctrl.s: save
533
- input @keydown.cmd.s: save # Mac Command key
534
- input @keydown.shift.enter: newLine
535
- input @keydown.ctrl.shift.z: redo
536
- button @click.ctrl: openInNewTab
537
- ```
538
-
539
- ## Text Content
540
-
541
- ### As Final Argument
542
-
543
- ```coffee
544
- button "Click me"
545
- span "Hello, #{name}!"
546
- h1 "Welcome"
547
- p "This is a paragraph of text."
548
- ```
549
-
550
- ### Variables as Text
551
-
552
- ```coffee
553
- span count
554
- span user.name
555
- span formatCurrency(total)
556
- td item.quantity
557
- ```
558
-
559
- ### Mixed Content
560
-
561
- ```coffee
562
- p
563
- "Hello, "
564
- strong name
565
- "! Welcome back."
566
-
567
- span
568
- "Total: "
569
- strong formatCurrency(total)
570
- ```
571
-
572
- ## Children & Nesting
573
-
574
- Rip uses implicit nesting based on indentation:
575
-
576
- ```coffee
577
- div.card
578
- header.card-header
579
- h2.title "Product"
580
- span.badge "New"
581
-
582
- div.card-body
583
- p description
584
-
585
- ul.features
586
- li "Feature one"
587
- li "Feature two"
588
- li "Feature three"
589
-
590
- footer.card-footer
591
- button.secondary "Cancel"
592
- button.primary "Buy Now"
593
- ```
594
-
595
- You can also use explicit arrow syntax for inline nesting:
596
-
597
- ```coffee
598
- div.card -> h1 "Title"
599
- ```
600
-
601
- ## Conditionals
602
-
603
- ### If/Else Blocks
604
-
605
- ```coffee
606
- div.status
607
- if loading
608
- span.spinner
609
- "Loading..."
610
- else if error
611
- span.error error.message
612
- else
613
- span.success "Loaded!"
614
- ```
615
-
616
- ### Inline Conditionals
617
-
618
- ```coffee
619
- span.badge "Admin" if user.isAdmin
620
- span.warning "Unsaved" unless saved
621
- div.alert error if error
622
- ```
623
-
624
- ### Ternary Expressions
625
-
626
- ```coffee
627
- span class: { active: isActive }
628
- isActive ? "On" : "Off"
629
-
630
- button class: { primary: isPrimary }
631
- isPrimary ? "Save" : "Continue"
632
- ```
633
-
634
- ## Loops
635
-
636
- ### Array Iteration with Key
637
-
638
- **Always provide a `key` for list items** to enable efficient updates:
639
-
640
- ```coffee
641
- ul.todo-list
642
- for todo in todos, key: todo.id
643
- li class: { completed: todo.done }
644
- span todo.text
645
- button @click: -> remove(todo), "×"
646
- ```
647
-
648
- ### With Index
649
-
650
- ```coffee
651
- ol
652
- for item, i in items, key: item.id
653
- li "#{i + 1}. #{item.name}"
654
-
655
- table
656
- for row, rowIndex in rows, key: row.id
657
- tr class: { even: rowIndex % 2 is 0 }
658
- for cell, colIndex in row.cells, key: colIndex
659
- td cell
660
- ```
661
-
662
- ### Object Iteration
663
-
664
- ```coffee
665
- dl
666
- for key, value of user
667
- dt key
668
- dd value
669
-
670
- div.metadata
671
- for prop, val of item.meta
672
- span.tag "#{prop}: #{val}"
673
- ```
674
-
675
- ### Ranges
676
-
677
- ```coffee
678
- # Numeric range
679
- ul
680
- for i in [1..5]
681
- li "Item #{i}"
682
-
683
- # Dynamic range
684
- ul
685
- for page in [1..totalPages], key: page
686
- button @click: -> goToPage(page), page
687
- ```
688
-
689
- ## Refs
690
-
691
- Element references for direct DOM access:
692
-
693
- ```coffee
694
- component SearchBox
695
- inputEl = null
696
-
697
- mounted: ->
698
- inputEl.focus()
699
-
700
- clear: ->
701
- inputEl.value = ""
702
- inputEl.focus()
703
-
704
- render
705
- div.search
706
- input ref: inputEl, type: "text", @input: handleInput
707
- button @click: clear, "Clear"
708
- ```
709
-
710
- ## SVG Support
711
-
712
- ```coffee
713
- svg viewBox: "0 0 24 24", width: 24, height: 24
714
- path d: "M12 2L2 7l10 5 10-5-10-5z"
715
- path d: "M2 17l10 5 10-5"
716
-
717
- # With dynamic values
718
- svg.icon class: { active: isActive }
719
- circle cx: 12, cy: 12, r: radius
720
- line x1: 0, y1: 0, x2: 24, y2: 24, stroke: color
721
- ```
722
-
723
- ## Fragment (Multiple Root Elements)
724
-
725
- When you need multiple root elements without a wrapper:
726
-
727
- ```coffee
728
- render
729
- <>
730
- Header
731
- main
732
- @children
733
- Footer
734
- ```
735
-
736
- ---
737
-
738
- # 3. Components
739
-
740
- Rip provides component syntax as **language-level constructs**, not library patterns.
741
-
742
- ## Basic Component
743
-
744
- ```coffee
745
- component HelloWorld
746
- render
747
- div "Hello, World!"
748
- ```
749
-
750
- That's it. No imports, no boilerplate, no `export default`.
751
-
752
- ### Creating & Mounting
753
-
754
- ```coffee
755
- # Ruby-style constructor (Rip enhancement)
756
- app = HelloWorld.new()
757
- app.mount "#app"
758
-
759
- # Or chain it
760
- HelloWorld.new().mount "#app"
761
-
762
- # Traditional JS style also works
763
- app = new HelloWorld()
764
- app.mount "#app"
765
-
766
- # With props
767
- Counter.new(label: "Score", initial: 10).mount "#counter"
768
- ```
769
-
770
- The `mount` method accepts either an element or a CSS selector string.
771
-
772
- ## Component Structure
773
-
774
- ```coffee
775
- component Name
776
- # ═══════════════════════════════════════════
777
- # Constants (readonly)
778
- # ═══════════════════════════════════════════
779
- MAX_ITEMS =! 100
780
-
781
- # ═══════════════════════════════════════════
782
- # Props (from parent)
783
- # ═══════════════════════════════════════════
784
- @title # Required prop
785
- @subtitle? # Optional prop (undefined if not provided)
786
- @count = 0 # Optional prop with default
787
- @onSelect # Callback prop
788
- @children # Nested content (slot)
789
- @...rest # Rest props (capture remaining)
790
-
791
- # ═══════════════════════════════════════════
792
- # State (local, reactive)
793
- # ═══════════════════════════════════════════
794
- expanded = false
795
- items = []
796
- searchTerm = ""
797
-
798
- # ═══════════════════════════════════════════
799
- # Derived (always equals)
800
- # ═══════════════════════════════════════════
801
- filtered ~= items.filter (i) -> i.active
802
- total ~= items.reduce ((sum, i) -> sum + i.price), 0
803
- isEmpty ~= items.length is 0
804
-
805
- # ═══════════════════════════════════════════
806
- # Methods (private)
807
- # ═══════════════════════════════════════════
808
- add: (item) ->
809
- items = [...items, item]
810
-
811
- remove: (item) ->
812
- items = items.filter (i) -> i isnt item
813
-
814
- # ═══════════════════════════════════════════
815
- # Exposed Methods (parent can call)
816
- # ═══════════════════════════════════════════
817
- clear: ∞>
818
- items = []
819
-
820
- focus: ∞>
821
- inputEl.focus()
822
-
823
- # ═══════════════════════════════════════════
824
- # Lifecycle
825
- # ═══════════════════════════════════════════
826
- mounted: ->
827
- # After first render, DOM available
828
- saved = localStorage.getItem "items"
829
- items = JSON.parse saved if saved
830
-
831
- unmounted: ->
832
- # Cleanup before removal
833
-
834
- updated: ->
835
- # After any reactive update
836
-
837
- # ═══════════════════════════════════════════
838
- # Effects (side effects)
839
- # ═══════════════════════════════════════════
840
- effect ->
841
- # Runs when dependencies change
842
- localStorage.setItem "items", JSON.stringify items
843
-
844
- effect ->
845
- # Return function for cleanup
846
- interval = setInterval (-> tick()), 1000
847
- -> clearInterval interval
848
-
849
- # ═══════════════════════════════════════════
850
- # Render
851
- # ═══════════════════════════════════════════
852
- render
853
- div.container
854
- h1 @title
855
- p @subtitle if @subtitle
856
- # ... template
857
- ```
858
-
859
- ## Props System
860
-
861
- ### Declaration
862
-
863
- ```coffee
864
- component Button
865
- # Required (error if not provided)
866
- @label
867
-
868
- # Optional (undefined if not provided)
869
- @icon?
870
-
871
- # Optional with default
872
- @variant = "default"
873
- @size = "md"
874
- @disabled = false
875
-
876
- # Callback prop
877
- @onClick
878
-
879
- # Children (nested content)
880
- @children
881
-
882
- # Rest props (capture all others)
883
- @...rest
884
- ```
885
-
886
- ### Usage
887
-
888
- ```coffee
889
- # Parent component
890
- render
891
- Button
892
- label: "Save"
893
- variant: "primary"
894
- onClick: handleSave
895
-
896
- Button label: "Cancel", variant: "ghost", onClick: handleCancel
897
-
898
- Button label: "Delete", variant: "danger"
899
- icon: "trash" # Named prop
900
- span "Are you sure?" # Becomes @children
901
- ```
902
-
903
- ### Prop Rules
904
-
905
- | Rule | Description |
906
- |------|-------------|
907
- | **Readonly** | Props cannot be reassigned inside component |
908
- | **Required** | `@prop` without default throws if not provided |
909
- | **Optional** | `@prop?` is undefined if not provided |
910
- | **Default** | `@prop = value` uses value if not provided |
911
- | **Callback** | Functions passed as props, call with `@onClick()` |
912
- | **Children** | `@children` receives unnamed nested content |
913
- | **Rest** | `@...rest` captures all non-declared props |
914
- | **Spread** | `...@rest` spreads captured props to element |
915
-
916
- ### Forwarding Props
917
-
918
- ```coffee
919
- component FancyInput
920
- @label
921
- @error?
922
- @...inputProps
923
-
924
- render
925
- div.field
926
- label @label
927
- input ...@inputProps # Spread all other props to input
928
- span.error @error if @error
929
- ```
930
-
931
- ## Lifecycle Hooks
932
-
933
- ```coffee
934
- component DataView
935
- @url
936
- data = null
937
- error = null
938
- loading = true
939
-
940
- mounted: ->
941
- # Runs once after first render
942
- # DOM is available
943
- try
944
- data = fetch! @url
945
- catch e
946
- error = e.message
947
- finally
948
- loading = false
949
-
950
- unmounted: ->
951
- # Runs before component is removed
952
- # Cleanup subscriptions, timers, etc.
953
-
954
- updated: ->
955
- # Runs after any reactive update
956
- console.log "Component updated"
957
-
958
- render
959
- div
960
- if loading
961
- Spinner()
962
- else if error
963
- ErrorMessage message: error
964
- else
965
- DataDisplay data: data
966
- ```
967
-
968
- | Hook | When | Use For |
969
- |------|------|---------|
970
- | `mounted:` | After first render | Initial fetch, DOM access, setup |
971
- | `unmounted:` | Before removal | Cleanup timers, subscriptions |
972
- | `updated:` | After reactive updates | Logging, analytics |
973
-
974
- ## Children / Slots
975
-
976
- ### Basic Children
977
-
978
- ```coffee
979
- component Card
980
- @title
981
- @children
982
-
983
- render
984
- div.card
985
- h2 @title
986
- div.card-body
987
- @children # Render children here
988
-
989
- # Usage:
990
- Card title: "My Card"
991
- p "This is the card content."
992
- p "It can have multiple elements."
993
- ```
994
-
995
- ### Named Slots
996
-
997
- ```coffee
998
- component Layout
999
- @header?
1000
- @footer?
1001
- @children
1002
-
1003
- render
1004
- div.layout
1005
- header @header if @header
1006
- main @children
1007
- footer @footer if @footer
1008
-
1009
- # Usage:
1010
- Layout
1011
- header:
1012
- h1 "My App"
1013
- nav ...
1014
- footer:
1015
- p "© 2024"
1016
-
1017
- # Default content → @children
1018
- p "Main content here"
1019
- ```
1020
-
1021
- ## Context API
1022
-
1023
- Pass data down through component trees without prop drilling.
1024
-
1025
- ```coffee
1026
- component App
1027
- # Set context in constructor (runs during component init)
1028
- mounted: ->
1029
- setContext "theme", { dark: true, primary: "#3b82f6" }
1030
-
1031
- render
1032
- div
1033
- Header()
1034
- Content()
1035
- Footer()
1036
-
1037
- component Header
1038
- # Get context from any ancestor
1039
- theme = getContext "theme"
1040
-
1041
- render
1042
- header.("bg-blue-500" if theme?.dark)
1043
- h1 "My App"
1044
-
1045
- component DeepNestedChild
1046
- # Works at any depth!
1047
- theme = getContext "theme"
1048
-
1049
- render
1050
- div style: "color: #{theme?.primary}"
1051
- "Themed content"
1052
- ```
1053
-
1054
- **API:**
1055
-
1056
- | Function | Description |
1057
- |----------|-------------|
1058
- | `setContext(key, value)` | Set a context value in current component |
1059
- | `getContext(key)` | Get context from nearest ancestor (or undefined) |
1060
- | `hasContext(key)` | Check if context exists in any ancestor |
1061
-
1062
- ## Complete Example
1063
-
1064
- ```coffee
1065
- component TodoApp
1066
- # Constants
1067
- STORAGE_KEY =! "todos"
1068
-
1069
- # State
1070
- todos = []
1071
- newTodo = ""
1072
- filter = "all"
1073
-
1074
- # Derived
1075
- filtered ~= switch filter
1076
- when "active" then todos.filter (t) -> not t.done
1077
- when "completed" then todos.filter (t) -> t.done
1078
- else todos
1079
-
1080
- remaining ~= todos.filter((t) -> not t.done).length
1081
- allDone ~= todos.length > 0 and remaining is 0
1082
-
1083
- # Methods
1084
- add: ->
1085
- return unless newTodo.trim()
1086
- todos = [...todos, { id: Date.now(), text: newTodo.trim(), done: false }]
1087
- newTodo = ""
1088
-
1089
- toggle: (todo) ->
1090
- todo.done = not todo.done
1091
- todos = [...todos] # Trigger reactivity
1092
-
1093
- # Lifecycle
1094
- mounted: ->
1095
- saved = localStorage.getItem STORAGE_KEY
1096
- todos = JSON.parse saved if saved
1097
-
1098
- # Effects
1099
- effect ->
1100
- localStorage.setItem STORAGE_KEY, JSON.stringify todos
1101
-
1102
- # Render
1103
- render
1104
- section.todoapp
1105
- header.header
1106
- h1 "todos"
1107
- input.new-todo
1108
- placeholder: "What needs to be done?"
1109
- value: newTodo
1110
- @input: (e) -> newTodo = e.target.value
1111
- @keydown.enter: add
1112
-
1113
- section.main if todos.length
1114
- ul.todo-list
1115
- for todo in filtered, key: todo.id
1116
- li class: { completed: todo.done }
1117
- input.toggle type: "checkbox", checked: todo.done, @change: -> toggle todo
1118
- label todo.text
1119
-
1120
- footer.footer if todos.length
1121
- span.todo-count
1122
- strong remaining
1123
- " items left"
1124
- ```
1125
-
1126
- ---
1127
-
1128
- # 4. Special Operators
308
+ # 2. Special Operators
1129
309
 
1130
310
  ## Dammit Operator (`!`)
1131
311
 
@@ -1280,7 +460,7 @@ result = riskyOperation() !? "default"
1280
460
 
1281
461
  ---
1282
462
 
1283
- # 5. Regex+ Features
463
+ # 3. Regex+ Features
1284
464
 
1285
465
  **Ruby-Inspired Regex Matching with Automatic Capture**
1286
466
 
@@ -1433,9 +613,7 @@ text =~ /line2/m # Works! (/m flag allows newlines)
1433
613
  3. **Minimal boilerplate** — No `useState`, no `.value` in most cases
1434
614
  4. **Familiar feel** — Looks like regular assignment, behaves reactively
1435
615
  5. **Zero dependencies** — Runtime is inlined, no external packages needed
1436
- 6. **Components are language constructs** — Not classes you extend, not functions you call
1437
- 7. **Templates are code** — The `render` block is Rip syntax, not a separate template language
1438
- 8. **Everything is reactive** — State, derived values, and effects just work
616
+ 6. **Framework-agnostic** — Use Rip's reactivity with any UI framework
1439
617
 
1440
618
  ---
1441
619