eyeling 1.28.0 → 1.28.2
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 +6 -13
- package/dist/browser/eyeling.browser.js +252 -28
- package/examples/alma-rdf-messages.n3 +202 -0
- package/examples/output/alma-rdf-messages.n3 +0 -0
- package/eyeling.js +259 -29
- package/lib/cli.js +140 -27
- package/lib/engine.js +9 -1
- package/lib/lexer.js +103 -0
- package/notes/rdf-message-logs.md +228 -0
- package/package.json +1 -1
- package/test/api.test.js +16 -0
- package/test/stream_messages.test.js +102 -3
- package/tools/bundle.js +7 -1
package/README.md
CHANGED
|
@@ -495,19 +495,9 @@ const result = reasonStream(input, { rdfjs: true });
|
|
|
495
495
|
console.log(result.closureQuads);
|
|
496
496
|
```
|
|
497
497
|
|
|
498
|
-
Supported RDF-JS input terms include named nodes, blank nodes, literals, variables, default graph terms, and
|
|
498
|
+
Supported RDF-JS input terms include named nodes, blank nodes, literals, variables, default graph terms, and default-graph quads. Named-graph input quads are rejected clearly unless handled through N3/TriG compatibility mode.
|
|
499
499
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
```n3
|
|
503
|
-
:graph log:nameOf {
|
|
504
|
-
:s :p :o .
|
|
505
|
-
} .
|
|
506
|
-
```
|
|
507
|
-
|
|
508
|
-
When `rdfjs: true` is used on output, singleton N3 quoted formulas are converted to RDF-JS `Quad` terms where RDF 1.2 can represent them, and `log:nameOf` graph terms are expanded back into named-graph RDF-JS quads.
|
|
509
|
-
|
|
510
|
-
Use RDF-JS when you want Eyeling to sit inside a JavaScript RDF pipeline. Use raw N3 input when you need N3-only features such as rules represented directly in source text.
|
|
500
|
+
Use RDF-JS when you want Eyeling to sit inside a JavaScript RDF pipeline. Use raw N3 input when you need N3-only features such as quoted formulas or N3 rules represented directly in source text.
|
|
511
501
|
|
|
512
502
|
---
|
|
513
503
|
|
|
@@ -592,8 +582,11 @@ See the included examples:
|
|
|
592
582
|
eyeling -r examples/rdf-messages.n3 examples/input/rdf-messages.trig
|
|
593
583
|
eyeling -r examples/rdf-message-flow.n3 examples/input/rdf-message-flow.trig
|
|
594
584
|
eyeling -r --stream-messages examples/rdf-message-flow.n3 examples/input/rdf-message-flow.trig
|
|
585
|
+
eyeling -r --stream-messages examples/alma-rdf-messages.n3 https://ugent-lib-opendata-prd.s3.ugent.be/alma-rdf/rdf-messages.20260404.nt
|
|
595
586
|
```
|
|
596
587
|
|
|
588
|
+
The Alma RDF Message Log example intentionally keeps the message log as a URL, because the source `.nt` file is larger than 9 GB.
|
|
589
|
+
|
|
597
590
|
---
|
|
598
591
|
|
|
599
592
|
## Built-ins
|
|
@@ -1195,7 +1188,7 @@ Default CLI output prints newly derived facts. Use `reasonStream()` with `includ
|
|
|
1195
1188
|
|
|
1196
1189
|
### RDF-JS conversion fails
|
|
1197
1190
|
|
|
1198
|
-
|
|
1191
|
+
Some N3 terms cannot be represented as ordinary RDF-JS quads. Use:
|
|
1199
1192
|
|
|
1200
1193
|
```js
|
|
1201
1194
|
reasonStream(input, { rdfjs: true, skipUnsupportedRdfJs: true });
|
|
@@ -4623,10 +4623,13 @@ module.exports = {
|
|
|
4623
4623
|
'use strict';
|
|
4624
4624
|
|
|
4625
4625
|
const fs = require('node:fs');
|
|
4626
|
-
const os = require('node:os');
|
|
4627
4626
|
const path = require('node:path');
|
|
4628
|
-
const { pathToFileURL, fileURLToPath } = require('node:url');
|
|
4627
|
+
const { pathToFileURL, fileURLToPath, URL } = require('node:url');
|
|
4629
4628
|
const { TextDecoder } = require('node:util');
|
|
4629
|
+
const http = require('node:http');
|
|
4630
|
+
const https = require('node:https');
|
|
4631
|
+
const readline = require('node:readline');
|
|
4632
|
+
const zlib = require('node:zlib');
|
|
4630
4633
|
|
|
4631
4634
|
const engine = require('./engine');
|
|
4632
4635
|
const deref = require('./deref');
|
|
@@ -4760,7 +4763,8 @@ function __httpFetchScriptBody({ prefixOnly = false } = {}) {
|
|
|
4760
4763
|
if (finished) return;
|
|
4761
4764
|
chunks.push(chunk);
|
|
4762
4765
|
bytes += chunk.length;
|
|
4763
|
-
|
|
4766
|
+
const text = Buffer.concat(chunks, bytes).toString('utf8');
|
|
4767
|
+
if (bytes >= limit || /^\\s*(?:@version|VERSION)\\s+(["'])(?:1\\.1|1\\.2|1\\.2-basic)-messages\\1\\s*\\.?\\s*(?:#.*)?$/im.test(text)) finish();
|
|
4764
4768
|
});
|
|
4765
4769
|
body.on('end', finish);
|
|
4766
4770
|
body.on('error', (e) => { console.error(e && e.message ? e.message : String(e)); process.exit(5); });
|
|
@@ -4789,22 +4793,95 @@ function __readHttpPrefixSync(sourceLabel, byteLimit = 64 * 1024) {
|
|
|
4789
4793
|
return r.stdout;
|
|
4790
4794
|
}
|
|
4791
4795
|
|
|
4792
|
-
function
|
|
4793
|
-
const
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
|
|
4799
|
-
|
|
4796
|
+
function __openHttpTextStream(sourceLabel, redirects = 0) {
|
|
4797
|
+
const maxRedirects = 10;
|
|
4798
|
+
return new Promise((resolve, reject) => {
|
|
4799
|
+
if (redirects > maxRedirects) {
|
|
4800
|
+
reject(new Error('Too many redirects'));
|
|
4801
|
+
return;
|
|
4802
|
+
}
|
|
4803
|
+
|
|
4804
|
+
let parsed;
|
|
4805
|
+
try {
|
|
4806
|
+
parsed = new URL(sourceLabel);
|
|
4807
|
+
} catch (e) {
|
|
4808
|
+
reject(e);
|
|
4809
|
+
return;
|
|
4810
|
+
}
|
|
4811
|
+
|
|
4812
|
+
const mod = parsed.protocol === 'https:' ? https : parsed.protocol === 'http:' ? http : null;
|
|
4813
|
+
if (!mod) {
|
|
4814
|
+
reject(new Error(`Unsupported protocol ${parsed.protocol}`));
|
|
4815
|
+
return;
|
|
4816
|
+
}
|
|
4817
|
+
|
|
4818
|
+
const req = mod.request(
|
|
4819
|
+
{
|
|
4820
|
+
protocol: parsed.protocol,
|
|
4821
|
+
hostname: parsed.hostname,
|
|
4822
|
+
port: parsed.port || undefined,
|
|
4823
|
+
path: parsed.pathname + parsed.search,
|
|
4824
|
+
headers: {
|
|
4825
|
+
accept: 'text/n3, text/turtle, application/trig, application/n-triples, application/n-quads, text/plain;q=0.8, */*;q=0.01',
|
|
4826
|
+
'accept-encoding': 'identity',
|
|
4827
|
+
'user-agent': 'eyeling-rdf-message-stream',
|
|
4828
|
+
},
|
|
4829
|
+
},
|
|
4830
|
+
(res) => {
|
|
4831
|
+
const sc = res.statusCode || 0;
|
|
4832
|
+
if (sc >= 300 && sc < 400 && res.headers && res.headers.location) {
|
|
4833
|
+
const next = new URL(res.headers.location, sourceLabel).toString();
|
|
4834
|
+
res.resume();
|
|
4835
|
+
resolve(__openHttpTextStream(next, redirects + 1));
|
|
4836
|
+
return;
|
|
4837
|
+
}
|
|
4838
|
+
if (sc < 200 || sc >= 300) {
|
|
4839
|
+
res.resume();
|
|
4840
|
+
reject(new Error(`HTTP status ${sc}`));
|
|
4841
|
+
return;
|
|
4842
|
+
}
|
|
4843
|
+
|
|
4844
|
+
const enc = String((res.headers && res.headers['content-encoding']) || '').toLowerCase();
|
|
4845
|
+
let body = res;
|
|
4846
|
+
if (enc.includes('gzip')) body = res.pipe(zlib.createGunzip());
|
|
4847
|
+
else if (enc.includes('deflate')) body = res.pipe(zlib.createInflate());
|
|
4848
|
+
else if (enc.includes('br')) body = res.pipe(zlib.createBrotliDecompress());
|
|
4849
|
+
resolve(body);
|
|
4850
|
+
},
|
|
4851
|
+
);
|
|
4852
|
+
req.on('error', reject);
|
|
4853
|
+
req.end();
|
|
4800
4854
|
});
|
|
4801
|
-
if (r.status !== 0) {
|
|
4802
|
-
try { fs.rmSync(dir, { recursive: true, force: true }); } catch {}
|
|
4803
|
-
throw new Error(`Failed to dereference ${sourceLabel}${r.stderr ? ': ' + String(r.stderr).trim() : ''}`);
|
|
4804
|
-
}
|
|
4805
|
-
return { file, cleanup: () => { try { fs.rmSync(dir, { recursive: true, force: true }); } catch {} } };
|
|
4806
4855
|
}
|
|
4807
4856
|
|
|
4857
|
+
async function forEachLineInHttpSource(sourceLabel, onLine) {
|
|
4858
|
+
const body = await __openHttpTextStream(sourceLabel);
|
|
4859
|
+
await new Promise((resolve, reject) => {
|
|
4860
|
+
let settled = false;
|
|
4861
|
+
function done(err) {
|
|
4862
|
+
if (settled) return;
|
|
4863
|
+
settled = true;
|
|
4864
|
+
if (err) reject(err);
|
|
4865
|
+
else resolve();
|
|
4866
|
+
}
|
|
4867
|
+
|
|
4868
|
+
const rl = readline.createInterface({ input: body, crlfDelay: Infinity });
|
|
4869
|
+
rl.on('line', (line) => {
|
|
4870
|
+
try {
|
|
4871
|
+
onLine(line + '\n');
|
|
4872
|
+
} catch (e) {
|
|
4873
|
+
try { rl.close(); } catch {}
|
|
4874
|
+
if (body && typeof body.destroy === 'function') {
|
|
4875
|
+
try { body.destroy(); } catch {}
|
|
4876
|
+
}
|
|
4877
|
+
done(e);
|
|
4878
|
+
}
|
|
4879
|
+
});
|
|
4880
|
+
rl.on('close', () => done());
|
|
4881
|
+
rl.on('error', done);
|
|
4882
|
+
body.on('error', done);
|
|
4883
|
+
});
|
|
4884
|
+
}
|
|
4808
4885
|
|
|
4809
4886
|
const RDF_MESSAGE_VERSION_RE = /^\s*(?:@version|VERSION)\s+(["'])(?:1\.1|1\.2|1\.2-basic)-messages\1\s*\.?\s*(?:#.*)?$/im;
|
|
4810
4887
|
const RDF_MESSAGE_VERSION_LINE_RE = /^\s*(?:@version|VERSION)\s+(["'])(?:1\.1|1\.2|1\.2-basic)-messages\1\s*\.?\s*(?:#.*)?$/i;
|
|
@@ -5022,15 +5099,50 @@ function __forEachRdfMessageChunkSync(sourceLabel, onMessage) {
|
|
|
5022
5099
|
return;
|
|
5023
5100
|
}
|
|
5024
5101
|
if (__isHttpSource(sourceLabel)) {
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5102
|
+
throw new Error('internal error: HTTP RDF Message Logs must be streamed asynchronously');
|
|
5103
|
+
}
|
|
5104
|
+
forEachRdfMessageChunkInText(__readInputSourceSync(sourceLabel), onMessage);
|
|
5105
|
+
}
|
|
5106
|
+
|
|
5107
|
+
|
|
5108
|
+
async function forEachRdfMessageChunkInHttpSource(sourceLabel, onMessage) {
|
|
5109
|
+
const directives = [];
|
|
5110
|
+
const seenDirectives = new Set();
|
|
5111
|
+
let chunk = '';
|
|
5112
|
+
let messageIndex = 1;
|
|
5113
|
+
let sawVersion = false;
|
|
5114
|
+
let sawDelimiter = false;
|
|
5115
|
+
|
|
5116
|
+
function emit() {
|
|
5117
|
+
onMessage({ messageIndex, chunk, directives: directives.slice() });
|
|
5118
|
+
messageIndex += 1;
|
|
5119
|
+
chunk = '';
|
|
5120
|
+
}
|
|
5121
|
+
|
|
5122
|
+
await forEachLineInHttpSource(sourceLabel, (line) => {
|
|
5123
|
+
if (RDF_MESSAGE_VERSION_LINE_RE.test(line)) {
|
|
5124
|
+
sawVersion = true;
|
|
5125
|
+
return;
|
|
5126
|
+
}
|
|
5127
|
+
if (RDF_MESSAGE_DELIMITER_LINE_RE.test(line)) {
|
|
5128
|
+
emit();
|
|
5129
|
+
sawDelimiter = true;
|
|
5130
|
+
return;
|
|
5030
5131
|
}
|
|
5132
|
+
addRdfDirective(directives, seenDirectives, line);
|
|
5133
|
+
chunk += line;
|
|
5134
|
+
});
|
|
5135
|
+
|
|
5136
|
+
if (!sawVersion) throw new Error('not an RDF Message Log: missing VERSION "*-messages" directive');
|
|
5137
|
+
if (sawDelimiter || hasRdfPayload(chunk)) emit();
|
|
5138
|
+
}
|
|
5139
|
+
|
|
5140
|
+
async function __forEachRdfMessageChunk(sourceLabel, onMessage) {
|
|
5141
|
+
if (__isHttpSource(sourceLabel)) {
|
|
5142
|
+
await forEachRdfMessageChunkInHttpSource(sourceLabel, onMessage);
|
|
5031
5143
|
return;
|
|
5032
5144
|
}
|
|
5033
|
-
|
|
5145
|
+
__forEachRdfMessageChunkSync(sourceLabel, onMessage);
|
|
5034
5146
|
}
|
|
5035
5147
|
|
|
5036
5148
|
function factsContainOutputStrings(triplesForOutput) {
|
|
@@ -5096,7 +5208,7 @@ function runParsedDocumentOnce(mergedDocument, { rdfMode = false, outputPrefixes
|
|
|
5096
5208
|
}
|
|
5097
5209
|
}
|
|
5098
5210
|
|
|
5099
|
-
function runStreamMessagesMode(sourceLabels, { rdfMode }) {
|
|
5211
|
+
async function runStreamMessagesMode(sourceLabels, { rdfMode }) {
|
|
5100
5212
|
const ordinarySourceLabels = [];
|
|
5101
5213
|
const messageSourceLabels = [];
|
|
5102
5214
|
|
|
@@ -5148,7 +5260,7 @@ function runStreamMessagesMode(sourceLabels, { rdfMode }) {
|
|
|
5148
5260
|
const fullIriPrefixes = new PrefixEnv({});
|
|
5149
5261
|
for (const messageSourceLabel of messageSourceLabels) {
|
|
5150
5262
|
try {
|
|
5151
|
-
|
|
5263
|
+
await __forEachRdfMessageChunk(messageSourceLabel, ({ messageIndex, chunk, directives }) => {
|
|
5152
5264
|
const messageText = buildSingleMessageReplayDocument({
|
|
5153
5265
|
sourceLabel: messageSourceLabel,
|
|
5154
5266
|
messageIndex,
|
|
@@ -5182,7 +5294,7 @@ function runStreamMessagesMode(sourceLabels, { rdfMode }) {
|
|
|
5182
5294
|
}
|
|
5183
5295
|
}
|
|
5184
5296
|
|
|
5185
|
-
function main() {
|
|
5297
|
+
async function main() {
|
|
5186
5298
|
// Drop "node" and script name; keep only user-provided args
|
|
5187
5299
|
// Expand combined short options: -pt == -p -t
|
|
5188
5300
|
const argvRaw = process.argv.slice(2);
|
|
@@ -5321,7 +5433,7 @@ function main() {
|
|
|
5321
5433
|
}
|
|
5322
5434
|
|
|
5323
5435
|
if (streamMessagesMode) {
|
|
5324
|
-
runStreamMessagesMode(sourceLabels, { rdfMode });
|
|
5436
|
+
await runStreamMessagesMode(sourceLabels, { rdfMode });
|
|
5325
5437
|
return;
|
|
5326
5438
|
}
|
|
5327
5439
|
|
|
@@ -5405,7 +5517,8 @@ function main() {
|
|
|
5405
5517
|
return false;
|
|
5406
5518
|
}
|
|
5407
5519
|
|
|
5408
|
-
|
|
5520
|
+
|
|
5521
|
+
function factsContainOutputStrings(triplesForOutput) {
|
|
5409
5522
|
return (
|
|
5410
5523
|
Array.isArray(triplesForOutput) &&
|
|
5411
5524
|
triplesForOutput.some(
|
|
@@ -9843,7 +9956,15 @@ function reasonRdfJs(input, opts = {}) {
|
|
|
9843
9956
|
// Minimal export surface for Node + browser/worker
|
|
9844
9957
|
function main() {
|
|
9845
9958
|
// Lazily require to avoid hard cycles in the module graph.
|
|
9846
|
-
|
|
9959
|
+
const result = require('./cli').main();
|
|
9960
|
+
if (result && typeof result.then === 'function') {
|
|
9961
|
+
result.catch((e) => {
|
|
9962
|
+
const msg = e && e.stack ? e.stack : e && e.message ? e.message : String(e);
|
|
9963
|
+
console.error(msg);
|
|
9964
|
+
process.exit(1);
|
|
9965
|
+
});
|
|
9966
|
+
}
|
|
9967
|
+
return result;
|
|
9847
9968
|
}
|
|
9848
9969
|
|
|
9849
9970
|
// ---------------------------------------------------------------------------
|
|
@@ -11019,11 +11140,114 @@ function normalizeRdfCompatibility(inputText) {
|
|
|
11019
11140
|
return text.slice(at, j);
|
|
11020
11141
|
}
|
|
11021
11142
|
|
|
11143
|
+
function readBalancedTermAt(text, at) {
|
|
11144
|
+
const open = text[at];
|
|
11145
|
+
const matching = { '{': '}', '[': ']', '(': ')' };
|
|
11146
|
+
if (!matching[open]) return null;
|
|
11147
|
+
|
|
11148
|
+
const stack = [matching[open]];
|
|
11149
|
+
let j = at + 1;
|
|
11150
|
+
while (j < text.length) {
|
|
11151
|
+
const ch = text[j];
|
|
11152
|
+
if (ch === '"' || ch === "'") {
|
|
11153
|
+
const str = readStringAt(text, j);
|
|
11154
|
+
j = str.end;
|
|
11155
|
+
continue;
|
|
11156
|
+
}
|
|
11157
|
+
if (ch === '<' && !text.startsWith('<<', j)) {
|
|
11158
|
+
const iri = readIriAt(text, j);
|
|
11159
|
+
j = iri.end;
|
|
11160
|
+
continue;
|
|
11161
|
+
}
|
|
11162
|
+
if (ch === '#') {
|
|
11163
|
+
while (j < text.length && text[j] !== '\n' && text[j] !== '\r') j += 1;
|
|
11164
|
+
continue;
|
|
11165
|
+
}
|
|
11166
|
+
if (matching[ch]) {
|
|
11167
|
+
stack.push(matching[ch]);
|
|
11168
|
+
j += 1;
|
|
11169
|
+
continue;
|
|
11170
|
+
}
|
|
11171
|
+
if (ch === stack[stack.length - 1]) {
|
|
11172
|
+
stack.pop();
|
|
11173
|
+
j += 1;
|
|
11174
|
+
if (stack.length === 0) return { text: text.slice(at, j), end: j };
|
|
11175
|
+
continue;
|
|
11176
|
+
}
|
|
11177
|
+
j += 1;
|
|
11178
|
+
}
|
|
11179
|
+
|
|
11180
|
+
throw new N3SyntaxError(`Unterminated term inside RDF 1.2 triple term, expected ${stack[stack.length - 1]}`);
|
|
11181
|
+
}
|
|
11182
|
+
|
|
11183
|
+
function readRdfTripleTermComponent(text, at) {
|
|
11184
|
+
const j = skipWsAndComments(text, at);
|
|
11185
|
+
if (j >= text.length) return null;
|
|
11186
|
+
const ch = text[j];
|
|
11187
|
+
|
|
11188
|
+
if (ch === '<') return readIriAt(text, j);
|
|
11189
|
+
|
|
11190
|
+
if (ch === '"' || ch === "'") {
|
|
11191
|
+
const str = readStringAt(text, j);
|
|
11192
|
+
let end = str.end;
|
|
11193
|
+
let termText = str.text;
|
|
11194
|
+
if (text.startsWith('^^', end)) {
|
|
11195
|
+
const datatype = readRdfTripleTermComponent(text, end + 2);
|
|
11196
|
+
if (datatype) {
|
|
11197
|
+
termText += '^^' + datatype.text;
|
|
11198
|
+
end = datatype.end;
|
|
11199
|
+
}
|
|
11200
|
+
} else if (text[end] === '@') {
|
|
11201
|
+
let k = end + 1;
|
|
11202
|
+
if (/[A-Za-z]/.test(text[k] || '')) {
|
|
11203
|
+
while (k < text.length && /[A-Za-z0-9-]/.test(text[k])) k += 1;
|
|
11204
|
+
termText += text.slice(end, k);
|
|
11205
|
+
end = k;
|
|
11206
|
+
}
|
|
11207
|
+
}
|
|
11208
|
+
return { text: termText, end };
|
|
11209
|
+
}
|
|
11210
|
+
|
|
11211
|
+
if (ch === '{' || ch === '[' || ch === '(') return readBalancedTermAt(text, j);
|
|
11212
|
+
|
|
11213
|
+
let k = j;
|
|
11214
|
+
while (k < text.length && !/\s/.test(text[k]) && !'{}[](),;'.includes(text[k])) k += 1;
|
|
11215
|
+
if (k === j) return null;
|
|
11216
|
+
const value = text.slice(j, k);
|
|
11217
|
+
if (!value || value.startsWith('@')) return null;
|
|
11218
|
+
return { text: value, end: k };
|
|
11219
|
+
}
|
|
11220
|
+
|
|
11221
|
+
function validateSingleRdfTripleTerm(rawTriple) {
|
|
11222
|
+
const triple = rawTriple.trim();
|
|
11223
|
+
if (!triple) throw new N3SyntaxError('RDF 1.2 triple term must contain exactly one subject, predicate, and object');
|
|
11224
|
+
|
|
11225
|
+
let pos = 0;
|
|
11226
|
+
for (const label of ['subject', 'predicate', 'object']) {
|
|
11227
|
+
const term = readRdfTripleTermComponent(triple, pos);
|
|
11228
|
+
if (!term) throw new N3SyntaxError(`RDF 1.2 triple term is missing a ${label}`);
|
|
11229
|
+
pos = term.end;
|
|
11230
|
+
}
|
|
11231
|
+
|
|
11232
|
+
const rest = skipWsAndComments(triple, pos);
|
|
11233
|
+
if (rest >= triple.length) return;
|
|
11234
|
+
|
|
11235
|
+
const found = triple[rest];
|
|
11236
|
+
if (found === ',') {
|
|
11237
|
+
throw new N3SyntaxError("RDF 1.2 triple terms must contain exactly one object; object lists using ',' are not valid inside <<( ... )>>");
|
|
11238
|
+
}
|
|
11239
|
+
if (found === ';') {
|
|
11240
|
+
throw new N3SyntaxError("RDF 1.2 triple terms must contain exactly one predicate-object pair; ';' is not valid inside <<( ... )>>");
|
|
11241
|
+
}
|
|
11242
|
+
throw new N3SyntaxError(`RDF 1.2 triple term must contain exactly one subject, predicate, and object; unexpected ${JSON.stringify(triple.slice(rest, rest + 20))}`);
|
|
11243
|
+
}
|
|
11244
|
+
|
|
11022
11245
|
function graphTermFromTripleBody(rawBody, parenthesized) {
|
|
11023
11246
|
let body = rawBody.trim();
|
|
11024
11247
|
if (parenthesized && body.startsWith('(') && body.endsWith(')')) body = body.slice(1, -1).trim();
|
|
11025
11248
|
const split = splitTopLevelReifier(body);
|
|
11026
11249
|
const triple = split.triple;
|
|
11250
|
+
validateSingleRdfTripleTerm(triple);
|
|
11027
11251
|
const graph = '{ ' + triple + ' }';
|
|
11028
11252
|
if (split.reifier) {
|
|
11029
11253
|
const reifier = firstTerm(split.reifier);
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# Alma MARC extraction over a remote RDF Message Log.
|
|
2
|
+
#
|
|
3
|
+
# This example is intended to be run with the UGent Alma RDF Message Log URL.
|
|
4
|
+
# The log is larger than 9 GB, so it deliberately remains remote and is not
|
|
5
|
+
# checked into examples/input/.
|
|
6
|
+
#
|
|
7
|
+
# CLI:
|
|
8
|
+
# eyeling -r --stream-messages examples/alma-rdf-messages.n3 https://ugent-lib-opendata-prd.s3.ugent.be/alma-rdf/rdf-messages.20260404.nt
|
|
9
|
+
#
|
|
10
|
+
# Playground:
|
|
11
|
+
# https://eyereasoner.github.io/eyeling/playground?state=g.H4sIAAAAAAAC_02N3QqDMAxG36XXtpEpu_BtUhur0EZJWrYx9u7Tjf1chBz44Jy7ITMY05i0v7mUTQeAGomLTYu360YcsKDdJDjt3GtxngBTRithgv1sJlWMpO7Uns5t3_aOy66sf0rBi4tLmauvSjKuXA7RuGagGwmhrkxycFo4gtCkMBMGhYwLA10xb4n0m_0ludtL3gxFKjVGPpDf8HgCVZUvyeAAAAA
|
|
12
|
+
#
|
|
13
|
+
# Each message is one bibliographic record shaped as a nested list of MARC
|
|
14
|
+
# fields: <record> :record (("245" "1" "0" "a" "Title…") ("650" …) …), where an
|
|
15
|
+
# inner list is (tag ind1 ind2 code value code value …). These backward helper
|
|
16
|
+
# rules (marc:field / marc:subf / marc:map / marc:join / marc:id) walk that
|
|
17
|
+
# structure to pull a subfield's value by (tag, subfield-code); the forward
|
|
18
|
+
# rules at the bottom emit :title / :subject / :type per record.
|
|
19
|
+
#
|
|
20
|
+
|
|
21
|
+
@prefix : <http://example.org/ns#> .
|
|
22
|
+
@prefix list: <http://www.w3.org/2000/10/swap/list#> .
|
|
23
|
+
@prefix log: <http://www.w3.org/2000/10/swap/log#> .
|
|
24
|
+
@prefix eymsg: <https://eyereasoner.github.io/eyeling/vocab/message#> .
|
|
25
|
+
@prefix string: <http://www.w3.org/2000/10/swap/string#> .
|
|
26
|
+
@prefix math: <http://www.w3.org/2000/10/swap/math#> .
|
|
27
|
+
@prefix marc: <https://codeberg.org/phochste/marcattacks#> .
|
|
28
|
+
|
|
29
|
+
## ---- Helper functions (backward rules) ----
|
|
30
|
+
|
|
31
|
+
# marc:splice - drop the first ?Idx members of a list (here: the tag + indicators)
|
|
32
|
+
{ (?List ?Idx) marc:splice ?Result }
|
|
33
|
+
<=
|
|
34
|
+
{
|
|
35
|
+
( ?X {
|
|
36
|
+
?List list:iterate (?Num ?X).
|
|
37
|
+
?Num math:notLessThan ?Idx.
|
|
38
|
+
} ?Result ) log:collectAllIn _:x.
|
|
39
|
+
}.
|
|
40
|
+
|
|
41
|
+
# marc:join - join a list with a separator
|
|
42
|
+
{ (?List ?Sep) marc:join ?Result }
|
|
43
|
+
<=
|
|
44
|
+
{
|
|
45
|
+
(?List ?Sep "") marc:join ?Result.
|
|
46
|
+
}.
|
|
47
|
+
|
|
48
|
+
{ ( () ?Sep ?Acc ) marc:join ?Acc }
|
|
49
|
+
<= true.
|
|
50
|
+
|
|
51
|
+
{ ( ?List ?Sep ?Acc ) marc:join ?Result }
|
|
52
|
+
<=
|
|
53
|
+
{
|
|
54
|
+
?List list:firstRest (?H ?T).
|
|
55
|
+
?Acc log:equalTo "".
|
|
56
|
+
( ?T ?Sep ?H ) marc:join ?Result .
|
|
57
|
+
}.
|
|
58
|
+
|
|
59
|
+
{ ( ?List ?Sep ?Acc ) marc:join ?Result }
|
|
60
|
+
<=
|
|
61
|
+
{
|
|
62
|
+
?List list:firstRest (?H ?T).
|
|
63
|
+
?Acc log:notEqualTo "".
|
|
64
|
+
( ?Acc ?Sep ?H) string:concatenation ?AccNew.
|
|
65
|
+
( ?T ?Sep ?AccNew ) marc:join ?Result .
|
|
66
|
+
}.
|
|
67
|
+
|
|
68
|
+
# marc:id - return the record id as an IRI (from the 001 control field)
|
|
69
|
+
{ ?Record marc:id ?Result }
|
|
70
|
+
<=
|
|
71
|
+
{
|
|
72
|
+
(?Record "001") marc:field0 ?F001.
|
|
73
|
+
?F001 marc:ctrl ?ID.
|
|
74
|
+
( "http://lib.ugent.be/record/" ?ID ) string:concatenation ?IRI_ID.
|
|
75
|
+
?Result log:uri ?IRI_ID.
|
|
76
|
+
}.
|
|
77
|
+
|
|
78
|
+
# marc:ctrl - return the control value of a field
|
|
79
|
+
{ ?Field marc:ctrl ?Result }
|
|
80
|
+
<=
|
|
81
|
+
{
|
|
82
|
+
(?Field 3) list:memberAt "_" .
|
|
83
|
+
(?Field 4) list:memberAt ?Result.
|
|
84
|
+
}.
|
|
85
|
+
|
|
86
|
+
# marc:subf - return all values matching a subfield regex
|
|
87
|
+
{ ( ?Field ?Regex) marc:subf ?Result }
|
|
88
|
+
<=
|
|
89
|
+
{
|
|
90
|
+
( ?Field 3) marc:splice ?FieldData.
|
|
91
|
+
( ?FieldData ?Regex ()) marc:subf ?Result.
|
|
92
|
+
} .
|
|
93
|
+
|
|
94
|
+
{ ( () ?Regex ?Acc ) marc:subf ?Acc }
|
|
95
|
+
<= true.
|
|
96
|
+
|
|
97
|
+
{ ( ?FieldData ?Regex ?Acc ) marc:subf ?Result }
|
|
98
|
+
<=
|
|
99
|
+
{
|
|
100
|
+
?FieldData list:firstRest (?Subf ?Rest).
|
|
101
|
+
?Rest list:firstRest (?Value ?Tail).
|
|
102
|
+
?Subf string:matches ?Regex.
|
|
103
|
+
( ?Acc (?Value)) list:append ?Acc2.
|
|
104
|
+
( ?Tail ?Regex ?Acc2 ) marc:subf ?Result.
|
|
105
|
+
}.
|
|
106
|
+
|
|
107
|
+
{ ( ?FieldData ?Regex ?Acc ) marc:subf ?Result }
|
|
108
|
+
<=
|
|
109
|
+
{
|
|
110
|
+
?FieldData list:firstRest (?Subf ?Rest).
|
|
111
|
+
?Rest list:firstRest (?Value ?Tail).
|
|
112
|
+
?Subf string:notMatches ?Regex.
|
|
113
|
+
( ?Tail ?Regex ?Acc ) marc:subf ?Result.
|
|
114
|
+
}.
|
|
115
|
+
|
|
116
|
+
# marc:field0 - collect the first row of a marc field
|
|
117
|
+
{ ( ?Record ?Field) marc:field0 ?Result }
|
|
118
|
+
<=
|
|
119
|
+
{
|
|
120
|
+
( ?Record ?Field) marc:field ?F.
|
|
121
|
+
?F list:first ?Result.
|
|
122
|
+
}.
|
|
123
|
+
|
|
124
|
+
# marc:field - collect all data for a marc field
|
|
125
|
+
{ ( ?Record ?Field) marc:field ?Result}
|
|
126
|
+
<=
|
|
127
|
+
{
|
|
128
|
+
( ?Record ?Field ()) marc:field ?Result.
|
|
129
|
+
}.
|
|
130
|
+
|
|
131
|
+
{ ( () ?Field ?Acc ) marc:field ?Acc}
|
|
132
|
+
<= true.
|
|
133
|
+
|
|
134
|
+
{ ( ?L ?Field ?Acc ) marc:field ?Result }
|
|
135
|
+
<=
|
|
136
|
+
{
|
|
137
|
+
?L list:firstRest (?H ?T).
|
|
138
|
+
( ?H 0 ) list:memberAt ?Field.
|
|
139
|
+
( ?Acc (?H) ) list:append ?AccNew.
|
|
140
|
+
(?T ?Field ?AccNew) marc:field ?Result.
|
|
141
|
+
}.
|
|
142
|
+
|
|
143
|
+
{ (?L ?Field ?Acc) marc:field ?Result }
|
|
144
|
+
<=
|
|
145
|
+
{
|
|
146
|
+
?L list:firstRest (?H ?T).
|
|
147
|
+
( ?H 0 ) list:memberAt ?X.
|
|
148
|
+
?Field log:notEqualTo ?X.
|
|
149
|
+
(?T ?Field ?Acc) marc:field ?Result.
|
|
150
|
+
}.
|
|
151
|
+
|
|
152
|
+
# marc:map - join all values of (tag, subfield) into one string
|
|
153
|
+
{ ( ?Record ?Tag ?Subfield ) marc:map ?Result }
|
|
154
|
+
<=
|
|
155
|
+
{
|
|
156
|
+
(?Record ?Tag) marc:field ?FL.
|
|
157
|
+
?FL list:member ?F.
|
|
158
|
+
(?F ?Subfield) marc:subf ?T .
|
|
159
|
+
(?T " ") marc:join ?Result.
|
|
160
|
+
}.
|
|
161
|
+
|
|
162
|
+
## ---- Extraction rules (forward) ----
|
|
163
|
+
|
|
164
|
+
# In --stream-messages mode the current RDF Message payload is exposed as a
|
|
165
|
+
# quoted graph via eymsg:payloadGraph/log:nameOf. Match :record inside that
|
|
166
|
+
# payload graph, then reuse the ordinary MARC helper rules on the bound list.
|
|
167
|
+
|
|
168
|
+
{
|
|
169
|
+
?Envelope eymsg:payloadGraph ?Payload.
|
|
170
|
+
?Payload log:nameOf ?PayloadContext.
|
|
171
|
+
?PayloadContext log:includes { ?X :record ?Record. }.
|
|
172
|
+
?Record marc:id ?ID.
|
|
173
|
+
(?Record "245" "a") marc:map ?Val.
|
|
174
|
+
}
|
|
175
|
+
=>
|
|
176
|
+
{
|
|
177
|
+
?ID :title ?Val.
|
|
178
|
+
}.
|
|
179
|
+
|
|
180
|
+
{
|
|
181
|
+
?Envelope eymsg:payloadGraph ?Payload.
|
|
182
|
+
?Payload log:nameOf ?PayloadContext.
|
|
183
|
+
?PayloadContext log:includes { ?X :record ?Record. }.
|
|
184
|
+
?Record marc:id ?ID.
|
|
185
|
+
(?Record "650" "a") marc:map ?Val.
|
|
186
|
+
}
|
|
187
|
+
=>
|
|
188
|
+
{
|
|
189
|
+
?ID :subject ?Val.
|
|
190
|
+
}.
|
|
191
|
+
|
|
192
|
+
{
|
|
193
|
+
?Envelope eymsg:payloadGraph ?Payload.
|
|
194
|
+
?Payload log:nameOf ?PayloadContext.
|
|
195
|
+
?PayloadContext log:includes { ?X :record ?Record. }.
|
|
196
|
+
?Record marc:id ?ID.
|
|
197
|
+
(?Record "920" "a") marc:map ?Val.
|
|
198
|
+
}
|
|
199
|
+
=>
|
|
200
|
+
{
|
|
201
|
+
?ID :type ?Val.
|
|
202
|
+
}.
|
|
File without changes
|