AtlatestRepositorysigil-site

sigil-site / tree / assets / replindex.html

1<!DOCTYPE html>
2<html>
3<head>
4 <meta charset="utf-8">
5 <title>Sigil Web REPL</title>
6 <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/css/xterm.css">
7 <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap">
8 <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&display=swap">
9 <style>
10 :root {
11 --bg: #0a0a0a;
12 --bg-secondary: #141414;
13 --fg: #e0e0e0;
14 --fg-muted: #888888;
15 --accent: #f0c040;
16 --accent-dim: #b89830;
17 --code-bg: #1a1a1a;
18 --border: #2a2a2a;
19 --font-sans: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
20 --font-mono: 'JetBrains Mono', 'SF Mono', Consolas, monospace;
21 }
22 * {
23 box-sizing: border-box;
24 }
25 html, body {
26 margin: 0;
27 padding: 0;
28 background: var(--bg);
29 color: var(--fg);
30 font-family: var(--font-sans);
31 font-size: 16px;
32 height: 100%;
33 overflow: hidden;
34 }
35 #main-container {
36 display: flex;
37 gap: 20px;
38 height: calc(100% - 57px); /* Account for header */
39 padding: 20px;
40 }
41 #editor-panel {
42 flex: 1;
43 display: flex;
44 flex-direction: column;
45 min-width: 0;
46 }
47 #terminal-panel {
48 flex: 1;
49 display: flex;
50 flex-direction: column;
51 min-width: 0;
52 }
53 .panel-header {
54 display: flex;
55 align-items: center;
56 gap: 10px;
57 margin-bottom: 10px;
58 height: 32px;
59 }
60 .panel-title {
61 color: var(--accent);
62 font-weight: bold;
63 }
64 .editor-controls {
65 display: flex;
66 align-items: center;
67 gap: 10px;
68 margin-left: auto;
69 }
70 #example-select {
71 background: var(--bg-secondary);
72 border: 1px solid var(--border);
73 border-radius: 4px;
74 color: var(--fg);
75 padding: 6px 10px;
76 font-family: inherit;
77 font-size: 13px;
78 }
79 #example-select:focus {
80 outline: none;
81 border-color: var(--accent);
82 }
83 .btn {
84 background: var(--accent);
85 border: none;
86 border-radius: 4px;
87 color: var(--bg);
88 padding: 6px 16px;
89 font-family: inherit;
90 font-size: 13px;
91 font-weight: bold;
92 cursor: pointer;
93 transition: background 0.2s;
94 }
95 .btn:hover {
96 background: var(--accent-dim);
97 }
98 .btn:disabled {
99 background: #3a3a3a;
100 color: var(--fg-muted);
101 cursor: not-allowed;
102 }
103 .btn-danger {
104 background: #dc2626;
105 color: white;
106 }
107 .btn-danger:hover {
108 background: #b91c1c;
109 }
110 .editor-wrapper {
111 flex: 1;
112 position: relative;
113 border-radius: 8px;
114 overflow: hidden;
115 }
116 #editor-highlight {
117 position: absolute;
118 top: 0;
119 left: 0;
120 right: 0;
121 bottom: 0;
122 background: var(--code-bg);
123 color: var(--fg);
124 padding: 15px;
125 margin: 0;
126 font-family: var(--font-mono);
127 font-size: 15px;
128 line-height: 1.5;
129 tab-size: 2;
130 white-space: pre-wrap;
131 word-wrap: break-word;
132 overflow: auto;
133 pointer-events: none;
134 }
135 #editor {
136 position: relative;
137 width: 100%;
138 height: 100%;
139 background: transparent;
140 border: none;
141 border-radius: 8px;
142 padding: 15px;
143 color: transparent;
144 caret-color: var(--fg);
145 font-family: var(--font-mono);
146 font-size: 15px;
147 line-height: 1.5;
148 resize: none;
149 tab-size: 2;
150 z-index: 1;
151 }
152 #editor:focus {
153 outline: none;
154 }
155 #editor::selection {
156 background: rgba(240, 192, 64, 0.3);
157 }
158 /* Syntax highlighting colors */
159 .hl-comment { color: #676e95; }
160 .hl-string { color: #c3e88d; }
161 .hl-keyword { color: #c792ea; }
162 .hl-boolean { color: #89ddff; }
163 .hl-number { color: #f78c6c; }
164 .hl-paren { color: #888; }
165 .hl-function { color: #82aaff; }
166 #terminal-container {
167 flex: 1;
168 border-radius: 8px;
169 overflow: hidden;
170 background: #1a1a1a;
171 }
172 #terminal-container .xterm {
173 height: 100%;
174 padding: 10px;
175 }
176 #terminal-container .xterm-screen {
177 border-radius: 0 0 8px 8px;
178 }
179 #loading {
180 position: absolute;
181 top: 50%;
182 left: 50%;
183 transform: translate(-50%, -50%);
184 color: var(--accent);
185 font-style: italic;
186 }
188 /* Mobile responsive */
189 @media (max-width: 768px) {
190 #main-container {
191 flex-direction: column;
192 padding: 10px;
193 gap: 10px;
194 }
195 #editor-panel {
196 flex: 1 1 50%;
197 min-height: 200px;
198 max-height: 50%;
199 }
200 #terminal-panel {
201 flex: 1 1 50%;
202 min-height: 200px;
203 }
204 .panel-header {
205 height: auto;
206 flex-wrap: wrap;
207 }
208 .editor-controls {
209 margin-left: 0;
210 width: 100%;
211 margin-top: 8px;
212 }
213 #example-select {
214 flex: 1;
215 }
216 #editor,
217 #editor-highlight {
218 font-size: 16px; /* Prevent iOS zoom on focus */
219 }
220 }
221 /* Header bar */
222 #header {
223 display: flex;
224 align-items: baseline;
225 justify-content: flex-start;
226 gap: 2rem;
227 padding: 14px 20px;
228 border-bottom: 1px solid var(--border);
229 background: var(--bg);
230 }
231 #header .brand {
232 font-size: 1.75rem;
233 font-weight: 700;
234 color: var(--fg);
235 text-decoration: none;
236 letter-spacing: -0.02em;
237 }
238 #header .brand:hover {
239 color: var(--accent);
240 }
241 #header nav {
242 display: flex;
243 gap: 1.5rem;
244 }
245 #header nav a {
246 color: var(--fg-muted);
247 text-decoration: none;
248 font-size: 1rem;
249 }
250 #header nav a:hover {
251 color: var(--accent);
252 }
253 </style>
254</head>
255<body>
256 <div id="loading">Loading Sigil...</div>
258 <header id="header" style="display: none;">
259 <a href="/" class="brand">sigil</a>
260 <nav>
261 <a href="/docs/">Docs</a>
262 <a href="https://codeberg.org/sigil/sigil">Source</a>
263 </nav>
264 </header>
266 <div id="main-container" style="display: none;">
267 <div id="editor-panel">
268 <div class="panel-header">
269 <span class="panel-title">Editor</span>
270 <div class="editor-controls">
271 <select id="example-select">
272 <optgroup label="Basics">
273 <option value="hello">Hello World</option>
274 <option value="fibonacci">Fibonacci</option>
275 <option value="factorial">Factorial</option>
276 <option value="lists">List Operations</option>
277 <option value="higherorder">Higher-Order Functions</option>
278 <option value="recursion">Recursion Patterns</option>
279 <option value="macros">Simple Macros</option>
280 <option value="match">Pattern Matching</option>
281 </optgroup>
282 <optgroup label="Libraries">
283 <option value="json">JSON</option>
284 <option value="sxml">SXML (HTML/XML)</option>
285 <option value="format">Strings</option>
286 <option value="time">Date &amp; Time</option>
287 <option value="channels">Channels</option>
288 <option value="ansi">ANSI Colors</option>
289 </optgroup>
290 </select>
291 <button id="run-btn" class="btn">Run</button>
292 <button id="stop-btn" class="btn btn-danger" disabled>Stop</button>
293 </div>
294 </div>
295 <div class="editor-wrapper">
296 <pre id="editor-highlight"></pre>
297 <textarea id="editor" spellcheck="false"></textarea>
298 </div>
299 </div>
301 <div id="terminal-panel">
302 <div class="panel-header">
303 <span class="panel-title">REPL</span>
304 </div>
305 <div id="terminal-container"></div>
306 </div>
307 </div>
309 <script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/xterm.min.js"></script>
310 <script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/xterm-addon-fit.min.js"></script>
311 <script>
312 // Example code snippets
313 const examples = {
314 hello: `;;; Hello World
315;;; The classic first program
317(display "Hello, World!")
318(newline)
320;; Greet someone by name
321(define (greet name)
322 (println "Hello, ~a!" name))
324(greet "Sigil")
325(greet "Web")`,
327 fibonacci: `;;; Fibonacci Sequence
328;;; Computing Fibonacci numbers
330;; Recursive (simple but slow)
331(define (fib-recursive n)
332 (if (< n 2)
333 n
334 (+ (fib-recursive (- n 1))
335 (fib-recursive (- n 2)))))
337;; Iterative (fast)
338(define (fib n)
339 (let loop ((a 0) (b 1) (count n))
340 (if (= count 0)
341 a
342 (loop b (+ a b) (- count 1)))))
344;; Try them out
345(display "fib(10) = ")
346(display (fib 10))
347(newline)
349(display "First 15 Fibonacci numbers:\\n")
350(let loop ((i 0))
351 (when (< i 15)
352 (display (fib i))
353 (display " ")
354 (loop (+ i 1))))
355(newline)`,
357 factorial: `;;; Factorial
358;;; Different ways to compute n!
360;; Recursive
361(define (factorial n)
362 (if (<= n 1)
363 1
364 (* n (factorial (- n 1)))))
366;; Tail-recursive
367(define (factorial-tail n)
368 (let loop ((n n) (acc 1))
369 (if (<= n 1)
370 acc
371 (loop (- n 1) (* acc n)))))
373;; Using fold
374(define (factorial-fold n)
375 (fold * 1 (iota n 1)))
377;; Test all three
378(display "10! = ")
379(display (factorial 10))
380(newline)
382(display "Factorials 1-10:\\n")
383(for-each
384 (lambda (n)
385 (display n)
386 (display "! = ")
387 (display (factorial n))
388 (newline))
389 (iota 10 1))`,
391 lists: `;;; List Operations
392;;; Working with Sigil's list primitives
394;; Create some lists
395(define numbers '(1 2 3 4 5))
396(define fruits '("apple" "banana" "cherry"))
398;; Basic operations
399(display "numbers: ") (write numbers) (newline)
400(display "length: ") (display (length numbers)) (newline)
401(display "first: ") (display (car numbers)) (newline)
402(display "rest: ") (write (cdr numbers)) (newline)
404;; Building lists
405(display "\\nBuilding lists:\\n")
406(display "cons 0 onto numbers: ")
407(write (cons 0 numbers))
408(newline)
410(display "append two lists: ")
411(write (append numbers '(6 7 8)))
412(newline)
414(display "reverse: ")
415(write (reverse numbers))
416(newline)
418;; List predicates
419(display "\\nPredicates:\\n")
420(display "null? '(): ") (display (null? '())) (newline)
421(display "pair? numbers: ") (display (pair? numbers)) (newline)
422(display "member 3 numbers: ") (write (member 3 numbers)) (newline)`,
424 higherorder: `;;; Higher-Order Functions
425;;; Functions that take or return functions
427(define numbers '(1 2 3 4 5 6 7 8 9 10))
429;; map - transform each element
430(display "squares: ")
431(write (map (lambda (x) (* x x)) numbers))
432(newline)
434;; filter - keep elements matching predicate
435(display "evens: ")
436(write (filter even? numbers))
437(newline)
439(display "odds: ")
440(write (filter odd? numbers))
441(newline)
443;; fold - reduce to single value
444(display "sum: ")
445(display (fold + 0 numbers))
446(newline)
448(display "product: ")
449(display (fold * 1 numbers))
450(newline)
452;; Composition
453(define (compose f g)
454 (lambda (x) (f (g x))))
456(define add1 (lambda (x) (+ x 1)))
457(define double (lambda (x) (* x 2)))
458(define add1-then-double (compose double add1))
460(display "\\n(add1-then-double 5) = ")
461(display (add1-then-double 5))
462(newline)`,
464 recursion: `;;; Recursion Patterns
465;;; Common recursive techniques
467;; Linear recursion
468(define (sum-list lst)
469 (if (null? lst)
470 0
471 (+ (car lst) (sum-list (cdr lst)))))
473;; Tail recursion (more efficient)
474(define (sum-list-tail lst)
475 (let loop ((lst lst) (acc 0))
476 (if (null? lst)
477 acc
478 (loop (cdr lst) (+ acc (car lst))))))
480;; Tree recursion (nested structures)
481(define (deep-sum tree)
482 (cond
483 ((null? tree) 0)
484 ((number? tree) tree)
485 ((pair? tree)
486 (+ (deep-sum (car tree))
487 (deep-sum (cdr tree))))
488 (else 0)))
490;; Test them
491(define nums '(1 2 3 4 5))
492(display "sum of (1 2 3 4 5): ")
493(display (sum-list nums))
494(newline)
496(define nested '(1 (2 3) ((4 5) 6)))
497(display "deep-sum of (1 (2 3) ((4 5) 6)): ")
498(display (deep-sum nested))
499(newline)`,
501 macros: `;;; Simple Macros
502;;; Extending the language with macros
504;; when - execute body if condition is true
505;; (already built-in, but here's how it works)
506(define-syntax my-when
507 (syntax-rules ()
508 ((my-when test body ...)
509 (if test (begin body ...)))))
511;; unless - opposite of when
512(define-syntax unless
513 (syntax-rules ()
514 ((unless test body ...)
515 (if (not test) (begin body ...)))))
517;; dotimes - repeat n times
518(define-syntax dotimes
519 (syntax-rules ()
520 ((dotimes (var count) body ...)
521 (let loop ((var 0))
522 (when (< var count)
523 body ...
524 (loop (+ var 1)))))))
526;; Test the macros
527(my-when #t
528 (display "my-when works!\\n"))
530(unless #f
531 (display "unless works!\\n"))
533(display "Counting: ")
534(dotimes (i 5)
535 (display i)
536 (display " "))
537(newline)`,
539 match: `;;; Pattern Matching
540;;; Destructuring data with match
541;;; Requires sigil-match (not bundled, loaded separately)
543(import (sigil match))
545;; Simple value matching (literals must be quoted)
546(define (describe-number n)
547 (match n
548 ('0 "zero")
549 ('1 "one")
550 ('2 "two")
551 (_ "many")))
553(display "describe 0: ") (display (describe-number 0)) (newline)
554(display "describe 2: ") (display (describe-number 2)) (newline)
555(display "describe 99: ") (display (describe-number 99)) (newline)
557;; List destructuring
558(define (first-two lst)
559 (match lst
560 ((a b . _) (list a b))
561 ((a) (list a))
562 (() '())))
564(display "\\nfirst-two of (1 2 3 4): ")
565(write (first-two '(1 2 3 4)))
566(newline)
568;; Nested patterns
569(define (sum-pairs lst)
570 (match lst
571 (() 0)
572 (((a . b) . rest)
573 (+ a b (sum-pairs rest)))))
575(display "sum-pairs ((1 . 2) (3 . 4)): ")
576(display (sum-pairs '((1 . 2) (3 . 4))))
577(newline)`,
579 json: `;;; JSON Library
580;;; Parse and generate JSON data
582(import (sigil json))
584;; Parse JSON strings
585(define data (json-decode "{\\"name\\": \\"Alice\\", \\"age\\": 30, \\"active\\": true}"))
587(display "Parsed JSON:\\n")
588(write data)
589(newline)
591;; Access nested data
592(define complex (json-decode "{\\"users\\": [{\\"id\\": 1}, {\\"id\\": 2}]}"))
593(display "\\nComplex structure:\\n")
594(write complex)
595(newline)
597;; Generate JSON from Scheme data
598(define person '((name . "Bob") (scores . (95 87 92))))
599(display "\\nScheme to JSON:\\n")
600(display (json-encode person))
601(newline)
603;; Pretty print
604(display "\\nPretty printed:\\n")
605(display (json-encode person indent: #t))
606(newline)
608;; Round-trip test
609(define original '((items . (1 2 3)) (meta . ((count . 3)))))
610(display "\\nRound-trip:\\n")
611(write (json-decode (json-encode original)))
612(newline)`,
614 sxml: `;;; SXML Library
615;;; Represent HTML/XML as S-expressions
616;;; Requires sigil-sxml (not bundled, loaded separately)
618(import (sigil sxml))
620;; SXML represents HTML/XML as nested lists
621;; (tag (@ attributes...) children...)
623(define page
624 '(html
625 (head (title "My Page"))
626 (body
627 (h1 "Welcome!")
628 (p "This is a paragraph.")
629 (ul
630 (li "Item 1")
631 (li "Item 2")
632 (li "Item 3")))))
634(display "SXML structure:\\n")
635(write page)
636(newline)
638;; Convert SXML to XML string
639(display "\\nAs XML:\\n")
640(display (sxml->xml page))
641(newline)
643;; SXML with attributes
644(define link
645 '(a (@ (href "https://usesigil.org")
646 (class "btn primary"))
647 "Visit Sigil"))
649(display "\\nLink with attributes:\\n")
650(display (sxml->xml link))
651(newline)
653;; Building SXML dynamically
654(define (make-list items)
655 \`(ul ,@(map (lambda (item) \`(li ,item)) items)))
657(display "\\nDynamic list:\\n")
658(display (sxml->xml (make-list '("Apple" "Banana" "Cherry"))))
659(newline)`,
661 format: `;;; String Operations
662;;; Building and manipulating strings
664(import (sigil string))
666;; Basic string operations
667(display "String append: ")
668(display (string-append "Hello, " "World!"))
669(newline)
671;; Building strings from parts
672(define (greet name)
673 (string-append "Hello, " name "!"))
675(display (greet "Alice"))
676(newline)
677(display (greet "Bob"))
678(newline)
680;; String predicates
681(display "\\nString predicates:\\n")
682(display " string=? 'foo' 'foo': ")
683(display (string=? "foo" "foo"))
684(newline)
685(display " string<? 'abc' 'def': ")
686(display (string<? "abc" "def"))
687(newline)
689;; Converting to/from strings
690(display "\\nConversions:\\n")
691(display " number->string 42: ")
692(display (number->string 42))
693(newline)
694(display " string->number '123': ")
695(display (string->number "123"))
696(newline)
697(display " symbol->string 'hello: ")
698(display (symbol->string 'hello))
699(newline)
701;; String length and access
702(define s "Sigil")
703(display "\\nString '")
704(display s)
705(display "':\\n")
706(display " length: ")
707(display (string-length s))
708(newline)
709(display " char at 0: ")
710(display (string-ref s 0))
711(newline)`,
713 time: `;;; Time Library
714;;; Date and time operations
716(import (sigil time))
718;; Get current time (seconds since epoch)
719(define now (current-second))
720(display "Current timestamp: ")
721(display now)
722(newline)
724;; Time components (pass timestamp to accessors)
725(display "\\nTime breakdown:\\n")
726(display " Year: ") (display (time-year now)) (newline)
727(display " Month: ") (display (time-month now)) (newline)
728(display " Day: ") (display (time-day now)) (newline)
729(display " Hour: ") (display (time-hour now)) (newline)
730(display " Minute: ") (display (time-minute now)) (newline)
731(display " Second: ") (display (time-second now)) (newline)
733;; Day of week (0 = Sunday)
734(define days '("Sunday" "Monday" "Tuesday" "Wednesday"
735 "Thursday" "Friday" "Saturday"))
736(display " Day of week: ")
737(display (list-ref days (time-weekday now)))
738(newline)
740;; High-resolution timing
741(display "\\nHigh-res timing:\\n")
742(display " Jiffies: ") (display (current-jiffy)) (newline)
743(display " Jiffies/sec: ") (display (jiffies-per-second)) (newline)`,
745 channels: `;;; Channels Library
746;;; Message passing with buffered channels
748(import (sigil channels))
750;; Create a buffered channel (capacity 3)
751(define ch (make-channel 3))
752(display "Created buffered channel\\n")
754;; Send messages (non-blocking with try-send)
755(display "\\nSending messages...\\n")
756(channel-try-send ch "hello")
757(display " Sent: hello\\n")
758(channel-try-send ch "world")
759(display " Sent: world\\n")
760(channel-try-send ch 42)
761(display " Sent: 42\\n")
763;; Try to send when full
764(display "\\nChannel full? ")
765(display (if (channel-try-send ch "overflow")
766 "no, sent another"
767 "yes, channel is full"))
768(newline)
770;; Receive messages (non-blocking with try-receive)
771(display "\\nReceiving messages...\\n")
772(display " Received: ") (write (channel-try-receive ch)) (newline)
773(display " Received: ") (write (channel-try-receive ch)) (newline)
774(display " Received: ") (write (channel-try-receive ch)) (newline)
776;; Try to receive when empty
777(display "\\nChannel empty? ")
778(display (if (channel-try-receive ch)
779 "no, got a value"
780 "yes, channel is empty"))
781(newline)
783;; Close channel
784(channel-close! ch)
785(display "\\nChannel closed.")
786(newline)`,
788 ansi: `;;; ANSI Library
789;;; Terminal colors and styles
791(import (sigil ansi))
793;; Basic colors (all functions use a: prefix)
794(display (a:red "This is red"))
795(newline)
796(display (a:green "This is green"))
797(newline)
798(display (a:blue "This is blue"))
799(newline)
800(display (a:yellow "This is yellow"))
801(newline)
803;; Styles
804(display (a:bold "Bold text"))
805(newline)
806(display (a:dim "Dim text"))
807(newline)
808(display (a:underline "Underlined"))
809(newline)
811;; Combined styles
812(display (a:bold (a:red "Bold and red!")))
813(newline)
815;; Building colored output
816(define (status-message status msg)
817 (display
818 (cond
819 ((eq? status 'ok) (a:green (string-append "[OK] " msg)))
820 ((eq? status 'warn) (a:yellow (string-append "[WARN] " msg)))
821 ((eq? status 'error) (a:red (string-append "[ERROR] " msg)))
822 (else msg)))
823 (newline))
825(newline)
826(status-message 'ok "Operation completed")
827(status-message 'warn "Low disk space")
828(status-message 'error "Connection failed")`
829 };
831 const mainContainer = document.getElementById('main-container');
832 const terminalContainer = document.getElementById('terminal-container');
833 const loading = document.getElementById('loading');
834 const editor = document.getElementById('editor');
835 const editorHighlight = document.getElementById('editor-highlight');
836 const exampleSelect = document.getElementById('example-select');
837 const runBtn = document.getElementById('run-btn');
838 const stopBtn = document.getElementById('stop-btn');
840 // Scheme syntax highlighting
841 const schemeKeywords = new Set([
842 'define', 'define-syntax', 'define-library', 'lambda', 'let', 'let*', 'letrec',
843 'if', 'cond', 'else', 'case', 'when', 'unless', 'begin', 'do',
844 'and', 'or', 'not', 'quote', 'quasiquote', 'unquote', 'unquote-splicing',
845 'set!', 'import', 'export', 'library', 'syntax-rules', 'match',
846 'guard', 'with-exception-handler', 'raise', 'error',
847 'for-each', 'map', 'filter', 'fold', 'apply', 'call/cc',
848 'display', 'newline', 'write', 'read'
849 ]);
851 function escapeHtml(text) {
852 return text
853 .replace(/&/g, '&amp;')
854 .replace(/</g, '&lt;')
855 .replace(/>/g, '&gt;');
856 }
858 function highlightScheme(code) {
859 let result = '';
860 let i = 0;
862 while (i < code.length) {
863 // Comments
864 if (code[i] === ';') {
865 let end = code.indexOf('\n', i);
866 if (end === -1) end = code.length;
867 result += '<span class="hl-comment">' + escapeHtml(code.slice(i, end)) + '</span>';
868 i = end;
869 continue;
870 }
872 // Strings
873 if (code[i] === '"') {
874 let end = i + 1;
875 while (end < code.length && code[end] !== '"') {
876 if (code[end] === '\\') end++;
877 end++;
878 }
879 end++; // include closing quote
880 result += '<span class="hl-string">' + escapeHtml(code.slice(i, end)) + '</span>';
881 i = end;
882 continue;
883 }
885 // Character literals
886 if (code[i] === '#' && code[i + 1] === '\\') {
887 let end = i + 2;
888 while (end < code.length && /[a-zA-Z]/.test(code[end])) end++;
889 if (end === i + 2) end++; // single char like #\a
890 result += '<span class="hl-string">' + escapeHtml(code.slice(i, end)) + '</span>';
891 i = end;
892 continue;
893 }
895 // Booleans
896 if (code[i] === '#' && (code[i + 1] === 't' || code[i + 1] === 'f')) {
897 result += '<span class="hl-boolean">' + escapeHtml(code.slice(i, i + 2)) + '</span>';
898 i += 2;
899 continue;
900 }
902 // Numbers
903 if (/[0-9]/.test(code[i]) || (code[i] === '-' && /[0-9]/.test(code[i + 1]))) {
904 let end = i;
905 if (code[end] === '-') end++;
906 while (end < code.length && /[0-9.]/.test(code[end])) end++;
907 // Make sure it's not part of an identifier
908 if (i === 0 || /[\s()\[\]'`,]/.test(code[i - 1])) {
909 result += '<span class="hl-number">' + escapeHtml(code.slice(i, end)) + '</span>';
910 i = end;
911 continue;
912 }
913 }
915 // Identifiers and keywords
916 if (/[a-zA-Z_!?*+\-<>=]/.test(code[i])) {
917 let end = i;
918 while (end < code.length && /[a-zA-Z0-9_!?*+\-<>=\/:]/.test(code[end])) end++;
919 const word = code.slice(i, end);
920 if (schemeKeywords.has(word)) {
921 result += '<span class="hl-keyword">' + escapeHtml(word) + '</span>';
922 } else {
923 result += escapeHtml(word);
924 }
925 i = end;
926 continue;
927 }
929 // Parentheses
930 if (code[i] === '(' || code[i] === ')' || code[i] === '[' || code[i] === ']') {
931 result += '<span class="hl-paren">' + code[i] + '</span>';
932 i++;
933 continue;
934 }
936 // Default: just output the character
937 result += escapeHtml(code[i]);
938 i++;
939 }
941 return result;
942 }
944 function updateHighlight() {
945 // Add a newline to ensure the pre matches textarea height when ending with newline
946 editorHighlight.innerHTML = highlightScheme(editor.value) + '\n';
947 }
949 // Sync scroll between editor and highlight
950 editor.addEventListener('scroll', function() {
951 editorHighlight.scrollTop = editor.scrollTop;
952 editorHighlight.scrollLeft = editor.scrollLeft;
953 });
955 // Update highlighting on input
956 editor.addEventListener('input', updateHighlight);
958 // Load initial example
959 editor.value = examples[exampleSelect.value];
960 updateHighlight();
962 // Handle example selection
963 exampleSelect.addEventListener('change', function() {
964 editor.value = examples[this.value];
965 updateHighlight();
966 });
968 // Handle tab key in editor
969 editor.addEventListener('keydown', function(e) {
970 if (e.key === 'Tab') {
971 e.preventDefault();
972 const start = this.selectionStart;
973 const end = this.selectionEnd;
974 this.value = this.value.substring(0, start) + ' ' + this.value.substring(end);
975 this.selectionStart = this.selectionEnd = start + 2;
976 updateHighlight();
977 }
978 });
979 </script>
980 <script type="module">
981 import { WASI, File, Directory, OpenFile, ConsoleStdout, PreopenDirectory }
982 from "https://cdn.jsdelivr.net/npm/@bjorn3/[email protected]/dist/index.js";
984 const mainContainer = document.getElementById('main-container');
985 const terminalContainer = document.getElementById('terminal-container');
986 const loading = document.getElementById('loading');
987 const editor = document.getElementById('editor');
988 const runBtn = document.getElementById('run-btn');
989 const stopBtn = document.getElementById('stop-btn');
991 // Create xterm.js terminal
992 const term = new Terminal({
993 theme: {
994 background: '#1a1a1a',
995 foreground: '#e0e0e0',
996 cursor: '#f0c040',
997 cursorAccent: '#1a1a1a',
998 selectionBackground: 'rgba(240, 192, 64, 0.3)'
999 },
1000 fontFamily: "'JetBrains Mono', 'SF Mono', Consolas, monospace",
1001 fontSize: 15,
1002 cursorBlink: true,
1003 convertEol: true
1004 });
1005 const fitAddon = new FitAddon.FitAddon();
1006 term.loadAddon(fitAddon);
1008 // State
1009 let currentLine = '';
1010 let history = [];
1011 let historyIndex = -1;
1012 let ready = false;
1013 let waitingForInput = false;
1014 let running = false;
1015 let sigilInput = null;
1016 let sigilEval = null;
1018 // Run code from editor
1019 function runCode() {
1020 if (!ready) return;
1021 const code = editor.value.trim();
1022 if (!code) return;
1024 running = true;
1025 runBtn.disabled = true;
1026 stopBtn.disabled = false;
1027 term.write('\r\n');
1029 sigilEval(code);
1031 running = false;
1032 runBtn.disabled = false;
1033 stopBtn.disabled = true;
1035 if (waitingForInput) {
1036 sigilInput('');
1040 function stopCode() {
1041 running = false;
1042 runBtn.disabled = false;
1043 stopBtn.disabled = true;
1044 term.write('\r\n^C\r\n');
1047 runBtn.addEventListener('click', runCode);
1048 stopBtn.addEventListener('click', stopCode);
1050 // Terminal input handling
1051 function handleInput(data) {
1052 if (!ready || !waitingForInput) return;
1054 for (let i = 0; i < data.length; i++) {
1055 const char = data[i];
1056 const code = data.charCodeAt(i);
1058 if (code === 13) { // Enter
1059 term.write('\r\n');
1060 if (currentLine.trim()) {
1061 history.push(currentLine);
1062 historyIndex = history.length;
1064 const line = currentLine;
1065 currentLine = '';
1066 waitingForInput = false;
1067 const status = sigilInput(line);
1068 if (status === 1) {
1069 waitingForInput = true;
1070 } else if (status === 0) {
1071 term.writeln('\r\n[REPL exited]');
1072 } else {
1073 waitingForInput = true;
1075 } else if (code === 127 || code === 8) { // Backspace
1076 if (currentLine.length > 0) {
1077 currentLine = currentLine.slice(0, -1);
1078 term.write('\b \b');
1080 } else if (code === 27) { // Escape sequence
1081 if (data.substring(i, i + 3) === '\x1b[A') { // Up arrow
1082 i += 2;
1083 if (historyIndex > 0) {
1084 while (currentLine.length > 0) {
1085 term.write('\b \b');
1086 currentLine = currentLine.slice(0, -1);
1088 historyIndex--;
1089 currentLine = history[historyIndex];
1090 term.write(currentLine);
1092 } else if (data.substring(i, i + 3) === '\x1b[B') { // Down arrow
1093 i += 2;
1094 while (currentLine.length > 0) {
1095 term.write('\b \b');
1096 currentLine = currentLine.slice(0, -1);
1098 if (historyIndex < history.length - 1) {
1099 historyIndex++;
1100 currentLine = history[historyIndex];
1101 term.write(currentLine);
1102 } else {
1103 historyIndex = history.length;
1104 currentLine = '';
1107 } else if (code === 3) { // Ctrl+C
1108 term.write('^C\r\n');
1109 currentLine = '';
1110 waitingForInput = false;
1111 const status = sigilInput('');
1112 if (status === 1) waitingForInput = true;
1113 } else if (code >= 32) { // Printable character
1114 currentLine += char;
1115 term.write(char);
1120 term.onData(handleInput);
1121 window.addEventListener('resize', () => { if (ready) fitAddon.fit(); });
1123 // Load module bundle into virtual filesystem
1124 // Bundle format: [uint32 count] then per file:
1125 // [uint16 path-len] [path bytes] [uint32 data-len] [data bytes]
1126 async function buildFs() {
1127 const { File, Directory } = await import("https://cdn.jsdelivr.net/npm/@bjorn3/[email protected]/dist/index.js");
1128 const files = {};
1129 try {
1130 const resp = await fetch('lib.bundle');
1131 if (!resp.ok) return { files, File, Directory };
1132 const buf = await resp.arrayBuffer();
1133 const view = new DataView(buf);
1134 const decoder = new TextDecoder();
1135 let offset = 0;
1137 const count = view.getUint32(offset, true); offset += 4;
1138 for (let i = 0; i < count; i++) {
1139 const pathLen = view.getUint16(offset, true); offset += 2;
1140 const path = decoder.decode(new Uint8Array(buf, offset, pathLen)); offset += pathLen;
1141 const dataLen = view.getUint32(offset, true); offset += 4;
1142 const data = new Uint8Array(buf, offset, dataLen); offset += dataLen;
1144 const parts = path.split('/');
1145 let dir = files;
1146 for (let j = 0; j < parts.length - 1; j++) {
1147 if (!dir[parts[j]]) dir[parts[j]] = {};
1148 dir = dir[parts[j]];
1150 dir[parts[parts.length - 1]] = new File(new Uint8Array(data));
1152 } catch (e) {
1153 console.warn('Bundle loading:', e.message);
1155 return { files, File, Directory };
1158 function makeDir(obj, FileClass, DirectoryClass) {
1159 const entries = new Map();
1160 for (const [name, value] of Object.entries(obj)) {
1161 if (value instanceof FileClass) {
1162 entries.set(name, value);
1163 } else {
1164 entries.set(name, makeDir(value, FileClass, DirectoryClass));
1167 return new DirectoryClass(entries);
1170 function callWithString(instance, fn, str) {
1171 const encoder = new TextEncoder();
1172 const bytes = encoder.encode(str + '\0');
1173 const ptr = instance.exports.malloc(bytes.length);
1174 new Uint8Array(instance.exports.memory.buffer, ptr, bytes.length).set(bytes);
1175 const result = fn(ptr);
1176 instance.exports.free(ptr);
1177 return result;
1180 // Custom WASI Fd that writes to xterm immediately (unbuffered)
1181 function makeTermWriter(termRef, wasiModule) {
1182 const decoder = new TextDecoder();
1183 const fd = new wasiModule.Fd();
1184 fd.fd_write = function(data) {
1185 const text = decoder.decode(data, { stream: true });
1186 termRef.write(text.replace(/\n/g, '\r\n'));
1187 return { ret: 0, nwritten: data.byteLength };
1188 };
1189 fd.fd_fdstat_get = function() {
1190 const { Fdstat } = wasiModule.wasi;
1191 return { ret: 0, fdstat: new Fdstat(2, 0) };
1192 };
1193 return fd;
1196 async function loadOptionalBridgeLoader() {
1197 const response = await fetch('sigil-wasm-bridges.js', { method: 'HEAD' });
1198 if (!response.ok) {
1199 return null;
1202 await new Promise((resolve, reject) => {
1203 const script = document.createElement('script');
1204 script.src = 'sigil-wasm-bridges.js';
1205 script.onload = resolve;
1206 script.onerror = () => reject(new Error('Failed to load sigil-wasm-bridges.js'));
1207 document.head.appendChild(script);
1208 });
1210 if (!window.SigilWasmBridges) {
1211 return null;
1214 return window.SigilWasmBridges.load();
1217 function makeWasmNetStubImports() {
1218 return {
1219 sigil_wasm_net: {
1220 websocket_open() { return 0; },
1221 websocket_send() { return 0; },
1222 websocket_close() { return 0; },
1223 websocket_poll() { return 0; },
1224 current_event_code() { return 0; },
1225 current_event_was_clean() { return 0; },
1226 current_event_data_length() { return 0; },
1227 current_event_reason_length() { return 0; },
1228 copy_current_event_data() { return 0; },
1229 copy_current_event_reason() { return 0; },
1230 },
1231 };
1234 // Initialize WASI and load WASM
1235 async function init() {
1236 const wasiModule = await import("https://cdn.jsdelivr.net/npm/@bjorn3/[email protected]/dist/index.js");
1237 const { WASI, File: WasiFile, Fd, Fdstat, OpenFile, PreopenDirectory } = wasiModule;
1239 const { files: libFiles, File: FileClass, Directory: DirectoryClass } = await buildFs();
1241 const fds = [
1242 new OpenFile(new WasiFile([])), // stdin
1243 makeTermWriter(term, wasiModule), // stdout
1244 makeTermWriter(term, wasiModule), // stderr
1245 new PreopenDirectory("/", new Map([
1246 ["lib", makeDir(libFiles, FileClass, DirectoryClass)]
1247 ])),
1248 ];
1250 const wasi = new WASI(["sigil-web-repl"], [], fds, { debug: false });
1252 try {
1253 const bridgeManifest = await loadOptionalBridgeLoader();
1254 const bridgeImports = {
1255 ...makeWasmNetStubImports(),
1256 ...(bridgeManifest
1257 ? window.SigilWasmBridges.createImports(bridgeManifest)
1258 : {}),
1259 };
1260 const wasmModule = await WebAssembly.compileStreaming(fetch('sigil-web-repl.wasm'));
1261 const instance = await WebAssembly.instantiate(wasmModule, {
1262 wasi_snapshot_preview1: wasi.wasiImport,
1263 ...bridgeImports,
1264 });
1265 if (bridgeManifest) {
1266 window.SigilWasmBridges.setInstance(bridgeManifest, instance);
1269 sigilInput = (str) => callWithString(instance, instance.exports.sigil_wasm_input, str);
1270 sigilEval = (str) => callWithString(instance, instance.exports.sigil_wasm_eval, str);
1272 // Start the VM
1273 wasi.start(instance);
1275 // Show UI
1276 loading.style.display = 'none';
1277 document.getElementById('header').style.display = 'flex';
1278 mainContainer.style.display = 'flex';
1279 term.open(terminalContainer);
1280 fitAddon.fit();
1282 ready = true;
1283 waitingForInput = true;
1284 term.focus();
1285 } catch (e) {
1286 loading.textContent = 'Error: ' + e.message;
1287 console.error(e);
1291 init();
1292 </script>
1293</body>
1294</html>