eyeling 1.7.2 → 1.7.4

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/LICENSE.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # MIT License
2
2
 
3
- ### Copyright 2021-2025 Jos De Roo, KNoWS office of IDLab, Ghent University - imec
3
+ ### Copyright 2021-2026 Jos De Roo, KNoWS office of IDLab, Ghent University - imec
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -215,7 +215,7 @@ Commonly used N3/Turtle features:
215
215
 
216
216
  - **crypto**: `crypto:md5` `crypto:sha` `crypto:sha256` `crypto:sha512`
217
217
  - **list**: `list:append` `list:first` `list:firstRest` `list:in` `list:iterate` `list:last` `list:length` `list:map` `list:member` `list:memberAt` `list:notMember` `list:remove` `list:rest` `list:reverse` `list:sort`
218
- - **log**: `log:collectAllIn` `log:equalTo` `log:forAllIn` `log:impliedBy` `log:implies` `log:notEqualTo` `log:notIncludes` `log:skolem` `log:uri`
218
+ - **log**: `log:collectAllIn` `log:content` `log:equalTo` `log:forAllIn` `log:impliedBy` `log:implies` `log:notEqualTo` `log:notIncludes` `log:semantics` `log:skolem` `log:uri`
219
219
  - **math**: `math:absoluteValue` `math:acos` `math:asin` `math:atan` `math:cos` `math:cosh` `math:degrees` `math:difference` `math:equalTo` `math:exponentiation` `math:greaterThan` `math:integerQuotient` `math:lessThan` `math:negation` `math:notEqualTo` `math:notGreaterThan` `math:notLessThan` `math:product` `math:quotient` `math:remainder` `math:rounded` `math:sin` `math:sinh` `math:sum` `math:tan` `math:tanh`
220
220
  - **string**: `string:concatenation` `string:contains` `string:containsIgnoringCase` `string:endsWith` `string:equalIgnoringCase` `string:format` `string:greaterThan` `string:jsonPointer` `string:lessThan` `string:matches` `string:notEqualIgnoringCase` `string:notGreaterThan` `string:notLessThan` `string:notMatches` `string:replace` `string:scrape` `string:startsWith`
221
221
  - **time**: `time:localTime`
@@ -0,0 +1,12 @@
1
+ @prefix : <http://example.org/> .
2
+ @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
3
+ @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
4
+
5
+ :gotContent :is "# Schema for test data\n#\n# This is only \n@prefix daml: <http://www.daml.org/2001/03/daml+oil#> .\n@prefix mech: <#> .\n\n@prefix : <#> .\n\n:includes a daml:TransitiveProperty .\n\n:partOf a daml:TransitiveProperty; daml:inverseOf :includes .\n\n:dependsOn a daml:TransitiveProperty ;\n daml:hasSubProperty :includes . # Real name of subproperty?\n\n\n\n\n"^^xsd:string .
6
+ :gotFormula :is {
7
+ <https://www.w3.org/2000/10/swap/test/s1.n3#includes> a <http://www.daml.org/2001/03/daml+oil#TransitiveProperty> .
8
+ <https://www.w3.org/2000/10/swap/test/s1.n3#partOf> a <http://www.daml.org/2001/03/daml+oil#TransitiveProperty> .
9
+ <https://www.w3.org/2000/10/swap/test/s1.n3#partOf> <http://www.daml.org/2001/03/daml+oil#inverseOf> <https://www.w3.org/2000/10/swap/test/s1.n3#includes> .
10
+ <https://www.w3.org/2000/10/swap/test/s1.n3#dependsOn> a <http://www.daml.org/2001/03/daml+oil#TransitiveProperty> .
11
+ <https://www.w3.org/2000/10/swap/test/s1.n3#dependsOn> <http://www.daml.org/2001/03/daml+oil#hasSubProperty> <https://www.w3.org/2000/10/swap/test/s1.n3#includes> .
12
+ } .
@@ -0,0 +1,19 @@
1
+ # =============================================
2
+ # Reaching out onto the Web
3
+ # See https://www.w3.org/2000/10/swap/doc/Reach
4
+ # =============================================
5
+
6
+ @prefix : <http://example.org/>.
7
+ @prefix log: <http://www.w3.org/2000/10/swap/log#>.
8
+
9
+ {
10
+ <https://www.w3.org/2000/10/swap/test/s1.n3>
11
+ log:content ?txt ;
12
+ log:semantics ?f .
13
+ }
14
+ =>
15
+ {
16
+ :gotContent :is ?txt .
17
+ :gotFormula :is ?f .
18
+ }.
19
+
package/eyeling.js CHANGED
@@ -82,6 +82,159 @@ const __parseNumericInfoCache = new Map(); // lit string -> info|null
82
82
  // Cache for string:jsonPointer: jsonText -> { parsed: any|null, ptrCache: Map<string, Term|null> }
