rip-lang 2.5.0 → 2.5.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 CHANGED
@@ -7,6 +7,26 @@ All notable changes to Rip will be documented in this file.
7
7
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
8
8
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
9
9
 
10
+ ## [2.5.1] - 2026-01-16
11
+
12
+ ### Template Enhancement
13
+
14
+ **Hyphenated Attributes Work Directly**:
15
+ ```coffee
16
+ render
17
+ # Before: needed quoted keys or spread syntax
18
+ i {"data-lucide": "search"}
19
+
20
+ # Now: just works!
21
+ i data-lucide: "search", aria-hidden: "true"
22
+ div data-testid: "container", aria-label: "Menu"
23
+ span data-foo-bar-baz: "multiple-hyphens-work"
24
+ ```
25
+
26
+ The lexer now automatically converts hyphenated attribute names (like `data-*`, `aria-*`) into quoted strings, making HTML-style data attributes intuitive and clean.
27
+
28
+ ---
29
+
10
30
  ## [2.5.0] - 2026-01-16
11
31
 
12
32
  ### Major Release - Parser Optimization + Complete Framework
package/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
  </p>
10
10
 
11
11
  <p align="center">
12
- <a href="CHANGELOG.md"><img src="https://img.shields.io/badge/version-2.5.0-blue.svg" alt="Version"></a>
12
+ <a href="CHANGELOG.md"><img src="https://img.shields.io/badge/version-2.5.1-blue.svg" alt="Version"></a>
13
13
  <a href="#zero-dependencies"><img src="https://img.shields.io/badge/dependencies-ZERO-brightgreen.svg" alt="Dependencies"></a>
14
14
  <a href="#"><img src="https://img.shields.io/badge/tests-1046%2F1046-brightgreen.svg" alt="Tests"></a>
15
15
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-green.svg" alt="License"></a>
@@ -21,10 +21,22 @@
21
21
 
22
22
  Rip is a modern reactive language that compiles to JavaScript. It takes the elegant, readable syntax that made CoffeeScript beloved and brings it into the modern era — with ES2022 output, built-in reactivity, and a clean component system for building UIs.
23
23
 
24
- The compiler is completely standalone with zero dependencies, and it's self-hosting: Rip compiles itself. At ~14,000 lines of code, it's smaller than CoffeeScript (17,760 LOC) while including a complete reactive framework with signals, templates, and components.
24
+ **The language IS the framework.** Unlike React, Vue, or Svelte where reactivity comes from libraries or compiler magic, Rip's reactive features are **language-level operators**:
25
+
26
+ ```coffee
27
+ count := 0 # Signal (reactive state)
28
+ doubled ~= count * 2 # Derived (auto-updates)
29
+ effect -> log doubled # Effect (side effects)
30
+ ```
31
+
32
+ No imports. No hooks. No dependency arrays. Just write code.
33
+
34
+ The compiler is completely standalone with **zero dependencies**, and it's self-hosting: Rip compiles itself. At ~14,000 lines of code, it's smaller than CoffeeScript while including a complete reactive framework.
25
35
 
26
36
  **What makes Rip different:**
27
- - **Reactive primitives** — signals, derived values, and effects built into the language
37
+ - **Reactive primitives** — `:=` signals, `~=` derived values, `effect` blocks as syntax
38
+ - **Components as syntax** — `component Counter` with props, lifecycle, fine-grained DOM updates
39
+ - **Templates** — Pug-style HTML in `render` blocks, two-way binding with `<=>`
28
40
  - **Modern output** — ES2022 with native classes, `?.`, `??`, modules
29
41
  - **Zero dependencies** — everything included, even the parser generator
30
42
  - **Self-hosting** — `bun run parser` rebuilds the compiler from source
@@ -164,90 +176,124 @@ fn?(arg) # Safe call
164
176
 
165
177
  ## Reactivity
166
178
 
167
- Built into the language, not a library:
179
+ **The language IS the framework.** Reactivity is built into Rip's syntax—not a library you import, not hooks you call. Just operators.
168
180
 
