eyeleng 1.0.4 → 1.0.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/README.md +43 -0
- package/dist/browser/eyeleng.browser.js +2462 -1085
- package/examples/cat-koko.srl +52 -0
- package/examples/graph-term-emulation.srl +83 -0
- package/examples/output/cat-koko.trig +3 -0
- package/examples/output/collection-nesting.trig +1 -1
- package/examples/output/graph-term-emulation.trig +11 -0
- package/examples/output/rdf-messages.trig +3 -0
- package/examples/rdf-messages.srl +15 -0
- package/examples/rdf-messages.trig +12 -0
- package/eyeleng.js +2466 -1083
- package/package.json +7 -2
- package/playground.html +1 -1
- package/src/api.js +4 -0
- package/src/cli.js +6 -0
- package/src/parser.js +95 -1
- package/src/rdfEntailment.js +571 -0
- package/src/rdfManifest.js +724 -0
- package/src/rdfMessages.js +321 -0
- package/src/rdfSyntax.js +955 -0
- package/test/api.test.js +63 -0
- package/test/harness.js +38 -10
- package/test/run.js +6 -3
- package/test/shacl12-rules.test.js +14 -13
- package/test/w3c-rdf.test.js +202 -0
- package/tools/browser-bundle.js +0 -0
- package/tools/bundle.js +0 -0
- package/tools/w3c-rdf.js +36 -0
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Small RDF/RDFS entailment support used by the W3C RDF-MT / RDF 1.2
|
|
4
|
+
// Semantics manifest runner. It intentionally operates on the grammar-hardened
|
|
5
|
+
// W3C RDF graph representation from rdfSyntax.js ({kind: ...} terms), not on
|
|
6
|
+
// the SRL rule-engine term representation.
|
|
7
|
+
|
|
8
|
+
const RDF = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#';
|
|
9
|
+
const RDFS = 'http://www.w3.org/2000/01/rdf-schema#';
|
|
10
|
+
const XSD = 'http://www.w3.org/2001/XMLSchema#';
|
|
11
|
+
|
|
12
|
+
const RDF_TYPE = `${RDF}type`;
|
|
13
|
+
const RDF_PROPERTY = `${RDF}Property`;
|
|
14
|
+
const RDF_XML_LITERAL = `${RDF}XMLLiteral`;
|
|
15
|
+
const RDF_HTML = `${RDF}HTML`;
|
|
16
|
+
const RDF_LANG_STRING = `${RDF}langString`;
|
|
17
|
+
const RDF_DIR_LANG_STRING = `${RDF}dirLangString`;
|
|
18
|
+
const RDF_JSON = `${RDF}JSON`;
|
|
19
|
+
const RDF_REIFIES = `${RDF}reifies`;
|
|
20
|
+
const RDFS_PROPOSITION = `${RDFS}Proposition`;
|
|
21
|
+
const RDF_PROPOSITION = `${RDF}Proposition`; // legacy/non-standard alias kept for compatibility with older local regressions
|
|
22
|
+
const RDF_TRIPLE_TERM = `${RDF}TripleTerm`; // legacy/internal helper; RDF 1.2 RDFS range uses rdfs:Proposition
|
|
23
|
+
const RDFS_RESOURCE = `${RDFS}Resource`;
|
|
24
|
+
const RDFS_CLASS = `${RDFS}Class`;
|
|
25
|
+
const RDFS_LITERAL = `${RDFS}Literal`;
|
|
26
|
+
const RDFS_DATATYPE = `${RDFS}Datatype`;
|
|
27
|
+
const RDFS_SUBCLASS_OF = `${RDFS}subClassOf`;
|
|
28
|
+
const RDFS_SUBPROPERTY_OF = `${RDFS}subPropertyOf`;
|
|
29
|
+
const RDFS_DOMAIN = `${RDFS}domain`;
|
|
30
|
+
const RDFS_RANGE = `${RDFS}range`;
|
|
31
|
+
const RDFS_CONTAINER_MEMBERSHIP_PROPERTY = `${RDFS}ContainerMembershipProperty`;
|
|
32
|
+
const RDFS_MEMBER = `${RDFS}member`;
|
|
33
|
+
const XSD_STRING = `${XSD}string`;
|
|
34
|
+
const XSD_BOOLEAN = `${XSD}boolean`;
|
|
35
|
+
const XSD_INTEGER = `${XSD}integer`;
|
|
36
|
+
const XSD_INT = `${XSD}int`;
|
|
37
|
+
const XSD_DECIMAL = `${XSD}decimal`;
|
|
38
|
+
const XSD_FLOAT = `${XSD}float`;
|
|
39
|
+
const XSD_DOUBLE = `${XSD}double`;
|
|
40
|
+
|
|
41
|
+
function iri(value) { return Object.freeze({ kind: 'iri', value: String(value) }); }
|
|
42
|
+
function triple(s, p, o, graph = null) { return Object.freeze({ s, p, o, graph }); }
|
|
43
|
+
|
|
44
|
+
function termKey(term) {
|
|
45
|
+
if (!term) return 'default';
|
|
46
|
+
if (term.kind === 'iri') return `I:${term.value}`;
|
|
47
|
+
if (term.kind === 'blank') return `B:${term.value}`;
|
|
48
|
+
if (term.kind === 'literal') return `L:${JSON.stringify(term.value)}^^${term.datatype || ''}@${(term.language || '').toLowerCase()}--${term.langDir || ''}`;
|
|
49
|
+
if (term.kind === 'triple') return `T:${termKey(term.s)} ${termKey(term.p)} ${termKey(term.o)}`;
|
|
50
|
+
return JSON.stringify(term);
|
|
51
|
+
}
|
|
52
|
+
function tripleKey(t) { return `${termKey(t.s)} ${termKey(t.p)} ${termKey(t.o)} ${termKey(t.graph)}`; }
|
|
53
|
+
|
|
54
|
+
function uniqueTriples(triples) {
|
|
55
|
+
const seen = new Set();
|
|
56
|
+
const out = [];
|
|
57
|
+
for (const t of triples || []) {
|
|
58
|
+
const key = tripleKey(t);
|
|
59
|
+
if (seen.has(key)) continue;
|
|
60
|
+
seen.add(key);
|
|
61
|
+
out.push(t);
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function addTriple(out, seen, s, p, o) {
|
|
67
|
+
const t = triple(s, p, o);
|
|
68
|
+
const key = tripleKey(t);
|
|
69
|
+
if (seen.has(key)) return false;
|
|
70
|
+
seen.add(key);
|
|
71
|
+
out.push(t);
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function isIri(term, value = null) { return term && term.kind === 'iri' && (value == null || term.value === value); }
|
|
76
|
+
function iriTerm(value) { return iri(value); }
|
|
77
|
+
|
|
78
|
+
function allTerms(triples) {
|
|
79
|
+
const terms = [];
|
|
80
|
+
function add(term) {
|
|
81
|
+
if (!term) return;
|
|
82
|
+
terms.push(term);
|
|
83
|
+
if (term.kind === 'triple') { add(term.s); add(term.p); add(term.o); }
|
|
84
|
+
}
|
|
85
|
+
for (const t of triples || []) { add(t.s); add(t.p); add(t.o); }
|
|
86
|
+
return terms;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function rdfAxiomaticTriples() {
|
|
90
|
+
const p = iriTerm(RDF_TYPE);
|
|
91
|
+
const prop = iriTerm(RDF_PROPERTY);
|
|
92
|
+
const cls = iriTerm(RDFS_CLASS);
|
|
93
|
+
const axioms = [];
|
|
94
|
+
const properties = [
|
|
95
|
+
RDF_TYPE, `${RDF}subject`, `${RDF}predicate`, `${RDF}object`, `${RDF}first`, `${RDF}rest`, `${RDF}value`,
|
|
96
|
+
RDFS_SUBCLASS_OF, RDFS_SUBPROPERTY_OF, RDFS_DOMAIN, RDFS_RANGE, `${RDFS}label`, `${RDFS}comment`, `${RDFS}seeAlso`, `${RDFS}isDefinedBy`, RDFS_MEMBER,
|
|
97
|
+
];
|
|
98
|
+
for (const property of properties) axioms.push(triple(iriTerm(property), p, prop));
|
|
99
|
+
for (let i = 1; i <= 10; i += 1) axioms.push(triple(iriTerm(`${RDF}_${i}`), p, prop));
|
|
100
|
+
return axioms;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function rdfsAxiomaticTriples(recognizedDatatypes = []) {
|
|
104
|
+
const p = iriTerm(RDF_TYPE);
|
|
105
|
+
const subClass = iriTerm(RDFS_SUBCLASS_OF);
|
|
106
|
+
const subProp = iriTerm(RDFS_SUBPROPERTY_OF);
|
|
107
|
+
const domain = iriTerm(RDFS_DOMAIN);
|
|
108
|
+
const range = iriTerm(RDFS_RANGE);
|
|
109
|
+
const axioms = rdfAxiomaticTriples();
|
|
110
|
+
const classes = [RDFS_RESOURCE, RDFS_CLASS, RDFS_LITERAL, RDFS_DATATYPE, RDF_PROPERTY, `${RDF}List`, RDFS_CONTAINER_MEMBERSHIP_PROPERTY, RDFS_PROPOSITION];
|
|
111
|
+
for (const c of classes) axioms.push(triple(iriTerm(c), p, iriTerm(RDFS_CLASS)));
|
|
112
|
+
axioms.push(triple(iriTerm(RDFS_DATATYPE), subClass, iriTerm(RDFS_CLASS)));
|
|
113
|
+
axioms.push(triple(iriTerm(RDF_LANG_STRING), p, iriTerm(RDFS_DATATYPE)));
|
|
114
|
+
axioms.push(triple(iriTerm(RDF_DIR_LANG_STRING), p, iriTerm(RDFS_DATATYPE)));
|
|
115
|
+
axioms.push(triple(iriTerm(RDF_XML_LITERAL), p, iriTerm(RDFS_DATATYPE)));
|
|
116
|
+
axioms.push(triple(iriTerm(RDF_HTML), p, iriTerm(RDFS_DATATYPE)));
|
|
117
|
+
axioms.push(triple(iriTerm(RDF_JSON), p, iriTerm(RDFS_DATATYPE)));
|
|
118
|
+
for (const dt of recognizedDatatypes || []) axioms.push(triple(iriTerm(dt), p, iriTerm(RDFS_DATATYPE)));
|
|
119
|
+
|
|
120
|
+
axioms.push(triple(iriTerm(RDFS_SUBCLASS_OF), domain, iriTerm(RDFS_CLASS)));
|
|
121
|
+
axioms.push(triple(iriTerm(RDFS_SUBCLASS_OF), range, iriTerm(RDFS_CLASS)));
|
|
122
|
+
axioms.push(triple(iriTerm(RDFS_SUBPROPERTY_OF), domain, iriTerm(RDF_PROPERTY)));
|
|
123
|
+
axioms.push(triple(iriTerm(RDFS_SUBPROPERTY_OF), range, iriTerm(RDF_PROPERTY)));
|
|
124
|
+
axioms.push(triple(iriTerm(RDFS_DOMAIN), domain, iriTerm(RDF_PROPERTY)));
|
|
125
|
+
axioms.push(triple(iriTerm(RDFS_DOMAIN), range, iriTerm(RDFS_CLASS)));
|
|
126
|
+
axioms.push(triple(iriTerm(RDFS_RANGE), domain, iriTerm(RDF_PROPERTY)));
|
|
127
|
+
axioms.push(triple(iriTerm(RDFS_RANGE), range, iriTerm(RDFS_CLASS)));
|
|
128
|
+
axioms.push(triple(iriTerm(RDFS_MEMBER), p, iriTerm(RDF_PROPERTY)));
|
|
129
|
+
axioms.push(triple(iriTerm(RDFS_MEMBER), domain, iriTerm(RDFS_RESOURCE)));
|
|
130
|
+
axioms.push(triple(iriTerm(RDFS_MEMBER), range, iriTerm(RDFS_RESOURCE)));
|
|
131
|
+
axioms.push(triple(iriTerm(RDF_REIFIES), p, iriTerm(RDF_PROPERTY)));
|
|
132
|
+
axioms.push(triple(iriTerm(RDF_REIFIES), domain, iriTerm(RDFS_RESOURCE)));
|
|
133
|
+
axioms.push(triple(iriTerm(RDF_REIFIES), range, iriTerm(RDFS_PROPOSITION)));
|
|
134
|
+
// Keep a compatibility bridge for the internal rdf:TripleTerm helper, but the
|
|
135
|
+
// RDF 1.2 Schema vocabulary uses rdfs:Proposition for propositions.
|
|
136
|
+
axioms.push(triple(iriTerm(RDF_TRIPLE_TERM), iriTerm(RDFS_SUBCLASS_OF), iriTerm(RDFS_PROPOSITION)));
|
|
137
|
+
axioms.push(triple(iriTerm(RDF_PROPOSITION), iriTerm(RDFS_SUBCLASS_OF), iriTerm(RDFS_PROPOSITION)));
|
|
138
|
+
axioms.push(triple(iriTerm(RDFS_CONTAINER_MEMBERSHIP_PROPERTY), subClass, iriTerm(RDF_PROPERTY)));
|
|
139
|
+
for (let i = 1; i <= 10; i += 1) axioms.push(triple(iriTerm(`${RDF}_${i}`), subProp, iriTerm(RDFS_MEMBER)));
|
|
140
|
+
return axioms;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function normalizeRegime(regime) {
|
|
144
|
+
const value = String(regime || 'simple').trim().toLowerCase();
|
|
145
|
+
if (value === 'rdfs' || value === 'rdfs-entailment') return 'rdfs';
|
|
146
|
+
if (value === 'rdf' || value === 'rdf-entailment') return 'rdf';
|
|
147
|
+
return 'simple';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function normalizeDatatype(dt) {
|
|
151
|
+
if (dt === XSD_INT) return XSD_INTEGER;
|
|
152
|
+
return dt;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function datatypeKind(dt) {
|
|
156
|
+
const n = normalizeDatatype(dt);
|
|
157
|
+
if (n === XSD_INTEGER || n === XSD_DECIMAL) return 'decimal';
|
|
158
|
+
if (n === XSD_FLOAT) return 'float';
|
|
159
|
+
if (n === XSD_DOUBLE) return 'double';
|
|
160
|
+
if (n === XSD_STRING) return 'string';
|
|
161
|
+
if (n === RDF_LANG_STRING) return 'langString';
|
|
162
|
+
if (n === RDF_DIR_LANG_STRING) return 'dirLangString';
|
|
163
|
+
if (n === RDF_XML_LITERAL) return 'xml';
|
|
164
|
+
if (n === RDF_JSON) return 'json';
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function datatypeCompatible(a, b) {
|
|
169
|
+
const ak = datatypeKind(a);
|
|
170
|
+
const bk = datatypeKind(b);
|
|
171
|
+
if (!ak || !bk) return true;
|
|
172
|
+
if (ak === bk) return true;
|
|
173
|
+
// xsd:integer/xsd:int value spaces are contained in xsd:decimal.
|
|
174
|
+
if (ak === 'decimal' && bk === 'decimal') return true;
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function literalDatatype(term) {
|
|
179
|
+
if (!term || term.kind !== 'literal') return null;
|
|
180
|
+
return term.datatype || (term.language ? RDF_LANG_STRING : XSD_STRING);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function integerInRange(lex, min = null, max = null) {
|
|
184
|
+
if (!/^[+-]?[0-9]+$/.test(lex)) return false;
|
|
185
|
+
try {
|
|
186
|
+
const value = BigInt(lex);
|
|
187
|
+
if (min != null && value < BigInt(min)) return false;
|
|
188
|
+
if (max != null && value > BigInt(max)) return false;
|
|
189
|
+
return true;
|
|
190
|
+
} catch (_) {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function validXmlLiteral(lex) {
|
|
196
|
+
// Minimal well-formed XML check sufficient for the RDF-MT tests: reject broken
|
|
197
|
+
// markup, mismatched tags, and raw ampersands while accepting plain character
|
|
198
|
+
// content. This is intentionally not a full XML parser.
|
|
199
|
+
const text = String(lex);
|
|
200
|
+
if (/[<]/.test(text) || /&/.test(text)) {
|
|
201
|
+
if (/&(?!amp;|lt;|gt;|quot;|apos;|#[0-9]+;|#x[0-9A-Fa-f]+;)/.test(text)) return false;
|
|
202
|
+
const stack = [];
|
|
203
|
+
const tagRe = /<([^!?/][^\s/>]*)(?:\s[^>]*)?>|<\/([^\s>]+)>|<([^!?/][^\s/>]*)(?:\s[^>]*)?\/>|<[^>]*$/g;
|
|
204
|
+
let match;
|
|
205
|
+
while ((match = tagRe.exec(text))) {
|
|
206
|
+
if (match[0].startsWith('<!--') || match[0].startsWith('<?')) continue;
|
|
207
|
+
if (match[0].endsWith('/>')) continue;
|
|
208
|
+
if (match[0].endsWith('') && match[0].startsWith('<') && !match[0].endsWith('>')) return false;
|
|
209
|
+
if (match[1]) stack.push(match[1]);
|
|
210
|
+
else if (match[2]) {
|
|
211
|
+
const open = stack.pop();
|
|
212
|
+
if (open !== match[2]) return false;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return stack.length === 0;
|
|
216
|
+
}
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function validLexicalLiteral(term, recognized) {
|
|
221
|
+
if (!term || term.kind !== 'literal' || !term.datatype) return true;
|
|
222
|
+
const dt = normalizeDatatype(term.datatype);
|
|
223
|
+
if (!recognized.has(term.datatype) && !recognized.has(dt)) return true;
|
|
224
|
+
const lex = String(term.value);
|
|
225
|
+
switch (dt) {
|
|
226
|
+
case XSD_INTEGER: return integerInRange(lex, term.datatype === XSD_INT ? -2147483648 : null, term.datatype === XSD_INT ? 2147483647 : null);
|
|
227
|
+
case XSD_DECIMAL: return /^[+-]?(?:[0-9]+\.[0-9]*|\.[0-9]+|[0-9]+)$/.test(lex);
|
|
228
|
+
case XSD_BOOLEAN: return /^(?:true|false|1|0)$/.test(lex);
|
|
229
|
+
case XSD_FLOAT:
|
|
230
|
+
case XSD_DOUBLE: return /^[+-]?(?:(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[eE][+-]?[0-9]+)?|INF|-INF|NaN)$/.test(lex);
|
|
231
|
+
case RDF_XML_LITERAL: return validXmlLiteral(lex);
|
|
232
|
+
case RDF_JSON:
|
|
233
|
+
try { JSON.parse(lex); return true; } catch (_) { return false; }
|
|
234
|
+
default: return true;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function collectTermsDeep(triples) {
|
|
239
|
+
const terms = [];
|
|
240
|
+
function add(term) {
|
|
241
|
+
if (!term) return;
|
|
242
|
+
terms.push(term);
|
|
243
|
+
if (term.kind === 'triple') { add(term.s); add(term.p); add(term.o); }
|
|
244
|
+
}
|
|
245
|
+
for (const t of triples || []) { add(t.s); add(t.p); add(t.o); add(t.graph); }
|
|
246
|
+
return terms;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function graphInconsistent(triples, options = {}) {
|
|
250
|
+
const recognized = new Set(options.recognizedDatatypes || []);
|
|
251
|
+
for (const dt of Array.from(recognized)) recognized.add(normalizeDatatype(dt));
|
|
252
|
+
for (const term of collectTermsDeep(triples || [])) {
|
|
253
|
+
if (!validLexicalLiteral(term, recognized)) return true;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const typeRows = new Map();
|
|
257
|
+
for (const t of triples || []) {
|
|
258
|
+
if (!isIri(t.p, RDF_TYPE) || !t.o || t.o.kind !== 'iri') continue;
|
|
259
|
+
const key = termKey(t.s);
|
|
260
|
+
if (!typeRows.has(key)) typeRows.set(key, { term: t.s, types: [] });
|
|
261
|
+
typeRows.get(key).types.push(t.o.value);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
for (const { term, types } of typeRows.values()) {
|
|
265
|
+
if (term.kind === 'literal') {
|
|
266
|
+
const litDt = literalDatatype(term);
|
|
267
|
+
for (const type of types) {
|
|
268
|
+
if (!recognized.has(type) && !recognized.has(normalizeDatatype(type))) continue;
|
|
269
|
+
if (!datatypeCompatible(litDt, type)) return true;
|
|
270
|
+
if (!validLexicalLiteral({ ...term, datatype: type }, recognized)) return true;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
for (let i = 0; i < types.length; i += 1) {
|
|
274
|
+
for (let j = i + 1; j < types.length; j += 1) {
|
|
275
|
+
const a = types[i];
|
|
276
|
+
const b = types[j];
|
|
277
|
+
if ((recognized.has(a) || recognized.has(normalizeDatatype(a))) && (recognized.has(b) || recognized.has(normalizeDatatype(b))) && !datatypeCompatible(a, b)) return true;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Intensional datatype clash: declaring a recognized datatype as a subclass of
|
|
283
|
+
// an incompatible recognized datatype is inconsistent for the test regimes here.
|
|
284
|
+
for (const t of triples || []) {
|
|
285
|
+
if (!isIri(t.p, RDFS_SUBCLASS_OF) || !t.s || !t.o || t.s.kind !== 'iri' || t.o.kind !== 'iri') continue;
|
|
286
|
+
if ((recognized.has(t.s.value) || recognized.has(normalizeDatatype(t.s.value))) && (recognized.has(t.o.value) || recognized.has(normalizeDatatype(t.o.value))) && !datatypeCompatible(t.s.value, t.o.value)) return true;
|
|
287
|
+
}
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function decimalCanonical(lex) {
|
|
292
|
+
if (!/^[+-]?(?:[0-9]+\.[0-9]*|\.[0-9]+|[0-9]+)$/.test(lex)) return null;
|
|
293
|
+
let sign = '';
|
|
294
|
+
let s = String(lex);
|
|
295
|
+
if (s[0] === '+' || s[0] === '-') { if (s[0] === '-') sign = '-'; s = s.slice(1); }
|
|
296
|
+
let [intPart, fracPart = ''] = s.split('.');
|
|
297
|
+
if (intPart === '') intPart = '0';
|
|
298
|
+
intPart = intPart.replace(/^0+(?=\d)/, '') || '0';
|
|
299
|
+
fracPart = fracPart.replace(/0+$/, '');
|
|
300
|
+
if (intPart === '0' && fracPart === '') sign = '';
|
|
301
|
+
return `decimal:${sign}${intPart}${fracPart ? `.${fracPart}` : ''}`;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function numberCanonical(value) {
|
|
305
|
+
const n = Number(value);
|
|
306
|
+
if (Number.isNaN(n)) return 'NaN';
|
|
307
|
+
if (Object.is(n, -0)) return '-0';
|
|
308
|
+
if (Object.is(n, 0)) return '+0';
|
|
309
|
+
if (n === Infinity) return 'Infinity';
|
|
310
|
+
if (n === -Infinity) return '-Infinity';
|
|
311
|
+
return String(n);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function jsonCanonicalValue(value) {
|
|
315
|
+
if (typeof value === 'number') return `number:${numberCanonical(value)}`;
|
|
316
|
+
if (typeof value === 'string') return `string:${JSON.stringify(value)}`;
|
|
317
|
+
if (typeof value === 'boolean') return `boolean:${value}`;
|
|
318
|
+
if (value === null) return 'null';
|
|
319
|
+
if (Array.isArray(value)) return `array:[${value.map(jsonCanonicalValue).join(',')}]`;
|
|
320
|
+
const keys = Object.keys(value).sort();
|
|
321
|
+
return `object:{${keys.map((k) => `${JSON.stringify(k)}:${jsonCanonicalValue(value[k])}`).join(',')}}`;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function canonicalLiteral(term, recognized = new Set()) {
|
|
325
|
+
if (!term || term.kind !== 'literal') return null;
|
|
326
|
+
for (const dt of Array.from(recognized)) recognized.add(normalizeDatatype(dt));
|
|
327
|
+
const dt = literalDatatype(term);
|
|
328
|
+
const ndt = normalizeDatatype(dt);
|
|
329
|
+
const lex = String(term.value);
|
|
330
|
+
if (!recognized.has(dt) && !recognized.has(ndt)) return `raw:${JSON.stringify(lex)}^^${dt}@${(term.language || '').toLowerCase()}`;
|
|
331
|
+
try {
|
|
332
|
+
if (ndt === XSD_INTEGER || ndt === XSD_DECIMAL) return decimalCanonical(lex) || `invalid:${dt}:${lex}`;
|
|
333
|
+
if (ndt === XSD_BOOLEAN) return `boolean:${lex === 'true' || lex === '1'}`;
|
|
334
|
+
if (ndt === XSD_FLOAT) return `float:${numberCanonical(Math.fround(Number(lex.replace(/^INF$/, 'Infinity').replace(/^-INF$/, '-Infinity'))))}`;
|
|
335
|
+
if (ndt === XSD_DOUBLE) return `double:${numberCanonical(Number(lex.replace(/^INF$/, 'Infinity').replace(/^-INF$/, '-Infinity')))}`;
|
|
336
|
+
if (ndt === XSD_STRING) return `string:${JSON.stringify(lex)}`;
|
|
337
|
+
if (ndt === RDF_JSON) return `json:${jsonCanonicalValue(JSON.parse(lex))}`;
|
|
338
|
+
if (ndt === RDF_XML_LITERAL) return `xml:${lex}`;
|
|
339
|
+
} catch (_) {
|
|
340
|
+
// malformed recognized literal is handled separately as inconsistency
|
|
341
|
+
}
|
|
342
|
+
return `raw:${JSON.stringify(lex)}^^${dt}@${(term.language || '').toLowerCase()}`;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function termsEqual(a, b, recognized = new Set()) {
|
|
346
|
+
if (!a || !b || a.kind !== b.kind) return false;
|
|
347
|
+
if (a.kind === 'iri' || a.kind === 'blank') return a.value === b.value;
|
|
348
|
+
if (a.kind === 'literal') {
|
|
349
|
+
if ((a.language || '').toLowerCase() !== (b.language || '').toLowerCase()) return false;
|
|
350
|
+
if ((a.langDir || '') !== (b.langDir || '')) return false;
|
|
351
|
+
const adt = literalDatatype(a);
|
|
352
|
+
const bdt = literalDatatype(b);
|
|
353
|
+
const ak = datatypeKind(adt);
|
|
354
|
+
const bk = datatypeKind(bdt);
|
|
355
|
+
if (ak && bk && ak !== bk && !(ak === 'decimal' && bk === 'decimal')) {
|
|
356
|
+
// Numeric integer/decimal may compare across datatype IRIs; other recognized
|
|
357
|
+
// datatype value spaces are distinct in these tests.
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
if (!ak && !bk && adt !== bdt) return false;
|
|
361
|
+
return canonicalLiteral(a, recognized) === canonicalLiteral(b, recognized);
|
|
362
|
+
}
|
|
363
|
+
if (a.kind === 'triple') return termsEqual(a.s, b.s, recognized) && termsEqual(a.p, b.p, recognized) && termsEqual(a.o, b.o, recognized);
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function addTermSemanticTriples(closure, seen, triples, regime, recognizedDatatypes = []) {
|
|
368
|
+
const recognized = new Set(recognizedDatatypes || []);
|
|
369
|
+
for (const dt of Array.from(recognized)) recognized.add(normalizeDatatype(dt));
|
|
370
|
+
const addForTerm = (term) => {
|
|
371
|
+
if (!term) return;
|
|
372
|
+
if (term.kind === 'literal') {
|
|
373
|
+
const dt = literalDatatype(term);
|
|
374
|
+
if (dt) addTriple(closure, seen, term, iriTerm(RDF_TYPE), iriTerm(dt));
|
|
375
|
+
if (regime === 'rdfs') addTriple(closure, seen, term, iriTerm(RDF_TYPE), iriTerm(RDFS_LITERAL));
|
|
376
|
+
} else if (term.kind === 'triple') {
|
|
377
|
+
addTriple(closure, seen, term, iriTerm(RDF_TYPE), iriTerm(RDF_TRIPLE_TERM));
|
|
378
|
+
addTriple(closure, seen, term, iriTerm(RDF_TYPE), iriTerm(RDFS_PROPOSITION));
|
|
379
|
+
addForTerm(term.s);
|
|
380
|
+
addForTerm(term.p);
|
|
381
|
+
addForTerm(term.o);
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
for (const t of triples || []) {
|
|
385
|
+
addForTerm(t.s);
|
|
386
|
+
addForTerm(t.p);
|
|
387
|
+
addForTerm(t.o);
|
|
388
|
+
addForTerm(t.graph);
|
|
389
|
+
if (isIri(t.p, RDF_REIFIES)) {
|
|
390
|
+
// RDF 1.2 reification semantics: a reifier denotes/provides access to a
|
|
391
|
+
// proposition, and the object of rdf:reifies is a triple term. Add these
|
|
392
|
+
// directly as well as through the RDFS range/subclass axioms so the
|
|
393
|
+
// entailment runner works for simple/RDF/RDFS manifest expectations.
|
|
394
|
+
addTriple(closure, seen, t.s, iriTerm(RDF_TYPE), iriTerm(RDFS_PROPOSITION));
|
|
395
|
+
addTriple(closure, seen, t.o, iriTerm(RDF_TYPE), iriTerm(RDFS_PROPOSITION));
|
|
396
|
+
addForTerm(t.o);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function addRdfSemanticTriples(input, regime, recognizedDatatypes = []) {
|
|
402
|
+
const closure = uniqueTriples(input || []).slice();
|
|
403
|
+
const seen = new Set(closure.map(tripleKey));
|
|
404
|
+
// RDF 1.2 triple-term/proposition and datatype-literal typing are useful even
|
|
405
|
+
// for the simple entailment tests, because they are part of the RDF 1.2 semantic
|
|
406
|
+
// contract exercised by the manifest.
|
|
407
|
+
addTermSemanticTriples(closure, seen, input || [], regime, recognizedDatatypes);
|
|
408
|
+
if (regime === 'rdf' || regime === 'rdfs') {
|
|
409
|
+
for (const ax of rdfAxiomaticTriples()) addTriple(closure, seen, ax.s, ax.p, ax.o);
|
|
410
|
+
for (const t of input || []) addTriple(closure, seen, t.p, iriTerm(RDF_TYPE), iriTerm(RDF_PROPERTY));
|
|
411
|
+
}
|
|
412
|
+
if (regime === 'rdfs') {
|
|
413
|
+
for (const ax of rdfsAxiomaticTriples(recognizedDatatypes)) addTriple(closure, seen, ax.s, ax.p, ax.o);
|
|
414
|
+
for (let i = 1; i <= 10; i += 1) {
|
|
415
|
+
addTriple(closure, seen, iriTerm(`${RDF}_${i}`), iriTerm(RDF_TYPE), iriTerm(RDFS_CONTAINER_MEMBERSHIP_PROPERTY));
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return { closure, seen };
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function rdfsClosure(input, options = {}) {
|
|
422
|
+
const regime = normalizeRegime(options.regime);
|
|
423
|
+
const recognized = options.recognizedDatatypes || [];
|
|
424
|
+
const { closure, seen } = addRdfSemanticTriples(input, regime, recognized);
|
|
425
|
+
if (regime !== 'rdfs') return closure;
|
|
426
|
+
let changed = true;
|
|
427
|
+
let iterations = 0;
|
|
428
|
+
while (changed && iterations < 1000) {
|
|
429
|
+
iterations += 1;
|
|
430
|
+
changed = false;
|
|
431
|
+
const snapshot = closure.slice();
|
|
432
|
+
for (const t of snapshot) {
|
|
433
|
+
// Every predicate occurring in a triple is an rdf:Property.
|
|
434
|
+
if (t.p && t.p.kind === 'iri') changed = addTriple(closure, seen, t.p, iriTerm(RDF_TYPE), iriTerm(RDF_PROPERTY)) || changed;
|
|
435
|
+
// Container membership properties are subproperties of rdfs:member.
|
|
436
|
+
if (t.p && t.p.kind === 'iri' && /^http:\/\/www\.w3\.org\/1999\/02\/22-rdf-syntax-ns#_[1-9][0-9]*$/.test(t.p.value)) {
|
|
437
|
+
changed = addTriple(closure, seen, t.p, iriTerm(RDFS_SUBPROPERTY_OF), iriTerm(RDFS_MEMBER)) || changed;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
const subProps = closure.filter((t) => isIri(t.p, RDFS_SUBPROPERTY_OF));
|
|
441
|
+
const subClasses = closure.filter((t) => isIri(t.p, RDFS_SUBCLASS_OF));
|
|
442
|
+
const domains = closure.filter((t) => isIri(t.p, RDFS_DOMAIN));
|
|
443
|
+
const ranges = closure.filter((t) => isIri(t.p, RDFS_RANGE));
|
|
444
|
+
|
|
445
|
+
for (const sp of subProps) {
|
|
446
|
+
changed = addTriple(closure, seen, sp.s, iriTerm(RDF_TYPE), iriTerm(RDF_PROPERTY)) || changed;
|
|
447
|
+
changed = addTriple(closure, seen, sp.o, iriTerm(RDF_TYPE), iriTerm(RDF_PROPERTY)) || changed;
|
|
448
|
+
}
|
|
449
|
+
for (const sc of subClasses) {
|
|
450
|
+
changed = addTriple(closure, seen, sc.s, iriTerm(RDF_TYPE), iriTerm(RDFS_CLASS)) || changed;
|
|
451
|
+
changed = addTriple(closure, seen, sc.o, iriTerm(RDF_TYPE), iriTerm(RDFS_CLASS)) || changed;
|
|
452
|
+
}
|
|
453
|
+
for (const d of domains) {
|
|
454
|
+
changed = addTriple(closure, seen, d.s, iriTerm(RDF_TYPE), iriTerm(RDF_PROPERTY)) || changed;
|
|
455
|
+
changed = addTriple(closure, seen, d.o, iriTerm(RDF_TYPE), iriTerm(RDFS_CLASS)) || changed;
|
|
456
|
+
}
|
|
457
|
+
for (const r of ranges) {
|
|
458
|
+
changed = addTriple(closure, seen, r.s, iriTerm(RDF_TYPE), iriTerm(RDF_PROPERTY)) || changed;
|
|
459
|
+
changed = addTriple(closure, seen, r.o, iriTerm(RDF_TYPE), iriTerm(RDFS_CLASS)) || changed;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Reflexivity for known properties/classes.
|
|
463
|
+
for (const t of closure) {
|
|
464
|
+
if (isIri(t.p, RDF_TYPE) && isIri(t.o, RDF_PROPERTY)) changed = addTriple(closure, seen, t.s, iriTerm(RDFS_SUBPROPERTY_OF), t.s) || changed;
|
|
465
|
+
if (isIri(t.p, RDF_TYPE) && isIri(t.o, RDFS_CLASS)) changed = addTriple(closure, seen, t.s, iriTerm(RDFS_SUBCLASS_OF), t.s) || changed;
|
|
466
|
+
if (isIri(t.p, RDF_TYPE) && isIri(t.o, RDFS_DATATYPE)) changed = addTriple(closure, seen, t.s, iriTerm(RDFS_SUBCLASS_OF), iriTerm(RDFS_LITERAL)) || changed;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Subproperty transitivity and property inheritance.
|
|
470
|
+
for (const a of subProps) {
|
|
471
|
+
for (const b of subProps) if (termsEqual(a.o, b.s)) changed = addTriple(closure, seen, a.s, iriTerm(RDFS_SUBPROPERTY_OF), b.o) || changed;
|
|
472
|
+
for (const t of snapshot) if (termsEqual(t.p, a.s)) changed = addTriple(closure, seen, t.s, a.o, t.o) || changed;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Subclass transitivity and type inheritance.
|
|
476
|
+
for (const a of subClasses) {
|
|
477
|
+
for (const b of subClasses) if (termsEqual(a.o, b.s)) changed = addTriple(closure, seen, a.s, iriTerm(RDFS_SUBCLASS_OF), b.o) || changed;
|
|
478
|
+
for (const t of snapshot) if (isIri(t.p, RDF_TYPE) && termsEqual(t.o, a.s)) changed = addTriple(closure, seen, t.s, iriTerm(RDF_TYPE), a.o) || changed;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Domain and range.
|
|
482
|
+
for (const d of domains) for (const t of snapshot) if (termsEqual(t.p, d.s)) changed = addTriple(closure, seen, t.s, iriTerm(RDF_TYPE), d.o) || changed;
|
|
483
|
+
for (const r of ranges) for (const t of snapshot) if (termsEqual(t.p, r.s)) changed = addTriple(closure, seen, t.o, iriTerm(RDF_TYPE), r.o) || changed;
|
|
484
|
+
}
|
|
485
|
+
return closure;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function matchExpectedTerm(expected, actual, binding, recognized) {
|
|
489
|
+
if (!expected || !actual) return null;
|
|
490
|
+
if (expected.kind === 'blank') {
|
|
491
|
+
const bound = binding.get(expected.value);
|
|
492
|
+
if (!bound) { const next = new Map(binding); next.set(expected.value, actual); return next; }
|
|
493
|
+
return termsEqual(bound, actual, recognized) ? binding : null;
|
|
494
|
+
}
|
|
495
|
+
if (expected.kind === 'triple') {
|
|
496
|
+
if (actual.kind !== 'triple') return null;
|
|
497
|
+
let next = matchExpectedTerm(expected.s, actual.s, binding, recognized);
|
|
498
|
+
if (!next) return null;
|
|
499
|
+
next = matchExpectedTerm(expected.p, actual.p, next, recognized);
|
|
500
|
+
if (!next) return null;
|
|
501
|
+
return matchExpectedTerm(expected.o, actual.o, next, recognized);
|
|
502
|
+
}
|
|
503
|
+
return termsEqual(expected, actual, recognized) ? binding : null;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function matchExpectedTriple(expected, actual, binding, recognized) {
|
|
507
|
+
let next = matchExpectedTerm(expected.s, actual.s, binding, recognized);
|
|
508
|
+
if (!next) return null;
|
|
509
|
+
next = matchExpectedTerm(expected.p, actual.p, next, recognized);
|
|
510
|
+
if (!next) return null;
|
|
511
|
+
next = matchExpectedTerm(expected.o, actual.o, next, recognized);
|
|
512
|
+
if (!next) return null;
|
|
513
|
+
if (expected.graph || actual.graph) next = matchExpectedTerm(expected.graph, actual.graph, next, recognized);
|
|
514
|
+
return next;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function entails(inputTriples, expectedTriples, options = {}) {
|
|
518
|
+
const regime = normalizeRegime(options.regime);
|
|
519
|
+
const recognized = new Set(options.recognizedDatatypes || []);
|
|
520
|
+
const closure = rdfsClosure(inputTriples, { regime, recognizedDatatypes: options.recognizedDatatypes || [] });
|
|
521
|
+
if (graphInconsistent(closure, { recognizedDatatypes: options.recognizedDatatypes || [] })) return true;
|
|
522
|
+
const expected = uniqueTriples(expectedTriples || []);
|
|
523
|
+
const order = expected.slice().sort((a, b) => candidateCount(a, closure, recognized) - candidateCount(b, closure, recognized));
|
|
524
|
+
function search(index, binding) {
|
|
525
|
+
if (index >= order.length) return true;
|
|
526
|
+
const pattern = order[index];
|
|
527
|
+
for (const candidate of closure) {
|
|
528
|
+
const next = matchExpectedTriple(pattern, candidate, binding, recognized);
|
|
529
|
+
if (next && search(index + 1, next)) return true;
|
|
530
|
+
}
|
|
531
|
+
return false;
|
|
532
|
+
}
|
|
533
|
+
return search(0, new Map());
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function candidateCount(pattern, closure, recognized) {
|
|
537
|
+
let count = 0;
|
|
538
|
+
for (const candidate of closure) if (matchExpectedTriple(pattern, candidate, new Map(), recognized)) count += 1;
|
|
539
|
+
return count || closure.length + 1;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function evaluateEntailmentTest(inputTriples, expectedTriples, options = {}) {
|
|
543
|
+
const positive = options.positive !== false;
|
|
544
|
+
const recognizedDatatypes = options.recognizedDatatypes || [];
|
|
545
|
+
const regime = normalizeRegime(options.regime);
|
|
546
|
+
const closure = rdfsClosure(inputTriples, { regime, recognizedDatatypes });
|
|
547
|
+
const inconsistent = graphInconsistent(closure, { recognizedDatatypes });
|
|
548
|
+
if (options.resultKind === 'false') {
|
|
549
|
+
const passed = positive ? inconsistent : !inconsistent;
|
|
550
|
+
return { passed, inconsistent, entailed: inconsistent, message: passed ? (inconsistent ? 'input graph is inconsistent as expected' : 'input graph is consistent as expected') : (positive ? 'expected inconsistency but graph was consistent' : 'expected consistency but graph was inconsistent') };
|
|
551
|
+
}
|
|
552
|
+
const entailed = entails(inputTriples, expectedTriples || [], options);
|
|
553
|
+
const passed = positive ? entailed : !entailed;
|
|
554
|
+
return { passed, inconsistent, entailed, message: passed ? (entailed ? 'entailed expected graph' : 'did not entail expected graph') : (positive ? 'expected graph was not entailed' : 'negative entailment graph was entailed') };
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
module.exports = {
|
|
558
|
+
RDF,
|
|
559
|
+
RDFS,
|
|
560
|
+
XSD,
|
|
561
|
+
RDF_TYPE,
|
|
562
|
+
RDFS_SUBCLASS_OF,
|
|
563
|
+
RDFS_SUBPROPERTY_OF,
|
|
564
|
+
RDFS_DOMAIN,
|
|
565
|
+
RDFS_RANGE,
|
|
566
|
+
normalizeRegime,
|
|
567
|
+
graphInconsistent,
|
|
568
|
+
rdfsClosure,
|
|
569
|
+
entails,
|
|
570
|
+
evaluateEntailmentTest,
|
|
571
|
+
};
|