83
83
  const jsonPointerCache = new Map();
84
84
 
85
+ // -----------------------------------------------------------------------------
86
+ // log:content / log:semantics support (basic, synchronous)
87
+ // -----------------------------------------------------------------------------
88
+ // Cache dereferenced resources within a single run.
89
+ // Key is the dereferenced document IRI *without* fragment.
90
+ const __logContentCache = new Map(); // iri -> string | null (null means fetch/read failed)
91
+ const __logSemanticsCache = new Map(); // iri -> GraphTerm | null (null means parse failed)
92
+
93
+ function __stripFragment(iri) {
94
+ const i = iri.indexOf('#');
95
+ return i >= 0 ? iri.slice(0, i) : iri;
96
+ }
97
+
98
+ function __isHttpIri(iri) {
99
+ return typeof iri === 'string' && (iri.startsWith('http://') || iri.startsWith('https://'));
100
+ }
101
+
102
+ function __isFileIri(iri) {
103
+ return typeof iri === 'string' && iri.startsWith('file://');
104
+ }
105
+
106
+ function __fileIriToPath(fileIri) {
107
+ // Basic file:// URI decoding. Handles file:///abs/path and file://localhost/abs/path.
108
+ try {
109
+ const u = new URL(fileIri);
110
+ return decodeURIComponent(u.pathname);
111
+ } catch {
112
+ return decodeURIComponent(fileIri.replace(/^file:\/\//, ''));
113
+ }
114
+ }
115
+
116
+ function __readFileText(pathOrFileIri) {
117
+ const fs = require('fs');
118
+ let path = pathOrFileIri;
119
+ if (__isFileIri(pathOrFileIri)) path = __fileIriToPath(pathOrFileIri);
120
+ try {
121
+ return fs.readFileSync(path, { encoding: 'utf8' });
122
+ } catch {
123
+ return null;
124
+ }
125
+ }
126
+
127
+ function __fetchHttpTextViaSubprocess(url) {
128
+ const cp = require('child_process');
129
+ // Use a subprocess so this code remains synchronous without rewriting the whole reasoner to async.
130
+ const script = `
131
+ const url = process.argv[1];
132
+ const maxRedirects = 10;
133
+ function get(u, n) {
134
+ if (n > maxRedirects) { console.error('Too many redirects'); process.exit(3); }
135
+ let mod;
136
+ if (u.startsWith('https://')) mod = require('https');
137
+ else if (u.startsWith('http://')) mod = require('http');
138
+ else { console.error('Not http(s)'); process.exit(2); }
139
+
140
+ const { URL } = require('url');
141
+ const uu = new URL(u);
142
+ const opts = {
143
+ protocol: uu.protocol,
144
+ hostname: uu.hostname,
145
+ port: uu.port || undefined,
146
+ path: uu.pathname + uu.search,
147
+ headers: {
148
+ 'accept': 'text/n3, text/turtle, application/n-triples, application/n-quads, text/plain;q=0.1, */*;q=0.01',
149
+ 'user-agent': 'eyeling-log-builtins'
150
+ }
151
+ };
152
+ const req = mod.request(opts, (res) => {
153
+ const sc = res.statusCode || 0;
154
+ if (sc >= 300 && sc < 400 && res.headers && res.headers.location) {
155
+ const next = new URL(res.headers.location, u).toString();
156
+ res.resume();
157
+ return get(next, n + 1);
158
+ }
159
+ if (sc < 200 || sc >= 300) {
160
+ res.resume();
161
+ console.error('HTTP status ' + sc);
162
+ process.exit(4);
163
+ }
164
+ res.setEncoding('utf8');
165
+ let data = '';
166
+ res.on('data', (c) => { data += c; });
167
+ res.on('end', () => { process.stdout.write(data); });
168
+ });
169
+ req.on('error', (e) => { console.error(e && e.message ? e.message : String(e)); process.exit(5); });
170
+ req.end();
171
+ }
172
+ get(url, 0);
173
+ `;
174
+ const r = cp.spawnSync(process.execPath, ['-e', script, url], {
175
+ encoding: 'utf8',
176
+ maxBuffer: 32 * 1024 * 1024
177
+ });
178
+ if (r.status !== 0) return null;
179
+ return r.stdout;
180
+ }
181
+
182
+ function __derefTextSync(iriNoFrag) {
183
+ if (__logContentCache.has(iriNoFrag)) return __logContentCache.get(iriNoFrag);
184
+
185
+ let text = null;
186
+ if (__isHttpIri(iriNoFrag)) {
187
+ text = __fetchHttpTextViaSubprocess(iriNoFrag);
188
+ } else {
189
+ // Treat any non-http(s) IRI as a local path (including file://), for basic usability.
190
+ text = __readFileText(iriNoFrag);
191
+ }
192
+ __logContentCache.set(iriNoFrag, text);
193
+ return text;
194
+ }
195
+
196
+ function __parseSemanticsToFormula(text, baseIri) {
197
+ const toks = lex(text);
198
+ const parser = new Parser(toks);
199
+ if (typeof baseIri === 'string' && baseIri) parser.prefixes.setBase(baseIri);
200
+
201
+ const [_prefixes, triples, frules, brules] = parser.parseDocument();
202
+
203
+ const all = triples.slice();
204
+ const impliesPred = internIri(LOG_NS + 'implies');
205
+ const impliedByPred = internIri(LOG_NS + 'impliedBy');
206
+
207
+ // Represent top-level => / <= rules as triples between formula terms,
208
+ // so the returned formula can include them.
209
+ for (const r of frules) {
210
+ all.push(new Triple(new GraphTerm(r.premise), impliesPred, new GraphTerm(r.conclusion)));
211
+ }
212
+ for (const r of brules) {
213
+ all.push(new Triple(new GraphTerm(r.conclusion), impliedByPred, new GraphTerm(r.premise)));
214
+ }
215
+
216
+ return new GraphTerm(all);
217
+ }
218
+
219
+ function __derefSemanticsSync(iriNoFrag) {
220
+ if (__logSemanticsCache.has(iriNoFrag)) return __logSemanticsCache.get(iriNoFrag);
221
+
222
+ const text = __derefTextSync(iriNoFrag);
223
+ if (typeof text !== 'string') {
224
+ __logSemanticsCache.set(iriNoFrag, null);
225
+ return null;
226
+ }
227
+ try {
228
+ const formula = __parseSemanticsToFormula(text, iriNoFrag);
229
+ __logSemanticsCache.set(iriNoFrag, formula);
230
+ return formula;
231
+ } catch {
232
+ __logSemanticsCache.set(iriNoFrag, null);
233
+ return null;
234
+ }
235
+ }
236
+
237
+
85
238
  // Controls whether human-readable proof comments are printed.
86
239
  let proofCommentsEnabled = false;
87
240
  // Super restricted mode: disable *all* builtins except => / <= (log:implies / log:impliedBy)
@@ -4690,6 +4843,53 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4690
4843
  return s2 !== null ? [s2] : [];
4691
4844
  }
4692
4845
 
4846
+ // log:content
4847
+ // Schema: $s+ log:content $o?
4848
+ // Dereferences $s and returns the online resource as an xsd:string.
4849
+ if (pv === LOG_NS + 'content') {
4850
+ const iri = iriValue(g.s);
4851
+ if (iri === null) return [];
4852
+ const docIri = __stripFragment(iri);
4853
+
4854
+ const text = __derefTextSync(docIri);
4855
+ if (typeof text !== 'string') return [];
4856
+
4857
+ const lit = internLiteral(`${JSON.stringify(text)}^^<${XSD_NS}string>`);
4858
+
4859
+ if (g.o instanceof Var) {
4860
+ const s2 = { ...subst };
4861
+ s2[g.o.name] = lit;
4862
+ return [s2];
4863
+ }
4864
+ if (g.o instanceof Blank) return [{ ...subst }];
4865
+
4866
+ const s2 = unifyTerm(g.o, lit, subst);
4867
+ return s2 !== null ? [s2] : [];
4868
+ }
4869
+
4870
+ // log:semantics
4871
+ // Schema: $s+ log:semantics $o?
4872
+ // Dereferences $s, parses the retrieved resource, and returns it as a formula.
4873
+ if (pv === LOG_NS + 'semantics') {
4874
+ const iri = iriValue(g.s);
4875
+ if (iri === null) return [];
4876
+ const docIri = __stripFragment(iri);
4877
+
4878
+ const formula = __derefSemanticsSync(docIri);
4879
+ if (!(formula instanceof GraphTerm)) return [];
4880
+
4881
+ if (g.o instanceof Var) {
4882
+ const s2 = { ...subst };
4883
+ s2[g.o.name] = formula;
4884
+ return [s2];
4885
+ }
4886
+ if (g.o instanceof Blank) return [{ ...subst }];
4887
+
4888
+ const s2 = unifyTerm(g.o, formula, subst);
4889
+ return s2 !== null ? [s2] : [];
4890
+ }
4891
+
4892
+
4693
4893
  // log:dtlit
4694
4894
  // Schema: ( $s.1? $s.2? )? log:dtlit $o?
4695
4895
  // true iff $o is a datatyped literal with string value $s.1 and datatype IRI $s.2
@@ -4934,6 +5134,11 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4934
5134
  if (!scopeFacts) return []; // DELAY until snapshot exists
4935
5135
  }
