eyeling 1.28.2 → 1.28.3
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/dist/browser/eyeling.browser.js +477 -5
- package/eyeling.js +477 -5
- package/lib/engine.js +12 -5
- package/lib/fast_rdf.js +451 -0
- package/lib/multisource.js +11 -0
- package/notes/rdf-message-logs.md +13 -14
- package/notes/rdf12-roundtrip.md +4 -6
- package/package.json +1 -1
- package/test/api.test.js +41 -0
package/lib/fast_rdf.js
ADDED
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fast RDF compatibility parser for line-oriented RDF inputs.
|
|
3
|
+
*
|
|
4
|
+
* This is deliberately conservative. It directly builds Eyeling AST terms for
|
|
5
|
+
* common RDF parser workloads (N-Triples-style Turtle, simple prefixed triples,
|
|
6
|
+
* N-Quads, and RDF Message Logs) and returns null for richer N3/Turtle/TriG
|
|
7
|
+
* constructs so the full lexer/parser remains the source of truth.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const { N3SyntaxError, decodeN3StringEscapes } = require('./lexer');
|
|
13
|
+
const {
|
|
14
|
+
RDF_NS,
|
|
15
|
+
LOG_NS,
|
|
16
|
+
XSD_NS,
|
|
17
|
+
Blank,
|
|
18
|
+
GraphTerm,
|
|
19
|
+
ListTerm,
|
|
20
|
+
Triple,
|
|
21
|
+
PrefixEnv,
|
|
22
|
+
internIri,
|
|
23
|
+
internLiteral,
|
|
24
|
+
resolveIriRef,
|
|
25
|
+
annotateQuotedGraphTerm,
|
|
26
|
+
} = require('./prelude');
|
|
27
|
+
|
|
28
|
+
const RDF_TYPE = internIri(RDF_NS + 'type');
|
|
29
|
+
const LOG_NAME_OF = internIri(LOG_NS + 'nameOf');
|
|
30
|
+
const XSD_INTEGER_IRI = XSD_NS + 'integer';
|
|
31
|
+
const XSD_INTEGER_LITERAL_SUFFIX = `^^<${XSD_INTEGER_IRI}>`;
|
|
32
|
+
|
|
33
|
+
const EYMSG_NS = 'https://eyereasoner.github.io/eyeling/vocab/message#';
|
|
34
|
+
const EYMSG_IRIS = Object.freeze({
|
|
35
|
+
RDFMessageStream: internIri(`${EYMSG_NS}RDFMessageStream`),
|
|
36
|
+
MessageEnvelope: internIri(`${EYMSG_NS}MessageEnvelope`),
|
|
37
|
+
envelope: internIri(`${EYMSG_NS}envelope`),
|
|
38
|
+
firstEnvelope: internIri(`${EYMSG_NS}firstEnvelope`),
|
|
39
|
+
lastEnvelope: internIri(`${EYMSG_NS}lastEnvelope`),
|
|
40
|
+
orderedEnvelopes: internIri(`${EYMSG_NS}orderedEnvelopes`),
|
|
41
|
+
messageCount: internIri(`${EYMSG_NS}messageCount`),
|
|
42
|
+
offset: internIri(`${EYMSG_NS}offset`),
|
|
43
|
+
nextEnvelope: internIri(`${EYMSG_NS}nextEnvelope`),
|
|
44
|
+
payloadGraph: internIri(`${EYMSG_NS}payloadGraph`),
|
|
45
|
+
payloadKind: internIri(`${EYMSG_NS}payloadKind`),
|
|
46
|
+
empty: internIri(`${EYMSG_NS}empty`),
|
|
47
|
+
nonEmpty: internIri(`${EYMSG_NS}nonEmpty`),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const VERSION_LINE_RE = /^\s*(?:@version|VERSION)\s+(["'])(?:1\.1|1\.2|1\.2-basic)(?:-messages)?\1\s*\.?\s*(?:#.*)?$/im;
|
|
51
|
+
const MESSAGE_VERSION_LINE_RE = /^\s*(?:@version|VERSION)\s+(["'])(?:1\.1|1\.2|1\.2-basic)-messages\1\s*\.?\s*(?:#.*)?$/im;
|
|
52
|
+
const MESSAGE_LINE_RE = /^\s*(?:MESSAGE\b|@message\s*\.?)\s*(?:#.*)?$/i;
|
|
53
|
+
|
|
54
|
+
function simpleHashText(s) {
|
|
55
|
+
let h = 0x811c9dc5;
|
|
56
|
+
for (let i = 0; i < s.length; i += 1) {
|
|
57
|
+
h ^= s.charCodeAt(i);
|
|
58
|
+
h = Math.imul(h, 0x01000193) >>> 0;
|
|
59
|
+
}
|
|
60
|
+
return h.toString(16).padStart(8, '0');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function addOffset(obj, offset) {
|
|
64
|
+
if (!obj || typeof offset !== 'number') return obj;
|
|
65
|
+
Object.defineProperty(obj, '__sourceOffset', {
|
|
66
|
+
value: offset,
|
|
67
|
+
enumerable: false,
|
|
68
|
+
writable: false,
|
|
69
|
+
configurable: true,
|
|
70
|
+
});
|
|
71
|
+
return obj;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function skipWs(s, i) {
|
|
75
|
+
while (i < s.length) {
|
|
76
|
+
const code = s.charCodeAt(i);
|
|
77
|
+
if (code !== 0x20 && code !== 0x09 && code !== 0x0a && code !== 0x0d && code !== 0x0c) break;
|
|
78
|
+
i += 1;
|
|
79
|
+
}
|
|
80
|
+
return i;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function maybeDecodeIriRef(raw, offset, prefixes) {
|
|
84
|
+
const iri = raw.includes('\\') ? decodeN3StringEscapes(raw, offset) : raw;
|
|
85
|
+
if (!prefixes.baseIri || /^[A-Za-z][A-Za-z0-9+.-]*:/.test(iri)) return iri;
|
|
86
|
+
return resolveIriRef(iri, prefixes.baseIri || '');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function readIriRef(s, i) {
|
|
90
|
+
if (s[i] !== '<' || s[i + 1] === '<') return null;
|
|
91
|
+
let j = i + 1;
|
|
92
|
+
while (j < s.length) {
|
|
93
|
+
const ch = s[j];
|
|
94
|
+
if (ch === '\\') {
|
|
95
|
+
j += 2;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (ch === '>') return { raw: s.slice(i + 1, j), end: j + 1 };
|
|
99
|
+
j += 1;
|
|
100
|
+
}
|
|
101
|
+
throw new N3SyntaxError('Unterminated IRI <...>', i);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function readQuoted(s, i) {
|
|
105
|
+
const quote = s[i];
|
|
106
|
+
if (quote !== '"' && quote !== "'") return null;
|
|
107
|
+
const long = s.startsWith(quote.repeat(3), i);
|
|
108
|
+
const start = i;
|
|
109
|
+
if (long) {
|
|
110
|
+
i += 3;
|
|
111
|
+
const contentStart = i;
|
|
112
|
+
let hasEscape = false;
|
|
113
|
+
while (i < s.length) {
|
|
114
|
+
if (s.startsWith(quote.repeat(3), i)) {
|
|
115
|
+
return { text: s.slice(contentStart, i), end: i + 3, hasEscape };
|
|
116
|
+
}
|
|
117
|
+
const ch = s[i];
|
|
118
|
+
if (ch === '\\') {
|
|
119
|
+
hasEscape = true;
|
|
120
|
+
i += 2;
|
|
121
|
+
} else {
|
|
122
|
+
i += 1;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
throw new N3SyntaxError(`Unterminated long string literal ${quote.repeat(3)}...${quote.repeat(3)}`, start);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
i += 1;
|
|
129
|
+
const contentStart = i;
|
|
130
|
+
let hasEscape = false;
|
|
131
|
+
while (i < s.length) {
|
|
132
|
+
const ch = s[i];
|
|
133
|
+
if (ch === '\\') {
|
|
134
|
+
hasEscape = true;
|
|
135
|
+
i += 2;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (ch === quote) return { text: s.slice(contentStart, i), end: i + 1, hasEscape };
|
|
139
|
+
i += 1;
|
|
140
|
+
}
|
|
141
|
+
throw new N3SyntaxError(`Unterminated string literal ${quote}...${quote}`, start);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function readToken(s, i) {
|
|
145
|
+
const start = i;
|
|
146
|
+
while (i < s.length) {
|
|
147
|
+
const ch = s[i];
|
|
148
|
+
if (/\s/.test(ch) || ch === '.' || ch === ',' || ch === ';' || ch === '[' || ch === ']' || ch === '{' || ch === '}' || ch === '(' || ch === ')') break;
|
|
149
|
+
i += 1;
|
|
150
|
+
}
|
|
151
|
+
if (i === start) return null;
|
|
152
|
+
return { text: s.slice(start, i), end: i };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function expandQNameToken(token, prefixes, usedPrefixes) {
|
|
156
|
+
if (token === 'a') return RDF_NS + 'type';
|
|
157
|
+
const sep = token.indexOf(':');
|
|
158
|
+
if (sep < 0) return null;
|
|
159
|
+
if (sep === 1 && token.charCodeAt(0) === 95) return null;
|
|
160
|
+
const pfx = token.slice(0, sep);
|
|
161
|
+
const local = token.slice(sep + 1);
|
|
162
|
+
const base = prefixes.map[pfx] || '';
|
|
163
|
+
if (!base && pfx !== '') return null;
|
|
164
|
+
if (usedPrefixes && pfx) usedPrefixes.add(pfx);
|
|
165
|
+
return base ? base + local : token;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function parseIriOrQName(s, i, prefixes, usedPrefixes) {
|
|
169
|
+
i = skipWs(s, i);
|
|
170
|
+
const iri = readIriRef(s, i);
|
|
171
|
+
if (iri) {
|
|
172
|
+
return { term: internIri(maybeDecodeIriRef(iri.raw, i, prefixes)), end: iri.end };
|
|
173
|
+
}
|
|
174
|
+
const tok = readToken(s, i);
|
|
175
|
+
if (!tok) return null;
|
|
176
|
+
const expanded = expandQNameToken(tok.text, prefixes, usedPrefixes);
|
|
177
|
+
if (expanded === null) return null;
|
|
178
|
+
return { term: internIri(expanded), end: tok.end };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function parseTerm(s, i, prefixes, usedPrefixes, blankPrefix = '') {
|
|
182
|
+
i = skipWs(s, i);
|
|
183
|
+
if (i >= s.length) return null;
|
|
184
|
+
const ch = s[i];
|
|
185
|
+
|
|
186
|
+
if (ch === '<') return parseIriOrQName(s, i, prefixes, usedPrefixes);
|
|
187
|
+
|
|
188
|
+
if (ch === '_' && s[i + 1] === ':') {
|
|
189
|
+
const tok = readToken(s, i);
|
|
190
|
+
if (!tok) return null;
|
|
191
|
+
let label = tok.text;
|
|
192
|
+
if (blankPrefix) label = blankPrefix + label.slice(2).replace(/[^A-Za-z0-9_]/g, '_');
|
|
193
|
+
return { term: new Blank(label), end: tok.end };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (ch === '"' || ch === "'") {
|
|
197
|
+
const q = readQuoted(s, i);
|
|
198
|
+
let end = q.end;
|
|
199
|
+
const rawText = q.text;
|
|
200
|
+
let value = q.hasEscape ? JSON.stringify(decodeN3StringEscapes(rawText, i)) : `"${rawText}"`;
|
|
201
|
+
|
|
202
|
+
if (s[end] === '@') {
|
|
203
|
+
let j = end + 1;
|
|
204
|
+
if (!/[A-Za-z]/.test(s[j] || '')) return null;
|
|
205
|
+
while (j < s.length && /[A-Za-z0-9-]/.test(s[j])) j += 1;
|
|
206
|
+
if (s[j] === '-' && s[j + 1] === '-') {
|
|
207
|
+
const dir = s.slice(j + 2, j + 5);
|
|
208
|
+
if ((dir === 'ltr' || dir === 'rtl') && !/[A-Za-z0-9-]/.test(s[j + 5] || '')) j += 5;
|
|
209
|
+
}
|
|
210
|
+
value += s.slice(end, j);
|
|
211
|
+
end = j;
|
|
212
|
+
} else if (s.startsWith('^^', end)) {
|
|
213
|
+
const dt = parseIriOrQName(s, end + 2, prefixes, usedPrefixes);
|
|
214
|
+
if (!dt || !(dt.term && typeof dt.term.value === 'string')) return null;
|
|
215
|
+
value += `^^<${dt.term.value}>`;
|
|
216
|
+
end = dt.end;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return { term: internLiteral(value), end };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (ch === '(') {
|
|
223
|
+
let pos = i + 1;
|
|
224
|
+
const items = [];
|
|
225
|
+
for (;;) {
|
|
226
|
+
pos = skipWs(s, pos);
|
|
227
|
+
if (pos >= s.length) throw new N3SyntaxError('Unterminated collection (...)', i);
|
|
228
|
+
if (s[pos] === ')') return { term: new ListTerm(items), end: pos + 1 };
|
|
229
|
+
const item = parseTerm(s, pos, prefixes, usedPrefixes, blankPrefix);
|
|
230
|
+
if (!item) return null;
|
|
231
|
+
items.push(item.term);
|
|
232
|
+
pos = item.end;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (ch === '[' || ch === '{') return null;
|
|
237
|
+
|
|
238
|
+
const tok = readToken(s, i);
|
|
239
|
+
if (!tok) return null;
|
|
240
|
+
const word = tok.text;
|
|
241
|
+
if (word === 'true' || word === 'false' || /^-?(?:[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?|\.[0-9]+(?:[eE][+-]?[0-9]+)?)$/.test(word)) {
|
|
242
|
+
return { term: internLiteral(word), end: tok.end };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const expanded = expandQNameToken(word, prefixes, usedPrefixes);
|
|
246
|
+
if (expanded !== null) return { term: internIri(expanded), end: tok.end };
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function parseDirective(line, prefixes, usedPrefixes) {
|
|
251
|
+
let m = /^\s*@prefix\s+([^\s]+)\s+<([^>]*)>\s*\.\s*(?:#.*)?$/i.exec(line);
|
|
252
|
+
if (!m) m = /^\s*PREFIX\s+([^\s]+)\s+<([^>]*)>\s*\.?\s*(?:#.*)?$/i.exec(line);
|
|
253
|
+
if (m) {
|
|
254
|
+
const raw = m[1];
|
|
255
|
+
if (!raw.endsWith(':')) throw new N3SyntaxError("Invalid @prefix directive: prefix name must end with ':'");
|
|
256
|
+
const pfx = raw.slice(0, -1);
|
|
257
|
+
prefixes.set(pfx, maybeDecodeIriRef(m[2], 0, prefixes));
|
|
258
|
+
if (usedPrefixes && pfx) usedPrefixes.add(pfx);
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
m = /^\s*@base\s+<([^>]*)>\s*\.\s*(?:#.*)?$/i.exec(line);
|
|
263
|
+
if (!m) m = /^\s*BASE\s+<([^>]*)>\s*\.?\s*(?:#.*)?$/i.exec(line);
|
|
264
|
+
if (m) {
|
|
265
|
+
prefixes.setBase(maybeDecodeIriRef(m[1], 0, prefixes));
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function parseFastStatement(line, prefixes, usedPrefixes, blankPrefix, sourceOffset = null) {
|
|
273
|
+
let s = String(line || '');
|
|
274
|
+
const leading = s.length - s.trimStart().length;
|
|
275
|
+
s = s.slice(leading);
|
|
276
|
+
if (!s || s[0] === '#') return [];
|
|
277
|
+
if (VERSION_LINE_RE.test(s)) return [];
|
|
278
|
+
if (MESSAGE_LINE_RE.test(s)) return '__MESSAGE__';
|
|
279
|
+
if (parseDirective(s, prefixes, usedPrefixes)) return [];
|
|
280
|
+
|
|
281
|
+
let pos = 0;
|
|
282
|
+
const subj = parseTerm(s, pos, prefixes, usedPrefixes, blankPrefix);
|
|
283
|
+
if (!subj) return null;
|
|
284
|
+
pos = subj.end;
|
|
285
|
+
const pred = parseIriOrQName(s, pos, prefixes, usedPrefixes);
|
|
286
|
+
if (!pred) return null;
|
|
287
|
+
pos = pred.end;
|
|
288
|
+
const obj = parseTerm(s, pos, prefixes, usedPrefixes, blankPrefix);
|
|
289
|
+
if (!obj) return null;
|
|
290
|
+
pos = skipWs(s, obj.end);
|
|
291
|
+
|
|
292
|
+
let graph = null;
|
|
293
|
+
if (s[pos] !== '.') {
|
|
294
|
+
const graphTerm = parseIriOrQName(s, pos, prefixes, usedPrefixes);
|
|
295
|
+
if (!graphTerm) return null;
|
|
296
|
+
graph = graphTerm.term;
|
|
297
|
+
pos = skipWs(s, graphTerm.end);
|
|
298
|
+
}
|
|
299
|
+
if (s[pos] !== '.') return null;
|
|
300
|
+
pos = skipWs(s, pos + 1);
|
|
301
|
+
if (pos !== s.length && s[pos] !== '#') return null;
|
|
302
|
+
|
|
303
|
+
const triple = addOffset(new Triple(subj.term, pred.term, obj.term), typeof sourceOffset === 'number' ? sourceOffset + leading : sourceOffset);
|
|
304
|
+
if (!graph) return [triple];
|
|
305
|
+
return [addOffset(new Triple(graph, LOG_NAME_OF, annotateQuotedGraphTerm(new GraphTerm([triple]))), sourceOffset)];
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function lineIterator(text) {
|
|
309
|
+
const s = String(text ?? '');
|
|
310
|
+
let start = 0;
|
|
311
|
+
return {
|
|
312
|
+
*[Symbol.iterator]() {
|
|
313
|
+
for (let i = 0; i <= s.length; i += 1) {
|
|
314
|
+
if (i === s.length || s[i] === '\n' || s[i] === '\r') {
|
|
315
|
+
const line = s.slice(start, i);
|
|
316
|
+
const offset = start;
|
|
317
|
+
if (i < s.length && s[i] === '\r' && s[i + 1] === '\n') i += 1;
|
|
318
|
+
start = i + 1;
|
|
319
|
+
yield { line, offset };
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
},
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function makeDoc(prefixes, triples, label, usedPrefixes) {
|
|
327
|
+
const doc = {
|
|
328
|
+
prefixes,
|
|
329
|
+
triples,
|
|
330
|
+
frules: [],
|
|
331
|
+
brules: [],
|
|
332
|
+
logQueryRules: [],
|
|
333
|
+
label,
|
|
334
|
+
};
|
|
335
|
+
Object.defineProperty(doc, 'usedPrefixes', {
|
|
336
|
+
value: usedPrefixes,
|
|
337
|
+
enumerable: false,
|
|
338
|
+
writable: false,
|
|
339
|
+
configurable: true,
|
|
340
|
+
});
|
|
341
|
+
Object.defineProperty(doc, '__fastRdf', {
|
|
342
|
+
value: true,
|
|
343
|
+
enumerable: false,
|
|
344
|
+
writable: false,
|
|
345
|
+
configurable: true,
|
|
346
|
+
});
|
|
347
|
+
return doc;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function parseLineOrAbort(line, prefixes, usedPrefixes, blankPrefix, sourceOffset) {
|
|
351
|
+
const parsed = parseFastStatement(line, prefixes, usedPrefixes, blankPrefix, sourceOffset);
|
|
352
|
+
if (parsed === null || parsed === '__MESSAGE__') return parsed;
|
|
353
|
+
return parsed;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function parseFastRdfText(text, opts = {}) {
|
|
357
|
+
const source = String(text ?? '');
|
|
358
|
+
// Try the fast line parser directly. It returns null on richer multi-line
|
|
359
|
+
// Turtle/N3 constructs, so ordinary complex programs still fall back safely.
|
|
360
|
+
const prefixes = PrefixEnv.newDefault();
|
|
361
|
+
if (opts.baseIri) prefixes.setBase(opts.baseIri);
|
|
362
|
+
const triples = [];
|
|
363
|
+
const usedPrefixes = new Set();
|
|
364
|
+
let sawUsefulLine = false;
|
|
365
|
+
|
|
366
|
+
for (const { line, offset } of lineIterator(source)) {
|
|
367
|
+
const parsed = parseLineOrAbort(line, prefixes, usedPrefixes, '', offset);
|
|
368
|
+
if (parsed === null || parsed === '__MESSAGE__') return null;
|
|
369
|
+
if (parsed.length) sawUsefulLine = true;
|
|
370
|
+
triples.push(...parsed);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (!sawUsefulLine && !/^\s*(?:@prefix|PREFIX|@base|BASE|@version|VERSION)\b/im.test(source)) return null;
|
|
374
|
+
return makeDoc(prefixes, triples, opts.label || '<input>', usedPrefixes);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function parseFastRdfMessageLog(text, opts = {}) {
|
|
378
|
+
const source = String(text ?? '');
|
|
379
|
+
if (!MESSAGE_VERSION_LINE_RE.test(source)) return null;
|
|
380
|
+
|
|
381
|
+
const prefixes = PrefixEnv.newDefault();
|
|
382
|
+
if (opts.baseIri) prefixes.setBase(opts.baseIri);
|
|
383
|
+
const usedPrefixes = new Set();
|
|
384
|
+
const payloads = [[]];
|
|
385
|
+
let current = payloads[0];
|
|
386
|
+
let messageIndex = 1;
|
|
387
|
+
let sawVersion = false;
|
|
388
|
+
|
|
389
|
+
for (const { line, offset } of lineIterator(source)) {
|
|
390
|
+
const stripped = String(line || '').trimStart();
|
|
391
|
+
if (!stripped || stripped[0] === '#') continue;
|
|
392
|
+
if (MESSAGE_VERSION_LINE_RE.test(stripped)) {
|
|
393
|
+
sawVersion = true;
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
if (VERSION_LINE_RE.test(stripped)) continue;
|
|
397
|
+
if (MESSAGE_LINE_RE.test(stripped)) {
|
|
398
|
+
payloads.push([]);
|
|
399
|
+
current = payloads[payloads.length - 1];
|
|
400
|
+
messageIndex += 1;
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const blankPrefix = `_:eyeling_m${String(messageIndex).padStart(3, '0')}_`;
|
|
405
|
+
const parsed = parseLineOrAbort(line, prefixes, usedPrefixes, blankPrefix, offset);
|
|
406
|
+
if (parsed === null || parsed === '__MESSAGE__') return null;
|
|
407
|
+
current.push(...parsed);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (!sawVersion) return null;
|
|
411
|
+
|
|
412
|
+
const hash = simpleHashText(source);
|
|
413
|
+
const base = `urn:eyeling:message-log:${hash}`;
|
|
414
|
+
const stream = internIri(`${base}#stream`);
|
|
415
|
+
const envelopes = payloads.map((unused, idx) => internIri(`${base}#m${String(idx + 1).padStart(3, '0')}`));
|
|
416
|
+
const payloadIris = payloads.map((unused, idx) => internIri(`${base}#m${String(idx + 1).padStart(3, '0')}/payload`));
|
|
417
|
+
const triples = [];
|
|
418
|
+
|
|
419
|
+
triples.push(new Triple(stream, RDF_TYPE, EYMSG_IRIS.RDFMessageStream));
|
|
420
|
+
triples.push(new Triple(stream, EYMSG_IRIS.messageCount, internLiteral(`"${payloads.length}"${XSD_INTEGER_LITERAL_SUFFIX}`)));
|
|
421
|
+
if (envelopes.length) {
|
|
422
|
+
triples.push(new Triple(stream, EYMSG_IRIS.orderedEnvelopes, new ListTerm(envelopes)));
|
|
423
|
+
triples.push(new Triple(stream, EYMSG_IRIS.firstEnvelope, envelopes[0]));
|
|
424
|
+
triples.push(new Triple(stream, EYMSG_IRIS.lastEnvelope, envelopes[envelopes.length - 1]));
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
for (let idx = 0; idx < payloads.length; idx += 1) {
|
|
428
|
+
const envelope = envelopes[idx];
|
|
429
|
+
const payload = payloadIris[idx];
|
|
430
|
+
const bodyTriples = payloads[idx];
|
|
431
|
+
const hasBody = bodyTriples.length > 0;
|
|
432
|
+
|
|
433
|
+
triples.push(new Triple(stream, EYMSG_IRIS.envelope, envelope));
|
|
434
|
+
triples.push(new Triple(envelope, RDF_TYPE, EYMSG_IRIS.MessageEnvelope));
|
|
435
|
+
triples.push(new Triple(envelope, EYMSG_IRIS.offset, internLiteral(`"${idx + 1}"${XSD_INTEGER_LITERAL_SUFFIX}`)));
|
|
436
|
+
triples.push(new Triple(envelope, EYMSG_IRIS.payloadKind, hasBody ? EYMSG_IRIS.nonEmpty : EYMSG_IRIS.empty));
|
|
437
|
+
if (idx + 1 < envelopes.length) triples.push(new Triple(envelope, EYMSG_IRIS.nextEnvelope, envelopes[idx + 1]));
|
|
438
|
+
if (hasBody) {
|
|
439
|
+
triples.push(new Triple(envelope, EYMSG_IRIS.payloadGraph, payload));
|
|
440
|
+
triples.push(new Triple(payload, LOG_NAME_OF, annotateQuotedGraphTerm(new GraphTerm(bodyTriples))));
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return makeDoc(prefixes, triples, opts.label || '<input>', usedPrefixes);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function tryParseFastRdfText(text, opts = {}) {
|
|
448
|
+
return parseFastRdfMessageLog(text, opts) || parseFastRdfText(text, opts);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
module.exports = { tryParseFastRdfText, parseFastRdfText, parseFastRdfMessageLog };
|
package/lib/multisource.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
const { lex } = require('./lexer');
|
|
12
12
|
const { Parser } = require('./parser');
|
|
13
|
+
const { tryParseFastRdfText } = require('./fast_rdf');
|
|
13
14
|
const {
|
|
14
15
|
Blank,
|
|
15
16
|
ListTerm,
|
|
@@ -172,6 +173,16 @@ function parseN3Text(text, opts = {}) {
|
|
|
172
173
|
sourceLocations = false,
|
|
173
174
|
rdf = false,
|
|
174
175
|
} = opts || {};
|
|
176
|
+
|
|
177
|
+
if (rdf) {
|
|
178
|
+
const fastDoc = tryParseFastRdfText(text, { baseIri, label });
|
|
179
|
+
if (fastDoc) {
|
|
180
|
+
if (sourceLocations) annotateParsedSourceLocations(fastDoc, text, label);
|
|
181
|
+
if (keepSourceArtifacts) fastDoc.text = text;
|
|
182
|
+
return fastDoc;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
175
186
|
const tokens = lex(text, { rdf });
|
|
176
187
|
const parser = new Parser(tokens);
|
|
177
188
|
if (baseIri) parser.prefixes.setBase(baseIri);
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# RDF Message Logs internals
|
|
2
2
|
|
|
3
|
-
Eyeling handles RDF Message Logs as an RDF-compatibility
|
|
3
|
+
Eyeling handles RDF Message Logs as an RDF-compatibility replay step. A message log is not parsed as one flat graph first; it is split into message chunks and exposed as ordinary N3 facts that describe the stream, its ordered message envelopes, and each message payload graph. Common line-oriented logs use the fast RDF AST builder in `lib/fast_rdf.js`; richer RDF/TriG inputs fall back to the normalization path in `lib/lexer.js`.
|
|
4
4
|
|
|
5
|
-
There are two
|
|
5
|
+
There are two replay modes:
|
|
6
6
|
|
|
7
|
-
- normal RDF mode, where a whole message log
|
|
7
|
+
- normal RDF mode, where a whole message log becomes one replay document;
|
|
8
8
|
- `--stream-messages`, where the CLI reads one message at a time and runs the rules once per replayed message.
|
|
9
9
|
|
|
10
10
|
Both paths expose the same basic `eymsg:` vocabulary and use N3 quoted formulas for payload graphs.
|
|
@@ -40,16 +40,15 @@ Old-style `@version` and `@message` forms are also recognized.
|
|
|
40
40
|
|
|
41
41
|
## Normal, whole-log RDF mode
|
|
42
42
|
|
|
43
|
-
When
|
|
43
|
+
When RDF mode sees a `*-messages` version directive, the common fast path is `parseFastRdfMessageLog()` in `lib/fast_rdf.js`. It directly builds the same Eyeling AST that the replay document would have produced. If the input uses richer RDF/TriG constructs outside the fast subset, Eyeling returns to `normalizeRdfMessageLog()` through the RDF compatibility layer in `lib/lexer.js`.
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
Both paths:
|
|
46
46
|
|
|
47
|
-
1.
|
|
48
|
-
2.
|
|
49
|
-
3.
|
|
50
|
-
4.
|
|
51
|
-
5.
|
|
52
|
-
6. emits ordinary N3 replay facts.
|
|
47
|
+
1. strip the version directive;
|
|
48
|
+
2. split the text at top-level `MESSAGE` / `@message` delimiters;
|
|
49
|
+
3. create deterministic stream, envelope, and payload IRIs from a hash of the whole source text;
|
|
50
|
+
4. keep each message in its own blank-node scope;
|
|
51
|
+
5. expose ordinary N3 replay facts.
|
|
53
52
|
|
|
54
53
|
Conceptually, the input above becomes something like:
|
|
55
54
|
|
|
@@ -98,7 +97,7 @@ Conceptually, the input above becomes something like:
|
|
|
98
97
|
} .
|
|
99
98
|
```
|
|
100
99
|
|
|
101
|
-
After this
|
|
100
|
+
After this replay conversion, the ordinary N3 reasoner handles the result. The fast path avoids printing and reparsing a synthetic N3 replay document, but it intentionally builds the same stream/envelope/payload AST shape.
|
|
102
101
|
|
|
103
102
|
## Payload graphs
|
|
104
103
|
|
|
@@ -127,7 +126,7 @@ This is the key design choice: message boundaries are preserved by putting each
|
|
|
127
126
|
|
|
128
127
|
## Per-message normalization
|
|
129
128
|
|
|
130
|
-
Each message body is
|
|
129
|
+
Each message body is converted before it is embedded as a payload formula. In the fast path this is intentionally limited to line-oriented RDF statements: IRIs, prefixed names, blank nodes, literals, numeric/boolean literals, simple N-Quads named graphs, and nested RDF/Turtle collections. Richer RDF 1.2/TriG features are handled by the fallback normalization path:
|
|
131
130
|
|
|
132
131
|
- RDF 1.2 triple terms are converted to singleton N3 graph terms;
|
|
133
132
|
- RDF 1.2 annotation syntax is expanded;
|
|
@@ -219,7 +218,7 @@ RDF Message Log support is best understood as:
|
|
|
219
218
|
```text
|
|
220
219
|
message-log syntax
|
|
221
220
|
-> split into message chunks
|
|
222
|
-
->
|
|
221
|
+
-> parse fast line-oriented payloads directly, or fall back to RDF/TriG normalization
|
|
223
222
|
-> wrap each chunk in an eymsg: envelope
|
|
224
223
|
-> expose the payload as log:nameOf { ... }
|
|
225
224
|
-> run the ordinary N3 reasoner
|
package/notes/rdf12-roundtrip.md
CHANGED
|
@@ -2,17 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
## Summary
|
|
4
4
|
|
|
5
|
-
Eyeling does **not** implement RDF 1.2 as a separate internal data model. RDF/TriG compatibility is an opt-in
|
|
5
|
+
Eyeling does **not** implement RDF 1.2 as a separate internal data model. RDF/TriG compatibility is an opt-in layer that either normalizes RDF/TriG surface syntax in `lib/lexer.js` or, for common line-oriented RDF inputs, directly builds the same Eyeling AST in `lib/fast_rdf.js`.
|
|
6
6
|
|
|
7
|
-
When parsing with RDF mode enabled, Eyeling
|
|
7
|
+
When parsing with RDF mode enabled, Eyeling maps RDF 1.2/TriG surface syntax into ordinary N3-compatible AST terms. The normal reasoner then operates on Eyeling's existing N3 AST. On output, `lib/printing.js` can print some N3 graph terms back as RDF 1.2 triple terms.
|
|
8
8
|
|
|
9
9
|
The flow is:
|
|
10
10
|
|
|
11
11
|
```text
|
|
12
12
|
RDF 1.2 / TriG input
|
|
13
|
-
--
|
|
14
|
-
normalized N3 text
|
|
15
|
-
-- Parser -->
|
|
13
|
+
-- normalizeRdfCompatibility() or tryParseFastRdfText() -->
|
|
16
14
|
Eyeling N3 AST
|
|
17
15
|
-- reasoner -->
|
|
18
16
|
derived N3 facts
|
|
@@ -131,7 +129,7 @@ becomes:
|
|
|
131
129
|
|
|
132
130
|
## Reasoning model
|
|
133
131
|
|
|
134
|
-
After
|
|
132
|
+
After compatibility conversion, RDF 1.2 constructs are ordinary N3 terms. For example, this rule can match reified RDF 1.2 triples:
|
|
135
133
|
|
|
136
134
|
```n3
|
|
137
135
|
{ ?r rdf:reifies { ?s ?p ?o } }
|
package/package.json
CHANGED
package/test/api.test.js
CHANGED
|
@@ -3082,6 +3082,47 @@ _:b :value 2 .
|
|
|
3082
3082
|
],
|
|
3083
3083
|
},
|
|
3084
3084
|
|
|
3085
|
+
{
|
|
3086
|
+
name: 'RDF message fast path handles line-oriented nested collections',
|
|
3087
|
+
opt: { proofComments: false, rdf: true },
|
|
3088
|
+
input: {
|
|
3089
|
+
sources: [
|
|
3090
|
+
{
|
|
3091
|
+
label: 'rules.n3',
|
|
3092
|
+
text: `@prefix : <http://example.org/msgtest#>.
|
|
3093
|
+
@prefix eymsg: <https://eyereasoner.github.io/eyeling/vocab/message#>.
|
|
3094
|
+
@prefix log: <http://www.w3.org/2000/10/swap/log#>.
|
|
3095
|
+
@prefix list: <http://www.w3.org/2000/10/swap/list#>.
|
|
3096
|
+
|
|
3097
|
+
{
|
|
3098
|
+
?Envelope eymsg:payloadGraph ?Payload.
|
|
3099
|
+
?Payload log:nameOf ?G.
|
|
3100
|
+
?G log:includes { ?Record :record ?Fields. }.
|
|
3101
|
+
?Fields list:length ?Len.
|
|
3102
|
+
} log:query {
|
|
3103
|
+
?Envelope :fieldCount ?Len.
|
|
3104
|
+
}.
|
|
3105
|
+
`,
|
|
3106
|
+
},
|
|
3107
|
+
{
|
|
3108
|
+
label: 'message-log.nt',
|
|
3109
|
+
text: `VERSION "1.2-messages"
|
|
3110
|
+
@prefix : <http://example.org/msgtest#> .
|
|
3111
|
+
<http://example.org/record/1> a :Record.
|
|
3112
|
+
<http://example.org/record/1> :record (("001" "_" "_" "_" "42") ("245" "1" "0" "a" "Title, with comma")) .
|
|
3113
|
+
MESSAGE
|
|
3114
|
+
<http://example.org/record/2> a :Record.
|
|
3115
|
+
<http://example.org/record/2> :record (("001" "_" "_" "_" "43")) .
|
|
3116
|
+
`,
|
|
3117
|
+
},
|
|
3118
|
+
],
|
|
3119
|
+
},
|
|
3120
|
+
expect: [
|
|
3121
|
+
/<urn:eyeling:message-log:[0-9a-f]+#m001>\s+:fieldCount\s+2\s*\./m,
|
|
3122
|
+
/<urn:eyeling:message-log:[0-9a-f]+#m002>\s+:fieldCount\s+1\s*\./m,
|
|
3123
|
+
],
|
|
3124
|
+
},
|
|
3125
|
+
|
|
3085
3126
|
{
|
|
3086
3127
|
name: 'API empty input returns empty output',
|
|
3087
3128
|
opt: { proofComments: false },
|