eyeling 1.22.16 → 1.23.0

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/eyeling.js CHANGED
@@ -4921,6 +4921,7 @@ const { pathToFileURL } = require('node:url');
4921
4921
  const engine = require('./engine');
4922
4922
  const deref = require('./deref');
4923
4923
  const { PrefixEnv } = require('./prelude');
4924
+ const { parseN3Text, mergeParsedDocuments } = require('./multisource');
4924
4925
 
4925
4926
  function offsetToLineCol(text, offset) {
4926
4927
  const chars = Array.from(text);
@@ -5004,8 +5005,9 @@ function main() {
5004
5005
 
5005
5006
  function printHelp(toStderr = false) {
5006
5007
  const msg =
5007
- `Usage: ${prog} [options] [file.n3|-]\n\n` +
5008
- `When no file is given and stdin is piped, read N3 from stdin.\n\n` +
5008
+ `Usage: ${prog} [options] [file-or-url.n3|- ...]\n\n` +
5009
+ `When no file is given and stdin is piped, read N3 from stdin.\n` +
5010
+ `When multiple inputs are given, parse each source separately, merge ASTs, then reason once.\n\n` +
5009
5011
  `Options:\n` +
5010
5012
  ` -a, --ast Print parsed AST as JSON and exit.\n` +
5011
5013
  ` --builtin <module.js> Load a custom builtin module (repeatable).\n` +
@@ -5049,7 +5051,7 @@ function main() {
5049
5051
  builtinModules.push(a.slice('--builtin='.length));
5050
5052
  continue;
5051
5053
  }
5052
- if (!a.startsWith('-')) positional.push(a);
5054
+ if (a === '-' || !a.startsWith('-')) positional.push(a);
5053
5055
  }
5054
5056
 
5055
5057
  const showAst = argv.includes('--ast') || argv.includes('-a');
@@ -5075,17 +5077,12 @@ function main() {
5075
5077
  if (typeof engine.setSuperRestrictedMode === 'function') engine.setSuperRestrictedMode(true);
5076
5078
  }
5077
5079
 
5078
- // Positional args (the N3 file)
5080
+ // Positional args (one or more N3 sources).
5079
5081
  const useImplicitStdin = positional.length === 0 && !process.stdin.isTTY;
5080
5082
  if (positional.length === 0 && !useImplicitStdin) {
5081
5083
  printHelp(false);
5082
5084
  process.exit(0);
5083
5085
  }
5084
- if (positional.length > 1) {
5085
- console.error('Error: expected at most one input [file.n3|-].');
5086
- printHelp(true);
5087
- process.exit(1);
5088
- }
5089
5086
 
5090
5087
  for (const spec of builtinModules) {
5091
5088
  try {
@@ -5097,35 +5094,47 @@ function main() {
5097
5094
  }
5098
5095
  }
5099
5096
 
5100
- const sourceLabel = useImplicitStdin || positional[0] === '-' ? '<stdin>' : positional[0];
5101
- const baseIri = __sourceLabelToBaseIri(sourceLabel);
5102
-
5103
- let text;
5104
- try {
5105
- text = __readInputSourceSync(sourceLabel);
5106
- } catch (e) {
5107
- if (sourceLabel === '<stdin>') console.error(`Error reading stdin: ${e.message}`);
5108
- else console.error(`Error reading file ${JSON.stringify(sourceLabel)}: ${e.message}`);
5097
+ const sourceLabels = useImplicitStdin ? ['<stdin>'] : positional.map((item) => (item === '-' ? '<stdin>' : item));
5098
+ if (sourceLabels.filter((item) => item === '<stdin>').length > 1) {
5099
+ console.error('Error: stdin can only be used once.');
5109
5100
  process.exit(1);
5110
5101
  }
5111
5102
 
5112
- let toks;
5113
- let prefixes, triples, frules, brules, qrules;
5114
- try {
5115
- toks = engine.lex(text);
5116
- const parser = new engine.Parser(toks);
5117
- if (baseIri) parser.prefixes.setBase(baseIri);
5118
- [prefixes, triples, frules, brules, qrules] = parser.parseDocument();
5119
- // Make the parsed prefixes available to log:trace output (CLI path)
5120
- engine.setTracePrefixes(prefixes);
5121
- } catch (e) {
5122
- if (e && e.name === 'N3SyntaxError') {
5123
- console.error(formatN3SyntaxError(e, text, sourceLabel));
5103
+ const parsedSources = [];
5104
+ for (const sourceLabel of sourceLabels) {
5105
+ let text;
5106
+ try {
5107
+ text = __readInputSourceSync(sourceLabel);
5108
+ } catch (e) {
5109
+ if (sourceLabel === '<stdin>') console.error(`Error reading stdin: ${e.message}`);
5110
+ else console.error(`Error reading source ${JSON.stringify(sourceLabel)}: ${e.message}`);
5124
5111
  process.exit(1);
5125
5112
  }
5126
- throw e;
5113
+
5114
+ try {
5115
+ parsedSources.push(
5116
+ parseN3Text(text, {
5117
+ baseIri: __sourceLabelToBaseIri(sourceLabel),
5118
+ label: sourceLabel,
5119
+ }),
5120
+ );
5121
+ } catch (e) {
5122
+ if (e && e.name === 'N3SyntaxError') {
5123
+ console.error(formatN3SyntaxError(e, text, sourceLabel));
5124
+ process.exit(1);
5125
+ }
5126
+ throw e;
5127
+ }
5127
5128
  }
5128
5129
 
5130
+ const mergedDocument = mergeParsedDocuments(parsedSources);
5131
+ const prefixes = mergedDocument.prefixes;
5132
+ const triples = mergedDocument.triples;
5133
+ const frules = mergedDocument.frules;
5134
+ const brules = mergedDocument.brules;
5135
+ const qrules = mergedDocument.logQueryRules;
5136
+ const tokenSets = parsedSources.map((source) => ({ tokens: source.tokens, prefixes: source.prefixes }));
5137
+
5129
5138
  if (showAst) {
5130
5139
  function astReplacer(unusedJsonKey, value) {
5131
5140
  if (value instanceof Set) return Array.from(value);
@@ -5267,7 +5276,10 @@ function main() {
5267
5276
  const mayAutoRenderOutputStrings = programMayProduceOutputStrings(triples, frules, qrules);
5268
5277
 
5269
5278
  if (streamMode && !hasQueries && !mayAutoRenderOutputStrings) {
5270
- const usedInInput = prefixesUsedInInputTokens(toks, prefixes);
5279
+ const usedInInput = new Set();
5280
+ for (const source of tokenSets) {
5281
+ for (const pfx of prefixesUsedInInputTokens(source.tokens, source.prefixes)) usedInInput.add(pfx);
5282
+ }
5271
5283
  const outPrefixes = restrictPrefixEnv(prefixes, usedInInput);
5272
5284
 
5273
5285
  // Ensure log:trace uses the same compact prefix set as the output.
@@ -5861,6 +5873,7 @@ const EMPTY_LIST_TERM = new ListTerm([]);
5861
5873
  const { lex, N3SyntaxError } = require('./lexer');
5862
5874
  const { Parser } = require('./parser');
5863
5875
  const { liftBlankRuleVars } = require('./rules');
5876
+ const { parseN3SourceList } = require('./multisource');
5864
5877
 
5865
5878
  const {
5866
5879
  makeBuiltins,
@@ -9202,7 +9215,8 @@ function reasonStream(input, opts = {}) {
9202
9215
  builtinModules = null,
9203
9216
  } = opts;
9204
9217
 
9205
- const parsedInput = normalizeParsedReasonerInputSync(input);
9218
+ const parsedSourceList = parseN3SourceList(input, { baseIri });
9219
+ const parsedInput = parsedSourceList || normalizeParsedReasonerInputSync(input);
9206
9220
  const rdfFactory = rdfjs ? getDataFactory(dataFactory) : null;
9207
9221
 
9208
9222
  const __oldEnforceHttps = deref.getEnforceHttpsEnabled();
@@ -9345,7 +9359,7 @@ function reasonRdfJs(input, opts = {}) {
9345
9359
 
9346
9360
  Promise.resolve().then(async () => {
9347
9361
  try {
9348
- const normalizedInput = await normalizeReasonerInputAsync(input);
9362
+ const normalizedInput = parseN3SourceList(input, restOpts) || (await normalizeReasonerInputAsync(input));
9349
9363
  reasonStream(normalizedInput, {
9350
9364
  ...restOpts,
9351
9365
  rdfjs: false,
@@ -10532,6 +10546,213 @@ function lex(inputText) {
10532
10546
 
10533
10547
  module.exports = { Token, N3SyntaxError, lex, decodeN3StringEscapes };
10534
10548
 
10549
+ };
10550
+ __modules["lib/multisource.js"] = function(require, module, exports){
10551
+ /**
10552
+ * Eyeling Reasoner — multi-source parsing helpers
10553
+ *
10554
+ * These helpers let the CLI/API parse several N3 documents independently and
10555
+ * merge their parsed ASTs before reasoning. This avoids building one giant N3
10556
+ * string while preserving the existing lexer/parser/engine pipeline.
10557
+ */
10558
+
10559
+ 'use strict';
10560
+
10561
+ const { lex } = require('./lexer');
10562
+ const { Parser } = require('./parser');
10563
+ const {
10564
+ Blank,
10565
+ ListTerm,
10566
+ OpenListTerm,
10567
+ GraphTerm,
10568
+ Triple,
10569
+ Rule,
10570
+ PrefixEnv,
10571
+ annotateQuotedGraphTerm,
10572
+ } = require('./prelude');
10573
+
10574
+ function emptyParsedDocument() {
10575
+ return {
10576
+ prefixes: PrefixEnv.newDefault(),
10577
+ triples: [],
10578
+ frules: [],
10579
+ brules: [],
10580
+ logQueryRules: [],
10581
+ };
10582
+ }
10583
+
10584
+ function parseN3Text(text, opts = {}) {
10585
+ const { baseIri = '', label = '<input>' } = opts || {};
10586
+ const tokens = lex(text);
10587
+ const parser = new Parser(tokens);
10588
+ if (baseIri) parser.prefixes.setBase(baseIri);
10589
+ const [prefixes, triples, frules, brules, logQueryRules] = parser.parseDocument();
10590
+ return { prefixes, triples, frules, brules, logQueryRules, tokens, text, label };
10591
+ }
10592
+
10593
+ function sourceBlankPrefix(sourceIndex) {
10594
+ return `_:src${sourceIndex}_`;
10595
+ }
10596
+
10597
+ function scopedBlankLabel(label, sourceIndex, mapping) {
10598
+ const key = String(label || '');
10599
+ let out = mapping.get(key);
10600
+ if (out) return out;
10601
+
10602
+ const bare = key.startsWith('_:') ? key.slice(2) : key;
10603
+ out = sourceBlankPrefix(sourceIndex) + bare;
10604
+ mapping.set(key, out);
10605
+ return out;
10606
+ }
10607
+
10608
+ function scopeBlankNodesInDocument(doc, sourceIndex) {
10609
+ const mapping = new Map();
10610
+
10611
+ function cloneTerm(term) {
10612
+ if (term instanceof Blank) return new Blank(scopedBlankLabel(term.label, sourceIndex, mapping));
10613
+ if (term instanceof ListTerm) return new ListTerm(term.elems.map(cloneTerm));
10614
+ if (term instanceof OpenListTerm) return new OpenListTerm(term.prefix.map(cloneTerm), term.tailVar);
10615
+ if (term instanceof GraphTerm) return annotateQuotedGraphTerm(new GraphTerm(term.triples.map(cloneTriple)));
10616
+ return term;
10617
+ }
10618
+
10619
+ function cloneTriple(triple) {
10620
+ return new Triple(cloneTerm(triple.s), cloneTerm(triple.p), cloneTerm(triple.o));
10621
+ }
10622
+
10623
+ function cloneRule(rule) {
10624
+ const headBlankLabels = new Set();
10625
+ if (rule && rule.headBlankLabels instanceof Set) {
10626
+ for (const label of rule.headBlankLabels) headBlankLabels.add(scopedBlankLabel(label, sourceIndex, mapping));
10627
+ }
10628
+
10629
+ const out = new Rule(
10630
+ (rule.premise || []).map(cloneTriple),
10631
+ (rule.conclusion || []).map(cloneTriple),
10632
+ rule.isForward,
10633
+ rule.isFuse,
10634
+ headBlankLabels,
10635
+ );
10636
+
10637
+ if (rule && Object.prototype.hasOwnProperty.call(rule, '__dynamicConclusionTerm')) {
10638
+ Object.defineProperty(out, '__dynamicConclusionTerm', {
10639
+ value: cloneTerm(rule.__dynamicConclusionTerm),
10640
+ enumerable: false,
10641
+ writable: false,
10642
+ configurable: true,
10643
+ });
10644
+ }
10645
+
10646
+ return out;
10647
+ }
10648
+
10649
+ return {
10650
+ prefixes: doc.prefixes,
10651
+ triples: (doc.triples || []).map(cloneTriple),
10652
+ frules: (doc.frules || []).map(cloneRule),
10653
+ brules: (doc.brules || []).map(cloneRule),
10654
+ logQueryRules: (doc.logQueryRules || []).map(cloneRule),
10655
+ tokens: doc.tokens,
10656
+ text: doc.text,
10657
+ label: doc.label,
10658
+ };
10659
+ }
10660
+
10661
+ function mergePrefixEnvs(target, source) {
10662
+ if (!source) return target;
10663
+ const map = source.map || {};
10664
+ for (const [prefix, iri] of Object.entries(map)) {
10665
+ // Every parser starts with an empty default namespace. Do not let a later
10666
+ // source that never declared ':' erase a useful default namespace from an
10667
+ // earlier source; prefix merging is for output readability only.
10668
+ if (iri || !Object.prototype.hasOwnProperty.call(target.map, prefix)) target.set(prefix, iri);
10669
+ }
10670
+ if (source.baseIri) target.setBase(source.baseIri);
10671
+ return target;
10672
+ }
10673
+
10674
+ function mergeParsedDocuments(docs, opts = {}) {
10675
+ const documents = Array.isArray(docs) ? docs : [];
10676
+ const scopeBlankNodes =
10677
+ typeof opts.scopeBlankNodes === 'boolean' ? opts.scopeBlankNodes : documents.length > 1;
10678
+
10679
+ const merged = emptyParsedDocument();
10680
+ const mergedSources = [];
10681
+
10682
+ for (let i = 0; i < documents.length; i++) {
10683
+ const originalDoc = documents[i] || emptyParsedDocument();
10684
+ const doc = scopeBlankNodes ? scopeBlankNodesInDocument(originalDoc, i + 1) : originalDoc;
10685
+
10686
+ mergePrefixEnvs(merged.prefixes, doc.prefixes);
10687
+ merged.triples.push(...(doc.triples || []));
10688
+ merged.frules.push(...(doc.frules || []));
10689
+ merged.brules.push(...(doc.brules || []));
10690
+ merged.logQueryRules.push(...(doc.logQueryRules || []));
10691
+ mergedSources.push(doc);
10692
+ }
10693
+
10694
+ Object.defineProperty(merged, 'sources', {
10695
+ value: mergedSources,
10696
+ enumerable: false,
10697
+ writable: false,
10698
+ configurable: true,
10699
+ });
10700
+
10701
+ return merged;
10702
+ }
10703
+
10704
+ function isN3SourceListInput(input) {
10705
+ return !!(
10706
+ input &&
10707
+ typeof input === 'object' &&
10708
+ !Array.isArray(input) &&
10709
+ Array.isArray(input.sources)
10710
+ );
10711
+ }
10712
+
10713
+ function normalizeN3SourceItem(source, index) {
10714
+ const sourceNumber = index + 1;
10715
+ if (typeof source === 'string') {
10716
+ return { text: source, label: `<source ${sourceNumber}>`, baseIri: '' };
10717
+ }
10718
+ if (!source || typeof source !== 'object' || Array.isArray(source)) {
10719
+ throw new TypeError('Each N3 source must be a string or an object with an n3/text field');
10720
+ }
10721
+
10722
+ const text = typeof source.n3 === 'string' ? source.n3 : typeof source.text === 'string' ? source.text : null;
10723
+ if (text === null) throw new TypeError('Each N3 source object must provide an n3 or text string');
10724
+
10725
+ return {
10726
+ text,
10727
+ label: typeof source.label === 'string' && source.label ? source.label : `<source ${sourceNumber}>`,
10728
+ baseIri: typeof source.baseIri === 'string' ? source.baseIri : '',
10729
+ };
10730
+ }
10731
+
10732
+ function parseN3SourceList(input, opts = {}) {
10733
+ if (!isN3SourceListInput(input)) return null;
10734
+ const sources = input.sources.map(normalizeN3SourceItem);
10735
+ const defaultBaseIri = typeof opts.baseIri === 'string' ? opts.baseIri : '';
10736
+ const parsed = sources.map((source, index) =>
10737
+ parseN3Text(source.text, {
10738
+ label: source.label,
10739
+ baseIri: source.baseIri || (sources.length === 1 ? defaultBaseIri : ''),
10740
+ }),
10741
+ );
10742
+ return mergeParsedDocuments(parsed, {
10743
+ scopeBlankNodes: typeof input.scopeBlankNodes === 'boolean' ? input.scopeBlankNodes : parsed.length > 1,
10744
+ });
10745
+ }
10746
+
10747
+ module.exports = {
10748
+ emptyParsedDocument,
10749
+ parseN3Text,
10750
+ mergeParsedDocuments,
10751
+ scopeBlankNodesInDocument,
10752
+ isN3SourceListInput,
10753
+ parseN3SourceList,
10754
+ };
10755
+
10535
10756
  };
10536
10757
  __modules["lib/parser.js"] = function(require, module, exports){
10537
10758
  /**
package/index.d.ts CHANGED
@@ -120,6 +120,13 @@ declare module 'eyeling' {
120
120
 
121
121
  export type EyelingAstBundle = [EyelingPrefixEnv, EyelingTriple[], EyelingRule[], EyelingRule[], EyelingRule[]?];
122
122
 
123
+ export type N3Source = string | { n3?: string; text?: string; baseIri?: string; label?: string };
124
+
125
+ export interface N3SourceListInput {
126
+ sources: N3Source[];
127
+ scopeBlankNodes?: boolean;
128
+ }
129
+
123
130
  export interface RdfJsReasonInput {
124
131
  n3?: string;
125
132
  quads?: Iterable<RdfJsQuad> | AsyncIterable<RdfJsQuad>;
@@ -189,13 +196,16 @@ declare module 'eyeling' {
189
196
  queryQuads?: RdfJsQuad[];
190
197
  }
191
198
 
192
- export function reason(opts: ReasonOptions, input: string | RdfJsReasonInput | EyelingAstBundle): string;
199
+ export function reason(
200
+ opts: ReasonOptions,
201
+ input: string | RdfJsReasonInput | EyelingAstBundle | N3SourceListInput,
202
+ ): string;
193
203
  export function reasonStream(
194
- input: string | RdfJsReasonInput | EyelingAstBundle,
204
+ input: string | RdfJsReasonInput | EyelingAstBundle | N3SourceListInput,
195
205
  opts?: ReasonStreamOptions,
196
206
  ): ReasonStreamResult;
197
207
  export function reasonRdfJs(
198
- input: string | RdfJsReasonInput | EyelingAstBundle,
208
+ input: string | RdfJsReasonInput | EyelingAstBundle | N3SourceListInput,
199
209
  opts?: Omit<ReasonStreamOptions, 'rdfjs' | 'onDerived'>,
200
210
  ): AsyncIterable<RdfJsQuad>;
201
211
 
@@ -212,16 +222,18 @@ declare module 'eyeling/browser' {
212
222
  export type RdfJsQuad = import('eyeling').RdfJsQuad;
213
223
  export type RdfJsReasonInput = import('eyeling').RdfJsReasonInput;
214
224
  export type EyelingAstBundle = import('eyeling').EyelingAstBundle;
225
+ export type N3Source = import('eyeling').N3Source;
226
+ export type N3SourceListInput = import('eyeling').N3SourceListInput;
215
227
  export type ReasonStreamOptions = import('eyeling').ReasonStreamOptions;
216
228
  export type ReasonStreamResult = import('eyeling').ReasonStreamResult;
217
229
  export type BuiltinHandler = import('eyeling').BuiltinHandler;
218
230
 
219
231
  export function reasonStream(
220
- input: string | RdfJsReasonInput | EyelingAstBundle,
232
+ input: string | RdfJsReasonInput | EyelingAstBundle | N3SourceListInput,
221
233
  opts?: ReasonStreamOptions,
222
234
  ): ReasonStreamResult;
223
235
  export function reasonRdfJs(
224
- input: string | RdfJsReasonInput | EyelingAstBundle,
236
+ input: string | RdfJsReasonInput | EyelingAstBundle | N3SourceListInput,
225
237
  opts?: Omit<ReasonStreamOptions, 'rdfjs' | 'onDerived'>,
226
238
  ): AsyncIterable<RdfJsQuad>;
227
239
 
package/index.js CHANGED
@@ -7,6 +7,7 @@ const cp = require('node:child_process');
7
7
 
8
8
  const bundleApi = require('./eyeling.js');
9
9
  const { dataFactory, normalizeReasonerInputSync } = require('./lib/rdfjs');
10
+ const { isN3SourceListInput } = require('./lib/multisource');
10
11
  const engine = require('./lib/engine');
11
12
 
12
13
  function reason(opt = {}, input = '') {
@@ -16,11 +17,6 @@ function reason(opt = {}, input = '') {
16
17
  if (Array.isArray(opt)) opt = { args: opt };
17
18
  if (opt == null || typeof opt !== 'object') opt = {};
18
19
 
19
- const n3Input = normalizeReasonerInputSync(input);
20
- if (typeof n3Input !== 'string') {
21
- throw new TypeError('reason(opt, input): input must resolve to an N3 string');
22
- }
23
-
24
20
  const args = [];
25
21
 
26
22
  // default: proof comments OFF for API output (machine-friendly)
@@ -53,13 +49,38 @@ function reason(opt = {}, input = '') {
53
49
  const maxBuffer = Number.isFinite(opt.maxBuffer) ? opt.maxBuffer : 50 * 1024 * 1024;
54
50
 
55
51
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'eyeling-'));
56
- const inputFile = path.join(dir, 'input.n3');
52
+
53
+ function normalizeSourceForTempFile(source) {
54
+ if (typeof source === 'string') return source;
55
+ if (source && typeof source === 'object' && !Array.isArray(source)) {
56
+ const text = typeof source.n3 === 'string' ? source.n3 : typeof source.text === 'string' ? source.text : null;
57
+ if (text !== null) {
58
+ return typeof source.baseIri === 'string' && source.baseIri ? `@base <${source.baseIri}> .\n${text}` : text;
59
+ }
60
+ }
61
+ throw new TypeError('reason(opt, input): each source must be a string or an object with an n3/text field');
62
+ }
57
63
 
58
64
  try {
59
- fs.writeFileSync(inputFile, n3Input, 'utf8');
65
+ const inputFiles = [];
66
+ if (isN3SourceListInput(input)) {
67
+ input.sources.forEach((source, index) => {
68
+ const inputFile = path.join(dir, `input-${index + 1}.n3`);
69
+ fs.writeFileSync(inputFile, normalizeSourceForTempFile(source), 'utf8');
70
+ inputFiles.push(inputFile);
71
+ });
72
+ } else {
73
+ const n3Input = normalizeReasonerInputSync(input);
74
+ if (typeof n3Input !== 'string') {
75
+ throw new TypeError('reason(opt, input): input must resolve to an N3 string');
76
+ }
77
+ const inputFile = path.join(dir, 'input.n3');
78
+ fs.writeFileSync(inputFile, n3Input, 'utf8');
79
+ inputFiles.push(inputFile);
80
+ }
60
81
 
61
82
  const eyelingPath = path.join(__dirname, 'eyeling.js');
62
- const res = cp.spawnSync(process.execPath, [eyelingPath, ...args, inputFile], { encoding: 'utf8', maxBuffer });
83
+ const res = cp.spawnSync(process.execPath, [eyelingPath, ...args, ...inputFiles], { encoding: 'utf8', maxBuffer });
63
84
 
64
85
  if (res.error) throw res.error;
65
86
 
package/lib/cli.js CHANGED
@@ -13,6 +13,7 @@ const { pathToFileURL } = require('node:url');
13
13
  const engine = require('./engine');
14
14
  const deref = require('./deref');
15
15
  const { PrefixEnv } = require('./prelude');
16
+ const { parseN3Text, mergeParsedDocuments } = require('./multisource');
16
17
 
17
18
  function offsetToLineCol(text, offset) {
18
19
  const chars = Array.from(text);
@@ -96,8 +97,9 @@ function main() {
96
97
 
97
98
  function printHelp(toStderr = false) {
98
99
  const msg =
99
- `Usage: ${prog} [options] [file.n3|-]\n\n` +
100
- `When no file is given and stdin is piped, read N3 from stdin.\n\n` +
100
+ `Usage: ${prog} [options] [file-or-url.n3|- ...]\n\n` +
101
+ `When no file is given and stdin is piped, read N3 from stdin.\n` +
102
+ `When multiple inputs are given, parse each source separately, merge ASTs, then reason once.\n\n` +
101
103
  `Options:\n` +
102
104
  ` -a, --ast Print parsed AST as JSON and exit.\n` +
103
105
  ` --builtin <module.js> Load a custom builtin module (repeatable).\n` +
@@ -141,7 +143,7 @@ function main() {
141
143
  builtinModules.push(a.slice('--builtin='.length));
142
144
  continue;
143
145
  }
144
- if (!a.startsWith('-')) positional.push(a);
146
+ if (a === '-' || !a.startsWith('-')) positional.push(a);
145
147
  }
146
148
 
147
149
  const showAst = argv.includes('--ast') || argv.includes('-a');
@@ -167,17 +169,12 @@ function main() {
167
169
  if (typeof engine.setSuperRestrictedMode === 'function') engine.setSuperRestrictedMode(true);
168
170
  }
169
171
 
170
- // Positional args (the N3 file)
172
+ // Positional args (one or more N3 sources).
171
173
  const useImplicitStdin = positional.length === 0 && !process.stdin.isTTY;
172
174
  if (positional.length === 0 && !useImplicitStdin) {
173
175
  printHelp(false);
174
176
  process.exit(0);
175
177
  }
176
- if (positional.length > 1) {
177
- console.error('Error: expected at most one input [file.n3|-].');
178
- printHelp(true);
179
- process.exit(1);
180
- }
181
178
 
182
179
  for (const spec of builtinModules) {
183
180
  try {
@@ -189,35 +186,47 @@ function main() {
189
186
  }
190
187
  }
191
188
 
192
- const sourceLabel = useImplicitStdin || positional[0] === '-' ? '<stdin>' : positional[0];
193
- const baseIri = __sourceLabelToBaseIri(sourceLabel);
194
-
195
- let text;
196
- try {
197
- text = __readInputSourceSync(sourceLabel);
198
- } catch (e) {
199
- if (sourceLabel === '<stdin>') console.error(`Error reading stdin: ${e.message}`);
200
- else console.error(`Error reading file ${JSON.stringify(sourceLabel)}: ${e.message}`);
189
+ const sourceLabels = useImplicitStdin ? ['<stdin>'] : positional.map((item) => (item === '-' ? '<stdin>' : item));
190
+ if (sourceLabels.filter((item) => item === '<stdin>').length > 1) {
191
+ console.error('Error: stdin can only be used once.');
201
192
  process.exit(1);
202
193
  }
203
194
 
204
- let toks;
205
- let prefixes, triples, frules, brules, qrules;
206
- try {
207
- toks = engine.lex(text);
208
- const parser = new engine.Parser(toks);
209
- if (baseIri) parser.prefixes.setBase(baseIri);
210
- [prefixes, triples, frules, brules, qrules] = parser.parseDocument();
211
- // Make the parsed prefixes available to log:trace output (CLI path)
212
- engine.setTracePrefixes(prefixes);
213
- } catch (e) {
214
- if (e && e.name === 'N3SyntaxError') {
215
- console.error(formatN3SyntaxError(e, text, sourceLabel));
195
+ const parsedSources = [];
196
+ for (const sourceLabel of sourceLabels) {
197
+ let text;
198
+ try {
199
+ text = __readInputSourceSync(sourceLabel);
200
+ } catch (e) {
201
+ if (sourceLabel === '<stdin>') console.error(`Error reading stdin: ${e.message}`);
202
+ else console.error(`Error reading source ${JSON.stringify(sourceLabel)}: ${e.message}`);
216
203
  process.exit(1);
217
204
  }
218
- throw e;
205
+
206
+ try {
207
+ parsedSources.push(
208
+ parseN3Text(text, {
209
+ baseIri: __sourceLabelToBaseIri(sourceLabel),
210
+ label: sourceLabel,
211
+ }),
212
+ );
213
+ } catch (e) {
214
+ if (e && e.name === 'N3SyntaxError') {
215
+ console.error(formatN3SyntaxError(e, text, sourceLabel));
216
+ process.exit(1);
217
+ }
218
+ throw e;
219
+ }
219
220
  }
220
221
 
222
+ const mergedDocument = mergeParsedDocuments(parsedSources);
223
+ const prefixes = mergedDocument.prefixes;
224
+ const triples = mergedDocument.triples;
225
+ const frules = mergedDocument.frules;
226
+ const brules = mergedDocument.brules;
227
+ const qrules = mergedDocument.logQueryRules;
228
+ const tokenSets = parsedSources.map((source) => ({ tokens: source.tokens, prefixes: source.prefixes }));
229
+
221
230
  if (showAst) {
222
231
  function astReplacer(unusedJsonKey, value) {
223
232
  if (value instanceof Set) return Array.from(value);
@@ -359,7 +368,10 @@ function main() {
359
368
  const mayAutoRenderOutputStrings = programMayProduceOutputStrings(triples, frules, qrules);
360
369
 
361
370
  if (streamMode && !hasQueries && !mayAutoRenderOutputStrings) {
362
- const usedInInput = prefixesUsedInInputTokens(toks, prefixes);
371
+ const usedInInput = new Set();
372
+ for (const source of tokenSets) {
373
+ for (const pfx of prefixesUsedInInputTokens(source.tokens, source.prefixes)) usedInInput.add(pfx);
374
+ }
363
375
  const outPrefixes = restrictPrefixEnv(prefixes, usedInInput);
364
376
 
365
377
  // Ensure log:trace uses the same compact prefix set as the output.
package/lib/engine.js CHANGED
@@ -38,6 +38,7 @@ const EMPTY_LIST_TERM = new ListTerm([]);
38
38
  const { lex, N3SyntaxError } = require('./lexer');
39
39
  const { Parser } = require('./parser');
40
40
  const { liftBlankRuleVars } = require('./rules');
41
+ const { parseN3SourceList } = require('./multisource');
41
42
 
42
43
  const {
43
44
  makeBuiltins,
@@ -3379,7 +3380,8 @@ function reasonStream(input, opts = {}) {
3379
3380
  builtinModules = null,
3380
3381
  } = opts;
3381
3382
 
3382
- const parsedInput = normalizeParsedReasonerInputSync(input);
3383
+ const parsedSourceList = parseN3SourceList(input, { baseIri });
3384
+ const parsedInput = parsedSourceList || normalizeParsedReasonerInputSync(input);
3383
3385
  const rdfFactory = rdfjs ? getDataFactory(dataFactory) : null;
3384
3386
 
3385
3387
  const __oldEnforceHttps = deref.getEnforceHttpsEnabled();
@@ -3522,7 +3524,7 @@ function reasonRdfJs(input, opts = {}) {
3522
3524
 
3523
3525
  Promise.resolve().then(async () => {
3524
3526
  try {
3525
- const normalizedInput = await normalizeReasonerInputAsync(input);
3527
+ const normalizedInput = parseN3SourceList(input, restOpts) || (await normalizeReasonerInputAsync(input));
3526
3528
  reasonStream(normalizedInput, {
3527
3529
  ...restOpts,
3528
3530
  rdfjs: false,