eyelang 0.1.11 → 1.1.13
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/README.md +1 -1
- package/package.json +1 -1
- package/playground.html +293 -74
- package/src/solver.js +162 -34
- package/test/run-regression.mjs +45 -3
package/README.md
CHANGED
package/package.json
CHANGED
package/playground.html
CHANGED
|
@@ -16,15 +16,24 @@
|
|
|
16
16
|
--accent: #0969da;
|
|
17
17
|
--accent-strong: #0757b8;
|
|
18
18
|
--danger: #b42318;
|
|
19
|
+
--editor-bg: #ffffff;
|
|
20
|
+
--editor-text: #172033;
|
|
21
|
+
--editor-caret: #111827;
|
|
22
|
+
--editor-selection: rgba(9, 105, 218, 0.18);
|
|
23
|
+
--editor-error-line: #fee2e2;
|
|
24
|
+
--editor-error-border: #dc2626;
|
|
25
|
+
--line-number-bg: #f8fafc;
|
|
26
|
+
--line-number-text: #64748b;
|
|
27
|
+
--line-number-border: #e2e8f0;
|
|
19
28
|
--code-bg: #0f172a;
|
|
20
29
|
--code-text: #dbeafe;
|
|
21
|
-
--comment: #
|
|
22
|
-
--string: #
|
|
23
|
-
--number: #
|
|
24
|
-
--variable: #
|
|
25
|
-
--keyword: #
|
|
26
|
-
--predicate: #
|
|
27
|
-
--punctuation: #
|
|
30
|
+
--comment: #64748b;
|
|
31
|
+
--string: #047857;
|
|
32
|
+
--number: #b45309;
|
|
33
|
+
--variable: #1d4ed8;
|
|
34
|
+
--keyword: #7c3aed;
|
|
35
|
+
--predicate: #be123c;
|
|
36
|
+
--punctuation: #475569;
|
|
28
37
|
}
|
|
29
38
|
|
|
30
39
|
@media (prefers-color-scheme: dark) {
|
|
@@ -50,15 +59,15 @@
|
|
|
50
59
|
line-height: 1.45;
|
|
51
60
|
}
|
|
52
61
|
|
|
53
|
-
header
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
top: 0;
|
|
59
|
-
z-index: 5;
|
|
62
|
+
header,
|
|
63
|
+
main,
|
|
64
|
+
footer {
|
|
65
|
+
width: min(100% - 2rem, 72rem);
|
|
66
|
+
margin: 0 auto;
|
|
60
67
|
}
|
|
61
68
|
|
|
69
|
+
header { padding: 1.25rem 0 0.9rem; }
|
|
70
|
+
|
|
62
71
|
h1 {
|
|
63
72
|
margin: 0;
|
|
64
73
|
font-size: clamp(1.35rem, 4vw, 2rem);
|
|
@@ -67,21 +76,17 @@
|
|
|
67
76
|
header p { margin: 0.35rem 0 0; color: var(--muted); }
|
|
68
77
|
|
|
69
78
|
main {
|
|
70
|
-
display:
|
|
71
|
-
|
|
72
|
-
gap: 1rem;
|
|
73
|
-
padding: 1rem clamp(1rem, 4vw, 2rem) 2rem;
|
|
74
|
-
max-width: 1500px;
|
|
75
|
-
margin: 0 auto;
|
|
79
|
+
display: block;
|
|
80
|
+
padding: 0 0 2rem;
|
|
76
81
|
}
|
|
77
82
|
|
|
78
|
-
section
|
|
79
|
-
aside {
|
|
83
|
+
section {
|
|
80
84
|
background: var(--panel);
|
|
81
85
|
border: 1px solid var(--border);
|
|
82
86
|
border-radius: 0.9rem;
|
|
83
87
|
box-shadow: 0 8px 28px rgba(15, 23, 42, 0.06);
|
|
84
88
|
overflow: hidden;
|
|
89
|
+
margin: 1rem 0;
|
|
85
90
|
}
|
|
86
91
|
|
|
87
92
|
.panel-body { padding: 1rem; }
|
|
@@ -89,7 +94,8 @@
|
|
|
89
94
|
.toolbar,
|
|
90
95
|
.example-row,
|
|
91
96
|
.button-row,
|
|
92
|
-
.option-row
|
|
97
|
+
.option-row,
|
|
98
|
+
.background-row {
|
|
93
99
|
display: flex;
|
|
94
100
|
flex-wrap: wrap;
|
|
95
101
|
gap: 0.6rem;
|
|
@@ -102,6 +108,18 @@
|
|
|
102
108
|
background: var(--panel-soft);
|
|
103
109
|
}
|
|
104
110
|
|
|
111
|
+
.toolbar:last-child { border-bottom: 0; }
|
|
112
|
+
|
|
113
|
+
details summary {
|
|
114
|
+
cursor: pointer;
|
|
115
|
+
font-weight: 700;
|
|
116
|
+
padding: 0.85rem 1rem;
|
|
117
|
+
background: var(--panel-soft);
|
|
118
|
+
border-bottom: 1px solid var(--border);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
details:not([open]) summary { border-bottom: 0; }
|
|
122
|
+
|
|
105
123
|
label { font-weight: 650; }
|
|
106
124
|
|
|
107
125
|
select,
|
|
@@ -147,7 +165,8 @@
|
|
|
147
165
|
button.primary:hover { background: var(--accent-strong); }
|
|
148
166
|
button:disabled { cursor: not-allowed; opacity: 0.6; }
|
|
149
167
|
|
|
150
|
-
.option-row label
|
|
168
|
+
.option-row label,
|
|
169
|
+
.background-row label {
|
|
151
170
|
display: inline-flex;
|
|
152
171
|
gap: 0.35rem;
|
|
153
172
|
align-items: center;
|
|
@@ -168,9 +187,48 @@
|
|
|
168
187
|
|
|
169
188
|
.editor {
|
|
170
189
|
position: relative;
|
|
171
|
-
min-height:
|
|
172
|
-
background: var(--
|
|
190
|
+
min-height: 56vh;
|
|
191
|
+
background: var(--editor-bg);
|
|
192
|
+
overflow: hidden;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.line-numbers,
|
|
196
|
+
.editor pre,
|
|
197
|
+
.editor textarea {
|
|
198
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
|
|
199
|
+
font-size: 0.95rem;
|
|
200
|
+
line-height: 1.55;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.line-numbers {
|
|
204
|
+
position: absolute;
|
|
205
|
+
inset: 0 auto 0 0;
|
|
206
|
+
width: 3.6rem;
|
|
207
|
+
padding: 1rem 0.65rem 1rem 0.35rem;
|
|
173
208
|
overflow: hidden;
|
|
209
|
+
text-align: right;
|
|
210
|
+
color: var(--line-number-text);
|
|
211
|
+
background: var(--line-number-bg);
|
|
212
|
+
border-right: 1px solid var(--line-number-border);
|
|
213
|
+
user-select: none;
|
|
214
|
+
pointer-events: none;
|
|
215
|
+
z-index: 3;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.line-numbers-inner { will-change: transform; }
|
|
219
|
+
|
|
220
|
+
.line-number {
|
|
221
|
+
display: block;
|
|
222
|
+
min-height: 1.55em;
|
|
223
|
+
padding-right: 0.1rem;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.line-number.error {
|
|
227
|
+
color: var(--danger);
|
|
228
|
+
background: var(--editor-error-line);
|
|
229
|
+
font-weight: 700;
|
|
230
|
+
margin: 0 -0.65rem 0 -0.35rem;
|
|
231
|
+
padding: 0 0.65rem 0 0.35rem;
|
|
174
232
|
}
|
|
175
233
|
|
|
176
234
|
.editor pre,
|
|
@@ -178,7 +236,7 @@
|
|
|
178
236
|
position: absolute;
|
|
179
237
|
inset: 0;
|
|
180
238
|
margin: 0;
|
|
181
|
-
padding: 1rem;
|
|
239
|
+
padding: 1rem 1rem 1rem 4.35rem;
|
|
182
240
|
border: 0;
|
|
183
241
|
width: 100%;
|
|
184
242
|
height: 100%;
|
|
@@ -186,25 +244,36 @@
|
|
|
186
244
|
overflow: auto;
|
|
187
245
|
white-space: pre;
|
|
188
246
|
tab-size: 2;
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.error-line-marker {
|
|
250
|
+
position: absolute;
|
|
251
|
+
left: 0;
|
|
252
|
+
right: 0;
|
|
253
|
+
top: 0;
|
|
254
|
+
display: none;
|
|
255
|
+
pointer-events: none;
|
|
256
|
+
background: var(--editor-error-line);
|
|
257
|
+
border-left: 0.28rem solid var(--editor-error-border);
|
|
258
|
+
z-index: 0;
|
|
192
259
|
}
|
|
193
260
|
|
|
194
261
|
.editor pre {
|
|
195
262
|
pointer-events: none;
|
|
196
|
-
color: var(--
|
|
263
|
+
color: var(--editor-text);
|
|
264
|
+
z-index: 1;
|
|
197
265
|
}
|
|
198
266
|
|
|
199
267
|
.editor textarea {
|
|
200
268
|
background: transparent;
|
|
201
|
-
color: rgba(
|
|
202
|
-
caret-color:
|
|
269
|
+
color: rgba(17, 24, 39, 0.025);
|
|
270
|
+
caret-color: var(--editor-caret);
|
|
203
271
|
outline: none;
|
|
204
|
-
-
|
|
272
|
+
z-index: 2;
|
|
273
|
+
-webkit-text-fill-color: rgba(17, 24, 39, 0.025);
|
|
205
274
|
}
|
|
206
275
|
|
|
207
|
-
.editor textarea::selection { background:
|
|
276
|
+
.editor textarea::selection { background: var(--editor-selection); }
|
|
208
277
|
|
|
209
278
|
.tok-comment { color: var(--comment); font-style: italic; }
|
|
210
279
|
.tok-string { color: var(--string); }
|
|
@@ -250,31 +319,34 @@
|
|
|
250
319
|
|
|
251
320
|
footer {
|
|
252
321
|
color: var(--muted);
|
|
253
|
-
padding: 0
|
|
322
|
+
padding: 0 0 1.5rem;
|
|
254
323
|
text-align: center;
|
|
255
324
|
font-size: 0.95rem;
|
|
256
325
|
}
|
|
257
326
|
|
|
258
327
|
footer a { color: var(--accent); }
|
|
259
328
|
|
|
260
|
-
@media (max-width: 900px) {
|
|
261
|
-
header { position: static; }
|
|
262
|
-
main { grid-template-columns: 1fr; }
|
|
263
|
-
.editor { min-height: 54vh; }
|
|
264
|
-
.output { max-height: 42vh; }
|
|
265
|
-
}
|
|
266
|
-
|
|
267
329
|
@media (max-width: 560px) {
|
|
330
|
+
header,
|
|
331
|
+
main,
|
|
332
|
+
footer { width: min(100% - 1rem, 72rem); }
|
|
268
333
|
.toolbar,
|
|
269
334
|
.panel-body,
|
|
270
|
-
.editor-label
|
|
335
|
+
.editor-label,
|
|
336
|
+
details summary { padding: 0.8rem; }
|
|
271
337
|
button,
|
|
272
338
|
select,
|
|
273
339
|
input[type="url"],
|
|
274
340
|
input[type="search"] { width: 100%; }
|
|
275
341
|
.button-row > button { flex: 1 1 8rem; }
|
|
342
|
+
.editor { min-height: 58vh; }
|
|
343
|
+
.line-numbers,
|
|
344
|
+
.editor pre,
|
|
345
|
+
.editor textarea { font-size: 0.9rem; }
|
|
346
|
+
.line-numbers { width: 3.2rem; padding: 0.8rem 0.55rem 0.8rem 0.25rem; }
|
|
347
|
+
.line-number.error { margin: 0 -0.55rem 0 -0.25rem; padding: 0 0.55rem 0 0.25rem; }
|
|
276
348
|
.editor pre,
|
|
277
|
-
.editor textarea {
|
|
349
|
+
.editor textarea { padding: 0.8rem 0.8rem 0.8rem 3.85rem; }
|
|
278
350
|
}
|
|
279
351
|
</style>
|
|
280
352
|
</head>
|
|
@@ -285,45 +357,61 @@
|
|
|
285
357
|
</header>
|
|
286
358
|
|
|
287
359
|
<main>
|
|
288
|
-
<section aria-labelledby="
|
|
360
|
+
<section aria-labelledby="examples-heading">
|
|
289
361
|
<div class="toolbar" aria-label="Example loader">
|
|
290
|
-
<label for="example-search">Load example</label>
|
|
362
|
+
<label id="examples-heading" for="example-search">Load an example</label>
|
|
291
363
|
<input id="example-search" type="search" placeholder="Filter examples" autocomplete="off">
|
|
292
364
|
<select id="examples" aria-label="Examples"></select>
|
|
293
|
-
<button id="load-example" type="button">Load</button>
|
|
294
|
-
</div>
|
|
295
|
-
|
|
296
|
-
<div class="toolbar" aria-label="URL loader">
|
|
297
|
-
<label for="source-url">Load from URL</label>
|
|
298
|
-
<input id="source-url" type="url" placeholder="https://example.org/program.pl">
|
|
299
|
-
<button id="load-url" type="button">Load URL</button>
|
|
365
|
+
<button id="load-example" type="button">Load example</button>
|
|
300
366
|
</div>
|
|
367
|
+
</section>
|
|
301
368
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
<
|
|
305
|
-
|
|
369
|
+
<section aria-labelledby="advanced-heading">
|
|
370
|
+
<details>
|
|
371
|
+
<summary id="advanced-heading">⚙ Advanced configuration</summary>
|
|
372
|
+
<div class="toolbar" aria-label="URL loader">
|
|
373
|
+
<label for="source-url">Load Eyelang from URL</label>
|
|
374
|
+
<input id="source-url" type="url" placeholder="https://example.org/program.pl">
|
|
375
|
+
<button id="load-url" type="button">Load from URL</button>
|
|
376
|
+
</div>
|
|
377
|
+
<div class="toolbar background-row">
|
|
378
|
+
<label><input id="load-background" type="checkbox"> Load into background knowledge instead of overwriting the editor</label>
|
|
379
|
+
<span id="background-status" class="hint">No background knowledge loaded.</span>
|
|
380
|
+
<button id="clear-background" type="button">Clear background</button>
|
|
381
|
+
</div>
|
|
382
|
+
<div class="toolbar option-row" aria-label="Run options">
|
|
383
|
+
<label><input id="proof" type="checkbox"> Show proof explanations</label>
|
|
384
|
+
<label><input id="stats" type="checkbox"> Show stats</label>
|
|
385
|
+
</div>
|
|
386
|
+
<div class="panel-body">
|
|
387
|
+
<p class="hint">Enter any Eyelang file URL and click “Load from URL”. Background knowledge is prepended when the program runs.</p>
|
|
388
|
+
</div>
|
|
389
|
+
</details>
|
|
390
|
+
</section>
|
|
306
391
|
|
|
392
|
+
<section aria-labelledby="editor-heading">
|
|
307
393
|
<div class="editor-shell">
|
|
308
394
|
<div class="editor-label">
|
|
309
395
|
<strong id="editor-heading">Editable Eyelang program</strong>
|
|
310
396
|
<span id="source-name" class="hint">custom program</span>
|
|
311
397
|
</div>
|
|
312
398
|
<div class="editor">
|
|
399
|
+
<div id="line-numbers" class="line-numbers" aria-hidden="true"><div class="line-numbers-inner"></div></div>
|
|
400
|
+
<div id="error-line-marker" class="error-line-marker" aria-hidden="true"></div>
|
|
313
401
|
<pre id="highlight" aria-hidden="true"><code></code></pre>
|
|
314
402
|
<textarea id="source" spellcheck="false" autocapitalize="off" autocomplete="off" aria-label="Editable Eyelang source"></textarea>
|
|
315
403
|
</div>
|
|
316
404
|
</div>
|
|
317
|
-
</section>
|
|
318
|
-
|
|
319
|
-
<aside aria-labelledby="output-heading">
|
|
320
405
|
<div class="toolbar button-row">
|
|
321
|
-
<button id="run" type="button" class="primary">Run</button>
|
|
406
|
+
<button id="run" type="button" class="primary">Run reasoning</button>
|
|
322
407
|
<button id="stop" type="button" disabled>Stop</button>
|
|
323
408
|
<button id="copy-source" type="button">Copy source</button>
|
|
324
409
|
<button id="copy-output" type="button">Copy output</button>
|
|
325
410
|
<button id="share" type="button">Copy share link</button>
|
|
326
411
|
</div>
|
|
412
|
+
</section>
|
|
413
|
+
|
|
414
|
+
<section aria-labelledby="output-heading">
|
|
327
415
|
<div class="editor-label">
|
|
328
416
|
<strong id="output-heading">Output</strong>
|
|
329
417
|
<span id="elapsed" class="hint">Idle.</span>
|
|
@@ -336,7 +424,7 @@
|
|
|
336
424
|
<div class="panel-body">
|
|
337
425
|
<p class="hint">For local use, serve the checkout with a small HTTP server first; browser modules and example loading usually do not work from <code>file://</code>.</p>
|
|
338
426
|
</div>
|
|
339
|
-
</
|
|
427
|
+
</section>
|
|
340
428
|
</main>
|
|
341
429
|
|
|
342
430
|
<footer>
|
|
@@ -477,6 +565,7 @@
|
|
|
477
565
|
const FALLBACK_SOURCE = `materialize(answer, 1).
|
|
478
566
|
answer(ok) :- eq(ok, ok).
|
|
479
567
|
`;
|
|
568
|
+
const HIGHLIGHT_LIMIT = 200000;
|
|
480
569
|
const KEYWORDS = new Set(['materialize', 'memoize']);
|
|
481
570
|
const BUILTINS = new Set([
|
|
482
571
|
"abs",
|
|
@@ -561,28 +650,41 @@ answer(ok) :- eq(ok, ok).
|
|
|
561
650
|
|
|
562
651
|
const source = document.querySelector('#source');
|
|
563
652
|
const highlight = document.querySelector('#highlight');
|
|
653
|
+
const lineNumbers = document.querySelector('#line-numbers');
|
|
654
|
+
const lineNumbersInner = lineNumbers.firstElementChild;
|
|
655
|
+
const errorLineMarker = document.querySelector('#error-line-marker');
|
|
564
656
|
const exampleSelect = document.querySelector('#examples');
|
|
565
657
|
const exampleSearch = document.querySelector('#example-search');
|
|
566
658
|
const output = document.querySelector('#output');
|
|
567
659
|
const status = document.querySelector('#status');
|
|
568
660
|
const elapsed = document.querySelector('#elapsed');
|
|
569
661
|
const sourceName = document.querySelector('#source-name');
|
|
662
|
+
const backgroundStatus = document.querySelector('#background-status');
|
|
663
|
+
const loadBackground = document.querySelector('#load-background');
|
|
570
664
|
const proof = document.querySelector('#proof');
|
|
571
665
|
const stats = document.querySelector('#stats');
|
|
572
666
|
const runButton = document.querySelector('#run');
|
|
573
667
|
const stopButton = document.querySelector('#stop');
|
|
668
|
+
let backgroundSource = '';
|
|
669
|
+
let backgroundName = '';
|
|
574
670
|
let activeWorker = null;
|
|
575
671
|
let activeWorkerUrl = null;
|
|
672
|
+
let renderToken = 0;
|
|
673
|
+
let syntaxErrorLine = null;
|
|
576
674
|
|
|
577
675
|
populateExamples(EXAMPLES);
|
|
578
676
|
restoreFromHash() || loadExample('ancestor');
|
|
579
677
|
loadVersion();
|
|
580
678
|
|
|
581
|
-
source.addEventListener('input',
|
|
679
|
+
source.addEventListener('input', () => {
|
|
680
|
+
clearSyntaxError();
|
|
681
|
+
render();
|
|
682
|
+
});
|
|
582
683
|
source.addEventListener('scroll', syncScroll);
|
|
583
684
|
exampleSearch.addEventListener('input', () => populateExamples(filterExamples(exampleSearch.value)));
|
|
584
685
|
document.querySelector('#load-example').addEventListener('click', () => loadExample(exampleSelect.value));
|
|
585
686
|
document.querySelector('#load-url').addEventListener('click', loadUrl);
|
|
687
|
+
document.querySelector('#clear-background').addEventListener('click', clearBackground);
|
|
586
688
|
document.querySelector('#run').addEventListener('click', runProgram);
|
|
587
689
|
document.querySelector('#stop').addEventListener('click', stopProgram);
|
|
588
690
|
document.querySelector('#copy-source').addEventListener('click', () => copyText(source.value, 'Source copied.'));
|
|
@@ -629,27 +731,71 @@ answer(ok) :- eq(ok, ok).
|
|
|
629
731
|
setStatus(`Loading ${url}…`);
|
|
630
732
|
const response = await fetch(url);
|
|
631
733
|
if (!response.ok) throw new Error(`${response.status} ${response.statusText}`);
|
|
632
|
-
|
|
633
|
-
|
|
734
|
+
const text = await response.text();
|
|
735
|
+
if (loadBackground.checked) {
|
|
736
|
+
backgroundSource = text;
|
|
737
|
+
backgroundName = url;
|
|
738
|
+
updateBackgroundStatus();
|
|
739
|
+
setStatus(`Loaded background knowledge from ${url}.`);
|
|
740
|
+
} else {
|
|
741
|
+
setSource(text, url);
|
|
742
|
+
setStatus(`Loaded ${url}.`);
|
|
743
|
+
}
|
|
634
744
|
} catch (error) {
|
|
635
745
|
setStatus(`Could not load URL: ${formatError(error)}`, true);
|
|
636
746
|
}
|
|
637
747
|
}
|
|
638
748
|
|
|
749
|
+
function clearBackground() {
|
|
750
|
+
backgroundSource = '';
|
|
751
|
+
backgroundName = '';
|
|
752
|
+
updateBackgroundStatus();
|
|
753
|
+
setStatus('Background knowledge cleared.');
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function updateBackgroundStatus() {
|
|
757
|
+
backgroundStatus.textContent = backgroundSource
|
|
758
|
+
? `Background loaded from ${backgroundName} (${backgroundSource.length.toLocaleString()} chars).`
|
|
759
|
+
: 'No background knowledge loaded.';
|
|
760
|
+
}
|
|
761
|
+
|
|
639
762
|
function setSource(text, name) {
|
|
640
763
|
source.value = text;
|
|
641
764
|
sourceName.textContent = name;
|
|
765
|
+
clearSyntaxError();
|
|
642
766
|
render();
|
|
643
767
|
}
|
|
644
768
|
|
|
645
769
|
function render() {
|
|
646
|
-
|
|
647
|
-
|
|
770
|
+
const token = ++renderToken;
|
|
771
|
+
const text = source.value;
|
|
772
|
+
requestAnimationFrame(() => {
|
|
773
|
+
if (token !== renderToken) return;
|
|
774
|
+
highlight.firstElementChild.innerHTML = renderHighlightedSource(text);
|
|
775
|
+
updateLineNumbers(text);
|
|
776
|
+
syncScroll();
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function renderHighlightedSource(text) {
|
|
781
|
+
const rendered = text.length > HIGHLIGHT_LIMIT ? escapeHtml(text) : colorize(text);
|
|
782
|
+
return rendered + (text.endsWith('\n') ? ' ' : '');
|
|
648
783
|
}
|
|
649
784
|
|
|
650
785
|
function syncScroll() {
|
|
651
786
|
highlight.scrollTop = source.scrollTop;
|
|
652
787
|
highlight.scrollLeft = source.scrollLeft;
|
|
788
|
+
lineNumbersInner.style.transform = `translateY(${-source.scrollTop}px)`;
|
|
789
|
+
updateErrorLineMarker();
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function updateLineNumbers(text = source.value) {
|
|
793
|
+
const count = Math.max(1, text.split('\n').length);
|
|
794
|
+
lineNumbersInner.innerHTML = Array.from({ length: count }, (_, index) => {
|
|
795
|
+
const line = index + 1;
|
|
796
|
+
const className = line === syntaxErrorLine ? 'line-number error' : 'line-number';
|
|
797
|
+
return `<span class="${className}">${line}</span>`;
|
|
798
|
+
}).join('');
|
|
653
799
|
}
|
|
654
800
|
|
|
655
801
|
function colorize(text) {
|
|
@@ -708,6 +854,7 @@ answer(ok) :- eq(ok, ok).
|
|
|
708
854
|
|
|
709
855
|
function runProgram() {
|
|
710
856
|
stopProgram(false);
|
|
857
|
+
clearSyntaxError();
|
|
711
858
|
output.textContent = '';
|
|
712
859
|
elapsed.textContent = 'Running…';
|
|
713
860
|
setStatus('Running Eyelang…');
|
|
@@ -737,20 +884,83 @@ answer(ok) :- eq(ok, ok).
|
|
|
737
884
|
activeWorker.onmessage = (event) => finishRun(event.data);
|
|
738
885
|
activeWorker.onerror = (event) => finishRun({ ok: false, error: event.message || 'worker error' });
|
|
739
886
|
activeWorker.postMessage({
|
|
740
|
-
source:
|
|
887
|
+
source: combinedSource(),
|
|
741
888
|
options: { proof: proof.checked, stats: stats.checked },
|
|
742
889
|
});
|
|
743
890
|
}
|
|
744
891
|
|
|
892
|
+
function combinedSource() {
|
|
893
|
+
if (!backgroundSource.trim()) return source.value;
|
|
894
|
+
return `${backgroundSource}\n${source.value}`;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function extractParseErrorLine(text) {
|
|
898
|
+
const match = String(text).match(/parse line (\d+)/i);
|
|
899
|
+
return match ? Number(match[1]) : null;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
function editorLineForParseLine(parseLine) {
|
|
903
|
+
if (!Number.isFinite(parseLine) || parseLine < 1) return null;
|
|
904
|
+
if (!backgroundSource.trim()) return parseLine;
|
|
905
|
+
const backgroundLines = backgroundSource.split('\n').length;
|
|
906
|
+
const editorLine = parseLine - backgroundLines;
|
|
907
|
+
return editorLine >= 1 ? editorLine : null;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
function markSyntaxErrorLine(line) {
|
|
911
|
+
syntaxErrorLine = line;
|
|
912
|
+
updateLineNumbers();
|
|
913
|
+
revealLine(line);
|
|
914
|
+
updateErrorLineMarker();
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function clearSyntaxError() {
|
|
918
|
+
syntaxErrorLine = null;
|
|
919
|
+
updateLineNumbers();
|
|
920
|
+
updateErrorLineMarker();
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
function revealLine(line) {
|
|
924
|
+
const metrics = editorMetrics();
|
|
925
|
+
const y = metrics.paddingTop + (line - 1) * metrics.lineHeight;
|
|
926
|
+
const top = source.scrollTop;
|
|
927
|
+
const bottom = top + source.clientHeight - metrics.lineHeight;
|
|
928
|
+
if (y < top || y > bottom) {
|
|
929
|
+
source.scrollTop = Math.max(0, y - source.clientHeight / 3);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
function updateErrorLineMarker() {
|
|
934
|
+
if (syntaxErrorLine == null) {
|
|
935
|
+
errorLineMarker.style.display = 'none';
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
const metrics = editorMetrics();
|
|
939
|
+
errorLineMarker.style.display = 'block';
|
|
940
|
+
errorLineMarker.style.height = `${metrics.lineHeight}px`;
|
|
941
|
+
errorLineMarker.style.top = `${metrics.paddingTop + (syntaxErrorLine - 1) * metrics.lineHeight - source.scrollTop}px`;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
function editorMetrics() {
|
|
945
|
+
const style = getComputedStyle(source);
|
|
946
|
+
const fontSize = parseFloat(style.fontSize) || 15;
|
|
947
|
+
const lineHeight = parseFloat(style.lineHeight) || fontSize * 1.55;
|
|
948
|
+
const paddingTop = parseFloat(style.paddingTop) || 0;
|
|
949
|
+
return { lineHeight, paddingTop };
|
|
950
|
+
}
|
|
951
|
+
|
|
745
952
|
function finishRun(message) {
|
|
746
|
-
const worker = activeWorker;
|
|
747
953
|
cleanupWorker();
|
|
748
954
|
runButton.disabled = false;
|
|
749
955
|
stopButton.disabled = true;
|
|
750
956
|
if (!message.ok) {
|
|
751
|
-
|
|
957
|
+
const errorText = message.error || 'Unknown error.';
|
|
958
|
+
const parseLine = extractParseErrorLine(errorText);
|
|
959
|
+
const editorLine = editorLineForParseLine(parseLine);
|
|
960
|
+
if (editorLine != null) markSyntaxErrorLine(editorLine);
|
|
961
|
+
output.textContent = errorText;
|
|
752
962
|
elapsed.textContent = 'Failed.';
|
|
753
|
-
setStatus('Run failed.'
|
|
963
|
+
setStatus(editorLine == null ? 'Run failed.' : `Syntax error on line ${editorLine}.`, true);
|
|
754
964
|
return;
|
|
755
965
|
}
|
|
756
966
|
let text = message.stdout || '(no materialized output)\n';
|
|
@@ -758,7 +968,7 @@ answer(ok) :- eq(ok, ok).
|
|
|
758
968
|
output.textContent = text;
|
|
759
969
|
const seconds = (message.elapsedMs / 1000).toFixed(3);
|
|
760
970
|
elapsed.textContent = `${seconds} sec`;
|
|
761
|
-
setStatus(
|
|
971
|
+
setStatus('Run complete.');
|
|
762
972
|
}
|
|
763
973
|
|
|
764
974
|
function stopProgram(report = true) {
|
|
@@ -791,7 +1001,13 @@ answer(ok) :- eq(ok, ok).
|
|
|
791
1001
|
}
|
|
792
1002
|
|
|
793
1003
|
async function copyShareLink() {
|
|
794
|
-
const payload = JSON.stringify({
|
|
1004
|
+
const payload = JSON.stringify({
|
|
1005
|
+
source: source.value,
|
|
1006
|
+
proof: proof.checked,
|
|
1007
|
+
stats: stats.checked,
|
|
1008
|
+
backgroundSource,
|
|
1009
|
+
backgroundName,
|
|
1010
|
+
});
|
|
795
1011
|
const data = new TextEncoder().encode(payload);
|
|
796
1012
|
let binary = '';
|
|
797
1013
|
for (const byte of data) binary += String.fromCharCode(byte);
|
|
@@ -809,6 +1025,9 @@ answer(ok) :- eq(ok, ok).
|
|
|
809
1025
|
setSource(String(payload.source || ''), 'shared program');
|
|
810
1026
|
proof.checked = Boolean(payload.proof);
|
|
811
1027
|
stats.checked = Boolean(payload.stats);
|
|
1028
|
+
backgroundSource = String(payload.backgroundSource || '');
|
|
1029
|
+
backgroundName = String(payload.backgroundName || 'shared background');
|
|
1030
|
+
updateBackgroundStatus();
|
|
812
1031
|
setStatus('Loaded shared program.');
|
|
813
1032
|
return true;
|
|
814
1033
|
} catch (error) {
|
package/src/solver.js
CHANGED
|
@@ -38,50 +38,122 @@ export class Solver {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
*solve(goals, env = new Env(), depth = 0) {
|
|
41
|
-
this.stats.solve_goals_calls++;
|
|
42
|
-
this.stats.max_depth = Math.max(this.stats.max_depth, depth);
|
|
43
|
-
this.stats.max_goal_count = Math.max(this.stats.max_goal_count, goals.length);
|
|
44
|
-
if (depth > this.maxDepth || this.solutionsSeen >= this.solutionLimit) return;
|
|
45
41
|
if (!Array.isArray(goals)) goals = [goals];
|
|
46
42
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
43
|
+
const savedActive = this.active;
|
|
44
|
+
try {
|
|
45
|
+
const stack = [{ kind: 'goals', goals, env, depth, active: savedActive.slice() }];
|
|
46
|
+
while (stack.length) {
|
|
47
|
+
const frame = stack.pop();
|
|
48
|
+
if (frame.kind === 'completeMemo') {
|
|
49
|
+
frame.entry.computing = false;
|
|
50
|
+
frame.entry.complete = true;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const goal = goals[selectedIndex];
|
|
59
|
-
const rest = selectedIndex === 0 ? goals.slice(1) : [...goals.slice(0, selectedIndex), ...goals.slice(selectedIndex + 1)];
|
|
60
|
-
if (goal.type === COMPOUND && goal.name === ',' && goal.arity === 2) {
|
|
61
|
-
yield* this.solve([...flattenConjunction(goal), ...rest], env, depth + 1);
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
54
|
+
goals = frame.goals;
|
|
55
|
+
env = frame.env;
|
|
56
|
+
depth = frame.depth;
|
|
57
|
+
let active = frame.active;
|
|
64
58
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (
|
|
59
|
+
while (true) {
|
|
60
|
+
this.stats.solve_goals_calls++;
|
|
61
|
+
this.stats.max_depth = Math.max(this.stats.max_depth, depth);
|
|
62
|
+
this.stats.max_goal_count = Math.max(this.stats.max_goal_count, goals.length);
|
|
63
|
+
if (depth > this.maxDepth || this.solutionsSeen >= this.solutionLimit) break;
|
|
64
|
+
|
|
65
|
+
if (goals.length === 0) {
|
|
66
|
+
this.solutionsSeen++;
|
|
67
|
+
this.stats.completed_goal_lists++;
|
|
68
|
+
this.active = active;
|
|
69
|
+
yield env;
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const first = goals[0];
|
|
74
|
+
if (first?.kind === 'releaseActive') {
|
|
75
|
+
active = active.slice(0, -1);
|
|
76
|
+
goals = goals.slice(1);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (first?.kind === 'memoStore') {
|
|
80
|
+
rememberMemoAnswer(first.entry, first.goal, env);
|
|
81
|
+
goals = goals.slice(1);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// eyelang normally solves left-to-right, but ready deterministic builtins can
|
|
86
|
+
// be run early as pure filters. Stop at internal sentinels so rule-body
|
|
87
|
+
// active guards are released before the caller's remaining goals are seen.
|
|
88
|
+
const selectedIndex = selectReadyDeterministicBuiltin(goals, env, this.registry);
|
|
89
|
+
const goal = goals[selectedIndex];
|
|
90
|
+
const rest = selectedIndex === 0 ? goals.slice(1) : [...goals.slice(0, selectedIndex), ...goals.slice(selectedIndex + 1)];
|
|
91
|
+
if (goal.type === COMPOUND && goal.name === ',' && goal.arity === 2) {
|
|
92
|
+
goals = [...flattenConjunction(goal), ...rest];
|
|
93
|
+
depth++;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const def = goal.type === COMPOUND ? this.registry.get(goal.name, goal.arity) : null;
|
|
98
|
+
this.active = active;
|
|
99
|
+
if (def && builtinIsReadyOrAuthoritative(def, this, goal, env)) {
|
|
100
|
+
const nextEnvs = [];
|
|
101
|
+
for (const next of def.handler({ solver: this, goal, env })) nextEnvs.push(next);
|
|
102
|
+
if (def.deterministic) {
|
|
103
|
+
if (nextEnvs.length) this.stats.deterministic_builtin_successes++;
|
|
104
|
+
else this.stats.deterministic_builtin_failures++;
|
|
105
|
+
}
|
|
106
|
+
if (nextEnvs.length === 0) break;
|
|
107
|
+
if (nextEnvs.length === 1) {
|
|
108
|
+
goals = rest;
|
|
109
|
+
env = nextEnvs[0];
|
|
110
|
+
depth++;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
for (let i = nextEnvs.length - 1; i >= 0; i--) {
|
|
114
|
+
stack.push({ kind: 'goals', goals: rest, env: nextEnvs[i], depth: depth + 1, active });
|
|
115
|
+
}
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
this.stats.solve_one_goal_calls++;
|
|
120
|
+
if (goal.type !== COMPOUND) break;
|
|
121
|
+
const group = this.program.findGroup(goal.name, goal.arity);
|
|
122
|
+
if (!group) break;
|
|
123
|
+
|
|
124
|
+
if (group.memoized) {
|
|
125
|
+
const key = memoKey(goal, env);
|
|
126
|
+
if (key.hasBound) {
|
|
127
|
+
const mapKey = `${goal.name}/${goal.arity}:${key.text}`;
|
|
128
|
+
let entry = this.memo.get(mapKey);
|
|
129
|
+
if (!entry) {
|
|
130
|
+
entry = { computing: false, complete: false, answers: [], answerKeys: new Set() };
|
|
131
|
+
this.memo.set(mapKey, entry);
|
|
132
|
+
}
|
|
133
|
+
if (entry.complete) {
|
|
134
|
+
pushMemoAnswerFrames(stack, entry, goal, rest, env, depth, active, this);
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
if (!entry.computing) {
|
|
138
|
+
entry.computing = true;
|
|
139
|
+
stack.push({ kind: 'completeMemo', entry });
|
|
140
|
+
pushUserGoalUncachedFrames(stack, this, group, goal, [{ kind: 'memoStore', entry, goal }, ...rest], env, depth, active);
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
pushUserGoalUncachedFrames(stack, this, group, goal, rest, env, depth, active);
|
|
147
|
+
break;
|
|
72
148
|
}
|
|
73
|
-
if (def.deterministic) {
|
|
74
|
-
if (produced) this.stats.deterministic_builtin_successes++;
|
|
75
|
-
else this.stats.deterministic_builtin_failures++;
|
|
76
149
|
}
|
|
77
|
-
|
|
150
|
+
} finally {
|
|
151
|
+
this.active = savedActive;
|
|
78
152
|
}
|
|
79
|
-
|
|
80
|
-
yield* this.solveUserGoal(goal, rest, env, depth);
|
|
81
153
|
}
|
|
82
154
|
|
|
83
155
|
activeVariant(goal, env) {
|
|
84
|
-
return
|
|
156
|
+
return activeVariantIn(goal, env, this.active);
|
|
85
157
|
}
|
|
86
158
|
|
|
87
159
|
*solveUserGoal(goal, rest, env, depth) {
|
|
@@ -180,6 +252,61 @@ export class Solver {
|
|
|
180
252
|
}
|
|
181
253
|
|
|
182
254
|
|
|
255
|
+
function pushMemoAnswerFrames(stack, entry, goal, rest, env, depth, active, solver) {
|
|
256
|
+
for (let answerIndex = entry.answers.length - 1; answerIndex >= 0; answerIndex--) {
|
|
257
|
+
const answerArgs = entry.answers[answerIndex];
|
|
258
|
+
const next = env.clone();
|
|
259
|
+
let ok = true;
|
|
260
|
+
for (let i = 0; i < goal.arity; i++) {
|
|
261
|
+
solver.stats.unify_calls++;
|
|
262
|
+
if (!unify(goal.args[i], answerArgs[i], next)) { ok = false; break; }
|
|
263
|
+
}
|
|
264
|
+
if (ok) stack.push({ kind: 'goals', goals: rest, env: next, depth: depth + 1, active });
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function pushUserGoalUncachedFrames(stack, solver, group, goal, rest, env, depth, active) {
|
|
269
|
+
if (activeVariantIn(goal, env, active)) return;
|
|
270
|
+
const candidates = selectClauseCandidates(group, goal, env);
|
|
271
|
+
const frames = [];
|
|
272
|
+
for (const pass of [candidates.primary, candidates.fallback]) {
|
|
273
|
+
for (const clause of pass) {
|
|
274
|
+
if (headCannotMatch(goal, clause.head, env)) continue;
|
|
275
|
+
const id = nextFreshId();
|
|
276
|
+
const freshHead = freshTerm(clause.head, id);
|
|
277
|
+
const freshBody = clause.body.map((term) => freshTerm(term, id));
|
|
278
|
+
const next = env.clone();
|
|
279
|
+
solver.stats.unify_calls++;
|
|
280
|
+
if (!unify(goal, freshHead, next)) continue;
|
|
281
|
+
if (freshBody.length === 0) {
|
|
282
|
+
frames.push({ kind: 'goals', goals: rest, env: next, depth: depth + 1, active });
|
|
283
|
+
} else {
|
|
284
|
+
frames.push({
|
|
285
|
+
kind: 'goals',
|
|
286
|
+
goals: [...freshBody, { kind: 'releaseActive' }, ...rest],
|
|
287
|
+
env: next,
|
|
288
|
+
depth: depth + 1,
|
|
289
|
+
active: [...active, { goal, env }],
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
for (let i = frames.length - 1; i >= 0; i--) stack.push(frames[i]);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function rememberMemoAnswer(entry, goal, env) {
|
|
298
|
+
const answerArgs = goal.args.map((arg) => importResolved(arg, env));
|
|
299
|
+
const key = answerArgs.map((arg) => termToString(arg, new Env(), true)).join('\x1f');
|
|
300
|
+
if (entry.answerKeys.has(key)) return;
|
|
301
|
+
entry.answerKeys.add(key);
|
|
302
|
+
entry.answers.push(answerArgs);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function activeVariantIn(goal, env, active) {
|
|
306
|
+
return active.some((entry) => variantTerms(goal, env, entry.goal, entry.env));
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
|
|
183
310
|
function builtinIsReadyOrAuthoritative(def, solver, goal, env) {
|
|
184
311
|
if (typeof def.shouldUse === 'function' && !def.shouldUse({ solver, goal, env })) return false;
|
|
185
312
|
if (typeof def.ready !== 'function') return true;
|
|
@@ -190,6 +317,7 @@ function builtinIsReadyOrAuthoritative(def, solver, goal, env) {
|
|
|
190
317
|
function selectReadyDeterministicBuiltin(goals, env, registry) {
|
|
191
318
|
for (let i = 0; i < goals.length; i++) {
|
|
192
319
|
const goal = goals[i];
|
|
320
|
+
if (goal?.kind === 'releaseActive' || goal?.kind === 'memoStore') return 0;
|
|
193
321
|
if (goal.type !== COMPOUND) continue;
|
|
194
322
|
const def = registry.get(goal.name, goal.arity);
|
|
195
323
|
if (!def?.deterministic || typeof def.ready !== 'function') continue;
|
package/test/run-regression.mjs
CHANGED
|
@@ -360,6 +360,16 @@ function apiCases() {
|
|
|
360
360
|
assertEqual(result.stdout, 'q(a, b).\n', 'stdout');
|
|
361
361
|
},
|
|
362
362
|
},
|
|
363
|
+
{
|
|
364
|
+
name: 'run keeps recursive materializations independent in one solver',
|
|
365
|
+
run: () => {
|
|
366
|
+
const text = fs.readFileSync(path.join(packageRoot, 'examples', 'alignment-demo.pl'), 'utf8');
|
|
367
|
+
const program = Program.parseSources([{ text, filename: 'alignment-demo.pl' }]);
|
|
368
|
+
const result = run(program);
|
|
369
|
+
assertIncludes(result.stdout, 'broaderTransitive(anpr_passenger_car, ref_car).\n', 'stdout');
|
|
370
|
+
assertIncludes(result.stdout, 'narrowerOrEqualOf(anpr_passenger_car, ref_car).\n', 'stdout');
|
|
371
|
+
},
|
|
372
|
+
},
|
|
363
373
|
{
|
|
364
374
|
name: 'makeProgram creates indexed programs',
|
|
365
375
|
run: () => {
|
|
@@ -525,6 +535,20 @@ function whiteBoxCases() {
|
|
|
525
535
|
assertEqual(group.recursive, true, 'collatz/2 recursive');
|
|
526
536
|
},
|
|
527
537
|
},
|
|
538
|
+
{
|
|
539
|
+
name: 'collatz example remains stack-safe for browser-sized stacks',
|
|
540
|
+
run: () => {
|
|
541
|
+
// Use a deliberately tiny stack to catch browser-worker recursion regressions.
|
|
542
|
+
const result = spawnSync(process.execPath, ['--stack-size=100', bin, 'examples/collatz-1000.pl'], {
|
|
543
|
+
cwd: packageRoot,
|
|
544
|
+
encoding: 'utf8',
|
|
545
|
+
});
|
|
546
|
+
assertEqual(result.status, 0, `exit status${result.stderr ? `\nstderr: ${result.stderr}` : ''}`);
|
|
547
|
+
assertEqual(result.stderr, '', 'stderr');
|
|
548
|
+
assertIncludes(result.stdout, 'collatzTrajectory(1000, [1000, 500, 250, 125', 'stdout');
|
|
549
|
+
assertIncludes(result.stdout, 'collatzTrajectory(1, [1]).\n', 'stdout');
|
|
550
|
+
},
|
|
551
|
+
},
|
|
528
552
|
];
|
|
529
553
|
}
|
|
530
554
|
|
|
@@ -623,19 +647,37 @@ function playgroundStaticIssues() {
|
|
|
623
647
|
const html = fs.readFileSync(playgroundPath, 'utf8');
|
|
624
648
|
const readme = fs.readFileSync(path.join(packageRoot, 'README.md'), 'utf8');
|
|
625
649
|
if (!pkg.files?.includes('playground.html')) issues.push('package files must include playground.html');
|
|
626
|
-
if (!readme.includes('[Playground](playground
|
|
650
|
+
if (!readme.includes('[Playground](https://eyereasoner.github.io/eyelang/playground)')) issues.push('README must link to the GitHub Pages playground URL');
|
|
627
651
|
if (!html.includes('<meta name="viewport" content="width=device-width, initial-scale=1">')) issues.push('missing mobile viewport meta');
|
|
628
|
-
if (!html.includes('
|
|
629
|
-
issues.push('playground must
|
|
652
|
+
if (!html.includes('main {') || !html.includes('display: block;')) {
|
|
653
|
+
issues.push('playground must use a simple vertical layout');
|
|
630
654
|
}
|
|
631
655
|
if (!html.includes('@media (max-width: 560px)') || !html.includes('button,') || !html.includes('width: 100%')) {
|
|
632
656
|
issues.push('playground must make controls usable at phone widths');
|
|
633
657
|
}
|
|
658
|
+
if (!html.includes('<summary id="advanced-heading">⚙ Advanced configuration</summary>')) {
|
|
659
|
+
issues.push('playground must keep URL/proof controls inside advanced configuration');
|
|
660
|
+
}
|
|
661
|
+
if (!html.includes('id="load-background"') || !html.includes('backgroundSource') || !html.includes('combinedSource()')) {
|
|
662
|
+
issues.push('playground must support loading URL content as background knowledge');
|
|
663
|
+
}
|
|
664
|
+
if (!html.includes('HIGHLIGHT_LIMIT') || !html.includes('text.length > HIGHLIGHT_LIMIT')) {
|
|
665
|
+
issues.push('playground must avoid full syntax coloring for very large examples');
|
|
666
|
+
}
|
|
634
667
|
if (!html.includes('<script type="module">')) issues.push('playground script must be an ES module');
|
|
635
668
|
if (!html.includes("new URL('./src/index.js', location.href)")) issues.push('playground must import the public browser API');
|
|
636
669
|
if (!html.includes('class="editor"') || !html.includes('id="highlight"') || !html.includes('id="source"')) {
|
|
637
670
|
issues.push('playground must include layered syntax-colored editor');
|
|
638
671
|
}
|
|
672
|
+
if (!html.includes('--editor-bg: #ffffff') || !html.includes('background: var(--editor-bg)')) {
|
|
673
|
+
issues.push('playground editor must use a light editor background');
|
|
674
|
+
}
|
|
675
|
+
if (!html.includes('id="error-line-marker"') || !html.includes('extractParseErrorLine') || !html.includes('markSyntaxErrorLine') || !html.includes('--editor-error-line')) {
|
|
676
|
+
issues.push('playground must highlight syntax-error lines in the editor');
|
|
677
|
+
}
|
|
678
|
+
if (!html.includes('id="line-numbers"') || !html.includes('updateLineNumbers') || !html.includes('lineNumbersInner.style.transform') || !html.includes('--line-number-bg')) {
|
|
679
|
+
issues.push('playground editor must include synced line numbers');
|
|
680
|
+
}
|
|
639
681
|
if (!html.includes('id="example-search"') || !html.includes('id="examples"')) issues.push('playground must include searchable examples');
|
|
640
682
|
const scriptMatch = html.match(new RegExp('<script type="module">\\n([\\s\\S]*?)\\n <\\/script>'));
|
|
641
683
|
if (scriptMatch == null) {
|