rip-lang 3.9.0 → 3.9.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.
@@ -0,0 +1,114 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Sierpinski Triangle — Rip UI</title>
7
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
8
+ <style>
9
+ * { margin: 0; padding: 0; box-sizing: border-box; }
10
+ body { background: #0a0a0a; color: white; font-family: 'JetBrains Mono', monospace; overflow: hidden; }
11
+ .scene { position: relative; width: 100vw; height: 100vh; }
12
+ .stats { position: fixed; top: 20px; left: 24px; z-index: 10; }
13
+ .stats-title { font-size: 18px; font-weight: bold; }
14
+ .stats-line { font-size: 13px; color: rgba(255,255,255,0.45); margin-top: 3px; }
15
+ .stats-note { font-size: 11px; color: rgba(255,255,255,0.25); margin-top: 8px; max-width: 280px; line-height: 1.5; }
16
+ .source-toggle { display: inline-block; margin-top: 10px; padding: 4px 10px; background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.15); border-radius: 4px; color: rgba(255,255,255,0.5); font-family: inherit; font-size: 12px; cursor: pointer; transition: all 0.2s; }
17
+ .source-toggle:hover { color: white; border-color: rgba(255,255,255,0.4); background: rgba(255,255,255,0.12); }
18
+ .source-panel { position: fixed; top: 0; right: 0; bottom: 0; width: min(520px, 90vw); z-index: 100; background: rgba(10,10,10,0.92); backdrop-filter: blur(12px); border-left: 1px solid rgba(255,255,255,0.1); transform: translateX(100%); transition: transform 0.3s ease; overflow-y: auto; }
19
+ .source-panel.open { transform: translateX(0); }
20
+ .source-header { display: flex; justify-content: space-between; align-items: center; padding: 16px 20px; border-bottom: 1px solid rgba(255,255,255,0.08); }
21
+ .source-label { font-size: 13px; font-weight: bold; color: rgba(255,255,255,0.6); letter-spacing: 0.05em; }
22
+ .source-close { background: none; border: none; color: rgba(255,255,255,0.4); font-size: 20px; cursor: pointer; padding: 4px 8px; border-radius: 4px; font-family: inherit; transition: all 0.2s; }
23
+ .source-close:hover { color: white; background: rgba(255,255,255,0.1); }
24
+ .source-code { padding: 16px 20px; font-size: 13px; line-height: 1.7; color: rgba(255,255,255,0.85); white-space: pre; overflow-x: auto; }
25
+ .container { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%) scale(var(--scale, 1)); width: 1000px; height: 870px; transform-origin: center center; }
26
+ .dot {
27
+ position: absolute; width: 24px; height: 24px; border-radius: 50%;
28
+ display: flex; align-items: center; justify-content: center;
29
+ font-size: 10px; font-weight: bold; color: rgba(255,255,255,0.9);
30
+ user-select: none; cursor: default; transition: transform 0.1s;
31
+ }
32
+ .dot:hover { transform: scale(2.5); z-index: 1; }
33
+ </style>
34
+ <script type="module" src="https://shreeve.github.io/rip-lang/dist/rip-ui.min.js"></script>
35
+ <script>
36
+ function updateScale() {
37
+ var s = Math.min(window.innerWidth / 1350, window.innerHeight / 1250, 1);
38
+ document.documentElement.style.setProperty('--scale', s);
39
+ }
40
+ updateScale();
41
+ window.addEventListener('resize', updateScale);
42
+ </script>
43
+ </head>
44
+ <body>
45
+ <div class="source-panel" id="source-panel">
46
+ <div class="source-header">
47
+ <span class="source-label">SOURCE — 48 LINES OF RIP</span>
48
+ <button class="source-close" onclick="document.getElementById('source-panel').classList.remove('open')">&times;</button>
49
+ </div>
50
+ <pre class="source-code" id="source-code"></pre>
51
+ </div>
52
+ <script type="text/rip">
53
+ # Sierpinski Triangle — 729 reactive dots at 60fps
54
+ # No virtual DOM. No tree diffing. Just fine-grained reactivity.
55
+
56
+ App = component
57
+ elapsed := 0
58
+ fps := 0
59
+
60
+ dots := do ->
61
+ result = []
62
+ build = (x, y, s) ->
63
+ if s <= 25
64
+ result.push { x, y, id: result.length }
65
+ else
66
+ build x , y - s / 4, s / 2
67
+ build x - s / 2, y + s / 4, s / 2
68
+ build x + s / 2, y + s / 4, s / 2
69
+ build 500, 435, 1000
70
+ result
71
+
72
+ mounted: ->
73
+ start = performance.now()
74
+ frames = 0
75
+ last = start
76
+ tick = ->
77
+ now = performance.now()
78
+ elapsed = (now - start) / 1000
79
+ frames++
80
+ if now - last >= 1000
81
+ fps = frames
82
+ frames = 0
83
+ last = now
84
+ requestAnimationFrame tick
85
+ requestAnimationFrame tick
86
+
87
+ render
88
+ .scene
89
+ .stats
90
+ .stats-title "Sierpinski Triangle"
91
+ .stats-line dots.length + " dots · " + dots.length * 2 + " reactive bindings"
92
+ .stats-line fps + " fps"
93
+ .stats-note "Each dot updates independently via fine-grained reactivity"
94
+ button.source-toggle @click: (-> document.getElementById('source-panel').classList.toggle('open')), "View Source"
95
+
96
+ .container
97
+ for dot in dots
98
+ .dot style: "left:" + (dot.x - 12) + "px;top:" + (dot.y - 12) + "px;background:hsl(" + ((dot.id / 729 * 360 + elapsed * 40) % 360) + " 70% 50%)"
99
+ Math.floor(elapsed % 10) + 1
100
+
101
+ App.new().mount document.body
102
+
103
+ # Populate source panel with this page's own Rip source
104
+ ripScript = document.querySelector('script[type="text/rip"]')
105
+ lines = ripScript.textContent.split("\n")
106
+ while lines.length and lines[0].trim() is ''
107
+ lines.shift()
108
+ cutoff = lines.findIndex((l) -> l.includes('Populate source panel'))
109
+ lines = lines.slice(0, cutoff).filter((l, i, a) -> i < a.length - 1 or l.trim() isnt '')
110
+ sourceEl = document.getElementById('source-code')
111
+ sourceEl.textContent = lines.join("\n")
112
+ </script>
113
+ </body>
114
+ </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rip-lang",
3
- "version": "3.9.0",
3
+ "version": "3.9.1",
4
4
  "description": "A modern language that compiles to JavaScript",