169
181
  ```coffee
170
- # Signals hold reactive state
171
- count := 0
172
- name := "world"
173
-
174
- # Derived values auto-update
175
- doubled ~= count * 2
176
- greeting ~= "Hello, #{name}!"
182
+ count := 0 # Signal reactive state
183
+ doubled ~= count * 2 # Derived — auto-updates when count changes
184
+ effect -> console.log doubled # Effect — runs when dependencies change
177
185
 
178
- # Effects run when dependencies change
179
- effect -> console.log greeting
186
+ count = 5 # doubled becomes 10, effect logs "10"
187
+ count = 10 # doubled becomes 20, effect logs "20"
188
+ ```
180
189
 
181
- name = "Rip" # Effect runs → "Hello, Rip!"
182
- count = 5 # Nothing (greeting doesn't depend on count)
190
+ **Compare to React:**
191
+ ```javascript
192
+ // React: imports, hooks, dependency arrays, rules...
193
+ import { useState, useMemo, useEffect } from 'react';
194
+ const [count, setCount] = useState(0);
195
+ const doubled = useMemo(() => count * 2, [count]);
196
+ useEffect(() => console.log(doubled), [doubled]);
183
197
  ```
184
198
 
185
- [Full reactivity guide →](docs/REACTIVITY.md)
199
+ **Rip: 3 lines. React: 5 lines + imports + dependency arrays + hook rules.**
200
+
201
+ | Concept | React | Vue | Solid | Rip |
202
+ |---------|-------|-----|-------|-----|
203
+ | State | `useState()` | `ref()` | `createSignal()` | `x := 0` |
204
+ | Derived | `useMemo()` | `computed()` | `createMemo()` | `x ~= y * 2` |
205
+ | Effect | `useEffect()` | `watch()` | `createEffect()` | `effect ->` |
206
+
207
+ No imports. No hooks. No dependency arrays. Just operators that do what they say.
208
+
209
+ [Full reactivity guide →](docs/GUIDE.md#reactivity)
186
210
 
187
211
  ---
188
212
 
189
213
  ## Components
190
214
 
191
- Build reactive UIs with fine-grained DOM updates:
215
+ Components are a **language construct**, not a pattern. Define with the `component` keyword, get props, state, lifecycle, and fine-grained DOM updates—all without a virtual DOM.
192
216
 
193
217
  ```coffee
194
218
  component Counter
195
- @label = "Count"
196
- count := 0
197
- inc: -> count += 1
219
+ @label = "Count" # Prop with default
220
+ @initial = 0 # Another prop
221
+
222
+ count := @initial # Reactive state (signal)
223
+ doubled ~= count * 2 # Derived value (auto-updates)
224
+
225
+ inc: -> count += 1 # Methods
198
226
  dec: -> count -= 1
199
227
 
200
228
  render
201
229
  div.counter
202
230
  h2 @label
203
231
  span.value count
204
- button @click: @inc, "+"
232
+ span.derived " (×2 = #{doubled})"
205
233
  button @click: @dec, "−"
206
- ```
207
-
208
- **Event handlers** — two patterns:
209
- ```coffee
210
- # Normal: define methods, reference with @
211
- inc: -> count += 1
212
- button @click: @inc, "+"
234
+ button @click: @inc, "+"
213
235
 
214
- # Compact: inline with fat arrow (parens required)
215
- button (@click: => @count++), "+"
236
+ # Mount with Ruby-style constructor
237
+ Counter.new(label: "Score", initial: 10).mount "#app"
216
238
  ```
217
239
 
218
- **Features:**
219
- - Props: `@prop`, `@prop?` (optional), `@prop = default`
220
- - Lifecycle: `mounted:`, `unmounted:`
221
- - Context API: `setContext`, `getContext`
222
- - Fine-grained updates: only changed nodes update, no virtual DOM
240
+ **What you get:**
241
+ - **Props:** `@prop` (required), `@prop?` (optional), `@prop = default`
242
+ - **State:** Signals (`:=`) and derived values (`~=`) just work
243
+ - **Lifecycle:** `mounted:`, `unmounted:`, `updated:`
244
+ - **Context:** `setContext`/`getContext` for deep prop passing
245
+ - **Fine-grained updates:** Only changed DOM nodes update—no virtual DOM diffing
223
246
 
224
- [Component guide →](docs/COMPONENTS.md)
247
+ [Component guide →](docs/GUIDE.md#components)
225
248
 
226
249
  ---
227
250
 
228
251
  ## Templates
229
252
 
230
- Indentation-based HTML with CSS-style selectors:
253
+ Indentation-based HTML with Pug-style selectors. Templates compile to **fine-grained DOM operations**—when a signal changes, only the affected text node or attribute updates. No virtual DOM, no diffing, no wasted work.
231
254
 
232
255
  ```coffee
233
256
  render
234
257
  div#app.container
235
258
  h1.title "Hello, #{name}!"
236
- input value: username, @input: updateName
237
- button.("btn", active && "primary") @click: submit
259
+
260
+ # Two-way binding with <=> operator
261
+ input type: "text", value <=> username
262
+ input type: "number", value <=> count # Auto-uses valueAsNumber!
263
+
264
+ # Dynamic classes (Tailwind-friendly)
265
+ button.btn.("primary" if active) @click: submit
238
266
  "Submit"
267
+
268
+ # Loops with keys for efficient updates
239
269
  ul.items
240
- for item in items
241
- li key: item.id, item.name
270
+ for item in items, key: item.id
271
+ li.item item.name
242
272
  ```
243
273
 
244
- - `div#id.class1.class2` — IDs and classes
245
- - `@click: @handler` Event handlers (method reference)
246
- - `(@click: => @count++)` — Inline handlers (fat arrow, parens required)
247
- - `.("class1", cond && "class2")` Dynamic classes (Tailwind-friendly)
248
- - `value <=> var` Two-way binding
274
+ **Template features:**
275
+ | Syntax | What it does |
276
+ |--------|--------------|
277
+ | `div#id.class1.class2` | IDs and classes (CSS selector style) |
278
+ | `@click: handler` | Event binding |
279
+ | `@click.prevent.stop:` | Event modifiers |
280
+ | `@keydown.enter:` | Key modifiers |
281
+ | `value <=> var` | Two-way binding (auto-syncs input ↔ variable) |
282
+ | `.("class", cond && "other")` | Dynamic classes |
283
+ | `for x in arr, key: x.id` | Keyed iteration |
284
+ | `span if condition` | Conditional rendering |
285
+
286
+ **The `<=>` operator** handles two-way binding automatically:
287
+ ```coffee
288
+ # This one line...
289
+ input type: "number", value <=> count
290
+
291
+ # ...replaces all this React ceremony:
292
+ # <input type="number" value={count}
293
+ # onChange={e => setCount(parseInt(e.target.value) || 0)} />
294
+ ```
249
295
 
250
- [Template guide →](docs/TEMPLATES.md)
296
+ [Template guide →](docs/GUIDE.md#templates)
251
297
 
252
298
  ---
253
299
 
@@ -336,7 +382,7 @@ rip file.rip # Run a file
336
382
  rip -c file.rip # Compile to JavaScript
337
383
  rip -s file.rip # Show S-expressions (debug parser)
338
384
  rip -t file.rip # Show tokens (debug lexer)
339
- bun run test # Run all 1033 tests
385
+ bun run test # Run all 1046 tests
340
386
  bun run parser # Rebuild parser (self-hosting!)
341
387
  bun run browser # Build browser bundle
342
388
  ```
@@ -348,9 +394,9 @@ bun run browser # Build browser bundle
348
394
  | Guide | Description |
349
395
  |-------|-------------|
350
396
  | [AGENT.md](AGENT.md) | Complete developer/AI guide |
351
- | [docs/REACTIVITY.md](docs/REACTIVITY.md) | Signals, effects, derived values |
352
- | [docs/COMPONENTS.md](docs/COMPONENTS.md) | Component system |
353
- | [docs/TEMPLATES.md](docs/TEMPLATES.md) | Template DSL |
397
+ | [docs/GUIDE.md](docs/GUIDE.md) | Language guide (reactivity, templates, components) |
398
+ | [docs/INTERNALS.md](docs/INTERNALS.md) | Compiler architecture, S-expressions |
399
+ | [docs/BROWSER.md](docs/BROWSER.md) | Browser usage, REPL |
354
400
  | [CONTRIBUTING.md](CONTRIBUTING.md) | How to contribute |
355
401
  | [CHANGELOG.md](CHANGELOG.md) | Version history |
356
402
 
package/docs/INTERNALS.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  > Technical reference for the Rip compiler architecture, code generation, and parsing.
6
6
 
7
- **Version:** 2.5.0
7
+ **Version:** 2.5.1
8
8
  **Test Coverage:** 1046/1046 rip tests (100%) ✅
9
9
  **Status:** Stable & Production Ready - Self-Hosting Complete
10
10
 
@@ -468,7 +468,7 @@ counter = ->
468
468
 
469
469
  # 3. Current Assessment
470
470
 
471
- > **v2.5.0 - Production-Ready with Fine-Grained Reactivity**
471
+ > **v2.5.1 - Production-Ready with Fine-Grained Reactivity**
472
472
 
473
473
  ## Summary Matrix
474
474
 
@@ -6,7 +6,7 @@
6
6
 
7
7
  That "Why Not" document makes strong arguments, but here's the **counter-argument**—a working, tested, **production-ready** language that offers a different path.
8
8
 
9
- **Rip isn't vaporware. It's real. Version 2.5.0. 1046/1046 tests passing. Self-hosting. Zero dependencies. Available now.**
9
+ **Rip isn't vaporware. It's real. Version 2.5.1. 1046/1046 tests passing. Self-hosting. Zero dependencies. Available now.**
10
10
 
11
11
  ### The Philosophical Divide: Freedom vs Fear
12
12
 
@@ -842,7 +842,7 @@ Rip isn't about going backward. It's about recognizing that **we took a wrong tu
842
842
 
843
843
  **The future isn't more dependencies. It's zero dependencies.**
844
844
 
845
- **The future is Rip. Version 2.5.0. Available today.**
845
+ **The future is Rip. Version 2.5.1. Available today.**
846
846
 
847
847
  ---
848
848
 
@@ -890,6 +890,6 @@ $ echo 'console.log "Hello, Rip!"' > test.rip && bun test.rip
890
890
  - ✅ **Two-way binding** (`<=>` operator - automatic for inputs)
891
891
  - ✅ **Ruby constructors** (`ClassName.new()` - elegant instantiation)
892
892
 
893
- **Version 2.5.0. Available now. Clone and go.**
893
+ **Version 2.5.1. Available now. Clone and go.**
894
894
 
895
895
  This approach is ready. Give it a try.
@@ -2705,6 +2705,28 @@ Rewriter = function() {
2705
2705
  }
2706
2706
  if (!inRender)
2707
2707
  return 1;
2708
+ if (tag === "IDENTIFIER" && !token.spaced) {
2709
+ let parts = [token[1]];
2710
+ let j = i + 1;
2711
+ while (j + 1 < tokens.length) {
2712
+ const hyphen = tokens[j];
2713
+ const nextPart = tokens[j + 1];
2714
+ if (hyphen[0] === "-" && !hyphen.spaced && (nextPart[0] === "IDENTIFIER" || nextPart[0] === "PROPERTY")) {
2715
+ parts.push(nextPart[1]);
2716
+ j += 2;
2717
+ if (nextPart[0] === "PROPERTY")
2718
+ break;
2719
+ } else {
2720
+ break;
2721
+ }
2722
+ }
2723
+ if (parts.length > 1 && j > i + 1 && tokens[j - 1][0] === "PROPERTY") {
2724
+ token[0] = "STRING";
2725
+ token[1] = `"${parts.join("-")}"`;
2726
+ tokens.splice(i + 1, j - i - 1);
2727
+ return 1;
2728
+ }
2729
+ }
2708
2730
  if (tag === ".") {
2709
2731
  const prevToken = i > 0 ? tokens[i - 1] : null;
2710
2732
  const prevTag = prevToken ? prevToken[0] : null;
@@ -9314,8 +9336,8 @@ function compileToJS(source, options = {}) {
9314
9336
  return new Compiler(options).compileToJS(source);
9315
9337
  }
9316
9338
  // src/browser.js
9317
- var VERSION = "2.5.0";
9318
- var BUILD_DATE = "2026-01-16@05:08:49GMT";
9339
+ var VERSION = "2.5.1";
9340
+ var BUILD_DATE = "2026-01-16@07:47:41GMT";
9319
9341
  var dedent = (s) => {
9320
9342
  const m = s.match(/^[ \t]*(?=\S)/gm);
9321
9343
  const i = Math.min(...(m || []).map((x) => x.length));