4936
5136
 
5137
+ // If sols is a blank node succeed without collecting/binding.
5138
+ if (listTerm instanceof Blank) {
5139
+ return [{ ...subst }];
5140
+ }
5141
+
4937
5142
  const visited2 = [];
4938
5143
  const sols = proveGoals(Array.from(clauseTerm.triples), {}, scopeFacts, scopeBackRules, depth + 1, visited2, varGen);
4939
5144
 
@@ -4995,8 +5200,8 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4995
5200
  if (pv === LOG_NS + 'uri') {
4996
5201
  // Direction 1: subject is an IRI -> object is its string representation
4997
5202
  if (g.s instanceof Iri) {
4998
- const uriStr = g.s.value; // raw IRI string, e.g. "https://www.w3.org"
4999
- const lit = makeStringLiteral(uriStr); // "https://www.w3.org"
5203
+ const uriStr = g.s.value; // raw IRI string
5204
+ const lit = makeStringLiteral(uriStr); // "..."
5000
5205
  const s2 = unifyTerm(goal.o, lit, subst);
5001
5206
  return s2 !== null ? [s2] : [];
5002
5207
  }
@@ -5005,6 +5210,14 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
5005
5210
  if (g.o instanceof Literal) {
5006
5211
  const uriStr = termToJsString(g.o); // JS string from the literal
5007
5212
  if (uriStr === null) return [];
5213
+
5214
+ // Reject strings that cannot be safely serialized as <...> in Turtle/N3.
5215
+ // Turtle IRIREF forbids control/space and these characters: < > " { } | ^ ` \
5216
+ // (and eyeling also prints IRIs starting with "_:" as blank-node labels)
5217
+ if (uriStr.startsWith('_:') || /[\u0000-\u0020<>"{}|^`\\]/.test(uriStr)) {
5218
+ return [];
5219
+ }
5220
+
5008
5221
  const iri = internIri(uriStr);
5009
5222
  const s2 = unifyTerm(goal.s, iri, subst);
5010
5223
  return s2 !== null ? [s2] : [];
@@ -5906,7 +6119,6 @@ function printExplanation(df, prefixes) {
5906
6119
  console.log('# ----------------------------------------------------------------------\n');
5907
6120
  }
5908
6121
 
5909
-
5910
6122
  function offsetToLineCol(text, offset) {
5911
6123
  const chars = Array.from(text);
5912
6124
  const n = Math.max(0, Math.min(typeof offset === 'number' ? offset : 0, chars.length));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyeling",
3
- "version": "1.7.2",
3
+ "version": "1.7.4",
4
4
  "description": "A minimal Notation3 (N3) reasoner in JavaScript.",
5
5
  "main": "./index.js",
6
6
  "keywords": [