5
5
  "type": "module",
6
6
  "main": "src/compiler.js",
package/src/browser.js CHANGED
@@ -71,8 +71,14 @@ export { processRipScripts };
71
71
  * Import a .rip file as an ES module
72
72
  * Fetches the URL, compiles Rip→JS, dynamically imports via Blob URL
73
73
  * Usage: const { launch } = await importRip('/ui.rip')
74
+ *
75
+ * Pre-compiled modules can be registered on importRip.modules to skip fetching.
76
+ * The rip-ui bundle uses this to embed ui.rip without a server round-trip.
74
77
  */
75
78
  export async function importRip(url) {
79
+ for (const [key, mod] of Object.entries(importRip.modules)) {
80
+ if (url.includes(key)) return mod;
81
+ }
76
82
  const source = await fetch(url).then(r => {
77
83
  if (!r.ok) throw new Error(`importRip: ${url} (${r.status})`);
78
84
  return r.text();
@@ -86,6 +92,7 @@ export async function importRip(url) {
86
92
  URL.revokeObjectURL(blobUrl);
87
93
  }
88
94
  }
95
+ importRip.modules = {};
89
96
 
90
97
  /**
91
98
  * Browser Console REPL
@@ -128,14 +135,16 @@ if (typeof globalThis !== 'undefined') {
128
135
  globalThis.__ripExports = { compile, compileToJS, formatSExpr, VERSION, BUILD_DATE, getReactiveRuntime, getComponentRuntime };
129
136
  }
130
137
 
131
- // Auto-process scripts when this module loads
132
- // Expose __ripScriptsReady promise so other modules can await completion
138
+ // Auto-process <script type="text/rip"> blocks.
139
+ // Deferred via queueMicrotask so bundled entry code (e.g. rip-ui.min.js registering
140
+ // importRip.modules) runs before script processing begins.
133
141
  if (typeof document !== 'undefined') {
134
- if (document.readyState === 'loading') {
135
- globalThis.__ripScriptsReady = new Promise(resolve => {
136
- document.addEventListener('DOMContentLoaded', () => processRipScripts().then(resolve));
137
- });
138
- } else {
139
- globalThis.__ripScriptsReady = processRipScripts();
140
- }
142
+ globalThis.__ripScriptsReady = new Promise(resolve => {
143
+ const run = () => processRipScripts().then(resolve);
144
+ if (document.readyState === 'loading') {
145
+ document.addEventListener('DOMContentLoaded', () => queueMicrotask(run));
146
+ } else {
147
+ queueMicrotask(run);
148
+ }
149
+ });
141
150
  }
package/src/compiler.js CHANGED
@@ -1094,6 +1094,9 @@ export class CodeGenerator {
1094
1094
  if (!this.reactiveVars) this.reactiveVars = new Set();
1095
1095
  let varName = str(name) ?? name;
1096
1096
  this.reactiveVars.add(varName);
1097
+ if (this.is(expr, 'block') && expr.length > 2) {
1098
+ return `const ${varName} = __computed(() => ${this.generateFunctionBody(expr)})`;
1099
+ }
1097
1100
  return `const ${varName} = __computed(() => ${this.generate(expr, 'value')})`;
1098
1101
  }
1099
1102
 
package/src/components.js CHANGED
@@ -51,6 +51,23 @@ function getMemberName(target) {
51
51
  return null;
52
52
  }
53
53
 
54
+ /**
55
+ * Detect fragment root and collect direct child variables for proper removal.
56
+ * After insertBefore, a DocumentFragment is empty — .remove() is a no-op.
57
+ * Callers must remove each child element individually.
58
+ */
59
+ function getFragChildren(rootVar, createLines, localizeVar) {
60
+ const root = localizeVar(rootVar);
61
+ if (!/_frag\d+$/.test(root)) return null;
62
+ const children = [];
63
+ const re = new RegExp(`^${root.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\.appendChild\\(([^)]+)\\);`);
64
+ for (const line of createLines) {
65
+ const m = localizeVar(line).match(re);
66
+ if (m) children.push(m[1]);
67
+ }
68
+ return children.length > 0 ? children : null;
69
+ }
70
+
54
71
  // ============================================================================
55
72
  // Prototype Installation
56
73
  // ============================================================================
@@ -211,8 +228,8 @@ export function installComponentSupport(CodeGenerator, Lexer) {
211
228
  }
212
229
 
213
230
  // ─────────────────────────────────────────────────────────────────────
214
- // Implicit div for class-only selectors
215
- // .card → div.card
231
+ // Implicit div for class-only or bare dot selectors
232
+ // .card → div.card | . (with children) → div
216
233
  // ─────────────────────────────────────────────────────────────────────
217
234
  if (tag === '.') {
218
235
  let prevToken = i > 0 ? tokens[i - 1] : null;
@@ -223,6 +240,12 @@ export function installComponentSupport(CodeGenerator, Lexer) {
223
240
  tokens.splice(i, 0, divToken);
224
241
  return 2;
225
242
  }
243
+ // Skip .('classes') — handled by dynamic classes handler below
244
+ if (!nextToken || nextToken[0] !== '(') {
245
+ token[0] = 'IDENTIFIER';
246
+ token[1] = 'div';
247
+ return 0;
248
+ }
226
249
  }
227
250
  }
228
251
 
@@ -343,7 +366,7 @@ export function installComponentSupport(CodeGenerator, Lexer) {
343
366
  isTemplateElement = true;
344
367
  } else if (tag === 'IDENTIFIER' && isTemplateTag(token[1]) && !isAfterControlFlow) {
345
368
  isTemplateElement = true;
346
- } else if (tag === 'PROPERTY' || tag === 'STRING' || tag === 'CALL_END' || tag === ')') {
369
+ } else if (tag === 'PROPERTY' || tag === 'STRING' || tag === 'STRING_END' || tag === 'CALL_END' || tag === ')') {
347
370
  isTemplateElement = startsWithTag(tokens, i);
348
371
  }
349
372
  else if (tag === 'IDENTIFIER' && i > 1 && tokens[i - 1][0] === '...') {
@@ -614,8 +637,14 @@ export function installComponentSupport(CodeGenerator, Lexer) {
614
637
 
615
638
  // Computed (derived)
616
639
  for (const { name, expr } of derivedVars) {
617
- const val = this.generateInComponent(expr, 'value');
618
- lines.push(` this.${name} = __computed(() => ${val});`);
640
+ if (this.is(expr, 'block') && expr.length > 2) {
641
+ const transformed = this.transformComponentMembers(expr);
642
+ const body = this.generateFunctionBody(transformed);
643
+ lines.push(` this.${name} = __computed(() => ${body});`);
644
+ } else {
645
+ const val = this.generateInComponent(expr, 'value');
646
+ lines.push(` this.${name} = __computed(() => ${val});`);
647
+ }
619
648
  }
620
649
 
621
650
  // Effects
@@ -991,10 +1020,10 @@ export function installComponentSupport(CodeGenerator, Lexer) {
991
1020
  const eventName = key[2];
992
1021
  // Bind method references to this
993
1022
  if (typeof value === 'string' && this.componentMembers?.has(value)) {
994
- this._createLines.push(`${elVar}.addEventListener('${eventName}', (e) => this.${value}(e));`);
1023
+ this._createLines.push(`${elVar}.addEventListener('${eventName}', (e) => __batch(() => this.${value}(e)));`);
995
1024
  } else {
996
1025
  const handlerCode = this.generateInComponent(value, 'value');
997
- this._createLines.push(`${elVar}.addEventListener('${eventName}', (e) => (${handlerCode})(e));`);
1026
+ this._createLines.push(`${elVar}.addEventListener('${eventName}', (e) => __batch(() => (${handlerCode})(e)));`);
998
1027
  }
999
1028
  continue;
1000
1029
  }
@@ -1221,7 +1250,14 @@ export function installComponentSupport(CodeGenerator, Lexer) {
1221
1250
  if (hasEffects) {
1222
1251
  factoryLines.push(` disposers.forEach(d => d());`);
1223
1252
  }
1224
- factoryLines.push(` if (detaching) ${localizeVar(rootVar)}.remove();`);
1253
+ const condFragChildren = getFragChildren(rootVar, createLines, localizeVar);
1254
+ if (condFragChildren) {
1255
+ for (const child of condFragChildren) {
1256
+ factoryLines.push(` if (detaching) ${child}.remove();`);
1257
+ }
1258
+ } else {
1259
+ factoryLines.push(` if (detaching) ${localizeVar(rootVar)}.remove();`);
1260
+ }
1225
1261
  factoryLines.push(` }`);
1226
1262
 
1227
1263
  factoryLines.push(` };`);
@@ -1311,9 +1347,16 @@ export function installComponentSupport(CodeGenerator, Lexer) {
1311
1347
  }
1312
1348
  factoryLines.push(` },`);
1313
1349
 
1314
- // m() - mount
1350
+ // m() - mount (also repositions already-mounted blocks)
1351
+ const loopFragChildren = getFragChildren(itemNode, itemCreateLines, localizeVar);
1315
1352
  factoryLines.push(` m(target, anchor) {`);
1316
- factoryLines.push(` target.insertBefore(${localizeVar(itemNode)}, anchor);`);
1353
+ if (loopFragChildren) {
1354
+ for (const child of loopFragChildren) {
1355
+ factoryLines.push(` target.insertBefore(${child}, anchor);`);
1356
+ }
1357
+ } else {
1358
+ factoryLines.push(` target.insertBefore(${localizeVar(itemNode)}, anchor);`);
1359
+ }
1317
1360
  factoryLines.push(` },`);
1318
1361
 
1319
1362
  // p() - update
@@ -1340,7 +1383,13 @@ export function installComponentSupport(CodeGenerator, Lexer) {
1340
1383
  if (hasEffects) {
1341
1384
  factoryLines.push(` disposers.forEach(d => d());`);
1342
1385
  }
1343
- factoryLines.push(` if (detaching) ${localizeVar(itemNode)}.remove();`);
1386
+ if (loopFragChildren) {
1387
+ for (const child of loopFragChildren) {
1388
+ factoryLines.push(` if (detaching) ${child}.remove();`);
1389
+ }
1390
+ } else {
1391
+ factoryLines.push(` if (detaching) ${localizeVar(itemNode)}.remove();`);
1392
+ }
1344
1393
  factoryLines.push(` }`);
1345
1394
 
1346
1395
  factoryLines.push(` };`);
@@ -1363,14 +1412,12 @@ export function installComponentSupport(CodeGenerator, Lexer) {
1363
1412
  setupLines.push(` const ${itemVar} = items[${indexVar}];`);
1364
1413
  setupLines.push(` const key = ${keyExpr};`);
1365
1414
  setupLines.push(` let block = map.get(key);`);
1366
- setupLines.push(` if (block) {`);
1367
- setupLines.push(` block.p(this, ${itemVar}, ${indexVar});`);
1368
- setupLines.push(` } else {`);
1415
+ setupLines.push(` if (!block) {`);
1369
1416
  setupLines.push(` block = ${blockName}(this, ${itemVar}, ${indexVar});`);
1370
1417
  setupLines.push(` block.c();`);
1371
- setupLines.push(` block.m(parent, anchor);`);
1372
- setupLines.push(` block.p(this, ${itemVar}, ${indexVar});`);
1373
1418
  setupLines.push(` }`);
1419
+ setupLines.push(` block.m(parent, anchor);`);
1420
+ setupLines.push(` block.p(this, ${itemVar}, ${indexVar});`);
1374
1421
  setupLines.push(` newMap.set(key, block);`);
1375
1422
  setupLines.push(` }`);
1376
1423
  setupLines.push(``);
@@ -197,7 +197,7 @@ grammar =
197
197
  ComputedAssign: [
198
198
  o 'Assignable COMPUTED_ASSIGN Expression' , '["computed", 1, 3]'
199
199
  o 'Assignable COMPUTED_ASSIGN TERMINATOR Expression' , '["computed", 1, 4]'
200
- o 'Assignable COMPUTED_ASSIGN INDENT Expression OUTDENT', '["computed", 1, 4]'
200
+ o 'Assignable COMPUTED_ASSIGN Block' , '["computed", 1, 3]'
201
201
  ]
202
202
 
203
203
  # Reactive readonly (=!) — constants that cannot be reassigned
@@ -443,7 +443,7 @@ grammar =
443
443
  Invocation: [
444
444
  o 'Value String' , '["tagged-template", 1, 2]' # Tagged template
445
445
  o 'Value Arguments' , '[1, ...2]' # Regular call
446
- o 'Value ES6_OPTIONAL_CALL Arguments' , '["optcall", 1, ...3]' # Optional call: x?.(args)
446
+ o 'Value ES6_OPTIONAL_CALL Arguments' , '["optcall", 1, ...3]' # Optional call: x?.(args)
447
447
  o 'SUPER Arguments' , '["super", ...2]' # Super call
448
448
  o 'DYNAMIC_IMPORT Arguments' , '[1, ...2]' # Dynamic import()
449
449
  ]