eyeling 1.21.0 → 1.21.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyeling",
3
- "version": "1.21.0",
3
+ "version": "1.21.1",
4
4
  "description": "A minimal Notation3 (N3) reasoner in JavaScript.",
5
5
  "main": "./index.js",
6
6
  "keywords": [
@@ -125,10 +125,14 @@ function findChromium() {
125
125
  const CODEMIRROR_STUB = String.raw`(function(){
126
126
  if (window.CodeMirror) return;
127
127
 
128
+ function normalizeText(text){
129
+ return String(text || '').replace(/\r\n/g, '\n').replace(/\r/g, '\n');
130
+ }
131
+
128
132
  function posToIndex(text, line, ch){
129
133
  line = Math.max(0, line|0);
130
134
  ch = Math.max(0, ch|0);
131
- const norm = String(text || '').replace(/\r\n/g, '\n').replace(/\r/g, '\n');
135
+ const norm = normalizeText(text);
132
136
  const lines = norm.split('\n');
133
137
  if (lines.length === 0) return 0;
134
138
  if (line >= lines.length) line = lines.length - 1;
@@ -152,30 +156,76 @@ const CODEMIRROR_STUB = String.raw`(function(){
152
156
  var code = document.createElement('div');
153
157
  code.className = 'CodeMirror-code';
154
158
 
155
- var pre = document.createElement('pre');
156
- pre.textContent = textarea.value || '';
157
-
158
- code.appendChild(pre);
159
159
  sizer.appendChild(code);
160
160
  scroll.appendChild(sizer);
161
161
  wrapper.appendChild(scroll);
162
162
 
163
- return { wrapper: wrapper, scroll: scroll, pre: pre };
163
+ return { wrapper: wrapper, scroll: scroll, sizer: sizer, code: code };
164
+ }
165
+
166
+ function makeLineHandle(lineNo, text){
167
+ var wrap = document.createElement('div');
168
+ wrap.className = 'CodeMirror-linewrap';
169
+ wrap.dataset.lineNumber = String(lineNo + 1);
170
+
171
+ var bg = document.createElement('div');
172
+ bg.className = 'CodeMirror-linebackground';
173
+
174
+ var pre = document.createElement('pre');
175
+ pre.className = 'CodeMirror-line';
176
+ pre.textContent = text && text.length ? text : ' ';
177
+
178
+ wrap.appendChild(bg);
179
+ wrap.appendChild(pre);
180
+ return { lineNo: lineNo, wrap: wrap, bg: bg, pre: pre };
164
181
  }
165
182
 
183
+ window.__cmStubsById = window.__cmStubsById || Object.create(null);
184
+
166
185
  window.CodeMirror = {
167
186
  fromTextArea: function(textarea/*, opts*/){
168
187
  var obj = mkWrapper(textarea);
188
+ var listeners = Object.create(null);
189
+ var lineHandles = [];
190
+ var cursor = { line: 0, ch: 0 };
191
+
192
+ function emit(name){
193
+ var hs = listeners[name] || [];
194
+ var args = Array.prototype.slice.call(arguments, 1);
195
+ for (var i = 0; i < hs.length; i++) {
196
+ try { hs[i].apply(null, args); } catch(_) {}
197
+ }
198
+ }
199
+
200
+ function getText(){
201
+ return normalizeText(textarea.value || '');
202
+ }
203
+
204
+ function getLines(){
205
+ return getText().split('\n');
206
+ }
207
+
208
+ function render(){
209
+ var lines = getLines();
210
+ obj.code.innerHTML = '';
211
+ lineHandles = [];
212
+ for (var i = 0; i < lines.length; i++) {
213
+ var h = makeLineHandle(i, lines[i]);
214
+ lineHandles.push(h);
215
+ obj.code.appendChild(h.wrap);
216
+ }
217
+ if (!lineHandles.length) {
218
+ var h0 = makeLineHandle(0, '');
219
+ lineHandles.push(h0);
220
+ obj.code.appendChild(h0.wrap);
221
+ }
222
+ }
223
+
169
224
  try {
170
225
  textarea.style.display = 'none';
171
226
  textarea.parentNode.insertBefore(obj.wrapper, textarea.nextSibling);
172
227
  } catch(_) {}
173
228
 
174
- function sync(){ obj.pre.textContent = textarea.value || ''; }
175
- function getLines(){
176
- return String(textarea.value || '').replace(/\r\n/g, '\n').replace(/\r/g, '\n').split('\n');
177
- }
178
-
179
229
  const doc = {
180
230
  posFromIndex: function(i){
181
231
  i = Math.max(0, i|0);
@@ -190,9 +240,9 @@ const CODEMIRROR_STUB = String.raw`(function(){
190
240
  }
191
241
  };
192
242
 
193
- return {
243
+ const api = {
194
244
  getValue: function(){ return textarea.value || ''; },
195
- setValue: function(v){ textarea.value = String(v == null ? '' : v); sync(); },
245
+ setValue: function(v){ textarea.value = String(v == null ? '' : v); render(); emit('change', api, { origin: 'setValue' }); },
196
246
 
197
247
  // Methods used by demo.html's streaming appender
198
248
  getScrollerElement: function(){ return obj.scroll; },
@@ -202,22 +252,37 @@ const CODEMIRROR_STUB = String.raw`(function(){
202
252
  const cur = String(textarea.value || '');
203
253
  const idx = posToIndex(cur, pos && pos.line, pos && pos.ch);
204
254
  textarea.value = cur.slice(0, idx) + String(text == null ? '' : text) + cur.slice(idx);
205
- sync();
255
+ render();
256
+ emit('change', api, { origin: '+input' });
206
257
  },
207
258
 
208
259
  // Misc methods used by layout / resizing code
209
260
  refresh: function(){},
210
261
  setSize: function(){},
211
262
  setOption: function(){},
212
- on: function(){},
263
+ on: function(name, fn){ if (!listeners[name]) listeners[name] = []; listeners[name].push(fn); },
213
264
  operation: function(fn){ try{ fn(); } catch(_){} },
214
265
  getWrapperElement: function(){ return obj.wrapper; },
215
266
  getScrollInfo: function(){ return { height: 0, clientHeight: 0, top: 0 }; },
216
267
  defaultTextHeight: function(){ return 17; },
217
-
218
- // Error highlighting hooks (no-op in stub)
219
- addLineClass: function(){},
220
- removeLineClass: function(){},
268
+ lineCount: function(){ return lineHandles.length; },
269
+ getLineHandle: function(n){ return (n >= 0 && n < lineHandles.length) ? lineHandles[n] : null; },
270
+ scrollIntoView: function(){},
271
+ setCursor: function(pos){ cursor = { line: pos && pos.line || 0, ch: pos && pos.ch || 0 }; },
272
+ getCursor: function(){ return { line: cursor.line, ch: cursor.ch }; },
273
+
274
+ // Error highlighting hooks
275
+ addLineClass: function(handle, where, cls){
276
+ if (!handle || !cls) return handle;
277
+ if (where === 'background' && handle.bg) handle.bg.classList.add(cls);
278
+ if (handle.wrap) handle.wrap.classList.add(cls);
279
+ return handle;
280
+ },
281
+ removeLineClass: function(handle, where, cls){
282
+ if (!handle || !cls) return;
283
+ if (where === 'background' && handle.bg) handle.bg.classList.remove(cls);
284
+ if (handle.wrap) handle.wrap.classList.remove(cls);
285
+ },
221
286
  clearGutter: function(){},
222
287
  setGutterMarker: function(){},
223
288
 
@@ -225,6 +290,11 @@ const CODEMIRROR_STUB = String.raw`(function(){
225
290
  getDoc: function(){ return doc; },
226
291
  doc: doc
227
292
  };
293
+
294
+ render();
295
+ textarea.__cmInstance = api;
296
+ window.__cmStubsById[textarea.id || ('cm-' + Object.keys(window.__cmStubsById).length)] = api;
297
+ return api;
228
298
  }
229
299
  };
230
300
  })();`;
@@ -499,73 +569,167 @@ async function main() {
499
569
  assert.ok(!nav.errorText, `demo.html navigation failed: ${nav.errorText}`);
500
570
  await loadFired;
501
571
 
502
- // Click the Run button.
503
- await cdp.send(
504
- 'Runtime.evaluate',
505
- {
506
- expression: `document.getElementById('run-btn') && document.getElementById('run-btn').click();`,
507
- returnByValue: true,
508
- },
509
- sessionId,
510
- );
511
-
512
- // Wait for completion and capture output.
513
- // The demo reports completion with status strings like:
514
- // "Done. Derived: …", "Done (paused). …", or "Done. (Run N)".
515
- let last = { status: '', output: '' };
516
- const deadline = Date.now() + 60000;
517
-
518
- while (Date.now() < deadline) {
519
- // Fail fast on runtime exceptions (often indicates a broken CodeMirror stub or worker init).
520
- if (exceptions.length) {
521
- throw new Error(`Uncaught exception in demo.html: ${JSON.stringify(exceptions[0] || {})}`);
522
- }
523
-
572
+ async function evalInPage(expression) {
524
573
  const r = await cdp.send(
525
574
  'Runtime.evaluate',
526
575
  {
527
- expression: `(() => {
528
- const s = document.getElementById('status');
529
- const o = document.getElementById('output-editor');
530
- return { status: s ? String(s.textContent || '') : '', output: o ? String(o.value || '') : '' };
531
- })()`,
576
+ expression,
532
577
  returnByValue: true,
578
+ awaitPromise: true,
533
579
  },
534
580
  sessionId,
535
581
  );
536
- last = r && r.result ? r.result.value : last;
537
-
538
- const st = last && typeof last.status === 'string' ? last.status : '';
582
+ return r && r.result ? r.result.value : undefined;
583
+ }
539
584
 
540
- // Treat any "Reasoning error" as failure.
541
- if (/Reasoning error/i.test(st)) {
542
- throw new Error(`Playground reported error: ${st}
543
- Output:
544
- ${last.output || ''}`);
585
+ function failFastOnExceptions() {
586
+ if (exceptions.length) {
587
+ throw new Error(`Uncaught exception in demo.html: ${JSON.stringify(exceptions[0] || {})}`);
545
588
  }
589
+ }
546
590
 
547
- // Success conditions: status starts with "Done" (covers "Done." and "Done (paused).")
548
- if (
549
- String(st || '')
550
- .trim()
551
- .startsWith('Done')
552
- )
553
- break;
591
+ async function getPlaygroundState() {
592
+ return (
593
+ (await evalInPage(`(() => {
594
+ const statusEl = document.getElementById('status');
595
+ const outputTa = document.getElementById('output-editor');
596
+ const inputCm = window.__cmStubsById && (window.__cmStubsById['n3-editor'] || window.__cmStubsById['input-editor']);
597
+ const outputCm = window.__cmStubsById && window.__cmStubsById['output-editor'];
598
+ const inputWrapper = inputCm && typeof inputCm.getWrapperElement === 'function' ? inputCm.getWrapperElement() : null;
599
+ const highlighted = inputWrapper
600
+ ? Array.from(inputWrapper.querySelectorAll('.CodeMirror-linebackground.cm-error-line')).map((el) => {
601
+ const wrap = el.parentElement;
602
+ const pre = wrap && wrap.querySelector('pre');
603
+ return {
604
+ line: wrap && wrap.dataset && wrap.dataset.lineNumber ? Number(wrap.dataset.lineNumber) : null,
605
+ text: pre ? String(pre.textContent || '') : '',
606
+ };
607
+ })
608
+ : [];
609
+ return {
610
+ status: statusEl ? String(statusEl.textContent || '') : '',
611
+ output: outputCm && typeof outputCm.getValue === 'function'
612
+ ? String(outputCm.getValue() || '')
613
+ : (outputTa ? String(outputTa.value || '') : ''),
614
+ highlighted,
615
+ };
616
+ })()`)) || { status: '', output: '', highlighted: [] }
617
+ );
618
+ }
619
+
620
+ async function setProgram(text) {
621
+ const payload = JSON.stringify(String(text));
622
+ await evalInPage(`(() => {
623
+ const cm = window.__cmStubsById && (window.__cmStubsById['n3-editor'] || window.__cmStubsById['input-editor']);
624
+ if (cm && typeof cm.setValue === 'function') {
625
+ cm.setValue(${payload});
626
+ return true;
627
+ }
628
+ const ta = document.getElementById('n3-editor') || document.getElementById('input-editor');
629
+ if (!ta) throw new Error('n3-editor textarea not found');
630
+ ta.value = ${payload};
631
+ return true;
632
+ })()`);
633
+ }
554
634
 
555
- await sleep(100);
635
+ async function clickRun() {
636
+ await evalInPage(`(() => {
637
+ const btn = document.getElementById('run-btn');
638
+ if (!btn) throw new Error('run-btn not found');
639
+ btn.click();
640
+ return true;
641
+ })()`);
556
642
  }
557
643
 
558
- assert.ok(
559
- last &&
560
- typeof last.status === 'string' &&
561
- String(last.status || '')
644
+ async function waitForState(label, predicate, timeoutMs = 60000) {
645
+ const deadline = Date.now() + timeoutMs;
646
+ let last = { status: '', output: '', highlighted: [] };
647
+ while (Date.now() < deadline) {
648
+ failFastOnExceptions();
649
+ last = await getPlaygroundState();
650
+ if (predicate(last)) return last;
651
+ await sleep(100);
652
+ }
653
+ throw new Error(`Timed out waiting for ${label}. Last state:
654
+ ${JSON.stringify(last, null, 2)}`);
655
+ }
656
+
657
+ const DEFAULT_PROGRAM_EXPECTS = [
658
+ [/Socrates/i, 'Expected output to mention Socrates'],
659
+ [/Mortal/i, 'Expected output to mention Mortal'],
660
+ ];
661
+ const syntaxErrorProgram = `@prefix : <#> .
662
+ :alice :name "Ada" .
663
+ ^
664
+ `;
665
+ const fuseProgram = fs.readFileSync(path.join(ROOT, 'examples', 'fuse.n3'), 'utf8');
666
+ const outputStringProgram = `@prefix : <#> .
667
+ @prefix log: <http://www.w3.org/2000/10/swap/log#> .
668
+ :report log:outputString "Hello from output string\nLine 2\n" .
669
+ `;
670
+
671
+ // 1) Baseline smoke test: the default program runs to completion.
672
+ await clickRun();
673
+ const baseline = await waitForState(
674
+ 'default program completion',
675
+ (st) =>
676
+ String(st.status || '')
562
677
  .trim()
563
678
  .startsWith('Done'),
564
- `Expected Done. Got: ${last.status}`,
679
+ 60000,
565
680
  );
566
- assert.ok(last && typeof last.output === 'string' && last.output.length > 0, 'Expected non-empty output');
567
- assert.match(last.output, /Socrates/i, 'Expected output to mention Socrates');
568
- assert.match(last.output, /Mortal/i, 'Expected output to mention Mortal');
681
+ assert.ok(typeof baseline.output === 'string' && baseline.output.length > 0, 'Expected non-empty output');
682
+ for (const [re, msg] of DEFAULT_PROGRAM_EXPECTS) assert.match(baseline.output, re, msg);
683
+ ok('playground runs the default Socrates program');
684
+
685
+ // 2) N3 syntax errors should be shown in Output and highlight the offending line.
686
+ await setProgram(syntaxErrorProgram);
687
+ await clickRun();
688
+ const syntaxErr = await waitForState(
689
+ 'syntax error reporting',
690
+ (st) => String(st.status || '').trim() === 'Error.' && /syntax error/i.test(String(st.output || '')),
691
+ 20000,
692
+ );
693
+ assert.match(syntaxErr.output, /Syntax error in input\.n3:3:1:/i, 'Expected line/column in syntax error output');
694
+ assert.match(syntaxErr.output, /\n\^\s*$/m, 'Expected caret line in syntax error output');
695
+ assert.equal(syntaxErr.highlighted[0].line, 3, 'Expected line 3 to be highlighted');
696
+ assert.equal(syntaxErr.highlighted[0].text, '^', 'Expected highlighted line text to match the broken line');
697
+ ok('playground shows syntax errors in Output and highlights the offending line');
698
+
699
+ // 3) Inference fuse output should be visible in the Output pane.
700
+ await setProgram(fuseProgram);
701
+ await clickRun();
702
+ const fuse = await waitForState(
703
+ 'inference fuse reporting',
704
+ (st) =>
705
+ String(st.status || '')
706
+ .trim()
707
+ .startsWith('Done') && /Inference fuse triggered/i.test(String(st.output || '')),
708
+ 30000,
709
+ );
710
+ assert.match(fuse.output, /Inference fuse triggered\./i, 'Expected fuse message in Output');
711
+ assert.match(fuse.output, /Fired rule:/i, 'Expected fired rule explanation in Output');
712
+ assert.match(fuse.output, /Matched instance:/i, 'Expected matched instance in Output');
713
+ ok('playground clearly shows inference fuse output');
714
+
715
+ // 4) log:outputString should render as clean text, not raw triples.
716
+ await setProgram(outputStringProgram);
717
+ await clickRun();
718
+ const rendered = await waitForState(
719
+ 'log:outputString rendering',
720
+ (st) =>
721
+ String(st.status || '')
722
+ .trim()
723
+ .startsWith('Done') && /Hello from output string/.test(String(st.output || '')),
724
+ 20000,
725
+ );
726
+ assert.match(rendered.output, /^Hello from output string\nLine 2\n?$/m, 'Expected rendered outputString text');
727
+ assert.doesNotMatch(
728
+ rendered.output,
729
+ /:report\s+log:outputString\s+"|# Derived triples/i,
730
+ 'Expected clean rendered output without raw triples',
731
+ );
732
+ ok('playground renders log:outputString cleanly in Output');
569
733
 
570
734
  // Ensure no uncaught runtime exceptions.
571
735
  assert.equal(exceptions.length, 0, `Uncaught exceptions in demo.html: ${JSON.stringify(exceptions[0] || {})}`);
@@ -578,8 +742,6 @@ ${last.output || ''}`);
578
742
  try {
579
743
  await cdp.send('Browser.close');
580
744
  } catch (_) {}
581
-
582
- ok('demo.html loads and runs the default program');
583
745
  } finally {
584
746
  await cleanup();
585
747
  }