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 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 RDF 1.2 `Quad` terms in subject/object positions. Input objects may combine `quads`/`dataset`/`facts` with `n3`; Eyeling merges the RDF-JS facts with the N3 text before reasoning.
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
- Named-graph RDF-JS input quads are accepted and represented internally with the same shape as RDF/TriG compatibility mode:
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
- RDF 1.2 singleton quoted formulas are converted to RDF-JS `Quad` terms, but general N3 formulas, lists with open tails, and other N3-only terms still cannot be represented as RDF-JS quads. Use:
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
- if (bytes >= limit) finish();
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 __downloadHttpSourceToTempFileSync(sourceLabel) {
4793
- const cp = require('node:child_process');
4794
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'eyeling-rdf-message-log-'));
4795
- const file = path.join(dir, 'source.txt');
4796
- const r = cp.spawnSync(process.execPath, ['-e', __httpFetchScriptBody({ prefixOnly: false }), sourceLabel, file], {
4797
- encoding: 'utf8',
4798
- maxBuffer: 1024 * 1024,
4799
- stdio: ['ignore', 'ignore', 'pipe'],
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
- const tmp = __downloadHttpSourceToTempFileSync(sourceLabel);
5026
- try {
5027
- forEachRdfMessageChunkInFileSync(tmp.file, onMessage);
5028
- } finally {
5029
- tmp.cleanup();
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
- forEachRdfMessageChunkInText(__readInputSourceSync(sourceLabel), onMessage);
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
- __forEachRdfMessageChunkSync(messageSourceLabel, ({ messageIndex, chunk, directives }) => {
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
- function factsContainOutputStrings(triplesForOutput) {
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
- return require('./cli').main();
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