eyeling 1.9.4 → 1.9.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/demo.html ADDED
@@ -0,0 +1,3045 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <!-- Mobile-friendly viewport + safe-area support -->
6
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
7
+
8
+ <title>Eyeling N3 Playground</title>
9
+
10
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/lib/codemirror.min.css" />
11
+
12
+ <style>
13
+ * {
14
+ box-sizing: border-box;
15
+ }
16
+
17
+ body {
18
+ margin: 0;
19
+ font-family:
20
+ system-ui,
21
+ -apple-system,
22
+ BlinkMacSystemFont,
23
+ 'Segoe UI',
24
+ sans-serif;
25
+ background: #f5f5f7;
26
+ color: #111827;
27
+
28
+ /* Avoid weird auto-scaling on mobile */
29
+ -webkit-text-size-adjust: 100%;
30
+ text-size-adjust: 100%;
31
+ }
32
+
33
+ .page {
34
+ max-width: 1500px;
35
+ margin: 0 auto;
36
+
37
+ /* Safe areas (iPhone notch/home bar) */
38
+ padding-top: calc(1.5rem + env(safe-area-inset-top));
39
+ padding-right: calc(1rem + env(safe-area-inset-right));
40
+ padding-bottom: calc(3rem + env(safe-area-inset-bottom));
41
+ padding-left: calc(1rem + env(safe-area-inset-left));
42
+
43
+ display: flex;
44
+ flex-direction: column;
45
+ gap: 1rem;
46
+ }
47
+
48
+ header h1 {
49
+ margin: 0;
50
+ font-size: 1.4rem;
51
+ font-weight: 600;
52
+ }
53
+
54
+ header p {
55
+ margin: 0.25rem 0 0;
56
+ font-size: 0.9rem;
57
+ color: #6b7280;
58
+ }
59
+
60
+ header a {
61
+ color: #2563eb;
62
+ text-decoration: none;
63
+ }
64
+ header a:hover {
65
+ text-decoration: underline;
66
+ }
67
+
68
+ .meta {
69
+ margin-top: 0.35rem;
70
+ font-size: 0.85rem;
71
+ color: #6b7280;
72
+ }
73
+
74
+ label {
75
+ font-size: 0.9rem;
76
+ font-weight: 500;
77
+ color: #374151;
78
+ margin-bottom: 0.35rem;
79
+ display: inline-block;
80
+ }
81
+
82
+ textarea {
83
+ width: 100%;
84
+ border-radius: 0.75rem;
85
+ border: 1px solid #d1d5db;
86
+ padding: 1rem;
87
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
88
+ font-size: 0.9rem;
89
+ line-height: 1.4;
90
+ resize: none;
91
+ overflow: hidden; /* no scrollbars inside the editor */
92
+ background: #ffffff;
93
+ }
94
+
95
+ textarea:focus {
96
+ outline: none;
97
+ border-color: #2563eb;
98
+ box-shadow: 0 0 0 1px rgba(37, 99, 235, 0.35);
99
+ }
100
+
101
+ .controls {
102
+ display: flex;
103
+ flex-direction: row;
104
+ align-items: center;
105
+ gap: 0.75rem;
106
+ margin-top: 0.25rem;
107
+ flex-wrap: wrap;
108
+ }
109
+
110
+ .toggle {
111
+ display: inline-flex;
112
+ align-items: center;
113
+ gap: 0.45rem;
114
+ font-size: 0.85rem;
115
+ font-weight: 500;
116
+ color: #374151;
117
+ user-select: none;
118
+ cursor: pointer;
119
+ }
120
+
121
+ .toggle input {
122
+ width: 1rem;
123
+ height: 1rem;
124
+ accent-color: #2563eb;
125
+ cursor: pointer;
126
+ touch-action: manipulation;
127
+ }
128
+
129
+ button {
130
+ border: none;
131
+ border-radius: 999px;
132
+ padding: 0.5rem 1.25rem;
133
+ font-size: 0.9rem;
134
+ font-weight: 500;
135
+ cursor: pointer;
136
+ background: #2563eb;
137
+ color: #ffffff;
138
+ box-shadow: 0 10px 15px rgba(37, 99, 235, 0.25);
139
+ transition:
140
+ transform 0.05s ease-out,
141
+ box-shadow 0.05s ease-out,
142
+ opacity 0.1s;
143
+
144
+ /* Better taps on mobile */
145
+ min-height: 44px;
146
+ touch-action: manipulation;
147
+ -webkit-tap-highlight-color: transparent;
148
+ }
149
+
150
+ button:hover:not(:disabled) {
151
+ transform: translateY(-1px);
152
+ box-shadow: 0 14px 24px rgba(37, 99, 235, 0.32);
153
+ }
154
+
155
+ button:active:not(:disabled) {
156
+ transform: translateY(0);
157
+ box-shadow: 0 8px 12px rgba(37, 99, 235, 0.2);
158
+ }
159
+
160
+ button:disabled {
161
+ opacity: 0.6;
162
+ cursor: default;
163
+ box-shadow: none;
164
+ }
165
+
166
+ /* Secondary + danger button variants (Playground) */
167
+ button.secondary {
168
+ background: #e5e7eb;
169
+ color: #111827;
170
+ box-shadow: none;
171
+ }
172
+ button.secondary:hover:not(:disabled) {
173
+ box-shadow: 0 10px 15px rgba(17, 24, 39, 0.12);
174
+ }
175
+
176
+ button.danger {
177
+ background: #dc2626;
178
+ color: #ffffff;
179
+ box-shadow: 0 10px 15px rgba(220, 38, 38, 0.25);
180
+ }
181
+ button.danger:hover:not(:disabled) {
182
+ box-shadow: 0 14px 24px rgba(220, 38, 38, 0.32);
183
+ }
184
+
185
+ /* Stream demo stop button */
186
+ #streamApp .stream-topbar button.danger {
187
+ background: #dc2626;
188
+ color: #fff;
189
+ }
190
+
191
+ .status {
192
+ font-size: 0.8rem;
193
+ color: #6b7280;
194
+ }
195
+
196
+ /* (fallback only; output is now CodeMirror) */
197
+ pre#output {
198
+ margin: 0.25rem 0 0;
199
+ border-radius: 0.75rem;
200
+ border: 1px solid #d1d5db;
201
+ padding: 1rem;
202
+ background: #ffffff;
203
+ color: #111827;
204
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
205
+ font-size: 0.85rem;
206
+ line-height: 1.4;
207
+ white-space: pre-wrap;
208
+ word-break: break-word;
209
+ overflow: hidden; /* no scrollbars inside the output box */
210
+ min-height: 5rem;
211
+ }
212
+
213
+ .card {
214
+ background: #ffffff;
215
+ border-radius: 1rem;
216
+ padding: 1rem 1.25rem 1.25rem;
217
+ box-shadow: 0 18px 40px rgba(15, 23, 42, 0.08);
218
+ border: 1px solid rgba(148, 163, 184, 0.25);
219
+ }
220
+
221
+ .section-title {
222
+ font-size: 1rem;
223
+ font-weight: 500;
224
+ margin: 0 0 0.5rem;
225
+ color: #111827;
226
+ }
227
+
228
+ .uri-row {
229
+ margin-bottom: 0.75rem;
230
+ }
231
+
232
+ .uri-input-row {
233
+ display: flex;
234
+ flex-direction: row;
235
+ gap: 0.5rem;
236
+ align-items: center;
237
+ margin-top: 0.25rem;
238
+ }
239
+
240
+ .uri-input-row input[type='text'] {
241
+ flex: 1;
242
+ border-radius: 999px;
243
+ border: 1px solid #d1d5db;
244
+ padding: 0.45rem 0.75rem;
245
+ font-size: 0.85rem;
246
+ font-family:
247
+ system-ui,
248
+ -apple-system,
249
+ BlinkMacSystemFont,
250
+ 'Segoe UI',
251
+ sans-serif;
252
+ background: #f9fafb;
253
+ }
254
+
255
+ .uri-input-row input[type='text']:focus {
256
+ outline: none;
257
+ border-color: #2563eb;
258
+ box-shadow: 0 0 0 1px rgba(37, 99, 235, 0.25);
259
+ background: #ffffff;
260
+ }
261
+
262
+ .hint {
263
+ display: block;
264
+ font-size: 0.75rem;
265
+ color: #9ca3af;
266
+ margin-top: 0.15rem;
267
+ }
268
+
269
+ /* --- CodeMirror (syntax highlighted) editors --- */
270
+ .CodeMirror {
271
+ height: auto;
272
+ border: 1px solid #d1d5db;
273
+ border-radius: 0.75rem;
274
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
275
+ font-size: 0.9rem;
276
+ line-height: 1.4;
277
+ background: #ffffff;
278
+ }
279
+
280
+ .CodeMirror.CodeMirror-focused {
281
+ outline: none;
282
+ border-color: #2563eb;
283
+ box-shadow: 0 0 0 1px rgba(37, 99, 235, 0.35);
284
+ }
285
+
286
+ .CodeMirror-scroll {
287
+ overflow: hidden !important; /* desktop: no scrollbars; we auto-size instead */
288
+ }
289
+
290
+ .CodeMirror-gutters {
291
+ border-right: 1px solid #e5e7eb;
292
+ background: #f9fafb;
293
+ }
294
+
295
+ .cm-output {
296
+ font-size: 0.85rem;
297
+ min-height: 5rem;
298
+ }
299
+
300
+ /* Output pane: allow internal scrolling when height-capped */
301
+ .cm-output .CodeMirror-scroll {
302
+ overflow: auto !important;
303
+ }
304
+
305
+ /* Highlight N3 syntax error line in the editor */
306
+ .cm-error-line {
307
+ background: rgba(239, 68, 68, 0.18) !important;
308
+ }
309
+ /* CodeMirror’s line background element (more specific) */
310
+ .CodeMirror-linebackground.cm-error-line {
311
+ background: rgba(239, 68, 68, 0.18) !important;
312
+ box-shadow: inset 3px 0 0 rgba(239, 68, 68, 0.65) !important;
313
+ }
314
+
315
+ /* ---------------------------
316
+ MOBILE POLISH (key changes)
317
+ --------------------------- */
318
+ @media (max-width: 640px) {
319
+ .page {
320
+ gap: 0.85rem;
321
+ padding-top: calc(1.1rem + env(safe-area-inset-top));
322
+ padding-right: calc(0.85rem + env(safe-area-inset-right));
323
+ padding-bottom: calc(2.5rem + env(safe-area-inset-bottom));
324
+ padding-left: calc(0.85rem + env(safe-area-inset-left));
325
+ }
326
+
327
+ header h1 {
328
+ font-size: 1.25rem;
329
+ }
330
+
331
+ header p {
332
+ font-size: 0.95rem;
333
+ }
334
+
335
+ .card {
336
+ padding: 0.95rem 1rem 1.05rem;
337
+ border-radius: 1rem;
338
+ }
339
+
340
+ /* Stack URL input + button (no squishing) */
341
+ .uri-input-row {
342
+ flex-direction: column;
343
+ align-items: stretch;
344
+ }
345
+
346
+ /* Prevent iOS "zoom on focus" (>=16px) */
347
+ .uri-input-row input[type='text'] {
348
+ font-size: 16px;
349
+ padding: 0.65rem 0.9rem;
350
+ width: 100%;
351
+ }
352
+
353
+ /* Make the load button full-width */
354
+ #load-uri-btn {
355
+ width: 100%;
356
+ justify-content: center;
357
+ }
358
+
359
+ /* Controls become a sticky action area on mobile */
360
+ .controls {
361
+ position: sticky;
362
+ bottom: calc(env(safe-area-inset-bottom) + 10px);
363
+ padding: 0.75rem 0;
364
+ margin-top: 0.75rem;
365
+ background: rgba(245, 245, 247, 0.88);
366
+ backdrop-filter: blur(10px);
367
+ border-top: 1px solid rgba(148, 163, 184, 0.35);
368
+
369
+ flex-direction: column;
370
+ align-items: stretch;
371
+ gap: 0.6rem;
372
+ }
373
+
374
+ /* Full-width run button, bigger type for easy taps */
375
+ #run-btn {
376
+ width: 100%;
377
+ font-size: 16px;
378
+ }
379
+
380
+ #pause-btn,
381
+ #stop-btn {
382
+ width: 100%;
383
+ font-size: 16px;
384
+ }
385
+
386
+ /* Make toggles/status readable and line-wrap */
387
+ .toggle {
388
+ width: 100%;
389
+ font-size: 0.95rem;
390
+ }
391
+ .status {
392
+ font-size: 0.9rem;
393
+ }
394
+
395
+ /* On mobile we cap editor height and allow internal scrolling */
396
+ .CodeMirror {
397
+ font-size: 0.95rem;
398
+ }
399
+ .CodeMirror-scroll {
400
+ overflow: auto !important;
401
+ -webkit-overflow-scrolling: touch;
402
+ }
403
+ }
404
+
405
+ /* Optional: Dark mode (keeps it pleasant on mobile) */
406
+ @media (prefers-color-scheme: dark) {
407
+ body {
408
+ background: #0b0f14;
409
+ color: #e7eaf0;
410
+ }
411
+ header p,
412
+ .meta,
413
+ .status {
414
+ color: #a7b0bf;
415
+ }
416
+ label,
417
+ .toggle {
418
+ color: #cbd5e1;
419
+ }
420
+ .card {
421
+ background: #0f1620;
422
+ border-color: rgba(148, 163, 184, 0.18);
423
+ box-shadow: 0 18px 40px rgba(0, 0, 0, 0.35);
424
+ }
425
+ .section-title {
426
+ color: #e7eaf0;
427
+ }
428
+
429
+ .cm-error-line {
430
+ background: rgba(239, 68, 68, 0.22);
431
+ }
432
+
433
+ textarea {
434
+ background: #0f1620;
435
+ border-color: #1f2a37;
436
+ color: #e7eaf0;
437
+ }
438
+ .CodeMirror {
439
+ background: #0f1620;
440
+ border-color: #1f2a37;
441
+ }
442
+ .CodeMirror-gutters {
443
+ background: #0b0f14;
444
+ border-right-color: #1f2a37;
445
+ }
446
+
447
+ .uri-input-row input[type='text'] {
448
+ background: #0b0f14;
449
+ border-color: #1f2a37;
450
+ color: #e7eaf0;
451
+ }
452
+ .uri-input-row input[type='text']:focus {
453
+ background: #0f1620;
454
+ }
455
+
456
+ @media (max-width: 640px) {
457
+ .controls {
458
+ background: rgba(11, 15, 20, 0.88);
459
+ border-top-color: rgba(148, 163, 184, 0.18);
460
+ }
461
+ }
462
+ }
463
+
464
+ /* --- Tabs (Playground / Streaming demo) --- */
465
+ .tabs {
466
+ display: flex;
467
+ gap: 0.5rem;
468
+ align-items: center;
469
+ flex-wrap: wrap;
470
+ margin-top: -0.25rem;
471
+ }
472
+ .tab-btn {
473
+ display: inline-flex;
474
+ align-items: center;
475
+ justify-content: center;
476
+ border: 1px solid rgba(148, 163, 184, 0.35);
477
+ background: rgba(255, 255, 255, 0.85);
478
+ color: #111827;
479
+ border-radius: 999px;
480
+ padding: 0.45rem 0.95rem;
481
+ font-size: 0.85rem;
482
+ font-weight: 600;
483
+ cursor: pointer;
484
+ -webkit-tap-highlight-color: transparent;
485
+ min-height: 40px;
486
+ }
487
+ .tab-btn[aria-selected='true'] {
488
+ background: #2563eb;
489
+ border-color: #2563eb;
490
+ color: #ffffff;
491
+ box-shadow: 0 10px 15px rgba(37, 99, 235, 0.22);
492
+ }
493
+ .tab-content {
494
+ display: none;
495
+ }
496
+
497
+ /* Radios drive tab visibility (works even if JS fails / is blocked) */
498
+ .tab-radio {
499
+ position: absolute;
500
+ left: -9999px;
501
+ width: 1px;
502
+ height: 1px;
503
+ overflow: hidden;
504
+ }
505
+
506
+ #tab-radio-playground:checked ~ #tab-playground {
507
+ display: block;
508
+ }
509
+ #tab-radio-stream:checked ~ #tab-stream {
510
+ display: block;
511
+ }
512
+
513
+ /* Selected tab styling without JS */
514
+ #tab-radio-playground:checked ~ .tabs .tab-btn[data-tab='playground'],
515
+ #tab-radio-stream:checked ~ .tabs .tab-btn[data-tab='stream'] {
516
+ background: #2563eb;
517
+ border-color: #2563eb;
518
+ color: #ffffff;
519
+ box-shadow: 0 10px 15px rgba(37, 99, 235, 0.22);
520
+ }
521
+
522
+ .playground-inner {
523
+ max-width: 960px;
524
+ margin: 0 auto;
525
+ display: flex;
526
+ flex-direction: column;
527
+ gap: 1rem;
528
+ }
529
+
530
+ /* --- Stream app (scoped) --- */
531
+ #streamApp {
532
+ --s-bg: #f6f7fb;
533
+ --s-panel: #ffffff;
534
+ --s-text: #1b2430;
535
+ --s-muted: #5b6b7c;
536
+ --s-line: #d6dee8;
537
+ --s-btn: #2563eb;
538
+ --s-btn2: #e7edf7;
539
+ --s-codebg: #f3f5f9;
540
+ background: var(--s-bg);
541
+ border-radius: 1rem;
542
+ border: 1px solid rgba(148, 163, 184, 0.25);
543
+ box-shadow: 0 18px 40px rgba(15, 23, 42, 0.06);
544
+ overflow: hidden;
545
+ }
546
+ #streamApp .stream-topbar {
547
+ padding: 10px 12px;
548
+ border-bottom: 1px solid var(--s-line);
549
+ display: flex;
550
+ gap: 10px;
551
+ align-items: center;
552
+ background: #fff;
553
+ flex-wrap: wrap;
554
+ position: sticky;
555
+ top: 0;
556
+ z-index: 10;
557
+ }
558
+ #streamApp .stream-topbar input {
559
+ background: #fff;
560
+ border: 1px solid var(--s-line);
561
+ color: var(--s-text);
562
+ padding: 9px 10px;
563
+ border-radius: 10px;
564
+ flex: 1;
565
+ min-width: 220px;
566
+ }
567
+ #streamApp .stream-topbar button {
568
+ background: var(--s-btn);
569
+ border: 0;
570
+ color: #fff;
571
+ padding: 9px 12px;
572
+ border-radius: 10px;
573
+ cursor: pointer;
574
+ font-weight: 700;
575
+ white-space: nowrap;
576
+ min-height: 40px;
577
+ }
578
+ #streamApp .stream-topbar button.secondary {
579
+ background: var(--s-btn2);
580
+ color: var(--s-text);
581
+ border: 1px solid var(--s-line);
582
+ font-weight: 700;
583
+ }
584
+ #streamApp .stream-topbar button:disabled {
585
+ opacity: 0.55;
586
+ cursor: not-allowed;
587
+ }
588
+ #streamApp .stream-layout {
589
+ display: grid;
590
+ grid-template-columns: 1fr 1fr;
591
+ gap: 12px;
592
+ padding: 12px;
593
+ box-sizing: border-box;
594
+ max-width: 1500px;
595
+ margin: 0 auto;
596
+ align-items: start;
597
+ }
598
+ @media (max-width: 980px) {
599
+ #streamApp .stream-layout {
600
+ grid-template-columns: 1fr;
601
+ }
602
+ }
603
+ #streamApp .stream-col {
604
+ display: flex;
605
+ flex-direction: column;
606
+ gap: 12px;
607
+ min-width: 0;
608
+ }
609
+
610
+ #streamApp .stream-panel {
611
+ background: var(--s-panel);
612
+ border: 1px solid var(--s-line);
613
+ border-radius: 14px;
614
+ display: flex;
615
+ flex-direction: column;
616
+ overflow: hidden;
617
+ box-shadow: 0 4px 16px rgba(16, 24, 40, 0.06);
618
+ }
619
+ #streamApp .stream-panel h2 {
620
+ margin: 0;
621
+ padding: 10px 12px;
622
+ font-size: 14px;
623
+ font-weight: 900;
624
+ border-bottom: 1px solid var(--s-line);
625
+ color: var(--s-muted);
626
+ letter-spacing: 0.2px;
627
+ }
628
+ #streamApp .stream-panel .content {
629
+ padding: 12px;
630
+ display: flex;
631
+ flex-direction: column;
632
+ gap: 10px;
633
+ }
634
+
635
+ #streamApp .row {
636
+ display: flex;
637
+ gap: 10px;
638
+ align-items: center;
639
+ flex-wrap: wrap;
640
+ }
641
+ #streamApp .row label {
642
+ font-size: 12px;
643
+ color: var(--s-muted);
644
+ display: flex;
645
+ gap: 6px;
646
+ align-items: center;
647
+ }
648
+ #streamApp .muted {
649
+ color: var(--s-muted);
650
+ font-size: 12px;
651
+ }
652
+ #streamApp .status {
653
+ font-size: 12px;
654
+ color: var(--s-muted);
655
+ }
656
+ #streamApp .badge {
657
+ display: inline-block;
658
+ padding: 3px 8px;
659
+ border-radius: 999px;
660
+ font-size: 12px;
661
+ border: 1px solid var(--s-line);
662
+ background: #fff;
663
+ color: var(--s-muted);
664
+ }
665
+
666
+ #streamApp textarea {
667
+ width: 100%;
668
+ background: var(--s-codebg);
669
+ border: 1px solid var(--s-line);
670
+ color: var(--s-text);
671
+ padding: 10px;
672
+ border-radius: 12px;
673
+ box-sizing: border-box;
674
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
675
+ font-size: 12px;
676
+ line-height: 1.35;
677
+ }
678
+ #streamApp textarea.preview {
679
+ height: 220px;
680
+ overflow: auto;
681
+ resize: vertical;
682
+ }
683
+ #streamApp textarea.full {
684
+ height: 100%;
685
+ resize: none;
686
+ overflow: auto;
687
+ }
688
+ #streamApp .list {
689
+ display: flex;
690
+ flex-direction: column;
691
+ gap: 8px;
692
+ max-height: 240px;
693
+ overflow: auto;
694
+ padding-right: 2px;
695
+ }
696
+ #streamApp .item {
697
+ padding: 10px 12px;
698
+ border: 1px solid var(--s-line);
699
+ border-radius: 12px;
700
+ background: #fff;
701
+ cursor: pointer;
702
+ user-select: none;
703
+ }
704
+ #streamApp .item:hover {
705
+ border-color: #9db6ee;
706
+ box-shadow: 0 2px 10px rgba(37, 99, 235, 0.1);
707
+ }
708
+ #streamApp .item.selected {
709
+ border-color: #2563eb;
710
+ box-shadow: 0 2px 12px rgba(37, 99, 235, 0.16);
711
+ }
712
+ #streamApp .mono {
713
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
714
+ }
715
+
716
+ #streamApp .out {
717
+ white-space: pre-wrap;
718
+ word-break: break-word;
719
+ background: var(--s-codebg);
720
+ border: 1px solid var(--s-line);
721
+ border-radius: 12px;
722
+ padding: 10px;
723
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
724
+ font-size: 12px;
725
+ line-height: 1.35;
726
+ max-height: min(52vh, 560px);
727
+ overflow: auto;
728
+ }
729
+
730
+ #streamApp .modalBackdrop {
731
+ position: fixed;
732
+ inset: 0;
733
+ background: rgba(12, 18, 28, 0.55);
734
+ display: none;
735
+ align-items: center;
736
+ justify-content: center;
737
+ padding: 18px;
738
+ z-index: 1000;
739
+ }
740
+ #streamApp .modal {
741
+ width: min(1100px, 96vw);
742
+ height: min(86vh, 900px);
743
+ background: #fff;
744
+ border-radius: 16px;
745
+ border: 1px solid var(--s-line);
746
+ box-shadow: 0 20px 80px rgba(0, 0, 0, 0.25);
747
+ display: flex;
748
+ flex-direction: column;
749
+ overflow: hidden;
750
+ }
751
+ #streamApp .modalHeader {
752
+ padding: 10px 12px;
753
+ border-bottom: 1px solid var(--s-line);
754
+ display: flex;
755
+ align-items: center;
756
+ justify-content: space-between;
757
+ gap: 10px;
758
+ background: #fff;
759
+ }
760
+ #streamApp .modalHeader .title {
761
+ font-weight: 900;
762
+ color: var(--s-muted);
763
+ font-size: 14px;
764
+ }
765
+ #streamApp .modalBody {
766
+ padding: 12px;
767
+ display: flex;
768
+ flex-direction: column;
769
+ gap: 10px;
770
+ height: 100%;
771
+ }
772
+ </style>
773
+
774
+ <script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/lib/codemirror.min.js"></script>
775
+ <script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/mode/turtle/turtle.min.js"></script>
776
+ <script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/mode/sparql/sparql.min.js"></script>
777
+ </head>
778
+
779
+ <body>
780
+ <div class="page">
781
+ <header>
782
+ <h1>Eyeling N3 Playground</h1>
783
+ <p>
784
+ Edit the N3 program below, load it from a URL, or share this page with the program encoded in the fragment.
785
+ </p>
786
+ <p class="meta">
787
+ Powered by
788
+ <a href="https://eyereasoner.github.io/eyeling/" target="_blank" rel="noopener noreferrer">Eyeling</a>
789
+ — running version <strong>v<span id="eyeling-version">…</span></strong
790
+ >.
791
+ </p>
792
+ </header>
793
+
794
+ <input class="tab-radio" type="radio" name="eyeling-demo-tab" id="tab-radio-playground" checked />
795
+ <input class="tab-radio" type="radio" name="eyeling-demo-tab" id="tab-radio-stream" />
796
+
797
+ <nav class="tabs" role="tablist" aria-label="Eyeling demos">
798
+ <label
799
+ class="tab-btn"
800
+ role="tab"
801
+ tabindex="0"
802
+ data-tab="playground"
803
+ aria-controls="tab-playground"
804
+ aria-selected="true"
805
+ for="tab-radio-playground"
806
+ >Playground</label
807
+ >
808
+ <label
809
+ class="tab-btn"
810
+ role="tab"
811
+ tabindex="0"
812
+ data-tab="stream"
813
+ aria-controls="tab-stream"
814
+ aria-selected="false"
815
+ for="tab-radio-stream"
816
+ >Streaming demo</label
817
+ >
818
+ </nav>
819
+
820
+ <div id="tab-playground" class="tab-content" role="tabpanel" aria-label="Playground">
821
+ <div class="playground-inner">
822
+ <section class="card">
823
+ <h2 class="section-title">Input N3</h2>
824
+
825
+ <div class="uri-row">
826
+ <label for="n3-uri">Load N3 from URL</label>
827
+ <div class="uri-input-row">
828
+ <input
829
+ id="n3-uri"
830
+ type="text"
831
+ value="https://raw.githubusercontent.com/eyereasoner/eyeling/refs/heads/main/examples/witch.n3" />
832
+ <button id="load-uri-btn" type="button">Load from URL</button>
833
+ </div>
834
+ <span class="hint"> Enter any N3 file URL (raw GitHub URLs work well) and click “Load from URL”. </span>
835
+ </div>
836
+
837
+ <label for="n3-editor">Editable N3 program</label>
838
+ <textarea id="n3-editor" spellcheck="false"></textarea>
839
+
840
+ <div class="controls">
841
+ <button id="run-btn">Run reasoning</button>
842
+ <button id="pause-btn" class="secondary" disabled>Pause</button>
843
+ <button id="stop-btn" class="danger" disabled>Stop</button>
844
+
845
+ <label class="toggle" title="Runs Eyeling with -p (proof comments)">
846
+ <input id="proof-comments" type="checkbox" />
847
+ Turn proof comments on (-p)
848
+ </label>
849
+ <label
850
+ class="toggle"
851
+ title="Rewrite http:// IRIs to https:// for log dereferencing builtins (mixed-content safe)">
852
+ <input id="enforce-https" type="checkbox" checked />
853
+ Enforce HTTPS for dereferencing (--enforce-https)
854
+ </label>
855
+
856
+ <span class="status" id="status">Idle.</span>
857
+ </div>
858
+ </section>
859
+
860
+ <section class="card">
861
+ <h2 class="section-title">Output</h2>
862
+ <textarea id="output-editor" spellcheck="false" readonly>(no output yet)</textarea>
863
+ </section>
864
+ </div>
865
+ </div>
866
+
867
+ <div id="tab-stream" class="tab-content" role="tabpanel" aria-label="Streaming demo">
868
+ <section class="card" style="padding: 0; overflow: hidden">
869
+ <div id="streamApp">
870
+ <div class="stream-topbar">
871
+ <input id="stream-searchBox" placeholder="Search Wikidata (e.g., René Magritte, Q7836)..." />
872
+ <button id="stream-searchBtn">Search</button>
873
+ <button id="stream-loadBtn" class="secondary" disabled>Load selection</button>
874
+ <button id="stream-toyBtn" class="secondary">Load toy example</button>
875
+ <button id="stream-clearBtn" class="secondary">Clear output</button>
876
+
877
+ <label class="muted" style="display: flex; align-items: center; gap: 6px; margin-left: 4px">
878
+ <input type="checkbox" id="stream-clearOnRun" />clear on run
879
+ </label>
880
+
881
+ <label
882
+ class="muted"
883
+ title="Rewrite http:// IRIs to https:// for log dereferencing builtins (mixed-content safe)"
884
+ style="display: flex; align-items: center; gap: 6px; margin-left: 4px">
885
+ <input type="checkbox" id="stream-enforceHttps" checked />enforce https
886
+ </label>
887
+
888
+ <span id="stream-eyelingVersion" class="badge" style="margin-left: auto">Eyeling</span>
889
+ <button id="stream-reasonBtn" disabled>Reason (stream)</button>
890
+ <button id="stream-pauseBtn" class="secondary" disabled>Pause</button>
891
+ <button id="stream-stopBtn" class="danger" disabled>Stop</button>
892
+ </div>
893
+
894
+ <div class="stream-layout">
895
+ <div class="stream-col">
896
+ <section class="stream-panel">
897
+ <h2>Wikidata browser + dataset</h2>
898
+ <div class="content">
899
+ <div class="row">
900
+ <label title="Uses Wikidata API (origin=*) instead of EntityData .ttl, so it works under CORS">
901
+ <input type="checkbox" id="stream-minimalOnly" checked />
902
+ minimal dataset (entity-value claims + sitelinks)
903
+ </label>
904
+ <label
905
+ title="If derived triples request wikiquote, fetch extract via it.wikiquote API (origin=*)">
906
+ <input type="checkbox" id="stream-autoFetchWikiquote" checked />
907
+ auto-fetch it.wikiquote extract when requested
908
+ </label>
909
+ <span class="badge" title="Default: René Magritte (Q7836)">default: Magritte</span>
910
+ </div>
911
+
912
+ <div class="muted">
913
+ Selected: <span id="stream-selId" class="mono">—</span>
914
+ <span id="stream-selLabel"></span>
915
+ <span id="stream-dsInfo" class="badge" style="display: none"></span>
916
+ </div>
917
+
918
+ <div class="list" id="stream-results"></div>
919
+
920
+ <div class="row">
921
+ <div class="muted" style="flex: 1">Dataset preview. Full dataset opens in a pop-up.</div>
922
+ <button id="stream-openDatasetBtn" class="secondary" disabled>Open full dataset</button>
923
+ </div>
924
+ <textarea id="stream-dataPreview" class="preview" spellcheck="false" readonly></textarea>
925
+ </div>
926
+ </section>
927
+ </div>
928
+
929
+ <div class="stream-col">
930
+ <section class="stream-panel">
931
+ <h2>Streaming deductive closure</h2>
932
+ <div class="content">
933
+ <div class="row">
934
+ <span class="status" id="stream-runStatus">Idle.</span>
935
+ <span class="status">Derived: <span id="stream-derivedCount">0</span></span>
936
+ <span class="status">Fetched facts: <span id="stream-fetchedCount">0</span></span>
937
+ </div>
938
+ <div class="out" id="stream-outBox"></div>
939
+ </div>
940
+ </section>
941
+
942
+ <section class="stream-panel">
943
+ <h2>N3 logic rules</h2>
944
+ <div class="content">
945
+ <div class="muted">
946
+ This version avoids Wikidata TTL fetches (CORS issues) by using the Wikidata API + a small N3
947
+ conversion. It also demonstrates “dynamic fetch” by turning a derived request into new facts
948
+ (wikiquote extract).
949
+ </div>
950
+ <textarea id="stream-rulesBox" spellcheck="false"></textarea>
951
+ </div>
952
+ </section>
953
+ </div>
954
+ </div>
955
+
956
+ <div class="modalBackdrop" id="stream-modalBackdrop" aria-hidden="true">
957
+ <div class="modal" role="dialog" aria-modal="true" aria-label="Full dataset">
958
+ <div class="modalHeader">
959
+ <div class="title">Full dataset (Turtle/N3)</div>
960
+ <button id="stream-modalCloseBtn" class="secondary">Close</button>
961
+ </div>
962
+ <div class="modalBody">
963
+ <div class="muted">This is the full Turtle/N3 that Eyeling reasons over.</div>
964
+ <textarea id="stream-dataFull" class="full" spellcheck="false" readonly></textarea>
965
+ </div>
966
+ </div>
967
+ </div>
968
+ </div>
969
+ </section>
970
+ </div>
971
+ </div>
972
+
973
+ <script src="eyeling.js"></script>
974
+
975
+ <script>
976
+ (function () {
977
+ const defaultN3 = `# ------------------
978
+ # Socrates inference
979
+ # ------------------
980
+
981
+ @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>.
982
+ @prefix : <http://example.org/socrates#>.
983
+
984
+ # facts
985
+ :Socrates a :Human.
986
+ :Human rdfs:subClassOf :Mortal.
987
+
988
+ # subclass rule
989
+ {
990
+ ?S a ?A.
991
+ ?A rdfs:subClassOf ?B.
992
+ } => {
993
+ ?S a ?B.
994
+ }.`;
995
+
996
+ const inputTextArea = document.getElementById('n3-editor');
997
+ const outputTextArea = document.getElementById('output-editor');
998
+ const runBtn = document.getElementById('run-btn');
999
+ const pauseBtn = document.getElementById('pause-btn');
1000
+ const stopBtn = document.getElementById('stop-btn');
1001
+ const statusEl = document.getElementById('status');
1002
+ const uriInput = document.getElementById('n3-uri');
1003
+ const loadUriBtn = document.getElementById('load-uri-btn');
1004
+ const proofCheckbox = document.getElementById('proof-comments');
1005
+ const enforceHttpsCheckbox = document.getElementById('enforce-https');
1006
+ const versionEl = document.getElementById('eyeling-version');
1007
+
1008
+ // Persist "enforce https" toggle (default ON)
1009
+ try {
1010
+ const saved = localStorage.getItem('eyeling.enforceHttps');
1011
+ if (saved !== null) enforceHttpsCheckbox.checked = saved === '1';
1012
+ enforceHttpsCheckbox.addEventListener('change', () => {
1013
+ localStorage.setItem('eyeling.enforceHttps', enforceHttpsCheckbox.checked ? '1' : '0');
1014
+ });
1015
+ } catch (_) {}
1016
+
1017
+ const EYELING_JS_URL = 'https://raw.githubusercontent.com/eyereasoner/eyeling/refs/heads/main/eyeling.js';
1018
+ const EYELING_PKG_URL = 'https://raw.githubusercontent.com/eyereasoner/eyeling/refs/heads/main/package.json';
1019
+
1020
+ let eyelingVersion = 'unknown';
1021
+ let versionPromise = null;
1022
+
1023
+ // --- Unified background streaming runner (shared with the Streaming demo tab) ---
1024
+ // Exposes: window.__eyelingUnified.startStreamRun({
1025
+ // program, enforceHttps, proof,
1026
+ // onBatch(derivedTriples[]),
1027
+ // onStdout(text), onStderr(text),
1028
+ // onDone({runId}), onError(message)
1029
+ // })
1030
+ (function initUnifiedEyelingRunner() {
1031
+ const U = window.__eyelingUnified || (window.__eyelingUnified = {});
1032
+ if (U && typeof U.startStreamRun === 'function') return;
1033
+
1034
+ let activeWorker = null;
1035
+ let nextRunId = 0;
1036
+
1037
+ function stopActiveWorker() {
1038
+ if (!activeWorker) return;
1039
+ try {
1040
+ activeWorker.terminate();
1041
+ } catch (_) {}
1042
+ activeWorker = null;
1043
+ }
1044
+
1045
+ function nowMs() {
1046
+ return window.performance && performance.now ? performance.now() : Date.now();
1047
+ }
1048
+
1049
+ // --- Syntax error formatting (for CodeMirror highlighting) ---
1050
+ // Eyeling's parser throws N3SyntaxError with a codepoint offset. We convert that into line/col.
1051
+ function offsetToLineCol(text, offset) {
1052
+ const chars = Array.from(String(text || ''));
1053
+ const n = Math.max(0, Math.min(typeof offset === 'number' ? offset : 0, chars.length));
1054
+ let line = 1;
1055
+ let col = 1;
1056
+ for (let i = 0; i < n; i++) {
1057
+ const c = chars[i];
1058
+ if (c === '\n') {
1059
+ line++;
1060
+ col = 1;
1061
+ } else if (c === '\r') {
1062
+ line++;
1063
+ col = 1;
1064
+ if (i + 1 < n && chars[i + 1] === '\n') i++; // swallow \n in CRLF
1065
+ } else {
1066
+ col++;
1067
+ }
1068
+ }
1069
+ return { line, col };
1070
+ }
1071
+
1072
+ function formatN3SyntaxError(err, text, label) {
1073
+ const off = err && typeof err.offset === 'number' ? err.offset : null;
1074
+ const lbl = label ? String(label) : '';
1075
+ if (off === null) {
1076
+ return 'Syntax error in ' + lbl + ': ' + (err && err.message ? err.message : String(err));
1077
+ }
1078
+ const lc = offsetToLineCol(text, off);
1079
+ const lines = String(text || '').split(/\r\n|\n|\r/);
1080
+ const lineText = lines[lc.line - 1] ?? '';
1081
+ const caret = ' '.repeat(Math.max(0, lc.col - 1)) + '^';
1082
+ return (
1083
+ 'Syntax error in ' +
1084
+ lbl +
1085
+ ':' +
1086
+ lc.line +
1087
+ ':' +
1088
+ lc.col +
1089
+ ': ' +
1090
+ (err && err.message ? err.message : 'Syntax error') +
1091
+ '\n' +
1092
+ lineText +
1093
+ '\n' +
1094
+ caret
1095
+ );
1096
+ }
1097
+
1098
+ function formatMaybeSyntaxError(err, text, label) {
1099
+ if (err && err.name === 'N3SyntaxError') {
1100
+ return formatN3SyntaxError(err, text, label);
1101
+ }
1102
+ return err && err.message ? err.message : String(err);
1103
+ }
1104
+
1105
+ // Start a run. Creates/terminates workers as needed (single active run at a time).
1106
+ U.startStreamRun = function startStreamRun(args) {
1107
+ const program = String(args && args.program ? args.program : '');
1108
+ const enforceHttps = !!(args && args.enforceHttps);
1109
+ const proof = !!(args && args.proof);
1110
+ const onBatch = args && typeof args.onBatch === 'function' ? args.onBatch : null;
1111
+ const onStdout = args && typeof args.onStdout === 'function' ? args.onStdout : null;
1112
+ const onStderr = args && typeof args.onStderr === 'function' ? args.onStderr : null;
1113
+ const onDone = args && typeof args.onDone === 'function' ? args.onDone : null;
1114
+ const onError = args && typeof args.onError === 'function' ? args.onError : null;
1115
+
1116
+ const runId = ++nextRunId;
1117
+
1118
+ // Best-effort fallback for environments without Web Workers.
1119
+ if (typeof Worker === 'undefined') {
1120
+ try {
1121
+ if (!window.eyeling || typeof window.eyeling.reasonStream !== 'function') {
1122
+ throw new Error('Eyeling reasonStream API not available.');
1123
+ }
1124
+ const batch = [];
1125
+ const BATCH_MAX = 16;
1126
+
1127
+ function flush(force) {
1128
+ if (!batch.length) return;
1129
+ if (force || batch.length >= BATCH_MAX) {
1130
+ onBatch && onBatch(batch.splice(0));
1131
+ }
1132
+ }
1133
+
1134
+ // Capture stderr (log:trace uses console.error in browser/worker)
1135
+ const __origConsoleError = console.error;
1136
+ console.error = (...args) => {
1137
+ try {
1138
+ onStderr && onStderr(args.join(' '));
1139
+ } catch (_) {}
1140
+ try {
1141
+ __origConsoleError && __origConsoleError.apply(console, args);
1142
+ } catch (_) {}
1143
+ };
1144
+
1145
+ try {
1146
+ window.eyeling.reasonStream(program, {
1147
+ enforceHttps,
1148
+ proof,
1149
+ onDerived: ({ triple }) => {
1150
+ batch.push(triple);
1151
+ flush(false);
1152
+ },
1153
+ });
1154
+ } finally {
1155
+ console.error = __origConsoleError;
1156
+ }
1157
+ flush(true);
1158
+ onDone && onDone({ runId });
1159
+ } catch (e) {
1160
+ onError && onError(formatMaybeSyntaxError(e, program, 'input.n3'));
1161
+ }
1162
+
1163
+ return { runId, cancel: () => {} };
1164
+ }
1165
+
1166
+ // Worker path (keeps UI responsive and enables true streaming)
1167
+ stopActiveWorker();
1168
+
1169
+ const eyelingUrl = new URL('eyeling.js', window.location.href).toString();
1170
+ const eyelingUrlEsc = eyelingUrl.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
1171
+
1172
+ const workerSrc = `
1173
+ importScripts("${eyelingUrlEsc}");
1174
+ const now = () => (self.performance && performance.now) ? performance.now() : Date.now();
1175
+
1176
+ self.onmessage = (ev) => {
1177
+ const msg = ev && ev.data ? ev.data : {};
1178
+ if (msg.type !== "run") return;
1179
+
1180
+ const runId = msg.runId;
1181
+ const program = String(msg.program || "");
1182
+ const enforceHttps = !!msg.enforceHttps;
1183
+ const proof = !!msg.proof;
1184
+
1185
+ // Forward stdout/stderr to the main thread.
1186
+ // - stdout is used for proof comments (when enabled)
1187
+ // - stderr is used by log:trace in browser/worker builds
1188
+ const __origLog = console.log;
1189
+ console.log = (...args) => {
1190
+ try { self.postMessage({ type: "stdout", runId, text: args.join(" ") }); } catch (_) {}
1191
+ try { __origLog && __origLog.apply(console, args); } catch (_) {}
1192
+ };
1193
+ const __origErr = console.error;
1194
+ console.error = (...args) => {
1195
+ try { self.postMessage({ type: "stderr", runId, text: args.join(" ") }); } catch (_) {}
1196
+ try { __origErr && __origErr.apply(console, args); } catch (_) {}
1197
+ };
1198
+
1199
+ const batch = [];
1200
+ const BATCH_MAX = 10; // small batches = more "real-time"
1201
+ const MAX_LATENCY_MS = 15; // try to post at least every ~15ms
1202
+ let lastFlush = now();
1203
+
1204
+ function flush(force) {
1205
+ const t = now();
1206
+ if (!batch.length) return;
1207
+ if (force || batch.length >= BATCH_MAX || (t - lastFlush) >= MAX_LATENCY_MS) {
1208
+ // Copy the array so the main thread can process immediately.
1209
+ self.postMessage({ type: "derived_batch", runId, triples: batch.splice(0) });
1210
+ lastFlush = t;
1211
+ }
1212
+ }
1213
+
1214
+ try {
1215
+ // Implement a streaming run that *also* supports proof comments.
1216
+ // The public API (eyeling.reasonStream) streams triples but does not print
1217
+ // proof explanations; those are printed only in the CLI path. Here we
1218
+ // reuse the same internal functions and forward console.log output.
1219
+ if (typeof lex !== "function" || typeof Parser !== "function" || typeof forwardChain !== "function") {
1220
+ throw new Error("Eyeling internals not available (lex/Parser/forwardChain missing).");
1221
+ }
1222
+
1223
+ const __oldEnforce = (typeof enforceHttpsEnabled !== "undefined") ? enforceHttpsEnabled : false;
1224
+ const __oldProof = (typeof proofCommentsEnabled !== "undefined") ? proofCommentsEnabled : false;
1225
+ const __oldTrace = (typeof __tracePrefixes !== "undefined") ? __tracePrefixes : null;
1226
+
1227
+ try {
1228
+ if (typeof enforceHttpsEnabled !== "undefined") enforceHttpsEnabled = !!enforceHttps;
1229
+ if (typeof proofCommentsEnabled !== "undefined") proofCommentsEnabled = !!proof;
1230
+
1231
+ const toks = lex(program);
1232
+ const parser = new Parser(toks);
1233
+ let prefixes, triples, frules, brules;
1234
+ [prefixes, triples, frules, brules] = parser.parseDocument();
1235
+
1236
+ // Make parsed prefixes available to log:trace output.
1237
+ try { if (typeof __tracePrefixes !== "undefined") __tracePrefixes = prefixes; } catch (_) {}
1238
+
1239
+ // Build rdf:List internal terms from rdf:first/rdf:rest.
1240
+ if (typeof materializeRdfLists === "function") {
1241
+ materializeRdfLists(triples, frules, brules);
1242
+ }
1243
+
1244
+ const facts = triples.filter((tr) => (typeof isGroundTriple === "function") ? isGroundTriple(tr) : true);
1245
+
1246
+ forwardChain(facts, frules, brules, (df) => {
1247
+ try {
1248
+ if (proof && typeof printExplanation === "function") {
1249
+ // Prints via console.log (forwarded to main thread as stdout).
1250
+ printExplanation(df, prefixes);
1251
+ }
1252
+ } catch (_) {}
1253
+
1254
+ // Derived triple (as N3, using the parsed prefixes)
1255
+ let t = "";
1256
+ try {
1257
+ if (typeof tripleToN3 === "function") t = tripleToN3(df.fact, prefixes);
1258
+ else if (df && df.fact && df.fact.toString) t = String(df.fact);
1259
+ } catch (_) {}
1260
+
1261
+ if (t) {
1262
+ batch.push(t);
1263
+ // When proof comments are enabled, flush immediately so the
1264
+ // derived triple follows the proof block closely.
1265
+ flush(!!proof);
1266
+ }
1267
+ });
1268
+ } finally {
1269
+ try { if (typeof enforceHttpsEnabled !== "undefined") enforceHttpsEnabled = __oldEnforce; } catch (_) {}
1270
+ try { if (typeof proofCommentsEnabled !== "undefined") proofCommentsEnabled = __oldProof; } catch (_) {}
1271
+ try { if (typeof __tracePrefixes !== "undefined") __tracePrefixes = __oldTrace; } catch (_) {}
1272
+ }
1273
+
1274
+ flush(true);
1275
+ self.postMessage({ type: "done", runId });
1276
+ } catch (e) {
1277
+ let msgText = (e && e.message) ? e.message : String(e);
1278
+ try {
1279
+ if (e && e.name === "N3SyntaxError" && typeof e.offset === "number") {
1280
+ const chars = Array.from(String(program || ""));
1281
+ const n = Math.max(0, Math.min(e.offset, chars.length));
1282
+ let line = 1;
1283
+ let col = 1;
1284
+ for (let i = 0; i < n; i++) {
1285
+ const c = chars[i];
1286
+ if (c === "\\n") { line++; col = 1; }
1287
+ else if (c === "\\r") { line++; col = 1; if (i + 1 < n && chars[i + 1] === "\\n") i++; }
1288
+ else { col++; }
1289
+ }
1290
+ const progLines = String(program || "").split(/\\r\\n|\\n|\\r/);
1291
+ const lineText = (progLines[line - 1] ?? "");
1292
+ const caret = " ".repeat(Math.max(0, col - 1)) + "^";
1293
+ msgText = "Syntax error in input.n3:" + line + ":" + col + ": " + (e.message || "Syntax error") +
1294
+ "\\n" + lineText + "\\n" + caret;
1295
+ }
1296
+ } catch (_e2) {}
1297
+ self.postMessage({
1298
+ type: "error",
1299
+ runId,
1300
+ message: msgText,
1301
+ });
1302
+ } finally {
1303
+ try { console.log = __origLog; } catch (_) {}
1304
+ try { console.error = __origErr; } catch (_) {}
1305
+ }
1306
+ };
1307
+ `;
1308
+
1309
+ const blob = new Blob([workerSrc], { type: 'text/javascript' });
1310
+ const url = URL.createObjectURL(blob);
1311
+ const w = new Worker(url);
1312
+ activeWorker = w;
1313
+
1314
+ let finished = false;
1315
+ function cleanup() {
1316
+ if (finished) return;
1317
+ finished = true;
1318
+ try {
1319
+ URL.revokeObjectURL(url);
1320
+ } catch (_) {}
1321
+ }
1322
+
1323
+ w.onmessage = (ev) => {
1324
+ const msg = ev && ev.data ? ev.data : {};
1325
+ if (msg.runId !== runId) return;
1326
+
1327
+ if (msg.type === 'derived_batch') {
1328
+ onBatch && onBatch(msg.triples || []);
1329
+ } else if (msg.type === 'stdout') {
1330
+ const t = msg.text == null ? '' : String(msg.text);
1331
+ if (t) onStdout && onStdout(t);
1332
+ } else if (msg.type === 'stderr') {
1333
+ const t = msg.text == null ? '' : String(msg.text);
1334
+ if (t) onStderr && onStderr(t);
1335
+ } else if (msg.type === 'done') {
1336
+ cleanup();
1337
+ onDone && onDone({ runId });
1338
+ } else if (msg.type === 'error') {
1339
+ cleanup();
1340
+ onError && onError(msg.message || 'Unknown error');
1341
+ }
1342
+ };
1343
+
1344
+ w.onerror = (ev) => {
1345
+ cleanup();
1346
+ onError && onError('Worker error: ' + (ev && ev.message ? ev.message : 'unknown'));
1347
+ };
1348
+
1349
+ // Kick off the work
1350
+ w.postMessage({
1351
+ type: 'run',
1352
+ runId,
1353
+ program,
1354
+ enforceHttps,
1355
+ proof,
1356
+ });
1357
+
1358
+ return {
1359
+ runId,
1360
+ cancel: () => {
1361
+ try {
1362
+ w.terminate();
1363
+ } catch (_) {}
1364
+ cleanup();
1365
+ if (activeWorker === w) activeWorker = null;
1366
+ },
1367
+ };
1368
+ };
1369
+ })();
1370
+
1371
+ function isSmallScreen() {
1372
+ return window.matchMedia && window.matchMedia('(max-width: 640px)').matches;
1373
+ }
1374
+
1375
+ function currentMaxHeights() {
1376
+ // On mobile we cap editor/output height and allow internal scrolling
1377
+ if (!isSmallScreen()) return { editor: null, output: null };
1378
+ const h = window.innerHeight || 800;
1379
+ return {
1380
+ editor: Math.max(260, Math.floor(h * 0.55)),
1381
+ output: Math.max(160, Math.floor(h * 0.35)),
1382
+ };
1383
+ }
1384
+
1385
+ function autoResizeCodeMirror(cm, minHeightPx, maxHeightPx) {
1386
+ // Ensure CodeMirror fits its *rendered* content (no internal scrollbars unless capped).
1387
+ // IMPORTANT: don't use the scroller's scrollHeight here — with overflow hidden
1388
+ // it can create a feedback loop where the editor grows a bit on every keystroke.
1389
+ cm.refresh();
1390
+
1391
+ const wrapper = cm.getWrapperElement();
1392
+
1393
+ // Desktop cap for the Playground Output pane: ~50 lines max (scroll inside).
1394
+ if (wrapper && wrapper.classList && wrapper.classList.contains('cm-output')) {
1395
+ const lineCapPx = Math.round(cm.defaultTextHeight() * 50 + cm.defaultTextHeight());
1396
+ if (Number.isFinite(lineCapPx) && lineCapPx > 0) {
1397
+ maxHeightPx = typeof maxHeightPx === 'number' ? Math.min(maxHeightPx, lineCapPx) : lineCapPx;
1398
+ }
1399
+ }
1400
+ const code = wrapper && wrapper.querySelector('.CodeMirror-code');
1401
+ const sizer = wrapper && wrapper.querySelector('.CodeMirror-sizer');
1402
+ const measureEl = code || sizer;
1403
+
1404
+ const contentHeight = measureEl ? measureEl.getBoundingClientRect().height : cm.getScrollInfo().height;
1405
+
1406
+ // Add a bit of breathing room so the last line isn't flush with the bottom.
1407
+ const padding = cm.defaultTextHeight();
1408
+
1409
+ let target = Math.max(Math.ceil(contentHeight + padding), minHeightPx);
1410
+ if (typeof maxHeightPx === 'number') target = Math.min(target, maxHeightPx);
1411
+
1412
+ cm.setSize(null, target + 'px');
1413
+ }
1414
+
1415
+ function resizeAll() {
1416
+ const mh = currentMaxHeights();
1417
+ autoResizeCodeMirror(editor, 220, mh.editor);
1418
+ autoResizeCodeMirror(outputEl, 80, mh.output);
1419
+ }
1420
+
1421
+ // --- INITIAL CONTENT FROM HASH OR DEFAULT ---
1422
+ function getProgramFromHash() {
1423
+ const hash = window.location.hash || '';
1424
+ if (!hash) return null;
1425
+ const raw = hash.startsWith('#') ? hash.slice(1) : hash;
1426
+ if (!raw) return null;
1427
+ try {
1428
+ return decodeURIComponent(raw);
1429
+ } catch (e) {
1430
+ return raw;
1431
+ }
1432
+ }
1433
+
1434
+ // --- Prefill + auto-load "Load N3 from URL" via query string ---
1435
+ // Usage:
1436
+ // demo?url=https://example.com/file.n3
1437
+ // Notes:
1438
+ // - If a #fragment is present, it takes precedence (existing behavior).
1439
+ // - When loading from ?url=..., we avoid rewriting the hash until the user edits,
1440
+ // so you can keep sharing a clean ?url=... link.
1441
+ function getN3UriFromQuery() {
1442
+ try {
1443
+ const url = new URL(window.location.href);
1444
+ // support a few aliases, but "url" is the main one
1445
+ return (
1446
+ url.searchParams.get('url') ||
1447
+ url.searchParams.get('n3url') ||
1448
+ url.searchParams.get('n3') ||
1449
+ url.searchParams.get('load') ||
1450
+ null
1451
+ );
1452
+ } catch (e) {
1453
+ return null;
1454
+ }
1455
+ }
1456
+
1457
+ const paramProgram = getProgramFromHash();
1458
+ const paramN3Uri = getN3UriFromQuery();
1459
+
1460
+ if (paramN3Uri) {
1461
+ uriInput.value = paramN3Uri;
1462
+ }
1463
+
1464
+ // In "URL-load mode" (no fragment, but ?url=...), don't start syncing to #hash
1465
+ // until the user actually edits the editor.
1466
+ let hashSyncEnabled = paramProgram !== null || !paramN3Uri;
1467
+
1468
+ const initialN3 = paramProgram !== null ? paramProgram : defaultN3;
1469
+
1470
+ // CodeMirror editors (syntax-highlighted)
1471
+ const editor = CodeMirror.fromTextArea(inputTextArea, {
1472
+ mode: { name: 'sparql' },
1473
+ lineNumbers: true,
1474
+ lineWrapping: true,
1475
+ viewportMargin: Infinity,
1476
+ });
1477
+
1478
+ const outputEl = CodeMirror.fromTextArea(outputTextArea, {
1479
+ mode: { name: 'sparql' },
1480
+ lineNumbers: true,
1481
+ lineWrapping: true,
1482
+ readOnly: true,
1483
+ viewportMargin: Infinity,
1484
+ });
1485
+
1486
+ // Slightly smaller font for output
1487
+ outputEl.getWrapperElement().classList.add('cm-output');
1488
+
1489
+ // --- N3 SYNTAX ERROR HIGHLIGHTING ---
1490
+ let errorLineHandle = null;
1491
+
1492
+ function clearErrorHighlight() {
1493
+ if (!errorLineHandle) return;
1494
+ editor.removeLineClass(errorLineHandle, 'background', 'cm-error-line');
1495
+ errorLineHandle = null;
1496
+ }
1497
+
1498
+ function highlightErrorFromOutput(outputText) {
1499
+ if (!outputText) return;
1500
+
1501
+ // Eyeling error messages vary a bit. We try to find the first occurrence of
1502
+ // something like: Syntax error ...:<line>:<col>:
1503
+ const text = String(outputText);
1504
+ const lines = text.split(/\r?\n/);
1505
+
1506
+ let m = null;
1507
+
1508
+ function tryMatchLine(ln) {
1509
+ if (!ln) return null;
1510
+
1511
+ // Common formats:
1512
+ // Syntax error in <file>:<line>:<col>: ...
1513
+ // Reasoning error: Syntax error ...:<line>:<col>: ...
1514
+ let x =
1515
+ ln.match(/(?:Syntax|Parse) error.*?:(\d+):(\d+)(?::|\b)/i) ||
1516
+ ln.match(/\bline\s+(\d+)\b[^\d]{0,40}\bcol(?:umn)?\s+(\d+)\b/i);
1517
+
1518
+ // If the line mentions "Syntax error", accept a bare :line:col: too.
1519
+ if (!x && /(?:syntax|parse) error/i.test(ln)) {
1520
+ x = ln.match(/:(\d+):(\d+)(?::|\b)/);
1521
+ }
1522
+
1523
+ return x;
1524
+ }
1525
+
1526
+ const scanLimit = Math.min(lines.length, 80);
1527
+
1528
+ // Prefer scanning lines that mention "Syntax error"
1529
+ for (let i = 0; i < scanLimit; i++) {
1530
+ const ln = lines[i];
1531
+ if (!/(?:syntax|parse) error/i.test(ln)) continue;
1532
+ m = tryMatchLine(ln);
1533
+ if (m) break;
1534
+ }
1535
+
1536
+ // Fallback: scan a little more loosely
1537
+ if (!m) {
1538
+ for (let i = 0; i < scanLimit; i++) {
1539
+ m = tryMatchLine(lines[i]);
1540
+ if (m) break;
1541
+ }
1542
+ }
1543
+
1544
+ if (!m) return;
1545
+
1546
+ // Clear any previous highlight now that we found a concrete line/col.
1547
+ clearErrorHighlight();
1548
+
1549
+ const line1 = parseInt(m[1], 10);
1550
+ const col1 = parseInt(m[2], 10);
1551
+ if (!Number.isFinite(line1) || line1 < 0) return;
1552
+
1553
+ // Eyeling reports line/col as 1-based in most cases, but some builds report 0-based.
1554
+ const line = line1 > 0 ? line1 - 1 : 0;
1555
+ const ch = Number.isFinite(col1) ? (col1 > 0 ? col1 - 1 : 0) : 0;
1556
+ if (line < 0 || line >= editor.lineCount()) return;
1557
+
1558
+ errorLineHandle = editor.getLineHandle(line);
1559
+ editor.addLineClass(errorLineHandle, 'background', 'cm-error-line');
1560
+
1561
+ // Nudge the editor to the error location.
1562
+ editor.scrollIntoView({ line, ch }, 80);
1563
+ editor.setCursor({ line, ch });
1564
+ }
1565
+
1566
+ editor.setValue(initialN3);
1567
+ resizeAll();
1568
+
1569
+ // Recompute caps on orientation/resize
1570
+ window.addEventListener(
1571
+ 'resize',
1572
+ () => {
1573
+ resizeAll();
1574
+ },
1575
+ { passive: true },
1576
+ );
1577
+
1578
+ // --- URL SHARING SUPPORT ---
1579
+ let shareUrlUpdateTimeout = null;
1580
+
1581
+ function updateShareUrl() {
1582
+ try {
1583
+ const encoded = encodeURIComponent(editor.getValue());
1584
+ const url = new URL(window.location.href);
1585
+ url.hash = encoded;
1586
+ window.history.replaceState(null, '', url.toString());
1587
+ } catch (e) {
1588
+ console && console.warn && console.warn('Failed to update URL hash:', e);
1589
+ }
1590
+ }
1591
+
1592
+ function scheduleUpdateShareUrl() {
1593
+ clearTimeout(shareUrlUpdateTimeout);
1594
+ shareUrlUpdateTimeout = setTimeout(updateShareUrl, 300);
1595
+ }
1596
+
1597
+ editor.on('change', function (_cm, changeObj) {
1598
+ clearErrorHighlight();
1599
+
1600
+ const mh = currentMaxHeights();
1601
+ autoResizeCodeMirror(editor, 220, mh.editor);
1602
+
1603
+ if (!hashSyncEnabled) {
1604
+ // Only start syncing once the user actually edits (not programmatic setValue).
1605
+ if (changeObj && changeObj.origin && changeObj.origin !== 'setValue') {
1606
+ hashSyncEnabled = true;
1607
+ } else {
1608
+ return;
1609
+ }
1610
+ }
1611
+
1612
+ scheduleUpdateShareUrl();
1613
+ });
1614
+
1615
+ // --- EYELING VERSION (UI + runner) ---
1616
+ function ensureEyelingVersion() {
1617
+ if (versionPromise) return versionPromise;
1618
+
1619
+ versionPromise = (async () => {
1620
+ try {
1621
+ const resp = await fetch(EYELING_PKG_URL, { cache: 'no-store' });
1622
+ if (!resp.ok) throw new Error('HTTP ' + resp.status);
1623
+ const pkg = await resp.json();
1624
+ if (pkg && pkg.version) {
1625
+ eyelingVersion = String(pkg.version);
1626
+ versionEl.textContent = eyelingVersion;
1627
+ } else {
1628
+ eyelingVersion = 'unknown';
1629
+ versionEl.textContent = 'unknown';
1630
+ }
1631
+ } catch (e) {
1632
+ eyelingVersion = 'unknown';
1633
+ versionEl.textContent = 'unknown';
1634
+ }
1635
+ })();
1636
+
1637
+ return versionPromise;
1638
+ }
1639
+
1640
+ // start loading version ASAP
1641
+ ensureEyelingVersion();
1642
+
1643
+ // --- EYELING RUNNER SETUP ---
1644
+ let runnerPromise = null;
1645
+
1646
+ // --- EYELING WORKER SETUP (keeps UI responsive) ---
1647
+ let workerRunnerPromise = null;
1648
+
1649
+ async function getEyelingWorkerRunner() {
1650
+ // Returns an object: { run(n3Text, {proof,enforceHttps}): Promise<{result,durationMs}> }
1651
+ // Falls back to null if Workers are unavailable.
1652
+ if (typeof Worker === 'undefined') return null;
1653
+ if (workerRunnerPromise) return workerRunnerPromise;
1654
+
1655
+ workerRunnerPromise = (async () => {
1656
+ // Ensure we have a version for the worker's ./package.json shim.
1657
+ await ensureEyelingVersion();
1658
+
1659
+ const workerSource = `
1660
+ let cleanedSource = null;
1661
+ let baseVersion = "unknown";
1662
+ let initPromise = null;
1663
+
1664
+ function cleanShebang(src) {
1665
+ // NOTE: This code lives inside a template literal on the main thread.
1666
+ // We need double escapes (\\n) here so the worker sees the backslash-n sequence.
1667
+ return String(src || "").replace(/^#![^\\n]*\\n/, "");
1668
+ }
1669
+
1670
+ async function init(payload) {
1671
+ if (initPromise) return initPromise;
1672
+ initPromise = (async () => {
1673
+ baseVersion = payload && payload.version ? String(payload.version) : "unknown";
1674
+ const jsUrl = payload && payload.eyelingJsUrl ? String(payload.eyelingJsUrl) : "";
1675
+ if (!jsUrl) throw new Error("Missing eyelingJsUrl");
1676
+
1677
+ const resp = await fetch(jsUrl);
1678
+ if (!resp.ok) throw new Error("Failed to load eyeling.js (" + resp.status + ")");
1679
+ const raw = await resp.text();
1680
+ cleanedSource = cleanShebang(raw);
1681
+ })();
1682
+ return initPromise;
1683
+ }
1684
+
1685
+ function runOnce(n3Input, proof, enforceHttps, versionOverride) {
1686
+ const lines = [];
1687
+
1688
+ const consoleShim = {
1689
+ log: (...args) => lines.push(args.join(" ")),
1690
+ error: (...args) => lines.push(args.join(" ")),
1691
+ };
1692
+
1693
+ const fsShim = {
1694
+ readFileSync: () => n3Input,
1695
+ };
1696
+
1697
+ const processShim = {
1698
+ argv: [
1699
+ "node",
1700
+ "eyeling.js",
1701
+ ...(proof ? ["-p"] : []),
1702
+ ...(enforceHttps ? ["--enforce-https"] : []),
1703
+ "input.n3",
1704
+ ],
1705
+ exit: (code) => {
1706
+ throw { __eyelingExit: true, code };
1707
+ },
1708
+ };
1709
+
1710
+ const moduleShim = { exports: {} };
1711
+
1712
+ function requireShim(id) {
1713
+ if (id === "./package.json") {
1714
+ return { version: (versionOverride || baseVersion || "unknown") };
1715
+ }
1716
+ if (id === "fs") return fsShim;
1717
+ if (id === "crypto") {
1718
+ return {
1719
+ createHash: () => ({
1720
+ update: () => {},
1721
+ digest: () => "",
1722
+ }),
1723
+ };
1724
+ }
1725
+ return {};
1726
+ }
1727
+
1728
+ // Make eyeling think this is the main module so main() runs.
1729
+ requireShim.main = moduleShim;
1730
+
1731
+ try {
1732
+ const fn = new Function(
1733
+ "require",
1734
+ "module",
1735
+ "exports",
1736
+ "process",
1737
+ "console",
1738
+ cleanedSource
1739
+ );
1740
+ fn(requireShim, moduleShim, moduleShim.exports, processShim, consoleShim);
1741
+ } catch (e) {
1742
+ if (!e || !e.__eyelingExit) {
1743
+ lines.push(
1744
+ "JavaScript error: " + (e && e.message ? e.message : String(e))
1745
+ );
1746
+ }
1747
+ }
1748
+
1749
+ return lines.join("\\n");
1750
+ }
1751
+
1752
+ const pending = new Map();
1753
+
1754
+ function resolvePending(id, payload) {
1755
+ const p = pending.get(id);
1756
+ if (!p) return;
1757
+ pending.delete(id);
1758
+ p.resolve(payload);
1759
+ }
1760
+
1761
+ function rejectPending(id, err) {
1762
+ const p = pending.get(id);
1763
+ if (!p) return;
1764
+ pending.delete(id);
1765
+ p.reject(err);
1766
+ }
1767
+
1768
+ self.onmessage = async (ev) => {
1769
+ const msg = ev && ev.data ? ev.data : {};
1770
+ try {
1771
+ if (msg.type === "init") {
1772
+ await init(msg);
1773
+ self.postMessage({ type: "inited" });
1774
+ return;
1775
+ }
1776
+ if (msg.type === "run") {
1777
+ if (!initPromise) {
1778
+ // If caller forgot init, try a best-effort init.
1779
+ await init(msg);
1780
+ } else {
1781
+ await initPromise;
1782
+ }
1783
+
1784
+ const t0 = self.performance && performance.now ? performance.now() : Date.now();
1785
+ const result = runOnce(
1786
+ msg.n3Input || "",
1787
+ !!msg.proof,
1788
+ !!msg.enforceHttps,
1789
+ msg.version || baseVersion
1790
+ );
1791
+ const t1 = self.performance && performance.now ? performance.now() : Date.now();
1792
+
1793
+ self.postMessage({
1794
+ type: "result",
1795
+ id: msg.id,
1796
+ result,
1797
+ durationMs: Math.max(0, t1 - t0),
1798
+ });
1799
+ return;
1800
+ }
1801
+ } catch (e) {
1802
+ const errText = e && e.message ? e.message : String(e);
1803
+
1804
+ if (msg && msg.type === "run" && msg.id != null) {
1805
+ self.postMessage({
1806
+ type: "result",
1807
+ id: msg.id,
1808
+ error: errText,
1809
+ durationMs: 0,
1810
+ });
1811
+ return;
1812
+ }
1813
+ self.postMessage({ type: "initError", error: errText });
1814
+ }
1815
+ };
1816
+ `;
1817
+
1818
+ const blob = new Blob([workerSource], { type: 'text/javascript' });
1819
+ const url = URL.createObjectURL(blob);
1820
+ const worker = new Worker(url);
1821
+
1822
+ let nextId = 1;
1823
+ const pending = new Map();
1824
+
1825
+ function failAll(err) {
1826
+ for (const [id, p] of pending.entries()) {
1827
+ pending.delete(id);
1828
+ p.reject(err);
1829
+ }
1830
+ }
1831
+
1832
+ worker.addEventListener('message', (ev) => {
1833
+ const msg = ev && ev.data ? ev.data : {};
1834
+ if (msg.type === 'inited') {
1835
+ return;
1836
+ }
1837
+ if (msg.type === 'initError') {
1838
+ failAll(new Error(msg.error || 'Worker init failed'));
1839
+ return;
1840
+ }
1841
+ if (msg.type === 'result') {
1842
+ const p = pending.get(msg.id);
1843
+ if (!p) return;
1844
+ pending.delete(msg.id);
1845
+
1846
+ if (msg.error) {
1847
+ p.reject(new Error(msg.error));
1848
+ } else {
1849
+ p.resolve({
1850
+ result: msg.result || '',
1851
+ durationMs: typeof msg.durationMs === 'number' ? msg.durationMs : 0,
1852
+ });
1853
+ }
1854
+ }
1855
+ });
1856
+
1857
+ worker.addEventListener('error', (ev) => {
1858
+ const err = new Error('Worker error: ' + (ev && ev.message ? ev.message : 'unknown'));
1859
+ failAll(err);
1860
+ });
1861
+
1862
+ // Wait for init confirmation.
1863
+ const initAck = new Promise((resolve, reject) => {
1864
+ function onMessage(ev) {
1865
+ const msg = ev && ev.data ? ev.data : {};
1866
+ if (msg.type === 'inited') {
1867
+ worker.removeEventListener('message', onMessage);
1868
+ worker.removeEventListener('message', onInitError);
1869
+ resolve();
1870
+ }
1871
+ }
1872
+ function onInitError(ev) {
1873
+ const msg = ev && ev.data ? ev.data : {};
1874
+ if (msg.type === 'initError') {
1875
+ worker.removeEventListener('message', onMessage);
1876
+ worker.removeEventListener('message', onInitError);
1877
+ reject(new Error(msg.error || 'Worker init failed'));
1878
+ }
1879
+ }
1880
+ worker.addEventListener('message', onMessage);
1881
+ worker.addEventListener('message', onInitError);
1882
+ });
1883
+
1884
+ worker.postMessage({
1885
+ type: 'init',
1886
+ eyelingJsUrl: EYELING_JS_URL,
1887
+ version: eyelingVersion || 'unknown',
1888
+ });
1889
+
1890
+ await initAck;
1891
+
1892
+ // Now that the worker has loaded the Blob source, we can revoke the URL.
1893
+ URL.revokeObjectURL(url);
1894
+
1895
+ return {
1896
+ run: (n3Input, opts) => {
1897
+ const id = nextId++;
1898
+ const proof = !!(opts && opts.proof);
1899
+ const enforceHttps = !!(opts && opts.enforceHttps);
1900
+
1901
+ return new Promise((resolve, reject) => {
1902
+ pending.set(id, { resolve, reject });
1903
+ worker.postMessage({
1904
+ type: 'run',
1905
+ id,
1906
+ n3Input: String(n3Input || ''),
1907
+ proof,
1908
+ enforceHttps,
1909
+ version: eyelingVersion || 'unknown',
1910
+ });
1911
+ });
1912
+ },
1913
+ };
1914
+ })();
1915
+
1916
+ return workerRunnerPromise;
1917
+ }
1918
+
1919
+ async function getEyelingRunner() {
1920
+ if (runnerPromise) return runnerPromise;
1921
+
1922
+ runnerPromise = (async () => {
1923
+ // Make sure version is known (so require("./package.json") can match UI)
1924
+ await ensureEyelingVersion();
1925
+
1926
+ const resp = await fetch(EYELING_JS_URL);
1927
+ if (!resp.ok) {
1928
+ throw new Error('Failed to load eyeling.js (' + resp.status + ')');
1929
+ }
1930
+
1931
+ const rawSource = await resp.text();
1932
+ const cleanedSource = rawSource.replace(/^#![^\n]*\n/, '');
1933
+
1934
+ // Build a function that runs eyeling as a CLI once, given N3 text.
1935
+ function runOnce(n3Input) {
1936
+ const lines = [];
1937
+
1938
+ const consoleShim = {
1939
+ log: (...args) => lines.push(args.join(' ')),
1940
+ error: (...args) => lines.push(args.join(' ')),
1941
+ };
1942
+
1943
+ const fsShim = {
1944
+ readFileSync: () => n3Input,
1945
+ };
1946
+
1947
+ const processShim = {
1948
+ argv: [
1949
+ 'node',
1950
+ 'eyeling.js',
1951
+ ...(proofCheckbox.checked ? ['-p'] : []),
1952
+ ...(enforceHttpsCheckbox.checked ? ['--enforce-https'] : []),
1953
+ 'input.n3',
1954
+ ],
1955
+ exit: (code) => {
1956
+ throw { __eyelingExit: true, code };
1957
+ },
1958
+ };
1959
+
1960
+ const moduleShim = { exports: {} };
1961
+
1962
+ function requireShim(id) {
1963
+ if (id === './package.json') {
1964
+ return { version: eyelingVersion || 'unknown' };
1965
+ }
1966
+ if (id === 'fs') return fsShim;
1967
+ if (id === 'crypto') {
1968
+ // Minimal stub – enough for examples that don't depend on crypto.
1969
+ return {
1970
+ createHash: () => ({
1971
+ update: () => {},
1972
+ digest: () => '',
1973
+ }),
1974
+ };
1975
+ }
1976
+ return {};
1977
+ }
1978
+
1979
+ // Make eyeling think this is the main module so main() runs.
1980
+ requireShim.main = moduleShim;
1981
+
1982
+ try {
1983
+ const fn = new Function('require', 'module', 'exports', 'process', 'console', cleanedSource);
1984
+ fn(requireShim, moduleShim, moduleShim.exports, processShim, consoleShim);
1985
+ } catch (e) {
1986
+ if (!e || !e.__eyelingExit) {
1987
+ lines.push('JavaScript error: ' + (e && e.message ? e.message : String(e)));
1988
+ }
1989
+ }
1990
+
1991
+ return lines.join('\n');
1992
+ }
1993
+
1994
+ return runOnce;
1995
+ })();
1996
+
1997
+ return runnerPromise;
1998
+ }
1999
+
2000
+ // --- Streaming output for the Playground tab (keeps UI responsive) ---
2001
+ let activePlayCancel = null;
2002
+ let activePlayAppender = null;
2003
+ let playPaused = false;
2004
+ let playPausedBuffer = [];
2005
+
2006
+ function setPlayPaused(next) {
2007
+ playPaused = !!next;
2008
+ if (pauseBtn) pauseBtn.textContent = playPaused ? 'Resume' : 'Pause';
2009
+ }
2010
+
2011
+ function enablePlayControls(running) {
2012
+ if (stopBtn) stopBtn.disabled = !running;
2013
+ const canResume = playPaused && playPausedBuffer.length > 0;
2014
+ if (pauseBtn) pauseBtn.disabled = !(running || canResume);
2015
+ if (pauseBtn) pauseBtn.textContent = playPaused ? 'Resume' : 'Pause';
2016
+ }
2017
+
2018
+ function flushPlayBuffer() {
2019
+ if (!playPausedBuffer.length) return;
2020
+ const text = playPausedBuffer.join('');
2021
+ playPausedBuffer = [];
2022
+ try {
2023
+ const appender =
2024
+ activePlayAppender ||
2025
+ makeCMAppender(outputEl, () => {
2026
+ const mh = currentMaxHeights();
2027
+ autoResizeCodeMirror(outputEl, 80, mh.output);
2028
+ });
2029
+ activePlayAppender = appender;
2030
+ appender.append(text);
2031
+ appender.flushNow();
2032
+ } catch (_) {}
2033
+ }
2034
+
2035
+ function stopPlayReasoning() {
2036
+ try {
2037
+ if (activePlayCancel) activePlayCancel();
2038
+ } catch (_) {}
2039
+ activePlayCancel = null;
2040
+ setPlayPaused(false);
2041
+ playPausedBuffer = [];
2042
+ enablePlayControls(false);
2043
+ runBtn.disabled = false;
2044
+ statusEl.textContent = 'Stopped.';
2045
+ }
2046
+
2047
+ if (pauseBtn) {
2048
+ pauseBtn.addEventListener('click', () => {
2049
+ // Toggle pause/resume. While paused, we buffer incoming batches.
2050
+ if (!activePlayCancel && !(playPaused && playPausedBuffer.length)) return;
2051
+
2052
+ if (!playPaused) {
2053
+ setPlayPaused(true);
2054
+ statusEl.textContent = 'Paused. Output frozen; buffering new triples…';
2055
+ enablePlayControls(true);
2056
+ } else {
2057
+ setPlayPaused(false);
2058
+ flushPlayBuffer();
2059
+ statusEl.textContent = activePlayCancel ? 'Reasoning (streaming)…' : 'Idle.';
2060
+ enablePlayControls(!!activePlayCancel);
2061
+ }
2062
+ });
2063
+ }
2064
+
2065
+ if (stopBtn) {
2066
+ stopBtn.addEventListener('click', stopPlayReasoning);
2067
+ }
2068
+
2069
+ function makeCMAppender(cm, afterFlush) {
2070
+ let pending = '';
2071
+ let scheduled = false;
2072
+
2073
+ function flush() {
2074
+ scheduled = false;
2075
+ if (!pending) return;
2076
+
2077
+ const scroller = cm.getScrollerElement();
2078
+ const nearBottom = scroller.scrollTop + scroller.clientHeight >= scroller.scrollHeight - 20;
2079
+
2080
+ const lastLine = cm.lastLine();
2081
+ const lastCh = cm.getLine(lastLine).length;
2082
+ cm.replaceRange(pending, { line: lastLine, ch: lastCh });
2083
+ pending = '';
2084
+
2085
+ if (nearBottom) scroller.scrollTop = scroller.scrollHeight;
2086
+ afterFlush && afterFlush();
2087
+ }
2088
+
2089
+ return {
2090
+ append(text) {
2091
+ pending += text;
2092
+ if (scheduled) return;
2093
+ scheduled = true;
2094
+ requestAnimationFrame(flush);
2095
+ },
2096
+ flushNow() {
2097
+ flush();
2098
+ },
2099
+ };
2100
+ }
2101
+
2102
+ async function runReasoner() {
2103
+ runBtn.disabled = true;
2104
+ setPlayPaused(false);
2105
+ playPausedBuffer = [];
2106
+ enablePlayControls(true);
2107
+
2108
+ clearErrorHighlight();
2109
+
2110
+ const startMs = performance.now();
2111
+ let derivedCount = 0;
2112
+
2113
+ statusEl.textContent = 'Reasoning (streaming)…';
2114
+
2115
+ // Clear output immediately.
2116
+ {
2117
+ const mh = currentMaxHeights();
2118
+ outputEl.setValue('# Derived triples (streaming):\n');
2119
+ autoResizeCodeMirror(outputEl, 80, mh.output);
2120
+ }
2121
+
2122
+ if (hashSyncEnabled) updateShareUrl();
2123
+
2124
+ // Let the browser paint before we start.
2125
+ await new Promise(requestAnimationFrame);
2126
+
2127
+ const appender = makeCMAppender(outputEl, () => {
2128
+ const mh = currentMaxHeights();
2129
+ autoResizeCodeMirror(outputEl, 80, mh.output);
2130
+ });
2131
+ activePlayAppender = appender;
2132
+
2133
+ // Cancel any prior run
2134
+ try {
2135
+ if (activePlayCancel) activePlayCancel();
2136
+ } catch (_) {}
2137
+ activePlayCancel = null;
2138
+
2139
+ const U = window.__eyelingUnified;
2140
+ if (!U || typeof U.startStreamRun !== 'function') {
2141
+ statusEl.textContent = 'Error: unified worker not available.';
2142
+ runBtn.disabled = false;
2143
+ return;
2144
+ }
2145
+
2146
+ const { cancel } = U.startStreamRun({
2147
+ program: editor.getValue(),
2148
+ enforceHttps: enforceHttpsCheckbox.checked,
2149
+ proof: proofCheckbox.checked,
2150
+ onStdout: (text) => {
2151
+ const s = String(text == null ? '' : text);
2152
+ if (!s) return;
2153
+ const chunk = s.endsWith('\n') ? s : s + '\n';
2154
+ if (playPaused) playPausedBuffer.push(chunk);
2155
+ else appender.append(chunk);
2156
+ },
2157
+ onStderr: (text) => {
2158
+ const s = String(text == null ? '' : text);
2159
+ if (!s) return;
2160
+ const lines = s.split(/\r\n|\n|\r/);
2161
+ const out =
2162
+ lines
2163
+ .filter((ln) => ln.length)
2164
+ .map((ln) => (ln.trimStart().startsWith('#') ? ln : '# ' + ln))
2165
+ .join('\n') + '\n';
2166
+ if (playPaused) playPausedBuffer.push(out);
2167
+ else appender.append(out);
2168
+ },
2169
+ onBatch: (triples) => {
2170
+ if (!triples || !triples.length) return;
2171
+ derivedCount += triples.length;
2172
+ if (playPaused) {
2173
+ playPausedBuffer.push(triples.join('\n') + '\n');
2174
+ } else {
2175
+ appender.append(triples.join('\n') + '\n');
2176
+ }
2177
+ statusEl.textContent =
2178
+ (playPaused ? 'Paused… Derived: ' : 'Reasoning (streaming)… Derived: ') + derivedCount.toLocaleString();
2179
+ },
2180
+ onDone: () => {
2181
+ appender.flushNow();
2182
+ activePlayCancel = null;
2183
+ // If paused, keep the pause button enabled so user can "Resume" and reveal buffered output.
2184
+ enablePlayControls(false);
2185
+
2186
+ const totalSeconds = (performance.now() - startMs) / 1000;
2187
+ statusEl.textContent = playPaused
2188
+ ? 'Done (paused). Derived: ' +
2189
+ derivedCount.toLocaleString() +
2190
+ ' — click Resume to display buffered output (' +
2191
+ totalSeconds.toFixed(2) +
2192
+ 's)'
2193
+ : 'Done. Derived: ' + derivedCount.toLocaleString() + ' (' + totalSeconds.toFixed(2) + 's)';
2194
+ runBtn.disabled = false;
2195
+ },
2196
+ onError: (msg) => {
2197
+ appender.flushNow();
2198
+ activePlayCancel = null;
2199
+ enablePlayControls(false);
2200
+ setPlayPaused(false);
2201
+ playPausedBuffer = [];
2202
+
2203
+ const mh = currentMaxHeights();
2204
+ outputEl.setValue('Reasoning error: ' + msg);
2205
+ autoResizeCodeMirror(outputEl, 80, mh.output);
2206
+
2207
+ // Highlight syntax errors back in the program editor (red line background)
2208
+ try {
2209
+ highlightErrorFromOutput('Reasoning error: ' + msg);
2210
+ } catch (_) {}
2211
+
2212
+ statusEl.textContent = 'Error.';
2213
+ runBtn.disabled = false;
2214
+ },
2215
+ });
2216
+
2217
+ activePlayCancel = cancel;
2218
+ }
2219
+
2220
+ async function loadFromUri() {
2221
+ const uri = uriInput.value.trim();
2222
+ if (!uri) return;
2223
+
2224
+ loadUriBtn.disabled = true;
2225
+ statusEl.textContent = 'Loading N3 from URL...';
2226
+ try {
2227
+ const resp = await fetch(uri);
2228
+ if (!resp.ok) {
2229
+ throw new Error('HTTP ' + resp.status + ' when fetching ' + uri);
2230
+ }
2231
+ const text = await resp.text();
2232
+ editor.setValue(text);
2233
+ clearErrorHighlight();
2234
+
2235
+ {
2236
+ const mh = currentMaxHeights();
2237
+ autoResizeCodeMirror(editor, 220, mh.editor);
2238
+ }
2239
+
2240
+ statusEl.textContent = 'Loaded N3 from URL.';
2241
+ if (hashSyncEnabled) updateShareUrl();
2242
+ } catch (e) {
2243
+ statusEl.textContent = 'Failed to load URL.';
2244
+
2245
+ {
2246
+ const mh = currentMaxHeights();
2247
+ outputEl.setValue('Error while loading N3 from URL:\n' + (e && e.message ? e.message : String(e)));
2248
+ autoResizeCodeMirror(outputEl, 80, mh.output);
2249
+ }
2250
+ } finally {
2251
+ loadUriBtn.disabled = false;
2252
+ }
2253
+ }
2254
+
2255
+ runBtn.addEventListener('click', runReasoner);
2256
+ loadUriBtn.addEventListener('click', loadFromUri);
2257
+
2258
+ // Initialize URL hash with whatever we started with (unless we're in URL-load mode)
2259
+ if (hashSyncEnabled) updateShareUrl();
2260
+
2261
+ // If ?url=... is present (and there's no #fragment), auto-load it.
2262
+ if (paramN3Uri && paramProgram === null) {
2263
+ loadFromUri();
2264
+ }
2265
+ })();
2266
+ </script>
2267
+
2268
+ <script>
2269
+ (function () {
2270
+ const TAB_KEY = 'eyeling.demo.activeTab';
2271
+ const tabButtons = Array.from(document.querySelectorAll('.tab-btn'));
2272
+ const radioPlay = document.getElementById('tab-radio-playground');
2273
+ const radioStream = document.getElementById('tab-radio-stream');
2274
+
2275
+ function applyFromRadios() {
2276
+ const tabName = radioStream && radioStream.checked ? 'stream' : 'playground';
2277
+
2278
+ // Keep aria-selected in sync (nice for a11y; also keeps old CSS working)
2279
+ for (const btn of tabButtons) {
2280
+ const selected = btn.getAttribute('data-tab') === tabName;
2281
+ btn.setAttribute('aria-selected', selected ? 'true' : 'false');
2282
+ }
2283
+
2284
+ try {
2285
+ localStorage.setItem(TAB_KEY, tabName);
2286
+ } catch (_) {}
2287
+
2288
+ if (tabName === 'stream' && !window.__eyelingStreamInited) {
2289
+ window.__eyelingStreamInited = true;
2290
+ initStreamApp();
2291
+ }
2292
+ }
2293
+
2294
+ function setSelected(tabName) {
2295
+ if (tabName === 'stream') {
2296
+ if (radioStream) radioStream.checked = true;
2297
+ } else {
2298
+ if (radioPlay) radioPlay.checked = true;
2299
+ }
2300
+ applyFromRadios();
2301
+ }
2302
+
2303
+ function initialTab() {
2304
+ try {
2305
+ const u = new URL(window.location.href);
2306
+ const q = (u.searchParams.get('tab') || '').toLowerCase();
2307
+ if (q === 'stream') return 'stream';
2308
+ if (q === 'playground') return 'playground';
2309
+ } catch (_) {}
2310
+ try {
2311
+ const saved = localStorage.getItem(TAB_KEY);
2312
+ if (saved === 'stream' || saved === 'playground') return saved;
2313
+ } catch (_) {}
2314
+ return 'playground';
2315
+ }
2316
+
2317
+ // Labels toggle radios by themselves; we just update aria/localStorage + lazy init.
2318
+ if (radioPlay) radioPlay.addEventListener('change', applyFromRadios);
2319
+ if (radioStream) radioStream.addEventListener('change', applyFromRadios);
2320
+
2321
+ // Keyboard support for labels (Space / Enter)
2322
+ for (const btn of tabButtons) {
2323
+ btn.addEventListener('keydown', (e) => {
2324
+ if (e.key === 'Enter' || e.key === ' ') {
2325
+ e.preventDefault();
2326
+ btn.click();
2327
+ }
2328
+ });
2329
+ }
2330
+
2331
+ setSelected(initialTab());
2332
+
2333
+ // ---------------------------
2334
+ // STREAM APP (lazy-initialized)
2335
+ // ---------------------------
2336
+ function initStreamApp() {
2337
+ const P = 'stream-';
2338
+ const $ = (id) => document.getElementById(P + id);
2339
+
2340
+ const DEFAULT_RULES = `# Eyeling rules (CORS-safe “dynamic fetch” demo)
2341
+ @prefix wd: <http://www.wikidata.org/entity/> .
2342
+ @prefix wdt: <http://www.wikidata.org/prop/direct/> .
2343
+ @prefix wikibase: <http://wikiba.se/ontology#> .
2344
+ @prefix schema: <http://schema.org/> .
2345
+ @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
2346
+ @prefix : <http://example.org/> .
2347
+
2348
+ # Basic typing lift
2349
+ { ?x wdt:P31 ?c } => { ?x a ?c } .
2350
+ { ?c wdt:P279 ?d } => { ?c rdfs:subClassOf ?d } .
2351
+ { ?a rdfs:subClassOf ?b . ?b rdfs:subClassOf ?c } => { ?a rdfs:subClassOf ?c } .
2352
+ { ?x a ?c . ?c rdfs:subClassOf ?d } => { ?x a ?d } .
2353
+
2354
+ # Find the Italian Wikiquote sitelink for an item and *request* a fetch.
2355
+ # (This avoids log:content / web fetching inside N3.)
2356
+ { ?article a schema:Article ;
2357
+ schema:about ?item ;
2358
+ schema:isPartOf <https://it.wikiquote.org/> ;
2359
+ schema:name ?title .
2360
+ } => { ?item :needsWikiquoteExtract ?title } .
2361
+
2362
+ # If we later inject :wikiquoteExtract facts, tag the item as having quotes.
2363
+ { ?item :wikiquoteExtract ?txt } => { ?item :hasWikiquote true } .
2364
+ `;
2365
+
2366
+ const TOY_DATA = `@prefix wd: <http://www.wikidata.org/entity/> .
2367
+ @prefix wdt: <http://www.wikidata.org/prop/direct/> .
2368
+ @prefix schema: <http://schema.org/> .
2369
+ @prefix : <http://example.org/> .
2370
+
2371
+ wd:Q7836 wdt:P31 wd:Q5 .
2372
+ <https://it.wikiquote.org/wiki/Ren%C3%A9_Magritte> a schema:Article ;
2373
+ schema:about wd:Q7836 ;
2374
+ schema:isPartOf <https://it.wikiquote.org/> ;
2375
+ schema:name "René Magritte" .
2376
+ `;
2377
+
2378
+ const MAGRITTE_LABEL = 'René Magritte';
2379
+ const MAGRITTE_QID = 'Q7836';
2380
+ const PREVIEW_LINES = 220;
2381
+
2382
+ // CodeMirror (syntax highlighted) for the rules pane
2383
+ const rulesCM = CodeMirror.fromTextArea($('rulesBox'), {
2384
+ mode: { name: 'sparql' },
2385
+ lineNumbers: true,
2386
+ lineWrapping: true,
2387
+ viewportMargin: Infinity,
2388
+ });
2389
+ rulesCM.setValue(DEFAULT_RULES);
2390
+
2391
+ // --- N3 SYNTAX ERROR HIGHLIGHTING (rules pane; best-effort) ---
2392
+ let rulesErrorLineHandle = null;
2393
+
2394
+ function clearRulesErrorHighlight() {
2395
+ if (!rulesErrorLineHandle) return;
2396
+ rulesCM.removeLineClass(rulesErrorLineHandle, 'background', 'cm-error-line');
2397
+ rulesErrorLineHandle = null;
2398
+ }
2399
+
2400
+ function parseSyntaxLineCol(message) {
2401
+ if (!message) return null;
2402
+ const text = String(message);
2403
+ const lines = text.split(/\r?\n/);
2404
+ const scanLimit = Math.min(lines.length, 80);
2405
+
2406
+ function tryMatchLine(ln) {
2407
+ if (!ln) return null;
2408
+ return (
2409
+ ln.match(/(?:Syntax|Parse) error.*?:(\d+):(\d+)(?::|\b)/i) ||
2410
+ ln.match(/\bline\s+(\d+)\b[^\d]{0,40}\bcol(?:umn)?\s+(\d+)\b/i) ||
2411
+ (/(?:syntax|parse) error/i.test(ln) ? ln.match(/:(\d+):(\d+)(?::|\b)/) : null)
2412
+ );
2413
+ }
2414
+
2415
+ // Prefer "Syntax error" lines
2416
+ for (let i = 0; i < scanLimit; i++) {
2417
+ const ln = lines[i];
2418
+ if (!/(?:syntax|parse) error/i.test(ln)) continue;
2419
+ const m = tryMatchLine(ln);
2420
+ if (m) return { line1: parseInt(m[1], 10), col1: parseInt(m[2], 10) };
2421
+ }
2422
+
2423
+ // Fallback: scan loosely
2424
+ for (let i = 0; i < scanLimit; i++) {
2425
+ const m = tryMatchLine(lines[i]);
2426
+ if (m) return { line1: parseInt(m[1], 10), col1: parseInt(m[2], 10) };
2427
+ }
2428
+
2429
+ return null;
2430
+ }
2431
+
2432
+ function highlightRulesErrorFromMessage(message, rulesText) {
2433
+ const lc = parseSyntaxLineCol(message);
2434
+ if (!lc) return;
2435
+ clearRulesErrorHighlight();
2436
+
2437
+ const line1 = lc.line1;
2438
+ const col1 = lc.col1;
2439
+ if (!Number.isFinite(line1) || line1 < 0) return;
2440
+
2441
+ const ruleLines = String(rulesText || '').split(/\r?\n/).length;
2442
+ if (line1 > ruleLines) return; // error likely in dataset portion
2443
+
2444
+ const line = line1 > 0 ? line1 - 1 : 0;
2445
+ const ch = Number.isFinite(col1) ? (col1 > 0 ? col1 - 1 : 0) : 0;
2446
+ if (line < 0 || line >= rulesCM.lineCount()) return;
2447
+
2448
+ rulesErrorLineHandle = rulesCM.getLineHandle(line);
2449
+ rulesCM.addLineClass(rulesErrorLineHandle, 'background', 'cm-error-line');
2450
+ rulesCM.scrollIntoView({ line, ch }, 80);
2451
+ rulesCM.setCursor({ line, ch });
2452
+ }
2453
+
2454
+ // Make it behave like a resizable textarea
2455
+ const wrap = rulesCM.getWrapperElement();
2456
+ wrap.style.height = 'min(42vh, 420px)';
2457
+ wrap.style.minHeight = '240px';
2458
+ wrap.style.resize = 'vertical';
2459
+ wrap.style.overflow = 'hidden';
2460
+
2461
+ // ---------- Shared enforce-https toggle ----------
2462
+ const demoEnforce = document.getElementById('enforce-https');
2463
+ const streamEnforce = $('enforceHttps');
2464
+
2465
+ function readEnforceHttpsDefault() {
2466
+ let v = true;
2467
+ try {
2468
+ const saved = localStorage.getItem('eyeling.enforceHttps');
2469
+ if (saved !== null) v = saved === '1';
2470
+ } catch (_) {}
2471
+ return v;
2472
+ }
2473
+
2474
+ function setAllEnforceHttps(val) {
2475
+ if (demoEnforce) demoEnforce.checked = !!val;
2476
+ if (streamEnforce) streamEnforce.checked = !!val;
2477
+ try {
2478
+ localStorage.setItem('eyeling.enforceHttps', val ? '1' : '0');
2479
+ } catch (_) {}
2480
+ }
2481
+
2482
+ setAllEnforceHttps(readEnforceHttpsDefault());
2483
+ if (streamEnforce) streamEnforce.addEventListener('change', () => setAllEnforceHttps(streamEnforce.checked));
2484
+ if (demoEnforce) demoEnforce.addEventListener('change', () => setAllEnforceHttps(demoEnforce.checked));
2485
+
2486
+ async function showEyelingVersion() {
2487
+ const el = $('eyelingVersion');
2488
+ if (!el) return;
2489
+
2490
+ const v = (window.eyeling && (window.eyeling.version || window.eyeling.VERSION)) || null;
2491
+ if (v) {
2492
+ el.textContent = `Eyeling v${v}`;
2493
+ return;
2494
+ }
2495
+
2496
+ try {
2497
+ const res = await fetch('./package.json', { cache: 'no-store' });
2498
+ if (res.ok) {
2499
+ const j = await res.json();
2500
+ if (j && j.version) {
2501
+ el.textContent = `Eyeling v${j.version}`;
2502
+ return;
2503
+ }
2504
+ }
2505
+ } catch (_) {}
2506
+
2507
+ el.textContent = 'Eyeling (version ?)';
2508
+ }
2509
+
2510
+ let selected = null;
2511
+ let worker = null;
2512
+ let runId = 0;
2513
+ let isRunning = false;
2514
+ let streamPaused = false;
2515
+ let streamPausedBuffer = [];
2516
+ let lastSelectedDiv = null;
2517
+ let resultDivById = new Map();
2518
+
2519
+ let datasetN3 = '';
2520
+ let fetchedFacts = new Set();
2521
+ let shownDerived = new Set();
2522
+
2523
+ // Efficient streaming output (append in chunks, avoid re-setting huge textContent)
2524
+ const outBox = $('outBox');
2525
+ let outPending = [];
2526
+ let outFlushScheduled = false;
2527
+
2528
+ function outAppendText(text) {
2529
+ if (!text) return;
2530
+ outPending.push(String(text));
2531
+ if (outFlushScheduled) return;
2532
+ outFlushScheduled = true;
2533
+
2534
+ requestAnimationFrame(() => {
2535
+ outFlushScheduled = false;
2536
+ if (!outPending.length) return;
2537
+
2538
+ const nearBottom = outBox.scrollTop + outBox.clientHeight >= outBox.scrollHeight - 20;
2539
+
2540
+ const chunk = outPending.join('');
2541
+ outPending = [];
2542
+ outBox.appendChild(document.createTextNode(chunk));
2543
+
2544
+ if (nearBottom) outBox.scrollTop = outBox.scrollHeight;
2545
+ });
2546
+ }
2547
+
2548
+ function updateButtons() {
2549
+ const hasSelection = !!selected;
2550
+ const hasData = !!datasetN3.trim();
2551
+
2552
+ $('loadBtn').disabled = isRunning || !hasSelection;
2553
+ $('reasonBtn').disabled = isRunning || !hasData;
2554
+ $('openDatasetBtn').disabled = !hasData;
2555
+
2556
+ const canResume = streamPaused && streamPausedBuffer.length > 0;
2557
+ $('pauseBtn').disabled = !(isRunning || canResume);
2558
+ $('stopBtn').disabled = !isRunning;
2559
+ $('pauseBtn').textContent = streamPaused ? 'Resume' : 'Pause';
2560
+ }
2561
+
2562
+ function setStatus(text) {
2563
+ $('runStatus').textContent = text;
2564
+ }
2565
+
2566
+ function clearOutput() {
2567
+ outBox.textContent = '';
2568
+ outPending = [];
2569
+ outAppendText('# Derived triples (streaming):\n');
2570
+ $('derivedCount').textContent = '0';
2571
+ shownDerived.clear();
2572
+ }
2573
+
2574
+ function addRunHeader(tag) {
2575
+ const stamp = new Date().toLocaleString();
2576
+ outAppendText(`\n# ---- ${tag} @ ${stamp} ----\n`);
2577
+ }
2578
+
2579
+ function appendTriples(triples) {
2580
+ const out = [];
2581
+ for (const t of triples || []) {
2582
+ if (shownDerived.has(t)) continue;
2583
+ shownDerived.add(t);
2584
+ out.push(t);
2585
+ }
2586
+ if (!out.length) return;
2587
+
2588
+ // Always keep counts up to date, even while paused.
2589
+ $('derivedCount').textContent = String(shownDerived.size);
2590
+
2591
+ if (streamPaused) {
2592
+ streamPausedBuffer.push(out.join('\n') + '\n');
2593
+ return;
2594
+ }
2595
+
2596
+ outAppendText(out.join('\n') + '\n');
2597
+ }
2598
+ function setDataset(text) {
2599
+ datasetN3 = text || '';
2600
+ const lines = datasetN3.split(/\r?\n/);
2601
+ const shown = lines.slice(0, PREVIEW_LINES).join('\n');
2602
+ const suffix =
2603
+ lines.length > PREVIEW_LINES
2604
+ ? `\n\n# … (${lines.length - PREVIEW_LINES} more lines hidden; click "Open full dataset")`
2605
+ : '';
2606
+ $('dataPreview').value = shown + suffix;
2607
+ $('dataFull').value = datasetN3;
2608
+
2609
+ const bytes = new Blob([datasetN3]).size;
2610
+ const kb = Math.round(bytes / 1024);
2611
+ const info = `${lines.length.toLocaleString()} lines · ~${kb.toLocaleString()} KB`;
2612
+ const badge = $('dsInfo');
2613
+ badge.style.display = datasetN3 ? 'inline-block' : 'none';
2614
+ badge.textContent = info;
2615
+
2616
+ $('fetchedCount').textContent = String(fetchedFacts.size);
2617
+ updateButtons();
2618
+ }
2619
+
2620
+ function addFactN3(tripleLine) {
2621
+ const key = tripleLine.trim();
2622
+ if (!key) return false;
2623
+ if (fetchedFacts.has(key)) return false;
2624
+ fetchedFacts.add(key);
2625
+ datasetN3 += '\n' + key + '\n';
2626
+ setDataset(datasetN3);
2627
+ return true;
2628
+ }
2629
+
2630
+ const WD_API = 'https://www.wikidata.org/w/api.php';
2631
+
2632
+ async function wdSearch(query) {
2633
+ const url = `${WD_API}?action=wbsearchentities&format=json&language=en&limit=10&origin=*&search=${encodeURIComponent(query)}`;
2634
+ const res = await fetch(url);
2635
+ if (!res.ok) throw new Error(`Wikidata search failed: HTTP ${res.status}`);
2636
+ const json = await res.json();
2637
+ return (json.search || []).map((x) => ({
2638
+ id: x.id,
2639
+ label: x.label || x.id,
2640
+ description: x.description || '',
2641
+ }));
2642
+ }
2643
+
2644
+ async function wbGetEntities(ids) {
2645
+ const url = `${WD_API}?action=wbgetentities&format=json&origin=*&ids=${encodeURIComponent(ids.join('|'))}&props=claims|sitelinks|labels`;
2646
+ const res = await fetch(url);
2647
+ if (!res.ok) throw new Error(`wbgetentities failed: HTTP ${res.status}`);
2648
+ return await res.json();
2649
+ }
2650
+
2651
+ function escLiteral(s) {
2652
+ const t = String(s).replace(/\\/g, '\\\\').replace(/"""/g, '\\"""');
2653
+ return '"""' + t + '"""';
2654
+ }
2655
+
2656
+ function qidToIri(id) {
2657
+ if (/^Q\d+$/.test(id)) return `wd:${id}`;
2658
+ if (/^P\d+$/.test(id)) return `wd:${id}`;
2659
+ return null;
2660
+ }
2661
+
2662
+ function siteToBaseUrl(site) {
2663
+ const mWq = site.match(/^([a-z-]+)wikiquote$/);
2664
+ if (mWq) return `https://${mWq[1]}.wikiquote.org/wiki/`;
2665
+ const mWp = site.match(/^([a-z-]+)wiki$/);
2666
+ if (mWp) return `https://${mWp[1]}.wikipedia.org/wiki/`;
2667
+ if (site === 'commonswiki') return 'https://commons.wikimedia.org/wiki/';
2668
+ return null;
2669
+ }
2670
+
2671
+ function n3PfxHeader() {
2672
+ return [
2673
+ '@prefix wd: <http://www.wikidata.org/entity/> .',
2674
+ '@prefix wdt: <http://www.wikidata.org/prop/direct/> .',
2675
+ '@prefix wikibase: <http://wikiba.se/ontology#> .',
2676
+ '@prefix schema: <http://schema.org/> .',
2677
+ '@prefix : <http://example.org/> .',
2678
+ '',
2679
+ ].join('\n');
2680
+ }
2681
+
2682
+ function entityJsonToMinimalN3(entity, minimalOnly) {
2683
+ const lines = [];
2684
+ const eid = entity.id;
2685
+ lines.push(`${qidToIri(eid)} a wikibase:${entity.type === 'property' ? 'Property' : 'Item'} .`);
2686
+
2687
+ const claims = entity.claims || {};
2688
+ for (const pid of Object.keys(claims)) {
2689
+ const stmts = claims[pid] || [];
2690
+ for (const st of stmts) {
2691
+ const mainsnak = st.mainsnak;
2692
+ if (!mainsnak || mainsnak.snaktype !== 'value') continue;
2693
+ const dv = mainsnak.datavalue;
2694
+ if (!dv) continue;
2695
+
2696
+ const subj = qidToIri(eid);
2697
+ const pred = `wdt:${pid}`;
2698
+
2699
+ if (dv.type === 'wikibase-entityid') {
2700
+ const valId = dv.value && dv.value.id;
2701
+ const obj = qidToIri(valId);
2702
+ if (!obj) continue;
2703
+ lines.push(`${subj} ${pred} ${obj} .`);
2704
+ } else if (!minimalOnly && dv.type === 'string') {
2705
+ lines.push(`${subj} ${pred} ${escLiteral(dv.value)} .`);
2706
+ }
2707
+ }
2708
+ }
2709
+
2710
+ const sitelinks = entity.sitelinks || {};
2711
+ for (const site of Object.keys(sitelinks)) {
2712
+ const base = siteToBaseUrl(site);
2713
+ if (!base) continue;
2714
+ const title = sitelinks[site].title;
2715
+ if (!title) continue;
2716
+
2717
+ const urlTitle = encodeURIComponent(title.replace(/ /g, '_'));
2718
+ const articleUrl = `<${base}${urlTitle}>`;
2719
+
2720
+ const isPartOf = site.endsWith('wikiquote')
2721
+ ? `<https://${site.replace('wikiquote', '')}.wikiquote.org/>`
2722
+ : site.endsWith('wiki')
2723
+ ? `<https://${site.replace('wiki', '')}.wikipedia.org/>`
2724
+ : null;
2725
+
2726
+ lines.push(
2727
+ `${articleUrl} a schema:Article ; schema:about ${qidToIri(eid)} ;` +
2728
+ (isPartOf ? ` schema:isPartOf ${isPartOf} ;` : '') +
2729
+ ` schema:name ${escLiteral(title)} .`,
2730
+ );
2731
+ }
2732
+
2733
+ return lines.join('\n');
2734
+ }
2735
+
2736
+ async function fetchItWikiquoteExtract(title) {
2737
+ const url = `https://it.wikiquote.org/w/api.php?action=query&prop=extracts&explaintext=1&exsectionformat=plain&format=json&origin=*&titles=${encodeURIComponent(title)}`;
2738
+ const res = await fetch(url);
2739
+ if (!res.ok) throw new Error(`wikiquote API failed: HTTP ${res.status}`);
2740
+ const j = await res.json();
2741
+ const pages = j && j.query && j.query.pages ? Object.values(j.query.pages) : [];
2742
+ const page = pages[0] || {};
2743
+ return page.extract || '';
2744
+ }
2745
+
2746
+ // --- Reasoning (background worker + true streaming) ---
2747
+ let activeStreamCancel = null;
2748
+ function runReasoning() {
2749
+ const rules = rulesCM.getValue();
2750
+ if (!datasetN3.trim() || !rules.trim()) return;
2751
+
2752
+ isRunning = true;
2753
+ streamPaused = false;
2754
+ streamPausedBuffer = [];
2755
+ updateButtons();
2756
+ setStatus('Reasoning (streaming)…');
2757
+ clearRulesErrorHighlight();
2758
+
2759
+ const U = window.__eyelingUnified;
2760
+ if (!U || typeof U.startStreamRun !== 'function') {
2761
+ setStatus('Error: unified worker not available.');
2762
+ isRunning = false;
2763
+ updateButtons();
2764
+ return;
2765
+ }
2766
+
2767
+ // Cancel any previous run (also avoids piling up output work)
2768
+ try {
2769
+ if (activeStreamCancel) activeStreamCancel();
2770
+ } catch (_) {}
2771
+ activeStreamCancel = null;
2772
+
2773
+ const program = rules + '\n\n' + datasetN3;
2774
+
2775
+ const { runId: thisRun, cancel } = U.startStreamRun({
2776
+ program,
2777
+ enforceHttps: !!streamEnforce?.checked,
2778
+ proof: false,
2779
+ onStdout: (text) => {
2780
+ const s = String(text == null ? '' : text);
2781
+ if (!s) return;
2782
+ outAppendText(s.endsWith('\n') ? s : s + '\n');
2783
+ },
2784
+ onStderr: (text) => {
2785
+ const s = String(text == null ? '' : text);
2786
+ if (!s) return;
2787
+ const lines = s.split(/\r\n|\n|\r/);
2788
+ const out =
2789
+ lines
2790
+ .filter((ln) => ln.length)
2791
+ .map((ln) => (ln.trimStart().startsWith('#') ? ln : '# ' + ln))
2792
+ .join('\n') + '\n';
2793
+ outAppendText(out);
2794
+ },
2795
+ onBatch: (triples) => {
2796
+ appendTriples(triples);
2797
+ // Fire-and-forget dynamic fetch; may schedule a rerun.
2798
+ try {
2799
+ maybeHandleDynamicFetch(triples);
2800
+ } catch (_) {}
2801
+ },
2802
+ onDone: () => {
2803
+ activeStreamCancel = null;
2804
+ setStatus(
2805
+ streamPaused
2806
+ ? `Done (paused). Click Resume to display buffered output. (Run ${thisRun})`
2807
+ : `Done. (Run ${thisRun})`,
2808
+ );
2809
+ isRunning = false;
2810
+ updateButtons();
2811
+ },
2812
+ onError: (msg) => {
2813
+ activeStreamCancel = null;
2814
+ setStatus(streamPaused ? `Reasoning error (paused): ${msg}` : `Reasoning error: ${msg}`);
2815
+
2816
+ // Also write the error into the streaming output pane (so it's visible).
2817
+ try {
2818
+ outAppendText(`\n# Reasoning error: ${msg}\n`);
2819
+ } catch (_) {}
2820
+
2821
+ // Best-effort highlight if the syntax error is in the rules portion.
2822
+ try {
2823
+ const combinedErr =
2824
+ (outBox?.textContent || '') +
2825
+ '\n' +
2826
+ (streamPausedBuffer ? streamPausedBuffer.join('') : '') +
2827
+ '\n' +
2828
+ msg;
2829
+ highlightRulesErrorFromMessage(combinedErr, rules);
2830
+ } catch (_) {}
2831
+
2832
+ isRunning = false;
2833
+ updateButtons();
2834
+ },
2835
+ });
2836
+
2837
+ activeStreamCancel = cancel;
2838
+ }
2839
+
2840
+ function selectResult(r, div) {
2841
+ selected = r;
2842
+ $('selId').textContent = r.id;
2843
+ $('selLabel').textContent = ` — ${r.label}`;
2844
+
2845
+ if (lastSelectedDiv) lastSelectedDiv.classList.remove('selected');
2846
+ if (div) {
2847
+ div.classList.add('selected');
2848
+ lastSelectedDiv = div;
2849
+ } else {
2850
+ lastSelectedDiv = null;
2851
+ }
2852
+
2853
+ updateButtons();
2854
+ }
2855
+
2856
+ async function doSearch() {
2857
+ const q = $('searchBox').value.trim();
2858
+ if (!q) return [];
2859
+
2860
+ $('results').innerHTML = '';
2861
+ resultDivById = new Map();
2862
+ selected = null;
2863
+ $('selId').textContent = '—';
2864
+ $('selLabel').textContent = '';
2865
+ lastSelectedDiv = null;
2866
+ setStatus('Searching…');
2867
+ updateButtons();
2868
+
2869
+ try {
2870
+ const results = await wdSearch(q);
2871
+ setStatus(`Found ${results.length} result(s). Click one, then “Load selection”.`);
2872
+
2873
+ for (const r of results) {
2874
+ const div = document.createElement('div');
2875
+ div.className = 'item';
2876
+ div.innerHTML = `<div class="mono">${r.id}</div><div style="font-weight:800">${r.label}</div><div class="muted">${r.description}</div>`;
2877
+ div.onclick = () => selectResult(r, div);
2878
+ $('results').appendChild(div);
2879
+ resultDivById.set(r.id, div);
2880
+ }
2881
+
2882
+ updateButtons();
2883
+ return results;
2884
+ } catch (e) {
2885
+ setStatus(`Search error: ${e.message || e}`);
2886
+ updateButtons();
2887
+ return [];
2888
+ }
2889
+ }
2890
+
2891
+ async function loadSelection() {
2892
+ if (!selected) return;
2893
+
2894
+ isRunning = true;
2895
+ updateButtons();
2896
+ setStatus(`Loading ${selected.id} (via Wikidata API)…`);
2897
+
2898
+ try {
2899
+ fetchedFacts = new Set();
2900
+ $('fetchedCount').textContent = '0';
2901
+
2902
+ const json = await wbGetEntities([selected.id]);
2903
+ const ent = json && json.entities && json.entities[selected.id];
2904
+ if (!ent) throw new Error(`No entity data returned for ${selected.id}`);
2905
+
2906
+ const minimalOnly = $('minimalOnly').checked;
2907
+ const n3 = n3PfxHeader() + entityJsonToMinimalN3(ent, minimalOnly);
2908
+
2909
+ setDataset(n3);
2910
+ setStatus(`Loaded ${selected.id}. Ready to reason.`);
2911
+ } catch (e) {
2912
+ setStatus(`Load error: ${e.message || e}`);
2913
+ } finally {
2914
+ isRunning = false;
2915
+ updateButtons();
2916
+ }
2917
+ }
2918
+
2919
+ function loadToy() {
2920
+ selected = null;
2921
+ if (lastSelectedDiv) {
2922
+ lastSelectedDiv.classList.remove('selected');
2923
+ lastSelectedDiv = null;
2924
+ }
2925
+ $('selId').textContent = 'toy';
2926
+ $('selLabel').textContent = ' — local demo data';
2927
+ fetchedFacts = new Set();
2928
+ $('fetchedCount').textContent = '0';
2929
+ setDataset(TOY_DATA);
2930
+ setStatus('Toy example loaded. Ready to reason.');
2931
+ updateButtons();
2932
+ }
2933
+
2934
+ function openModal() {
2935
+ $('modalBackdrop').style.display = 'flex';
2936
+ $('modalBackdrop').setAttribute('aria-hidden', 'false');
2937
+ }
2938
+ function closeModal() {
2939
+ $('modalBackdrop').style.display = 'none';
2940
+ $('modalBackdrop').setAttribute('aria-hidden', 'true');
2941
+ }
2942
+
2943
+ $('openDatasetBtn').onclick = openModal;
2944
+ $('modalCloseBtn').onclick = closeModal;
2945
+ $('modalBackdrop').addEventListener('click', (e) => {
2946
+ if (e.target === $('modalBackdrop')) closeModal();
2947
+ });
2948
+ window.addEventListener('keydown', (e) => {
2949
+ if (e.key === 'Escape') closeModal();
2950
+ });
2951
+
2952
+ $('searchBtn').onclick = doSearch;
2953
+ $('loadBtn').onclick = loadSelection;
2954
+ $('toyBtn').onclick = () => {
2955
+ loadToy();
2956
+ setTimeout(() => {
2957
+ if ($('clearOnRun').checked) clearOutput();
2958
+ addRunHeader('Toy run');
2959
+ runReasoning();
2960
+ }, 60);
2961
+ };
2962
+ $('clearBtn').onclick = () => {
2963
+ clearOutput();
2964
+ setStatus('Cleared.');
2965
+ };
2966
+ $('reasonBtn').onclick = () => {
2967
+ if ($('clearOnRun').checked) clearOutput();
2968
+ addRunHeader('Manual run');
2969
+ runReasoning();
2970
+ };
2971
+
2972
+ $('pauseBtn').onclick = () => {
2973
+ const canResume = streamPaused && streamPausedBuffer.length > 0;
2974
+ if (!(isRunning || canResume)) return;
2975
+
2976
+ if (!streamPaused) {
2977
+ streamPaused = true;
2978
+ setStatus('Paused. Output frozen; buffering new triples…');
2979
+ } else {
2980
+ streamPaused = false;
2981
+ if (streamPausedBuffer.length) {
2982
+ outAppendText(streamPausedBuffer.join(''));
2983
+ streamPausedBuffer = [];
2984
+ }
2985
+ setStatus(isRunning ? 'Reasoning (streaming)…' : 'Resumed. Buffered output appended.');
2986
+ }
2987
+
2988
+ updateButtons();
2989
+ };
2990
+
2991
+ $('stopBtn').onclick = () => {
2992
+ try {
2993
+ if (activeStreamCancel) activeStreamCancel();
2994
+ } catch (_) {}
2995
+ activeStreamCancel = null;
2996
+ streamPaused = false;
2997
+ streamPausedBuffer = [];
2998
+ isRunning = false;
2999
+ setStatus('Stopped.');
3000
+ updateButtons();
3001
+ };
3002
+
3003
+ $('searchBox').addEventListener('keydown', (e) => {
3004
+ if (e.key === 'Enter') doSearch();
3005
+ });
3006
+
3007
+ async function loadDefaultMagritte() {
3008
+ $('searchBox').value = MAGRITTE_LABEL;
3009
+ const results = await doSearch();
3010
+ const target = results.find((r) => r.id === MAGRITTE_QID) || results[0];
3011
+ if (!target) return;
3012
+ const div = resultDivById.get(target.id);
3013
+ selectResult(target, div);
3014
+ await loadSelection();
3015
+ setTimeout(() => {
3016
+ if ($('clearOnRun').checked) clearOutput();
3017
+ addRunHeader('Auto run (default)');
3018
+ runReasoning();
3019
+ }, 80);
3020
+ }
3021
+
3022
+ // Initial view
3023
+ clearOutput();
3024
+ showEyelingVersion()
3025
+ .then(loadDefaultMagritte)
3026
+ .catch((e) => {
3027
+ setStatus(`Default load failed (${e.message || e}). Loading toy example instead…`);
3028
+ loadToy();
3029
+ setTimeout(() => {
3030
+ if ($('clearOnRun').checked) clearOutput();
3031
+ addRunHeader('Auto run (toy fallback)');
3032
+ runReasoning();
3033
+ }, 60);
3034
+ });
3035
+
3036
+ setTimeout(() => {
3037
+ try {
3038
+ rulesCM.refresh();
3039
+ } catch (_) {}
3040
+ }, 60);
3041
+ }
3042
+ })();
3043
+ </script>
3044
+ </body>
3045
+ </html>