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.
@@ -1,1645 +0,0 @@
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>Rip Playground</title>
7
- <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg width='420' height='420' viewBox='0 0 420 420' fill='none' xmlns='http://www.w3.org/2000/svg'><circle cx='210' cy='210' r='210' fill='white'/><path d='M114.5 263C79.772 263 45.7872 275.051 34.271 283.579C33.4994 284.151 34.012 285.229 34.9656 285.117C73.1916 280.629 115.309 292.74 146.5 304C178.5 315.552 221 336 269 336C314.651 336 372.074 310.499 386.401 293.944C387.038 293.208 386.301 292.353 385.435 292.799C376.614 297.341 364.243 306.018 316.073 306.948C265.273 307.928 159 263 114.5 263Z' fill='%230389FF'/><path d='M223.46 84C239.53 84 253.592 86.9253 265.645 92.7754C277.697 98.6254 287.071 107.048 293.767 118.043C300.462 129.038 303.811 142.219 303.811 157.584C303.811 173.09 300.357 186.165 293.449 196.808C287.068 206.742 278.259 214.402 267.026 219.792L309.603 297.958C297.885 297.851 283.094 295.413 266.52 291.62C257.58 289.574 248.214 287.157 238.645 284.547L209.127 229.054H188.782V270.333C176.996 266.968 165.515 263.788 154.823 261.15C146.038 258.984 137.665 257.152 130 255.885V84H223.46ZM188.782 183.381H209.505C216.412 183.381 222.297 182.535 227.16 180.844C232.094 179.082 235.865 176.297 238.473 172.491C241.151 168.685 242.49 163.716 242.49 157.584C242.49 151.382 241.151 146.342 238.473 142.466C235.865 138.519 232.094 135.628 227.16 133.796C222.297 131.893 216.412 130.941 209.505 130.941H188.782V183.381Z' fill='%23BB0000'/></svg>">
8
- <style>
9
- * {
10
- margin: 0;
11
- padding: 0;
12
- box-sizing: border-box;
13
- }
14
-
15
- /* Hide body until fully initialized to prevent render flicker */
16
- body {
17
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
18
- background: #1e1e1e;
19
- color: #d4d4d4;
20
- height: 100vh;
21
- display: flex;
22
- flex-direction: column;
23
- opacity: 0;
24
- visibility: hidden;
25
- }
26
-
27
- body.ready {
28
- visibility: visible;
29
- opacity: 1;
30
- transition: opacity 0.15s ease-in;
31
- }
32
-
33
- .header {
34
- background: #252526;
35
- padding: 15px 20px;
36
- border-bottom: 1px solid #3e3e42;
37
- display: flex;
38
- justify-content: space-between;
39
- align-items: center;
40
- }
41
-
42
- .header-left {
43
- display: flex;
44
- flex-direction: column;
45
- }
46
-
47
- h1 {
48
- display: flex;
49
- align-items: center;
50
- gap: 12px;
51
- font-size: 20px;
52
- font-weight: 600;
53
- color: #cccccc;
54
- margin-bottom: 5px;
55
- }
56
-
57
- .logo {
58
- height: 32px;
59
- width: auto;
60
- }
61
-
62
- .subtitle {
63
- font-size: 13px;
64
- color: #858585;
65
- }
66
-
67
- .github-link {
68
- color: #858585;
69
- transition: color 0.2s;
70
- align-self: flex-start;
71
- margin-top: 4px;
72
- }
73
-
74
- .github-link:hover {
75
- color: #ffffff;
76
- }
77
-
78
- .github-link svg {
79
- width: 24px;
80
- height: 24px;
81
- fill: currentColor;
82
- }
83
-
84
- .tabs {
85
- display: flex;
86
- background: #2d2d30;
87
- border-bottom: 1px solid #3e3e42;
88
- }
89
-
90
- .tab {
91
- padding: 12px 24px;
92
- background: transparent;
93
- border: none;
94
- color: #858585;
95
- cursor: pointer;
96
- font-size: 13px;
97
- font-weight: 500;
98
- border-bottom: 2px solid transparent;
99
- transition: all 0.2s;
100
- }
101
-
102
- .tab:hover {
103
- color: #cccccc;
104
- background: #2a2a2d;
105
- }
106
-
107
- .tab.active {
108
- color: #ffffff;
109
- border-bottom-color: #007acc;
110
- background: #1e1e1e;
111
- }
112
-
113
- .content {
114
- flex: 1;
115
- display: flex;
116
- flex-direction: column;
117
- overflow: hidden;
118
- }
119
-
120
- .pane {
121
- display: none;
122
- flex: 1;
123
- overflow: hidden;
124
- }
125
-
126
- .pane.active {
127
- display: flex;
128
- flex-direction: column;
129
- }
130
-
131
- /* REPL Styles */
132
- .repl-container {
133
- flex: 1;
134
- display: flex;
135
- flex-direction: column;
136
- padding: 20px;
137
- overflow: hidden;
138
- }
139
-
140
- .repl-output {
141
- flex: 1;
142
- overflow-y: auto;
143
- font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
144
- font-size: 14px;
145
- line-height: 1.6;
146
- padding: 10px;
147
- background: #1e1e1e;
148
- border: 1px solid #3e3e42;
149
- border-radius: 4px;
150
- margin-bottom: 10px;
151
- }
152
-
153
- .repl-line {
154
- margin-bottom: 8px;
155
- }
156
-
157
- .prompt {
158
- color: #4ec9b0;
159
- font-weight: 600;
160
- }
161
-
162
- .repl-code {
163
- display: inline;
164
- }
165
-
166
- .repl-code > span {
167
- display: inline !important;
168
- background: none !important;
169
- padding: 0 !important;
170
- }
171
-
172
- .result {
173
- color: #ce9178;
174
- margin-left: 20px;
175
- }
176
-
177
- .error {
178
- color: #f48771;
179
- margin-left: 20px;
180
- }
181
-
182
- .command-output {
183
- color: #858585;
184
- margin-left: 20px;
185
- white-space: pre-wrap;
186
- }
187
-
188
- .repl-input-area {
189
- display: flex;
190
- align-items: flex-start;
191
- gap: 4px;
192
- background: #2d2d30;
193
- padding: 8px 10px;
194
- border: 1px solid #3e3e42;
195
- border-radius: 4px;
196
- }
197
-
198
- .repl-prompt-text {
199
- color: #4ec9b0;
200
- font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
201
- font-weight: 600;
202
- font-size: 14px;
203
- line-height: 24px;
204
- margin-top: 0;
205
- }
206
-
207
- #repl-editor-container {
208
- flex: 1;
209
- overflow: hidden;
210
- border-radius: 4px;
211
- }
212
-
213
- #repl-editor-container .monaco-editor,
214
- #repl-editor-container .monaco-editor .focused,
215
- #repl-editor-container .monaco-editor-background {
216
- outline: none !important;
217
- box-shadow: none !important;
218
- }
219
-
220
- #repl-editor-container .monaco-editor,
221
- #repl-editor-container .monaco-editor .overflow-guard {
222
- border-radius: 0;
223
- }
224
-
225
- #repl-editor-container .monaco-editor .lines-content {
226
- padding-right: 0 !important;
227
- }
228
-
229
- /* Compiler Styles */
230
- .compiler-container {
231
- flex: 1;
232
- min-height: 0;
233
- display: flex;
234
- background: #3e3e42;
235
- padding: 20px;
236
- position: relative;
237
- }
238
-
239
- .resizer {
240
- width: 10px;
241
- background: #3e3e42;
242
- cursor: col-resize;
243
- position: relative;
244
- flex-shrink: 0;
245
- }
246
-
247
- .resizer:hover {
248
- background: #007acc;
249
- }
250
-
251
- .resizer::after {
252
- content: '';
253
- position: absolute;
254
- top: 50%;
255
- left: 50%;
256
- transform: translate(-50%, -50%);
257
- width: 4px;
258
- height: 40px;
259
- background: #888888;
260
- border-radius: 2px;
261
- }
262
-
263
- .editor-pane {
264
- display: flex;
265
- flex-direction: column;
266
- background: #1e1e1e;
267
- border-radius: 4px;
268
- overflow: hidden;
269
- min-height: 0;
270
- flex: 1;
271
- min-width: 200px;
272
- }
273
-
274
- #editor-left {
275
- flex-basis: 50%;
276
- }
277
-
278
- #editor-right {
279
- flex-basis: 50%;
280
- }
281
-
282
- .pane-header {
283
- background: #2d2d30;
284
- padding: 10px 15px;
285
- border-bottom: 1px solid #3e3e42;
286
- font-size: 13px;
287
- font-weight: 600;
288
- color: #cccccc;
289
- display: flex;
290
- justify-content: space-between;
291
- align-items: center;
292
- min-height: 49px;
293
- box-sizing: border-box;
294
- }
295
-
296
- .pane-header-buttons {
297
- display: flex;
298
- gap: 8px;
299
- }
300
-
301
- .pane-header button {
302
- background: #5a5a5d;
303
- color: #ffffff;
304
- border: none;
305
- padding: 6px 12px;
306
- border-radius: 3px;
307
- font-size: 12px;
308
- font-weight: 500;
309
- cursor: pointer;
310
- transition: background 0.2s;
311
- }
312
-
313
- .pane-header button:hover {
314
- background: #6e6e71;
315
- }
316
-
317
- .pane-header button.active {
318
- background: #007acc;
319
- }
320
-
321
- .pane-header button.active:hover {
322
- background: #005a9e;
323
- }
324
-
325
- .editor-wrapper {
326
- flex: 1;
327
- min-height: 0;
328
- overflow: hidden;
329
- }
330
-
331
- .theme-select {
332
- background: #3c3c3c;
333
- color: #cccccc;
334
- border: 1px solid #5a5a5d;
335
- padding: 4px 8px;
336
- border-radius: 3px;
337
- font-size: 12px;
338
- cursor: pointer;
339
- outline: none;
340
- }
341
-
342
- .theme-select:hover {
343
- border-color: #007acc;
344
- }
345
-
346
- /* Scrollbar styling */
347
- ::-webkit-scrollbar {
348
- width: 10px;
349
- height: 10px;
350
- }
351
-
352
- ::-webkit-scrollbar-track {
353
- background: #1e1e1e;
354
- }
355
-
356
- ::-webkit-scrollbar-thumb {
357
- background: #555555;
358
- border-radius: 5px;
359
- }
360
-
361
- ::-webkit-scrollbar-thumb:hover {
362
- background: #686868;
363
- }
364
-
365
- ::-webkit-scrollbar-corner {
366
- background: #1e1e1e;
367
- }
368
-
369
- .monaco-editor .slider {
370
- border-radius: 5px !important;
371
- }
372
-
373
- .monaco-editor .deprecated {
374
- text-decoration: none !important;
375
- }
376
-
377
- /* Welcome message */
378
- .welcome {
379
- color: #858585;
380
- font-style: italic;
381
- margin-bottom: 15px;
382
- }
383
-
384
- .help-text {
385
- color: #6a9955;
386
- }
387
-
388
- .command {
389
- color: #4fc1ff;
390
- }
391
-
392
- .var-name {
393
- color: #9cdcfe;
394
- }
395
-
396
- .var-value {
397
- color: #ce9178;
398
- }
399
-
400
- /* Light/Dark mode toggle */
401
- .mode-toggle {
402
- background: none;
403
- border: 1px solid #5a5a5d;
404
- border-radius: 4px;
405
- color: #858585;
406
- cursor: pointer;
407
- width: 32px;
408
- height: 32px;
409
- font-size: 16px;
410
- line-height: 32px;
411
- text-align: center;
412
- padding: 0;
413
- transition: all 0.2s;
414
- margin-right: 8px;
415
- }
416
-
417
- .mode-toggle:hover {
418
- color: #ffffff;
419
- border-color: #007acc;
420
- }
421
-
422
- .header-right {
423
- display: flex;
424
- align-items: center;
425
- gap: 4px;
426
- }
427
-
428
- /* Light mode overrides */
429
- body.light {
430
- background: #ffffff;
431
- color: #1e1e1e;
432
- }
433
-
434
- body.light .header {
435
- background: #f3f3f3;
436
- border-bottom-color: #d4d4d4;
437
- }
438
-
439
- body.light h1 { color: #333333; }
440
- body.light .subtitle { color: #666666; }
441
- body.light .github-link { color: #666666; }
442
- body.light .github-link:hover { color: #000000; }
443
- body.light .mode-toggle { color: #666666; border-color: #cccccc; }
444
- body.light .mode-toggle:hover { color: #000000; border-color: #007acc; }
445
-
446
- body.light .tabs {
447
- background: #f3f3f3;
448
- border-bottom-color: #d4d4d4;
449
- }
450
-
451
- body.light .tab { color: #666666; }
452
- body.light .tab:hover { color: #333333; background: #e8e8e8; }
453
- body.light .tab.active { color: #333333; border-bottom-color: #007acc; background: #ffffff; }
454
-
455
- body.light .compiler-container { background: #e0e0e0; }
456
- body.light .editor-pane { background: #ffffff; border-color: #d4d4d4; }
457
- body.light .content { background: #ffffff; }
458
-
459
- body.light .pane-header {
460
- background: #f3f3f3;
461
- border-bottom-color: #d4d4d4;
462
- color: #333333;
463
- }
464
-
465
- body.light .pane-header button { background: #cccccc; color: #333333; }
466
- body.light .pane-header button:hover { background: #bbbbbb; }
467
- body.light .pane-header button.active { background: #007acc; color: #ffffff; }
468
- body.light .theme-select { background: #f3f3f3; color: #333333; border-color: #cccccc; }
469
-
470
- body.light .resizer { background: #e0e0e0; }
471
- body.light .resizer:hover { background: #007acc; }
472
-
473
- body.light ::-webkit-scrollbar-track { background: #f3f3f3; }
474
- body.light ::-webkit-scrollbar-thumb { background: #c1c1c1; }
475
- body.light ::-webkit-scrollbar-thumb:hover { background: #a0a0a0; }
476
- body.light ::-webkit-scrollbar-corner { background: #f3f3f3; }
477
-
478
- body.light .repl-container { background: #e8e8e8; }
479
- body.light .repl-output { background: #ffffff; border-color: #d4d4d4; color: #1e1e1e; }
480
- body.light .repl-output .prompt { color: #007acc; }
481
- body.light .repl-output .result { color: #a31515; }
482
- body.light .repl-output .error { color: #d32f2f; }
483
- body.light .repl-output .welcome { color: #666666; }
484
- body.light .repl-output .command { color: #0070c1; }
485
- body.light .repl-output .command-output { color: #555555; }
486
- body.light .repl-input-area { background: #f3f3f3; border-color: #d4d4d4; border-radius: 6px; }
487
- body.light .repl-prompt-text { color: #007acc; }
488
- </style>
489
- <link rel="preload" href="https://cdn.jsdelivr.net/npm/monaco-editor@0.52.0/min/vs/loader.js" as="script">
490
- <script type="module" src="dist/rip.browser.min.js"></script>
491
- <script type="text/rip" src="examples.rip"></script>
492
- </head>
493
- <body>
494
- <div class="header">
495
- <div class="header-left">
496
- <h1>
497
- <img src="rip.svg" alt="Rip" class="logo" style="background-color: #fff; border-radius: 5px; padding: 4px;">
498
- Rip Playground
499
- </h1>
500
- <div class="subtitle"></div>
501
- </div>
502
- <div class="header-right">
503
- <button id="mode-toggle" class="mode-toggle" title="Toggle light/dark mode">&#9790;</button>
504
- <a href="https://github.com/shreeve/rip-lang" target="_blank" class="github-link" title="View on GitHub">
505
- <svg viewBox="0 0 16 16" aria-hidden="true"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"></path></svg>
506
- </a>
507
- </div>
508
- </div>
509
-
510
- <div class="tabs">
511
- <button class="tab active" data-tab="compiler">Live Compiler</button>
512
- <button class="tab" data-tab="repl">REPL Console</button>
513
- </div>
514
-
515
- <div class="content">
516
- <!-- Compiler Pane -->
517
- <div class="pane active" id="compiler-pane">
518
- <div class="compiler-container">
519
- <div class="editor-pane" id="editor-left">
520
- <div class="pane-header">
521
- <span>Rip Source</span>
522
- <div class="pane-header-buttons">
523
- <select id="example-select" class="theme-select" title="Load example">
524
- <option value="demo">Full Demo</option>
525
- <option value="basics">Basics</option>
526
- <option value="reactive">Reactive State</option>
527
- <option value="classes">Classes</option>
528
- <option value="regex">Regex & Strings</option>
529
- <option value="async">Async & Dammit</option>
530
- <option value="custom">Custom</option>
531
- </select>
532
- <select id="theme-select" class="theme-select" title="Editor theme">
533
- <optgroup label="Dark Themes">
534
- <option value="vs-dark">Dark+ (default)</option>
535
- <option value="hc-black">High Contrast Dark</option>
536
- <option value="cdn:GitHub Dark">GitHub Dark</option>
537
- <option value="cdn:Dracula">Dracula</option>
538
- <option value="cdn:Monokai">Monokai</option>
539
- <option value="cdn:Nord">Nord</option>
540
- </optgroup>
541
- <optgroup label="Light Themes">
542
- <option value="vs">Light+</option>
543
- <option value="hc-light">High Contrast Light</option>
544
- <option value="cdn:GitHub Light">GitHub Light</option>
545
- <option value="cdn:Solarized-light">Solarized Light</option>
546
- </optgroup>
547
- </select>
548
- <button id="clear-btn" title="Clear the editor">Clear</button>
549
- <button class="active" id="run-btn" title="Execute code and output to browser console (F5, Cmd+Enter, Ctrl+Enter)">Run</button>
550
- </div>
551
- </div>
552
- <div class="editor-wrapper" id="editor-container"></div>
553
- </div>
554
-
555
- <div class="resizer" id="resizer"></div>
556
-
557
- <div class="editor-pane" id="editor-right">
558
- <div class="pane-header">
559
- <span>Output</span>
560
- <div class="pane-header-buttons">
561
- <button id="show-tokens" title="Toggle token stream display">Tokens</button>
562
- <button id="show-sexp" title="Toggle S-expression display">S-Expressions</button>
563
- <button id="show-preamble" title="Toggle preamble code display">Preamble</button>
564
- <button id="show-types" title="Toggle .d.ts type declarations">Types</button>
565
- <button id="show-js" title="Toggle JavaScript output">JS</button>
566
- </div>
567
- </div>
568
- <div class="editor-wrapper" id="output-container"></div>
569
- </div>
570
- </div>
571
- </div>
572
-
573
- <!-- REPL Pane -->
574
- <div class="pane" id="repl-pane">
575
- <div class="repl-container">
576
- <div class="repl-output" id="repl-output">
577
- <div class="welcome">
578
- <strong>Rip Playground</strong> - Interactive Environment<br>
579
- Type <span class="command">.help</span> for commands, try Rip expressions!
580
- </div>
581
- </div>
582
- <div class="repl-input-area">
583
- <span class="repl-prompt-text" id="prompt">rip&gt;</span>
584
- <div id="repl-editor-container" style="flex: 1; height: 24px;"></div>
585
- </div>
586
- </div>
587
- </div>
588
- </div>
589
-
590
- <script type="module">
591
- // ========================================================================
592
- // Monaco Editor Setup
593
- // ========================================================================
594
-
595
- // Load Monaco from CDN
596
- const MONACO_CDN = 'https://cdn.jsdelivr.net/npm/monaco-editor@0.52.0';
597
-
598
- // Configure Monaco AMD loader
599
- const loaderScript = document.createElement('script');
600
- loaderScript.src = `${MONACO_CDN}/min/vs/loader.js`;
601
- document.head.appendChild(loaderScript);
602
-
603
- await new Promise(resolve => loaderScript.onload = resolve);
604
-
605
- require.config({ paths: { vs: `${MONACO_CDN}/min/vs` } });
606
-
607
- const monaco = await new Promise(resolve => {
608
- require(['vs/editor/editor.main'], resolve);
609
- });
610
-
611
- // ========================================================================
612
- // Rip Monarch Grammar (Monaco-native syntax highlighting)
613
- // ========================================================================
614
-
615
- monaco.languages.register({ id: 'rip', extensions: ['.rip'] });
616
-
617
- monaco.languages.setMonarchTokensProvider('rip', {
618
- defaultToken: '',
619
- tokenPostfix: '.rip',
620
-
621
- keywords: [
622
- 'if', 'else', 'unless', 'then', 'switch', 'when', 'for', 'while', 'until',
623
- 'loop', 'do', 'return', 'break', 'continue', 'throw', 'try', 'catch', 'finally',
624
- 'yield', 'await', 'import', 'export', 'from', 'default', 'delete', 'typeof',
625
- 'instanceof', 'new', 'super', 'debugger', 'use', 'own', 'extends', 'in', 'of',
626
- 'by', 'as', 'class', 'def', 'enum', 'interface', 'component', 'render',
627
- ],
628
-
629
- logicalWords: ['and', 'or', 'not', 'is', 'isnt'],
630
- booleans: ['true', 'false', 'yes', 'no', 'on', 'off'],
631
- constants: ['null', 'undefined', 'NaN', 'Infinity', 'this'],
632
- typeKeywords: ['number', 'string', 'boolean', 'void', 'any', 'never', 'unknown', 'object', 'symbol', 'bigint'],
633
-
634
- operators: [
635
- '|>', '::=', '::', ':=', '~=', '~>', '<=>', '=!', '!?', '=~', '??', '?.', '...',
636
- '..', '**', '//', '%%', '++', '--', '&&', '||', '===', '!==', '==', '!=',
637
- '<=', '>=', '=>', '->', '+=', '-=', '*=', '/=', '%=', '**=', '&&=', '||=',
638
- '??=', '<<=', '>>=', '>>>=', '&=', '|=', '^=', '>>>', '>>', '<<',
639
- '+', '-', '*', '/', '%', '&', '|', '^', '~', '<', '>', '=', '!', '?',
640
- ],
641
-
642
- symbols: /[=><!~?:&|+\-*\/\^%\.#]+/,
643
- escapes: /\\(?:[abfnrtv\\"']|x[0-9A-Fa-f]{1,4}|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})/,
644
-
645
- tokenizer: {
646
- root: [
647
- // Block comments ###...###
648
- [/###/, 'comment', '@blockComment'],
649
-
650
- // Line comments
651
- [/#(?!\{).*$/, 'comment'],
652
-
653
- // Heredocs (before regular strings)
654
- [/"""/, 'string', '@heredocDouble'],
655
- [/'''/, 'string', '@heredocSingle'],
656
-
657
- // Strings
658
- [/"/, 'string', '@stringDouble'],
659
- [/'/, 'string', '@stringSingle'],
660
-
661
- // Heregex
662
- [/\/\/\//, 'regexp', '@heregex'],
663
-
664
- // Regex (after operator context)
665
- [/\/(?![/*])(?:[^[\/\n\\]|\\.|\[(?:\\[^\n]|[^\]\n\\])*\])*(\/)([gimsuy]*)/, 'regexp'],
666
-
667
- // Inline JavaScript
668
- [/`[^`]*`/, 'string.other'],
669
-
670
- // Instance variables @name
671
- [/@[a-zA-Z_$][\w$]*/, 'variable.instance'],
672
- [/@/, 'variable.instance'],
673
-
674
- // Type names (PascalCase) — before identifiers
675
- [/[A-Z][\w]*/, 'type.identifier'],
676
-
677
- // Identifiers and keywords
678
- [/[a-zA-Z_$][\w$]*[!?]?/, {
679
- cases: {
680
- '@keywords': 'keyword',
681
- '@logicalWords': 'keyword.operator',
682
- '@booleans': 'constant.language',
683
- '@constants': 'constant.language',
684
- '@typeKeywords': 'support.type',
685
- '@default': 'identifier',
686
- }
687
- }],
688
-
689
- // Numbers
690
- [/0x[0-9a-fA-F](?:_?[0-9a-fA-F])*n?/, 'number.hex'],
691
- [/0o[0-7](?:_?[0-7])*n?/, 'number.octal'],
692
- [/0b[01](?:_?[01])*n?/, 'number.binary'],
693
- [/\d[\d_]*(?:\.[\d][\d_]*)?(?:[eE][+-]?\d+)?n?/, 'number'],
694
-
695
- // Operators (multi-char first)
696
- [/::=/, 'keyword.operator.type'],
697
- [/::(?!=)/, 'keyword.operator.type'],
698
- [/~=|:=|~>|<=>/, 'keyword.operator.reactive'],
699
- [/=!/, 'keyword.operator.readonly'],
700
- [/!\?/, 'keyword.operator'],
701
- [/=~/, 'keyword.operator'],
702
- [/\?\?/, 'keyword.operator'],
703
- [/\?\./, 'keyword.operator'],
704
- [/\.\.\./, 'keyword.operator'],
705
- [/\.\./, 'keyword.operator'],
706
- [/===|!==|==|!=|<=|>=/, 'keyword.operator.comparison'],
707
- [/=>|->/, 'keyword.operator.arrow'],
708
- [/\*\*|\/\/|%%/, 'keyword.operator.arithmetic'],
709
- [/&&=|\|\|=|\?\?=|\+=|-=|\*=|\/=|%=/, 'keyword.operator.assignment'],
710
- [/&&|\|\|/, 'keyword.operator.logical'],
711
- [/>>>|>>|<</, 'keyword.operator.bitwise'],
712
- [/[+\-*\/%]/, 'keyword.operator.arithmetic'],
713
- [/[&|^~]/, 'keyword.operator.bitwise'],
714
- [/[<>]/, 'keyword.operator.comparison'],
715
- [/=/, 'keyword.operator.assignment'],
716
- [/!/, 'keyword.operator'],
717
- [/\?/, 'keyword.operator'],
718
-
719
- // Brackets
720
- [/[{}()\[\]]/, '@brackets'],
721
-
722
- // Delimiters
723
- [/[;,.]/, 'delimiter'],
724
- ],
725
-
726
- blockComment: [
727
- [/###/, 'comment', '@pop'],
728
- [/./, 'comment'],
729
- ],
730
-
731
- heredocDouble: [
732
- [/"""/, 'string', '@pop'],
733
- [/#\{/, { token: 'string.interpolation', next: '@interpolation' }],
734
- [/@escapes/, 'string.escape'],
735
- [/./, 'string'],
736
- ],
737
-
738
- heredocSingle: [
739
- [/'''/, 'string', '@pop'],
740
- [/@escapes/, 'string.escape'],
741
- [/./, 'string'],
742
- ],
743
-
744
- stringDouble: [
745
- [/"/, 'string', '@pop'],
746
- [/#\{/, { token: 'string.interpolation', next: '@interpolation' }],
747
- [/@escapes/, 'string.escape'],
748
- [/[^"\\#]+/, 'string'],
749
- [/./, 'string'],
750
- ],
751
-
752
- stringSingle: [
753
- [/'/, 'string', '@pop'],
754
- [/@escapes/, 'string.escape'],
755
- [/[^'\\]+/, 'string'],
756
- [/./, 'string'],
757
- ],
758
-
759
- interpolation: [
760
- [/\}/, { token: 'string.interpolation', next: '@pop' }],
761
- { include: 'root' },
762
- ],
763
-
764
- heregex: [
765
- [/\/\/\/[gimsuy]*/, 'regexp', '@pop'],
766
- [/#.*$/, 'comment'],
767
- [/@escapes/, 'regexp.escape'],
768
- [/#\{/, { token: 'string.interpolation', next: '@interpolation' }],
769
- [/./, 'regexp'],
770
- ],
771
- },
772
- });
773
-
774
- // Language configuration (brackets, comments, etc.)
775
- monaco.languages.setLanguageConfiguration('rip', {
776
- comments: { lineComment: '#', blockComment: ['###', '###'] },
777
- brackets: [['{', '}'], ['[', ']'], ['(', ')']],
778
- autoClosingPairs: [
779
- { open: '{', close: '}' },
780
- { open: '[', close: ']' },
781
- { open: '(', close: ')' },
782
- { open: '"', close: '"', notIn: ['string'] },
783
- { open: "'", close: "'", notIn: ['string'] },
784
- ],
785
- surroundingPairs: [
786
- { open: '{', close: '}' },
787
- { open: '[', close: ']' },
788
- { open: '(', close: ')' },
789
- { open: '"', close: '"' },
790
- { open: "'", close: "'" },
791
- ],
792
- indentationRules: {
793
- increaseIndentPattern: /^\s*(?:def|class|component|render|if|unless|else|for|while|until|loop|switch|when|try|catch|finally|enum|interface)\b.*$|[-=]>\s*$/,
794
- decreaseIndentPattern: /^\s*(else|catch|finally)\b/,
795
- },
796
- folding: { offSide: true },
797
- });
798
-
799
- // ========================================================================
800
- // Create Monaco Editor Instance
801
- // ========================================================================
802
-
803
- // Examples loaded from external examples.rip
804
- const examples = globalThis.ripExamples;
805
- const defaultCode = examples.demo;
806
-
807
- // Restore saved source (or default)
808
- const savedSource = localStorage.getItem('rip-playground-source');
809
- const savedExample = localStorage.getItem('rip-playground-example') || 'demo';
810
- const initialCode = savedSource != null ? savedSource : defaultCode;
811
-
812
- // Shared editor configuration
813
- const sharedConfig = {
814
- theme: 'vs-dark',
815
- fontSize: 14,
816
- fontFamily: "'Monaco', 'Menlo', 'Consolas', monospace",
817
- lineHeight: 22,
818
- minimap: { enabled: false },
819
- scrollBeyondLastLine: false,
820
- automaticLayout: true,
821
- overviewRulerLanes: 0,
822
- hideCursorInOverviewRuler: true,
823
- renderWhitespace: 'none',
824
- scrollbar: { verticalScrollbarSize: 10, horizontalScrollbarSize: 10, verticalSliderSize: 10, horizontalSliderSize: 10 },
825
- padding: { top: 10 },
826
- };
827
-
828
- const monacoEditor = monaco.editor.create(document.getElementById('editor-container'), {
829
- ...sharedConfig,
830
- value: initialCode,
831
- language: 'rip',
832
- tabSize: 2,
833
- insertSpaces: true,
834
- });
835
-
836
- // Output editor (read-only JavaScript display)
837
- const outputEditor = monaco.editor.create(document.getElementById('output-container'), {
838
- ...sharedConfig,
839
- value: '',
840
- language: 'javascript',
841
- readOnly: true,
842
- domReadOnly: true,
843
- lineNumbers: 'off',
844
- folding: false,
845
- glyphMargin: false,
846
- contextmenu: false,
847
- });
848
-
849
- // Theme selector (dropdown) with lazy CDN loading
850
- const themeSelect = document.getElementById('theme-select');
851
- const THEME_CDN = 'https://cdn.jsdelivr.net/npm/monaco-themes@0.4.4/themes';
852
- const loadedThemes = new Set(['vs', 'vs-dark', 'hc-black', 'hc-light']);
853
-
854
- async function setTheme(value) {
855
- let themeId = value;
856
-
857
- if (value.startsWith('cdn:')) {
858
- const themeName = value.slice(4);
859
- themeId = themeName.toLowerCase().replace(/[\s_]+/g, '-');
860
-
861
- if (!loadedThemes.has(themeId)) {
862
- try {
863
- const res = await fetch(`${THEME_CDN}/${encodeURIComponent(themeName)}.json`);
864
- const data = await res.json();
865
- monaco.editor.defineTheme(themeId, data);
866
- loadedThemes.add(themeId);
867
- } catch (err) {
868
- console.error(`Failed to load theme: ${themeName}`, err);
869
- return;
870
- }
871
- }
872
- }
873
-
874
- monaco.editor.setTheme(themeId);
875
- localStorage.setItem('rip-repl-theme', value);
876
- }
877
-
878
- themeSelect.addEventListener('change', async (e) => await setTheme(e.target.value));
879
-
880
- // Light/dark mode toggle (persisted via localStorage)
881
- const savedMode = localStorage.getItem('rip-repl-mode');
882
- let isDark = savedMode ? savedMode !== 'light' : !window.matchMedia('(prefers-color-scheme: light)').matches;
883
- const modeToggle = document.getElementById('mode-toggle');
884
-
885
- function applyMode() {
886
- document.body.classList.toggle('light', !isDark);
887
- modeToggle.innerHTML = isDark ? '&#9790;' : '&#9788;';
888
- modeToggle.title = isDark ? 'Switch to light mode' : 'Switch to dark mode';
889
- localStorage.setItem('rip-repl-mode', isDark ? 'dark' : 'light');
890
- }
891
-
892
- applyMode();
893
-
894
- modeToggle.addEventListener('click', () => {
895
- isDark = !isDark;
896
- applyMode();
897
- // Only reset theme if currently using a built-in default
898
- const current = themeSelect.value;
899
- if (current === 'vs' || current === 'vs-dark') {
900
- const defaultTheme = isDark ? 'vs-dark' : 'vs';
901
- themeSelect.value = defaultTheme;
902
- setTheme(defaultTheme);
903
- }
904
- });
905
-
906
- // ========================================================================
907
- // Load Rip Compiler + Examples
908
- // ========================================================================
909
-
910
- // Compiler already loaded via <script type="module" src="dist/rip.browser.min.js">
911
- const { compile, formatSExpr, VERSION, BUILD_DATE } = window.__ripExports;
912
-
913
- window.compile = compile;
914
- window.toSexpr = formatSExpr;
915
-
916
- const localBuildDate = new Date(BUILD_DATE.replace('@', 'T').replace('GMT', 'Z'))
917
- .toLocaleString(undefined, {
918
- year: 'numeric', month: 'short', day: 'numeric',
919
- hour: 'numeric', minute: '2-digit', timeZoneName: 'short'
920
- });
921
-
922
- document.querySelector('.subtitle').textContent =
923
- `Interactive environment • Version ${VERSION} • Built ${localBuildDate}`;
924
-
925
- // Reveal page immediately — layout is ready, editors will populate shortly
926
- document.body.classList.add('ready');
927
-
928
- // Wait for external examples.rip to be processed
929
- await globalThis.__ripScriptsReady;
930
-
931
- // ========================================================================
932
- // DOM Element References
933
- // ========================================================================
934
-
935
- const showTokensBtn = document.getElementById('show-tokens');
936
- const showSexpBtn = document.getElementById('show-sexp');
937
- const showPreambleBtn = document.getElementById('show-preamble');
938
- const showTypesBtn = document.getElementById('show-types');
939
- const showJSBtn = document.getElementById('show-js');
940
- const replOutput = document.getElementById('repl-output');
941
- const promptSpan = document.getElementById('prompt');
942
-
943
- // REPL input editor (single-line Monaco)
944
- const replEditor = monaco.editor.create(document.getElementById('repl-editor-container'), {
945
- value: '',
946
- language: 'rip',
947
- theme: isDark ? 'vs-dark' : 'vs',
948
- fontSize: 14,
949
- fontFamily: "'Monaco', 'Menlo', 'Consolas', monospace",
950
- lineHeight: 24,
951
- minimap: { enabled: false },
952
- scrollBeyondLastLine: false,
953
- automaticLayout: true,
954
- lineNumbers: 'off',
955
- glyphMargin: false,
956
- folding: false,
957
- lineDecorationsWidth: 6,
958
- lineNumbersMinChars: 0,
959
- overviewRulerLanes: 0,
960
- hideCursorInOverviewRuler: true,
961
- scrollbar: { vertical: 'hidden', horizontal: 'hidden', handleMouseWheel: false, verticalScrollbarSize: 0, horizontalScrollbarSize: 0 },
962
- overviewRulerBorder: false,
963
- renderLineHighlight: 'none',
964
- wordWrap: 'off',
965
- contextmenu: false,
966
- tabSize: 2,
967
- insertSpaces: true,
968
- padding: { top: 0, bottom: 0 },
969
- quickSuggestions: false,
970
- suggestOnTriggerCharacters: false,
971
- acceptSuggestionOnEnter: 'off',
972
- tabCompletion: 'off',
973
- parameterHints: { enabled: false },
974
- find: { addExtraSpaceOnTop: false, autoFindInSelection: 'never' },
975
- });
976
- const resizer = document.getElementById('resizer');
977
- const leftPane = document.getElementById('editor-left');
978
- const rightPane = document.getElementById('editor-right');
979
-
980
- // ========================================================================
981
- // Compiler Implementation
982
- // ========================================================================
983
-
984
- let showTokens = localStorage.getItem('rip-repl-tokens') === 'true';
985
- let showSexp = localStorage.getItem('rip-repl-sexp') === 'true';
986
- let showPreamble = localStorage.getItem('rip-preamble') === 'true';
987
- let showTypes = localStorage.getItem('rip-types') === 'true';
988
- let showJS = localStorage.getItem('rip-repl-js') !== 'false'; // default true
989
-
990
- // Format tokens for display — clean, minimal output
991
- const SUPPRESS_VALUE = new Set([
992
- 'DEF', 'IF', 'ELSE', 'UNLESS', 'FOR', 'WHILE', 'UNTIL', 'LOOP',
993
- 'SWITCH', 'WHEN', 'TRY', 'CATCH', 'FINALLY', 'THROW', 'RETURN',
994
- 'CLASS', 'EXTENDS', 'SUPER', 'NEW', 'DELETE', 'TYPEOF', 'INSTANCEOF',
995
- 'IMPORT', 'FROM', 'EXPORT', 'DEFAULT', 'ENUM', 'INTERFACE',
996
- 'AND', 'OR', 'NOT', 'IS', 'ISNT', 'IN', 'OF',
997
- 'TRUE', 'FALSE', 'NULL', 'UNDEFINED', 'YES', 'NO',
998
- 'CALL_START', 'CALL_END', 'PARAM_START', 'PARAM_END',
999
- 'INDEX_START', 'INDEX_END', 'TERMINATOR',
1000
- ]);
1001
-
1002
- function formatTokens(tokens) {
1003
- let maxTag = 0;
1004
- for (const t of tokens) maxTag = Math.max(maxTag, t[0].length);
1005
- return tokens.map(t => {
1006
- let tag = t[0], val = t[1];
1007
- if (SUPPRESS_VALUE.has(tag)) return tag;
1008
- if (tag === val || tag.toLowerCase() === val) return tag;
1009
- if (tag === 'INDENT' || tag === 'OUTDENT') return `${tag.padEnd(maxTag)} ${val}`;
1010
- if (tag === 'STRING' || tag === 'REGEX') return `${tag.padEnd(maxTag)} ${val}`;
1011
- return `${tag.padEnd(maxTag)} ${JSON.stringify(val)}`;
1012
- }).join('\n');
1013
- }
1014
-
1015
- function compileCode() {
1016
- try {
1017
- const source = monacoEditor.getValue();
1018
- const opts = {};
1019
- if (!showPreamble) opts.skipPreamble = true;
1020
- if (showTypes) opts.types = 'emit';
1021
- const result = compile(source, opts);
1022
-
1023
- let parts = [];
1024
-
1025
- if (showTokens) {
1026
- parts.push(formatTokens(result.tokens));
1027
- }
1028
-
1029
- if (showSexp) {
1030
- parts.push(window.toSexpr ? window.toSexpr(result.sexpr, 0, true) : JSON.stringify(result.sexpr, null, 1));
1031
- }
1032
-
1033
- if (showTypes && result.dts) {
1034
- parts.push('// .d.ts\n' + result.dts.trim());
1035
- }
1036
-
1037
- if (showJS) {
1038
- parts.push(result.code);
1039
- }
1040
-
1041
- const outputText = parts.join('\n\n');
1042
-
1043
- // Update output editor language and content
1044
- const model = outputEditor.getModel();
1045
- let lang = (showTokens || showSexp) ? 'plaintext' : (showTypes && !showJS) ? 'typescript' : 'javascript';
1046
- monaco.editor.setModelLanguage(model, lang);
1047
- outputEditor.setValue(outputText);
1048
- } catch (error) {
1049
- const model = outputEditor.getModel();
1050
- monaco.editor.setModelLanguage(model, 'plaintext');
1051
- outputEditor.setValue(`Error: ${error.message}`);
1052
- }
1053
- }
1054
-
1055
- // ========================================================================
1056
- // Tab Switching
1057
- // ========================================================================
1058
-
1059
- async function switchToTab(tabName) {
1060
- // Update URL hash
1061
- window.location.hash = tabName;
1062
-
1063
- // Update active tab
1064
- document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
1065
- document.querySelector(`.tab[data-tab="${tabName}"]`).classList.add('active');
1066
-
1067
- // Update active pane
1068
- document.querySelectorAll('.pane').forEach(p => p.classList.remove('active'));
1069
- document.getElementById(`${tabName}-pane`).classList.add('active');
1070
-
1071
- // Focus appropriate input and run setup
1072
- if (tabName === 'repl') {
1073
- // REPL uses simple light/dark based on page mode
1074
- monaco.editor.setTheme(isDark ? 'vs-dark' : 'vs');
1075
- replEditor.focus();
1076
- } else if (tabName === 'compiler') {
1077
- // Compiler restores user's selected theme
1078
- await setTheme(themeSelect.value);
1079
- compileCode();
1080
- }
1081
- }
1082
-
1083
- document.querySelectorAll('.tab').forEach(tab => {
1084
- tab.addEventListener('click', () => switchToTab(tab.dataset.tab));
1085
- });
1086
-
1087
- // Handle browser back/forward
1088
- window.addEventListener('hashchange', () => {
1089
- const hash = window.location.hash.slice(1);
1090
- if (['compiler', 'repl'].includes(hash)) {
1091
- switchToTab(hash);
1092
- }
1093
- });
1094
-
1095
- // Restore toggle button states from localStorage (before page reveal)
1096
- showTokensBtn.classList.toggle('active', showTokens);
1097
- showSexpBtn.classList.toggle('active', showSexp);
1098
- showPreambleBtn.classList.toggle('active', showPreamble);
1099
- showTypesBtn.classList.toggle('active', showTypes);
1100
- showJSBtn.classList.toggle('active', showJS);
1101
-
1102
- // Initialize from URL hash on page load (default to compiler)
1103
- const initialTab = window.location.hash.slice(1);
1104
- if (['compiler', 'repl'].includes(initialTab)) {
1105
- await switchToTab(initialTab);
1106
- } else {
1107
- // No hash - compiler is default
1108
- compileCode();
1109
- }
1110
-
1111
- // Restore saved theme (after all editors are created and initialized)
1112
- const savedTheme = localStorage.getItem('rip-repl-theme');
1113
- if (savedTheme) {
1114
- themeSelect.value = savedTheme;
1115
- await setTheme(savedTheme);
1116
- } else {
1117
- await setTheme(isDark ? 'vs-dark' : 'vs');
1118
- }
1119
-
1120
- // ========================================================================
1121
- // Resizable Panes (Live Compiler)
1122
- // ========================================================================
1123
-
1124
- if (resizer && leftPane && rightPane) {
1125
- let isResizing = false;
1126
-
1127
- resizer.addEventListener('mousedown', (e) => {
1128
- isResizing = true;
1129
- document.body.style.cursor = 'col-resize';
1130
- e.preventDefault();
1131
- });
1132
-
1133
- document.addEventListener('mousemove', (e) => {
1134
- if (!isResizing) return;
1135
-
1136
- const container = document.querySelector('.compiler-container');
1137
- const containerRect = container.getBoundingClientRect();
1138
- const offsetX = e.clientX - containerRect.left;
1139
- const containerWidth = containerRect.width;
1140
-
1141
- // Calculate percentage (20% min, 80% max for each side)
1142
- let leftPercent = (offsetX / containerWidth) * 100;
1143
- leftPercent = Math.max(20, Math.min(80, leftPercent));
1144
-
1145
- leftPane.style.flexBasis = `${leftPercent}%`;
1146
- rightPane.style.flexBasis = `${100 - leftPercent}%`;
1147
- });
1148
-
1149
- document.addEventListener('mouseup', () => {
1150
- if (isResizing) {
1151
- isResizing = false;
1152
- document.body.style.cursor = 'default';
1153
- }
1154
- });
1155
- }
1156
-
1157
- // ========================================================================
1158
- // REPL Implementation
1159
- // ========================================================================
1160
-
1161
- let replHistory = [];
1162
- let historyIndex = -1;
1163
- let replBuffer = '';
1164
- let reactiveVars = new Set(); // Track reactive variables across evaluations
1165
-
1166
- // Create isolated iframe context for REPL (like vm.createContext in Node)
1167
- const iframe = document.createElement('iframe');
1168
- iframe.style.display = 'none';
1169
- document.body.appendChild(iframe);
1170
- const replContext = iframe.contentWindow;
1171
-
1172
- // Add builtins to iframe context
1173
- replContext.console = console;
1174
- replContext.showSexp = false;
1175
- replContext.showTokens = false;
1176
- replContext.__reactiveVars = {}; // Store reactive objects persistently
1177
-
1178
- // Inject reactive preamble into iframe context
1179
- (function injectReactivePreamble() {
1180
- const ctx = replContext;
1181
- ctx.__currentEffect = null;
1182
- ctx.__pendingEffects = new Set();
1183
-
1184
- ctx.__state = function(v) {
1185
- const subs = new Set();
1186
- let notifying = false, locked = false, dead = false;
1187
- const s = {
1188
- get value() { if (dead) return v; if (ctx.__currentEffect) { subs.add(ctx.__currentEffect); ctx.__currentEffect.dependencies.add(subs); } return v; },
1189
- set value(n) {
1190
- if (dead || locked || n === v || notifying) return;
1191
- v = n;
1192
- notifying = true;
1193
- for (const sub of subs) if (sub.markDirty) sub.markDirty();
1194
- for (const sub of subs) if (!sub.markDirty) ctx.__pendingEffects.add(sub);
1195
- const fx = [...ctx.__pendingEffects]; ctx.__pendingEffects.clear();
1196
- for (const e of fx) e.run();
1197
- notifying = false;
1198
- },
1199
- read() { return v; },
1200
- lock() { locked = true; return s; },
1201
- free() { subs.clear(); return s; },
1202
- kill() { dead = true; subs.clear(); return v; },
1203
- valueOf() { return this.value; },
1204
- toString() { return String(this.value); },
1205
- [Symbol.toPrimitive](hint) { return hint === 'string' ? this.toString() : this.valueOf(); }
1206
- };
1207
- return s;
1208
- };
1209
-
1210
- ctx.__computed = function(fn) {
1211
- let v, dirty = true, locked = false, dead = false;
1212
- const subs = new Set();
1213
- const c = {
1214
- dependencies: new Set(),
1215
- markDirty() {
1216
- if (dead || locked || dirty) return;
1217
- dirty = true;
1218
- for (const s of subs) if (s.markDirty) s.markDirty();
1219
- for (const s of subs) if (!s.markDirty) ctx.__pendingEffects.add(s);
1220
- },
1221
- get value() {
1222
- if (dead) return v;
1223
- if (ctx.__currentEffect) { subs.add(ctx.__currentEffect); ctx.__currentEffect.dependencies.add(subs); }
1224
- if (dirty && !locked) {
1225
- for (const d of c.dependencies) d.delete(c); c.dependencies.clear();
1226
- const prev = ctx.__currentEffect; ctx.__currentEffect = c;
1227
- try { v = fn(); } finally { ctx.__currentEffect = prev; }
1228
- dirty = false;
1229
- }
1230
- return v;
1231
- },
1232
- read() { return dead ? v : c.value; },
1233
- lock() { locked = true; c.value; return c; },
1234
- free() { for (const d of c.dependencies) d.delete(c); c.dependencies.clear(); subs.clear(); return c; },
1235
- kill() { dead = true; const result = v; c.free(); return result; },
1236
- valueOf() { return this.value; },
1237
- toString() { return String(this.value); },
1238
- [Symbol.toPrimitive](hint) { return hint === 'string' ? this.toString() : this.valueOf(); }
1239
- };
1240
- return c;
1241
- };
1242
-
1243
- ctx.__effect = function(fn) {
1244
- const e = {
1245
- dependencies: new Set(),
1246
- run() {
1247
- for (const d of e.dependencies) d.delete(e); e.dependencies.clear();
1248
- const prev = ctx.__currentEffect; ctx.__currentEffect = e;
1249
- try { fn(); } finally { ctx.__currentEffect = prev; }
1250
- },
1251
- free() { for (const d of e.dependencies) d.delete(e); e.dependencies.clear(); }
1252
- };
1253
- e.run();
1254
- return () => e.free();
1255
- };
1256
-
1257
- ctx.__batch = function(fn) { fn(); };
1258
- ctx.__readonly = function(v) { return Object.freeze({ value: v }); };
1259
- })();
1260
-
1261
- function addOutput(content, className = '') {
1262
- const line = document.createElement('div');
1263
- line.className = `repl-line ${className}`;
1264
- line.innerHTML = content;
1265
- replOutput.appendChild(line);
1266
- replOutput.scrollTop = replOutput.scrollHeight;
1267
- }
1268
-
1269
- function evaluateRip(code) {
1270
- try {
1271
- // Pass reactiveVars to compiler so it knows which vars need .value access
1272
- const result = compile(code, { reactiveVars, skipPreamble: true });
1273
- let js = result.code;
1274
-
1275
- // Track new reactive variables
1276
- if (result.reactiveVars) {
1277
- for (const v of result.reactiveVars) {
1278
- reactiveVars.add(v);
1279
- }
1280
- }
1281
-
1282
- // REPL strategy: Strip let/const declarations entirely
1283
- // Assignments will create properties on iframe's window (global scope)
1284
- // This allows variables to persist between eval() calls
1285
-
1286
- // Remove: let x, y, z;\n
1287
- js = js.replace(/^let\s+[^;]+;\s*\n+/m, '');
1288
-
1289
- // Transform reactive declarations to persist in __reactiveVars
1290
- // const x = __state(...) → x = __reactiveVars.x ?? (__reactiveVars.x = __state(...))
1291
- js = js.replace(
1292
- /^const\s+(\w+)\s*=\s*((?:__state|__computed|__effect)\(.+\));?$/gm,
1293
- (match, varName, rhs) => {
1294
- return `${varName} = __reactiveVars['${varName}'] ?? (__reactiveVars['${varName}'] = ${rhs});`;
1295
- }
1296
- );
1297
-
1298
- // Remove remaining const (but keep the assignment) for non-reactive
1299
- js = js.replace(/^const\s+(\w+)\s*=/gm, '$1 =');
1300
-
1301
- // Evaluate in iframe context - assignments create globals
1302
- const evalResult = replContext.eval(js);
1303
-
1304
- // Store in _ (unwrap reactive values)
1305
- if (evalResult !== undefined) {
1306
- replContext._ = evalResult;
1307
- }
1308
-
1309
- return { success: true, value: evalResult, result };
1310
- } catch (error) {
1311
- return { success: false, error: error.message };
1312
- }
1313
- }
1314
-
1315
- // Built-in properties to preserve when clearing REPL context
1316
- const replBuiltins = new Set([
1317
- 'console', 'showSexp', 'showTokens', 'eval', 'window', 'document',
1318
- 'location', 'navigator', 'self', 'top', 'parent', 'frames',
1319
- '__state', '__computed', '__effect', '__batch', '__readonly',
1320
- '__currentEffect', '__pendingEffects', '__reactiveVars',
1321
- '__rip', '__flushEffects', '__primitiveCoercion',
1322
- '__setErrorHandler', '__handleError', '__catchErrors', '__errorHandler',
1323
- '__batching',
1324
- ]);
1325
-
1326
- function handleCommand(cmd) {
1327
- switch (cmd) {
1328
- case '.help':
1329
- addOutput(`
1330
- <span class="help-text">Rip Playground Commands:</span>
1331
-
1332
- <span class="command">.help</span> - Show this help
1333
- <span class="command">.clear</span> - Clear output
1334
- <span class="command">.vars</span> - Show variables
1335
- <span class="command">.sexp</span> - Toggle s-expression display
1336
- <span class="command">.tokens</span> - Toggle token display
1337
-
1338
- <span class="help-text">Features:</span>
1339
- - Variables persist across evaluations
1340
- - Previous result in <span class="var-name">_</span> variable
1341
- - Shift+Enter for multi-line input
1342
- - All Rip features: heregex, regex+, classes, etc.
1343
- `, 'command-output');
1344
- break;
1345
-
1346
- case '.clear':
1347
- replOutput.innerHTML = '';
1348
- // Reset reactive tracking
1349
- reactiveVars.clear();
1350
- replContext.__reactiveVars = {};
1351
- // Clear all user-defined globals in iframe
1352
- for (const key of Object.keys(replContext)) {
1353
- if (!replBuiltins.has(key) && !key.startsWith('__')) {
1354
- try { delete replContext[key]; } catch {}
1355
- }
1356
- }
1357
- addOutput('<div class="welcome">Output and context cleared.</div>');
1358
- break;
1359
-
1360
- case '.vars':
1361
- const vars = Object.keys(replContext).filter(k =>
1362
- !replBuiltins.has(k) && !k.startsWith('__') || k === '_'
1363
- );
1364
- if (vars.length === 0 || (vars.length === 1 && vars[0] === '_' && replContext._ === undefined)) {
1365
- addOutput('<span class="help-text">No variables defined</span>', 'command-output');
1366
- } else {
1367
- let output = '<span class="help-text">Variables:</span>\\n';
1368
- vars.forEach(v => {
1369
- try {
1370
- let val = replContext[v];
1371
- let typeIndicator = '=';
1372
- // Check if it's a reactive value
1373
- if (val && typeof val === 'object' && 'value' in val) {
1374
- if (typeof val.markDirty === 'function') {
1375
- typeIndicator = '<span style="color:#c586c0">~=</span>'; // computed
1376
- } else if (typeof val === 'function') {
1377
- typeIndicator = '<span style="color:#c586c0">~&gt;</span>'; // effect
1378
- } else {
1379
- typeIndicator = '<span style="color:#c586c0">:=</span>'; // state
1380
- }
1381
- val = val.value;
1382
- }
1383
- const formatted = formatValue(val);
1384
- output += ` <span class="var-name">${v}</span> ${typeIndicator} <span class="var-value">${escapeHtml(formatted)}</span>\\n`;
1385
- } catch {
1386
- output += ` <span class="var-name">${v}</span> = <span class="var-value">[object]</span>\\n`;
1387
- }
1388
- });
1389
- addOutput(output, 'command-output');
1390
- }
1391
- break;
1392
-
1393
- case '.sexp':
1394
- replContext.showSexp = !replContext.showSexp;
1395
- addOutput(`S-expression display: ${replContext.showSexp ? 'ON' : 'OFF'}`, 'command-output');
1396
- break;
1397
-
1398
- case '.tokens':
1399
- replContext.showTokens = !replContext.showTokens;
1400
- addOutput(`Token display: ${replContext.showTokens ? 'ON' : 'OFF'}`, 'command-output');
1401
- break;
1402
-
1403
- default:
1404
- addOutput(`Unknown command: ${cmd}`, 'error');
1405
- addOutput('Type .help for available commands', 'help-text');
1406
- }
1407
- }
1408
-
1409
- function handleLine(line) {
1410
- // Add to history
1411
- if (line.trim() && (replHistory.length === 0 || replHistory[replHistory.length - 1] !== line)) {
1412
- replHistory.push(line);
1413
- }
1414
- historyIndex = replHistory.length;
1415
-
1416
- // Show input with syntax highlighting
1417
- const promptText = replBuffer ? '....>' : 'rip>';
1418
- const codeId = `repl-code-${Date.now()}`;
1419
- addOutput(`<span class="prompt">${promptText}</span> <span class="repl-code" id="${codeId}">${escapeHtml(line)}</span>`);
1420
- monaco.editor.colorize(line, 'rip', {}).then(highlighted => {
1421
- const el = document.getElementById(codeId);
1422
- if (el) el.innerHTML = highlighted;
1423
- });
1424
-
1425
- // Handle commands
1426
- if (line.startsWith('.')) {
1427
- handleCommand(line);
1428
- return;
1429
- }
1430
-
1431
- // Add to buffer
1432
- replBuffer = replBuffer ? replBuffer + '\n' + line : line;
1433
-
1434
- // Try to evaluate
1435
- const evalResult = evaluateRip(replBuffer);
1436
-
1437
- if (evalResult.success) {
1438
- // Show debug info if enabled
1439
- if (replContext.showTokens) {
1440
- addOutput(formatTokens(evalResult.result.tokens).replace(/\n/g, '<br>'), 'command-output');
1441
- }
1442
-
1443
- if (replContext.showSexp) {
1444
- const sexp = window.toSexpr ? window.toSexpr(evalResult.result.sexpr, 0, true) : JSON.stringify(evalResult.result.sexpr, null, 1);
1445
- const formatted = sexp.replace(/\n/g, '<br>').replace(/ /g, '&nbsp;');
1446
- addOutput(`${formatted}`, 'command-output');
1447
- }
1448
-
1449
- // Show result (unwrap reactive values)
1450
- if (evalResult.value !== undefined) {
1451
- let displayValue = evalResult.value;
1452
- // Unwrap reactive objects to show their .value
1453
- if (displayValue && typeof displayValue === 'object' && 'value' in displayValue) {
1454
- displayValue = displayValue.value;
1455
- }
1456
- const formatted = formatValue(displayValue);
1457
- addOutput(`<span class="result">→ ${escapeHtml(formatted)}</span>`);
1458
- }
1459
-
1460
- replBuffer = '';
1461
- promptSpan.textContent = 'rip>';
1462
- } else if (evalResult.error.includes('Unexpected end')) {
1463
- // Incomplete - wait for more input
1464
- promptSpan.textContent = '....>';
1465
- } else {
1466
- // Real error
1467
- addOutput(`<span class="error">✗ ${escapeHtml(evalResult.error)}</span>`);
1468
- replBuffer = '';
1469
- promptSpan.textContent = 'rip>';
1470
- }
1471
- }
1472
-
1473
- function escapeHtml(str) {
1474
- return String(str)
1475
- .replace(/&/g, '&amp;')
1476
- .replace(/</g, '&lt;')
1477
- .replace(/>/g, '&gt;')
1478
- .replace(/"/g, '&quot;');
1479
- }
1480
-
1481
- // Format values for display (like Node's util.inspect)
1482
- function formatValue(val) {
1483
- if (val === null) return 'null';
1484
- if (val === undefined) return 'undefined';
1485
- if (val instanceof RegExp) return val.toString();
1486
- if (typeof val === 'function') return val.toString();
1487
- if (typeof val === 'string') return JSON.stringify(val);
1488
- if (typeof val === 'number' || typeof val === 'boolean') return String(val);
1489
- if (Array.isArray(val)) {
1490
- try { return JSON.stringify(val); } catch { return '[Array]'; }
1491
- }
1492
- if (typeof val === 'object') {
1493
- try { return JSON.stringify(val, null, 2); } catch { return '[Object]'; }
1494
- }
1495
- return String(val);
1496
- }
1497
-
1498
- // Auto-resize REPL input as lines are added
1499
- const replEditorEl = document.getElementById('repl-editor-container');
1500
- const replLineHeight = 24;
1501
-
1502
- function resizeReplInput() {
1503
- const lineCount = replEditor.getModel().getLineCount();
1504
- const newHeight = Math.max(replLineHeight, lineCount * replLineHeight);
1505
- replEditorEl.style.height = newHeight + 'px';
1506
- replEditor.layout();
1507
- }
1508
-
1509
- replEditor.onDidChangeModelContent(() => resizeReplInput());
1510
-
1511
- // REPL input handling (Monaco keybindings)
1512
- replEditor.onKeyDown((e) => {
1513
- if (e.keyCode === monaco.KeyCode.Enter && !e.shiftKey) {
1514
- e.preventDefault();
1515
- e.stopPropagation();
1516
- const line = replEditor.getValue();
1517
- replEditor.setValue('');
1518
- resizeReplInput();
1519
- if (line.trim()) handleLine(line);
1520
- } else if (e.keyCode === monaco.KeyCode.Enter && e.shiftKey) {
1521
- // Allow Shift+Enter to insert newline (Monaco default)
1522
- return;
1523
- } else if (e.keyCode === monaco.KeyCode.UpArrow) {
1524
- e.preventDefault();
1525
- e.stopPropagation();
1526
- if (historyIndex > 0) {
1527
- historyIndex--;
1528
- replEditor.setValue(replHistory[historyIndex] || '');
1529
- const model = replEditor.getModel();
1530
- const lastCol = model.getLineMaxColumn(1);
1531
- replEditor.setPosition({ lineNumber: 1, column: lastCol });
1532
- }
1533
- } else if (e.keyCode === monaco.KeyCode.DownArrow) {
1534
- e.preventDefault();
1535
- e.stopPropagation();
1536
- if (historyIndex < replHistory.length) {
1537
- historyIndex++;
1538
- replEditor.setValue(replHistory[historyIndex] || '');
1539
- const model = replEditor.getModel();
1540
- const lastCol = model.getLineMaxColumn(1);
1541
- replEditor.setPosition({ lineNumber: 1, column: lastCol });
1542
- }
1543
- }
1544
- });
1545
-
1546
- // ========================================================================
1547
- // Compiler Event Handlers
1548
- // ========================================================================
1549
-
1550
- // ========================================================================
1551
- // Source Persistence & Example Snippets
1552
- // ========================================================================
1553
-
1554
- const exampleSelect = document.getElementById('example-select');
1555
- exampleSelect.value = savedExample;
1556
- let settingExample = false;
1557
- let saveTimer = null;
1558
-
1559
- function saveSource() {
1560
- if (saveTimer) clearTimeout(saveTimer);
1561
- saveTimer = setTimeout(() => {
1562
- localStorage.setItem('rip-playground-source', monacoEditor.getValue());
1563
- }, 2000);
1564
- }
1565
-
1566
- // Auto-compile on change + persist + track example
1567
- monacoEditor.onDidChangeModelContent(() => {
1568
- compileCode();
1569
- saveSource();
1570
- if (!settingExample && exampleSelect.value !== 'custom') {
1571
- exampleSelect.value = 'custom';
1572
- localStorage.setItem('rip-playground-example', 'custom');
1573
- }
1574
- });
1575
-
1576
- // Example dropdown handler
1577
- exampleSelect.addEventListener('change', (e) => {
1578
- const name = e.target.value;
1579
- if (name === 'custom') {
1580
- localStorage.setItem('rip-playground-example', 'custom');
1581
- } else if (examples[name]) {
1582
- settingExample = true;
1583
- monacoEditor.setValue(examples[name]);
1584
- settingExample = false;
1585
- localStorage.setItem('rip-playground-example', name);
1586
- localStorage.setItem('rip-playground-source', examples[name]);
1587
- }
1588
- });
1589
-
1590
- const toggles = [
1591
- { btn: showTokensBtn, key: 'rip-repl-tokens', get: () => showTokens, set: v => showTokens = v },
1592
- { btn: showSexpBtn, key: 'rip-repl-sexp', get: () => showSexp, set: v => showSexp = v },
1593
- { btn: showPreambleBtn, key: 'rip-preamble', get: () => showPreamble, set: v => showPreamble = v },
1594
- { btn: showTypesBtn, key: 'rip-types', get: () => showTypes, set: v => showTypes = v },
1595
- { btn: showJSBtn, key: 'rip-repl-js', get: () => showJS, set: v => showJS = v },
1596
- ];
1597
-
1598
- for (const { btn, key, get, set } of toggles) {
1599
- btn.addEventListener('click', () => {
1600
- set(!get());
1601
- btn.classList.toggle('active', get());
1602
- localStorage.setItem(key, get());
1603
- compileCode();
1604
- });
1605
- }
1606
-
1607
- // Clear button - clears the Rip source editor
1608
- document.getElementById('clear-btn').addEventListener('click', () => {
1609
- monacoEditor.setValue('');
1610
- compileCode();
1611
- monacoEditor.focus();
1612
- });
1613
-
1614
- // Run button - executes the Rip code and outputs to console
1615
- async function runCode() {
1616
- try {
1617
- const source = monacoEditor.getValue();
1618
- const result = compile(source);
1619
- const js = result.code;
1620
-
1621
- // Execute the compiled JavaScript and log to console
1622
- console.clear();
1623
- console.log('%c=== Rip Code Execution ===', 'color: #007acc; font-weight: bold');
1624
- await (0, eval)(`(async()=>{\n${js}\n})()`);
1625
- console.log('%c=== Execution Complete ===', 'color: #4ec9b0; font-weight: bold');
1626
- } catch (error) {
1627
- console.error('Execution Error:', error);
1628
- }
1629
- }
1630
-
1631
- document.getElementById('run-btn').addEventListener('click', runCode);
1632
-
1633
- // Keyboard shortcuts: Cmd+Enter (Mac), Ctrl+Enter (Windows/Linux)
1634
- monacoEditor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, runCode);
1635
-
1636
- // F5 runs code from anywhere on the page
1637
- document.addEventListener('keydown', (e) => {
1638
- if (e.key === 'F5') {
1639
- e.preventDefault();
1640
- runCode();
1641
- }
1642
- });
1643
- </script>
1644
- </body>
1645
- </html>