rip-lang 2.9.2 → 3.0.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/CHANGELOG.md +45 -2
- package/README.md +55 -42
- package/docs/RIP-INTERNALS.md +576 -0
- package/docs/RIP-LANG.md +1126 -0
- package/docs/{TYPES.md → RIP-TYPES.md} +3 -3
- package/docs/dist/rip.browser.js +3296 -5482
- package/docs/dist/rip.browser.min.js +248 -332
- package/docs/dist/rip.browser.min.js.br +0 -0
- package/package.json +1 -1
- package/src/browser.js +3 -20
- package/src/compiler.js +1683 -4550
- package/src/grammar/grammar.rip +531 -489
- package/src/lexer.js +1327 -3034
- package/src/parser.js +213 -220
- package/src/repl.js +16 -9
- package/docs/BROWSER.md +0 -990
- package/docs/GUIDE.md +0 -636
- package/docs/INTERNALS.md +0 -857
- package/docs/RATIONALE.md +0 -180
- package/docs/examples/README.md +0 -33
- package/docs/examples/arrows.rip +0 -74
- package/docs/examples/async-await.rip +0 -59
- package/docs/examples/existential.rip +0 -86
- package/docs/examples/fibonacci.rip +0 -12
- package/docs/examples/module.rip +0 -48
- package/docs/examples/ranges.rip +0 -45
- package/docs/examples/reactivity.rip +0 -48
- package/docs/examples/switch.rip +0 -50
- package/docs/examples/ternary.rip +0 -36
- /package/docs/{REACTIVITY.md → RIP-REACTIVITY.md} +0 -0
package/docs/GUIDE.md
DELETED
|
@@ -1,636 +0,0 @@
|
|
|
1
|
-
<p><img src="rip.svg" alt="Rip Logo" width="100"></p>
|
|
2
|
-
|
|
3
|
-
# Rip Language Guide
|
|
4
|
-
|
|
5
|
-
> **Modern CoffeeScript with Built-in Reactivity**
|
|
6
|
-
|
|
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 import—state management is built into the syntax itself.
|
|
8
|
-
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
## Table of Contents
|
|
12
|
-
|
|
13
|
-
1. [Reactivity](#1-reactivity)
|
|
14
|
-
2. [Special Operators](#2-special-operators)
|
|
15
|
-
3. [Regex+ Features](#3-regex-features)
|
|
16
|
-
|
|
17
|
-
---
|
|
18
|
-
|
|
19
|
-
# 1. Reactivity
|
|
20
|
-
|
|
21
|
-
Rip provides reactive primitives as **language-level operators**, not library imports.
|
|
22
|
-
|
|
23
|
-
## Reactive Operators
|
|
24
|
-
|
|
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 |
|
|
32
|
-
|
|
33
|
-
## Reactive State (`:=`) — "has state"
|
|
34
|
-
|
|
35
|
-
The state operator creates reactive state:
|
|
36
|
-
|
|
37
|
-
```coffee
|
|
38
|
-
count := 0 # count has state 0
|
|
39
|
-
name := "world" # name has state "world"
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
State changes automatically trigger updates in any computed values or effects that depend on them.
|
|
43
|
-
|
|
44
|
-
## Computed Values (`~=`) — "always equals"
|
|
45
|
-
|
|
46
|
-
The computed operator creates a value that automatically recomputes when its dependencies change:
|
|
47
|
-
|
|
48
|
-
```coffee
|
|
49
|
-
count := 0
|
|
50
|
-
doubled ~= count * 2 # doubled always equals count * 2
|
|
51
|
-
|
|
52
|
-
count = 5 # doubled automatically becomes 10
|
|
53
|
-
count = 10 # doubled automatically becomes 20
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
## Side Effects (`~>`) — "reacts to"
|
|
57
|
-
|
|
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:
|
|
59
|
-
|
|
60
|
-
```coffee
|
|
61
|
-
count := 0
|
|
62
|
-
|
|
63
|
-
~> console.log "Count changed to:", count
|
|
64
|
-
|
|
65
|
-
count = 5 # Logs: "Count changed to: 5"
|
|
66
|
-
count = 10 # Logs: "Count changed to: 10"
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
Effects are useful for:
|
|
70
|
-
- Logging and debugging
|
|
71
|
-
- Syncing with external systems
|
|
72
|
-
- Analytics tracking
|
|
73
|
-
- Local storage persistence
|
|
74
|
-
|
|
75
|
-
### Controllable Effects
|
|
76
|
-
|
|
77
|
-
Assign the effect to a variable to control it:
|
|
78
|
-
|
|
79
|
-
```coffee
|
|
80
|
-
count := 0
|
|
81
|
-
|
|
82
|
-
# Fire and forget (no assignment)
|
|
83
|
-
~> console.log count
|
|
84
|
-
|
|
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
|
|
91
|
-
```
|
|
92
|
-
|
|
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.
|
|
110
|
-
|
|
111
|
-
## Auto-Unwrapping
|
|
112
|
-
|
|
113
|
-
Reactive variables automatically unwrap in most contexts:
|
|
114
|
-
|
|
115
|
-
```coffee
|
|
116
|
-
count := 10
|
|
117
|
-
|
|
118
|
-
# All of these work automatically:
|
|
119
|
-
doubled ~= count * 2 # Arithmetic
|
|
120
|
-
message = "Count: #{count}" # String interpolation
|
|
121
|
-
console.log count # Function arguments
|
|
122
|
-
|
|
123
|
-
# Explicit access when needed:
|
|
124
|
-
count.read() # Get value without tracking dependencies
|
|
125
|
-
+count # Unary plus (same as count.value)
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
## Reactive Variable Methods
|
|
129
|
-
|
|
130
|
-
| Method | Purpose |
|
|
131
|
-
|--------|---------|
|
|
132
|
-
| `x.read()` | Get value without tracking (for effects that shouldn't re-run) |
|
|
133
|
-
| `x.value` | Direct access to the underlying value |
|
|
134
|
-
| `+x` | Shorthand for `x.value` (triggers tracking in computed/effects) |
|
|
135
|
-
| `x.lock()` | Make value readonly (can read but can't change) |
|
|
136
|
-
| `x.free()` | Unsubscribe from all dependencies (state still works) |
|
|
137
|
-
| `x.kill()` | Clean up everything and return final value |
|
|
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
|
-
|
|
150
|
-
## Dependency Tracking
|
|
151
|
-
|
|
152
|
-
Understanding when dependencies are tracked is key to effective reactive programming.
|
|
153
|
-
|
|
154
|
-
### What Tracks Dependencies?
|
|
155
|
-
|
|
156
|
-
| Expression | Tracks? | Why |
|
|
157
|
-
|------------|---------|-----|
|
|
158
|
-
| `count * 2` | ✅ Yes | Arithmetic triggers `.valueOf()` |
|
|
159
|
-
| `"Count: #{count}"` | ✅ Yes | Interpolation triggers `.toString()` |
|
|
160
|
-
| `console.log count` | ✅ Yes | Coercion triggers `.valueOf()` |
|
|
161
|
-
| `+count` | ✅ Yes | Unary plus triggers `.valueOf()` |
|
|
162
|
-
| `count.value` | ✅ Yes | Direct `.value` access |
|
|
163
|
-
| `count.read()` | ❌ No | Explicit non-tracking read |
|
|
164
|
-
| `y = count` | ❌ No | Assigns state object, not value |
|
|
165
|
-
|
|
166
|
-
### Example: Tracking vs Non-Tracking
|
|
167
|
-
|
|
168
|
-
```coffee
|
|
169
|
-
count := 10
|
|
170
|
-
|
|
171
|
-
# Effect A: Subscribes to count (will re-run when count changes)
|
|
172
|
-
~> console.log "A: #{count}"
|
|
173
|
-
|
|
174
|
-
# Reading without tracking (for comparisons, etc.)
|
|
175
|
-
currentValue = count.read() # Does not create dependency
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
## Lifecycle & Cleanup
|
|
179
|
-
|
|
180
|
-
### Locking a State
|
|
181
|
-
|
|
182
|
-
Make a state readonly (subscriptions stay active):
|
|
183
|
-
|
|
184
|
-
```coffee
|
|
185
|
-
config := { theme: "dark" }
|
|
186
|
-
config.lock()
|
|
187
|
-
|
|
188
|
-
config = { theme: "light" } # Silently ignored
|
|
189
|
-
config.theme # Still "dark"
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
### Freeing Subscriptions
|
|
193
|
-
|
|
194
|
-
Unsubscribe a computed/effect from its dependencies:
|
|
195
|
-
|
|
196
|
-
```coffee
|
|
197
|
-
count := 0
|
|
198
|
-
doubled ~= count * 2
|
|
199
|
-
|
|
200
|
-
doubled.free() # No longer updates when count changes
|
|
201
|
-
count = 10 # doubled stays at its last value
|
|
202
|
-
```
|
|
203
|
-
|
|
204
|
-
### Killing a State
|
|
205
|
-
|
|
206
|
-
Clean up completely and get the final value:
|
|
207
|
-
|
|
208
|
-
```coffee
|
|
209
|
-
count := 10
|
|
210
|
-
finalValue = count.kill() # Returns 10, state is now dead
|
|
211
|
-
|
|
212
|
-
count = 20 # Error or no-op (state is dead)
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
### Effect Cleanup
|
|
216
|
-
|
|
217
|
-
Use the effect controller to manage lifecycle:
|
|
218
|
-
|
|
219
|
-
```coffee
|
|
220
|
-
# Assign to a variable for control
|
|
221
|
-
ticker ~>
|
|
222
|
-
interval = setInterval (-> tick()), 1000
|
|
223
|
-
-> clearInterval interval # Cleanup function returned
|
|
224
|
-
|
|
225
|
-
# Later, when done:
|
|
226
|
-
ticker.cancel! # Stops the effect and runs cleanup
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
## Real-World Example
|
|
230
|
-
|
|
231
|
-
A complete reactive counter with persistence:
|
|
232
|
-
|
|
233
|
-
```coffee
|
|
234
|
-
# Reactive state — count has state (loaded from localStorage)
|
|
235
|
-
count := parseInt(localStorage.getItem("count")) or 0
|
|
236
|
-
|
|
237
|
-
# Computed values — always equal to their expressions
|
|
238
|
-
doubled ~= count * 2
|
|
239
|
-
isEven ~= count % 2 == 0
|
|
240
|
-
message ~= "Count is #{count} (#{isEven ? 'even' : 'odd'})"
|
|
241
|
-
|
|
242
|
-
# Side effects — react to dependencies (auto-tracked)
|
|
243
|
-
~> localStorage.setItem "count", count # Persist
|
|
244
|
-
~> console.log message # Log
|
|
245
|
-
|
|
246
|
-
# Usage
|
|
247
|
-
count = 5
|
|
248
|
-
# Console: "Count is 5 (odd)"
|
|
249
|
-
# localStorage: "5"
|
|
250
|
-
|
|
251
|
-
count = 10
|
|
252
|
-
# Console: "Count is 10 (even)"
|
|
253
|
-
# localStorage: "10"
|
|
254
|
-
```
|
|
255
|
-
|
|
256
|
-
## How It Works
|
|
257
|
-
|
|
258
|
-
The Rip compiler transforms reactive operators into efficient JavaScript:
|
|
259
|
-
|
|
260
|
-
```coffee
|
|
261
|
-
# Rip source
|
|
262
|
-
count := 0 # count has state 0
|
|
263
|
-
doubled ~= count * 2 # doubled always equals count * 2
|
|
264
|
-
~> console.log count # reacts to count changes
|
|
265
|
-
```
|
|
266
|
-
|
|
267
|
-
```javascript
|
|
268
|
-
// Compiled output (conceptual)
|
|
269
|
-
const count = __state(0);
|
|
270
|
-
const doubled = __computed(() => count.value * 2);
|
|
271
|
-
__effect(() => { console.log(count.value); });
|
|
272
|
-
```
|
|
273
|
-
|
|
274
|
-
The runtime is **automatically inlined** - no external dependencies required.
|
|
275
|
-
|
|
276
|
-
## Zero Overhead for Non-Reactive Code
|
|
277
|
-
|
|
278
|
-
If your code doesn't use reactive features, no runtime is injected:
|
|
279
|
-
|
|
280
|
-
```coffee
|
|
281
|
-
# Non-reactive code
|
|
282
|
-
x = 10
|
|
283
|
-
y = x * 2
|
|
284
|
-
console.log y
|
|
285
|
-
```
|
|
286
|
-
|
|
287
|
-
```javascript
|
|
288
|
-
// Clean output - no reactive runtime
|
|
289
|
-
let x, y;
|
|
290
|
-
x = 10;
|
|
291
|
-
y = x * 2;
|
|
292
|
-
console.log(y);
|
|
293
|
-
```
|
|
294
|
-
|
|
295
|
-
## Comparison with Other Frameworks
|
|
296
|
-
|
|
297
|
-
| Concept | React | Vue | Solid | Rip |
|
|
298
|
-
|---------|-------|-----|-------|-----|
|
|
299
|
-
| State | `useState()` | `ref()` | `createSignal()` | `x := 0` |
|
|
300
|
-
| Computed | `useMemo()` | `computed()` | `createMemo()` | `x ~= y * 2` |
|
|
301
|
-
| Effect | `useEffect()` | `watch()` | `createEffect()` | `~> body` or `x ~> body` |
|
|
302
|
-
| Constant | `const` | `const` | `const` | `x =! 0` |
|
|
303
|
-
|
|
304
|
-
Rip's approach: **No imports, no hooks, no special functions. Just operators.**
|
|
305
|
-
|
|
306
|
-
---
|
|
307
|
-
|
|
308
|
-
# 2. Special Operators
|
|
309
|
-
|
|
310
|
-
## Dammit Operator (`!`)
|
|
311
|
-
|
|
312
|
-
The **dammit operator (`!`)** is a trailing suffix that does TWO things:
|
|
313
|
-
1. **Calls the function** (even without parentheses)
|
|
314
|
-
2. **Awaits the result** (prepends `await`)
|
|
315
|
-
|
|
316
|
-
### Quick Examples
|
|
317
|
-
|
|
318
|
-
```coffee
|
|
319
|
-
# Simple call and await
|
|
320
|
-
result = fetchData! # → await fetchData()
|
|
321
|
-
|
|
322
|
-
# With arguments
|
|
323
|
-
user = getUser!(id) # → await getUser(id)
|
|
324
|
-
|
|
325
|
-
# Method calls
|
|
326
|
-
data = api.get! # → await api.get()
|
|
327
|
-
|
|
328
|
-
# In expressions
|
|
329
|
-
total = 5 + getValue! # → 5 + await getValue()
|
|
330
|
-
```
|
|
331
|
-
|
|
332
|
-
### Basic Usage
|
|
333
|
-
|
|
334
|
-
```coffee
|
|
335
|
-
# WITHOUT dammit - reference only
|
|
336
|
-
fn = loadConfig
|
|
337
|
-
typeof fn # → 'function'
|
|
338
|
-
|
|
339
|
-
# WITH dammit - calls immediately
|
|
340
|
-
config = loadConfig! # → await loadConfig()
|
|
341
|
-
```
|
|
342
|
-
|
|
343
|
-
### Comparison: Before & After
|
|
344
|
-
|
|
345
|
-
**Before (Explicit Await):**
|
|
346
|
-
```coffee
|
|
347
|
-
user = await db.findUser(id)
|
|
348
|
-
posts = await db.getPosts(user.id)
|
|
349
|
-
comments = await db.getComments(posts[0].id)
|
|
350
|
-
result = await buildResponse(comments)
|
|
351
|
-
```
|
|
352
|
-
|
|
353
|
-
**After (Dammit Operator):**
|
|
354
|
-
```coffee
|
|
355
|
-
user = db.findUser!(id)
|
|
356
|
-
posts = db.getPosts!(user.id)
|
|
357
|
-
comments = db.getComments!(posts[0].id)
|
|
358
|
-
result = buildResponse!(comments)
|
|
359
|
-
```
|
|
360
|
-
|
|
361
|
-
**Benefit:** ~50% shorter, same clarity, **zero performance traps**
|
|
362
|
-
|
|
363
|
-
### Usage Guidelines
|
|
364
|
-
|
|
365
|
-
**✅ When to Use `!`:**
|
|
366
|
-
|
|
367
|
-
```coffee
|
|
368
|
-
# Sequential async code (most common case)
|
|
369
|
-
user = findUser!(id)
|
|
370
|
-
posts = getPosts!(user.id)
|
|
371
|
-
render!(user, posts)
|
|
372
|
-
|
|
373
|
-
# Simple async chains
|
|
374
|
-
config = loadConfig!
|
|
375
|
-
db = connectDB!(config)
|
|
376
|
-
server = startServer!(db)
|
|
377
|
-
```
|
|
378
|
-
|
|
379
|
-
**❌ When NOT to Use `!`:**
|
|
380
|
-
|
|
381
|
-
```coffee
|
|
382
|
-
# DON'T (serialized - slow):
|
|
383
|
-
a = fetch1!
|
|
384
|
-
b = fetch2!
|
|
385
|
-
c = fetch3!
|
|
386
|
-
|
|
387
|
-
# DO (parallel - fast):
|
|
388
|
-
[a, b, c] = await Promise.all([fetch1(), fetch2(), fetch3()])
|
|
389
|
-
```
|
|
390
|
-
|
|
391
|
-
## Void Functions (`!` at Definition)
|
|
392
|
-
|
|
393
|
-
The `!` at definition suppresses implicit returns (side-effect only functions):
|
|
394
|
-
|
|
395
|
-
```coffee
|
|
396
|
-
def processItems!
|
|
397
|
-
for item in items
|
|
398
|
-
item.update()
|
|
399
|
-
# ← Returns undefined, not last expression
|
|
400
|
-
|
|
401
|
-
# With explicit return (value stripped)
|
|
402
|
-
def validate!(x)
|
|
403
|
-
return if x < 0 # → Just "return" (no value)
|
|
404
|
-
console.log "valid"
|
|
405
|
-
# ← Returns undefined
|
|
406
|
-
```
|
|
407
|
-
|
|
408
|
-
**Works with all function types:**
|
|
409
|
-
```coffee
|
|
410
|
-
c! = (x) -> # Void thin arrow
|
|
411
|
-
x * 2 # Executes but doesn't return value
|
|
412
|
-
|
|
413
|
-
process! = (data) => # Void fat arrow
|
|
414
|
-
data.toUpperCase() # Executes but returns undefined
|
|
415
|
-
```
|
|
416
|
-
|
|
417
|
-
## Floor Division (`//`)
|
|
418
|
-
|
|
419
|
-
True floor division (not just integer division):
|
|
420
|
-
|
|
421
|
-
```coffee
|
|
422
|
-
7 // 3 # → 2
|
|
423
|
-
-7 // 3 # → -3 (floors toward negative infinity)
|
|
424
|
-
```
|
|
425
|
-
|
|
426
|
-
## True Modulo (`%%`)
|
|
427
|
-
|
|
428
|
-
True mathematical modulo (not remainder like `%`):
|
|
429
|
-
|
|
430
|
-
```coffee
|
|
431
|
-
-7 %% 3 # → 2 (always positive)
|
|
432
|
-
-7 % 3 # → -1 (remainder, can be negative)
|
|
433
|
-
```
|
|
434
|
-
|
|
435
|
-
## Ternary Operator (`?:`)
|
|
436
|
-
|
|
437
|
-
Rip supports both JavaScript-style ternary AND CoffeeScript-style:
|
|
438
|
-
|
|
439
|
-
```coffee
|
|
440
|
-
# JavaScript style
|
|
441
|
-
status = active ? 'on' : 'off'
|
|
442
|
-
|
|
443
|
-
# CoffeeScript style
|
|
444
|
-
status = if active then 'on' else 'off'
|
|
445
|
-
|
|
446
|
-
# Works with property access, function calls, expressions
|
|
447
|
-
result = valid ? obj.field : null
|
|
448
|
-
output = ready ? compute() : fallback
|
|
449
|
-
value = x > 0 ? x + 1 : 0
|
|
450
|
-
|
|
451
|
-
# Nested
|
|
452
|
-
level = score > 90 ? 'A' : score > 80 ? 'B' : score > 70 ? 'C' : 'F'
|
|
453
|
-
```
|
|
454
|
-
|
|
455
|
-
**Note:** Subscript access in the true branch needs parentheses:
|
|
456
|
-
|
|
457
|
-
```coffee
|
|
458
|
-
# Wrap subscript in parens
|
|
459
|
-
item = found ? (arr[0]) : default
|
|
460
|
-
value = valid ? (data[key]) : null
|
|
461
|
-
```
|
|
462
|
-
|
|
463
|
-
**Why possible:** By using `??` for nullish, `?` became available for ternary.
|
|
464
|
-
|
|
465
|
-
## Otherwise Operator (`!?`)
|
|
466
|
-
|
|
467
|
-
The otherwise operator handles both null/undefined AND thrown errors:
|
|
468
|
-
|
|
469
|
-
```coffee
|
|
470
|
-
result = riskyOperation() !? "default"
|
|
471
|
-
# If riskyOperation() throws or returns null/undefined, result = "default"
|
|
472
|
-
```
|
|
473
|
-
|
|
474
|
-
---
|
|
475
|
-
|
|
476
|
-
# 3. Regex+ Features
|
|
477
|
-
|
|
478
|
-
**Ruby-Inspired Regex Matching with Automatic Capture**
|
|
479
|
-
|
|
480
|
-
Rip extends CoffeeScript with two powerful regex features inspired by Ruby: the **`=~` match operator** and **regex indexing**. Both features automatically manage match results in a global `_` variable.
|
|
481
|
-
|
|
482
|
-
## `=~` Match Operator
|
|
483
|
-
|
|
484
|
-
### Syntax
|
|
485
|
-
|
|
486
|
-
```coffee
|
|
487
|
-
text =~ /pattern/
|
|
488
|
-
```
|
|
489
|
-
|
|
490
|
-
### Behavior
|
|
491
|
-
|
|
492
|
-
- Executes: `(_ = toSearchable(text).match(/pattern/))`
|
|
493
|
-
- Stores match result in `_` variable (accessible immediately)
|
|
494
|
-
- Returns: the match result (truthy) or `null`
|
|
495
|
-
|
|
496
|
-
### Examples
|
|
497
|
-
|
|
498
|
-
**Basic matching:**
|
|
499
|
-
```coffee
|
|
500
|
-
text = "hello world"
|
|
501
|
-
if text =~ /world/
|
|
502
|
-
console.log("Found:", _[0]) # "world"
|
|
503
|
-
```
|
|
504
|
-
|
|
505
|
-
**Capture groups:**
|
|
506
|
-
```coffee
|
|
507
|
-
email = "user@example.com"
|
|
508
|
-
if email =~ /(.+)@(.+)/
|
|
509
|
-
username = _[1] # "user"
|
|
510
|
-
domain = _[2] # "example.com"
|
|
511
|
-
```
|
|
512
|
-
|
|
513
|
-
**Phone number parsing:**
|
|
514
|
-
```coffee
|
|
515
|
-
phone = "2125551234"
|
|
516
|
-
if phone =~ /^([2-9]\d\d)([2-9]\d\d)(\d{4})$/
|
|
517
|
-
formatted = "(#{_[1]}) #{_[2]}-#{_[3]}"
|
|
518
|
-
# Result: "(212) 555-1234"
|
|
519
|
-
```
|
|
520
|
-
|
|
521
|
-
## Regex Indexing
|
|
522
|
-
|
|
523
|
-
### Syntax
|
|
524
|
-
|
|
525
|
-
```coffee
|
|
526
|
-
value[/pattern/] # Returns full match (capture 0)
|
|
527
|
-
value[/pattern/, n] # Returns capture group n
|
|
528
|
-
```
|
|
529
|
-
|
|
530
|
-
### Examples
|
|
531
|
-
|
|
532
|
-
**Simple match:**
|
|
533
|
-
```coffee
|
|
534
|
-
"steve"[/eve/] # Returns "eve"
|
|
535
|
-
```
|
|
536
|
-
|
|
537
|
-
**Capture group:**
|
|
538
|
-
```coffee
|
|
539
|
-
"steve"[/e(v)e/, 1] # Returns "v"
|
|
540
|
-
```
|
|
541
|
-
|
|
542
|
-
**Email domain:**
|
|
543
|
-
```coffee
|
|
544
|
-
domain = "user@example.com"[/@(.+)$/, 1]
|
|
545
|
-
# Returns: "example.com"
|
|
546
|
-
```
|
|
547
|
-
|
|
548
|
-
## Combined Usage
|
|
549
|
-
|
|
550
|
-
The real power comes from using both features together:
|
|
551
|
-
|
|
552
|
-
```coffee
|
|
553
|
-
# Parse, validate, and format in clean steps
|
|
554
|
-
email = "Admin@Company.COM"
|
|
555
|
-
if email =~ /^([^@]+)@([^@]+)$/i
|
|
556
|
-
username = _[1].toLowerCase() # "admin"
|
|
557
|
-
domain = _[2].toLowerCase() # "company.com"
|
|
558
|
-
"#{username}@#{domain}" # Normalized email
|
|
559
|
-
```
|
|
560
|
-
|
|
561
|
-
## Elegant Validator Pattern
|
|
562
|
-
|
|
563
|
-
One of the most powerful use cases is building validators:
|
|
564
|
-
|
|
565
|
-
```coffee
|
|
566
|
-
validators =
|
|
567
|
-
# Extract and validate in one expression
|
|
568
|
-
id: (v) -> v[/^([1-9]\d{0,19})$/] and parseInt(_[1])
|
|
569
|
-
email: (v) -> v[/^([^@]+)@([^@]+\.[a-z]{2,})$/i] and _[0]
|
|
570
|
-
zip: (v) -> v[/^(\d{5})/] and _[1]
|
|
571
|
-
phone: (v) -> v[/^(\d{10})$/] and formatPhone(_[1])
|
|
572
|
-
|
|
573
|
-
# Normalize formats
|
|
574
|
-
ssn: (v) -> v[/^(\d{3})-?(\d{2})-?(\d{4})$/] and "#{_[1]}#{_[2]}#{_[3]}"
|
|
575
|
-
zipplus4: (v) -> v[/^(\d{5})-?(\d{4})$/] and "#{_[1]}-#{_[2]}"
|
|
576
|
-
|
|
577
|
-
# Boolean validators with =~
|
|
578
|
-
truthy: (v) -> (v =~ /^(true|t|1|yes|y|on)$/i) and true
|
|
579
|
-
falsy: (v) -> (v =~ /^(false|f|0|no|n|off)$/i) and true
|
|
580
|
-
```
|
|
581
|
-
|
|
582
|
-
**Each validator:**
|
|
583
|
-
- Validates format
|
|
584
|
-
- Extracts/transforms data
|
|
585
|
-
- Returns normalized value or falsy
|
|
586
|
-
- **All in one line!**
|
|
587
|
-
|
|
588
|
-
## Heregex (Extended Regular Expressions)
|
|
589
|
-
|
|
590
|
-
Rip supports heregexes - extended regular expressions that allow whitespace and comments for readability:
|
|
591
|
-
|
|
592
|
-
```coffee
|
|
593
|
-
pattern = ///
|
|
594
|
-
^ \d+ # starts with digits
|
|
595
|
-
\s* # optional whitespace
|
|
596
|
-
[a-z]+ # followed by letters
|
|
597
|
-
$ # end of string
|
|
598
|
-
///i
|
|
599
|
-
|
|
600
|
-
# Compiles to: /^\d+\s*[a-z]+$/i
|
|
601
|
-
# Comments and whitespace automatically stripped!
|
|
602
|
-
```
|
|
603
|
-
|
|
604
|
-
## Security Features
|
|
605
|
-
|
|
606
|
-
### Injection Protection
|
|
607
|
-
|
|
608
|
-
By default, **rejects strings with newlines**:
|
|
609
|
-
|
|
610
|
-
```coffee
|
|
611
|
-
# Safe - rejects malicious input
|
|
612
|
-
userInput = "test\nmalicious"
|
|
613
|
-
userInput =~ /^test$/ # Returns null! (newline detected)
|
|
614
|
-
|
|
615
|
-
# Explicit multiline when needed
|
|
616
|
-
text = "line1\nline2"
|
|
617
|
-
text =~ /line2/m # Works! (/m flag allows newlines)
|
|
618
|
-
```
|
|
619
|
-
|
|
620
|
-
---
|
|
621
|
-
|
|
622
|
-
## Design Philosophy
|
|
623
|
-
|
|
624
|
-
1. **Syntax over API** — Reactive primitives are operators, not function calls
|
|
625
|
-
2. **Implicit tracking** — Dependencies are detected automatically
|
|
626
|
-
3. **Minimal boilerplate** — No `useState`, no `.value` in most cases
|
|
627
|
-
4. **Familiar feel** — Looks like regular assignment, behaves reactively
|
|
628
|
-
5. **Zero dependencies** — Runtime is inlined, no external packages needed
|
|
629
|
-
6. **Framework-agnostic** — Use Rip's reactivity with any UI framework
|
|
630
|
-
|
|
631
|
-
---
|
|
632
|
-
|
|
633
|
-
**See Also:**
|
|
634
|
-
- [INTERNALS.md](INTERNALS.md) - Compiler and parser details
|
|
635
|
-
- [RATIONALE.md](RATIONALE.md) - Why Rip exists
|
|
636
|
-
- [BROWSER.md](BROWSER.md) - Browser usage and REPL guide
|