n3.js-messages 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pieter Colpaert
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,218 @@
1
+ # n3.js-messages
2
+
3
+ [RDF Messages](https://w3c-cg.github.io/rsp/spec/messages) for [N3.js](https://github.com/rdfjs/N3.js):
4
+ parse, stream and write message-delimited **Turtle, TriG, N-Triples and N-Quads**, with ordinary RDF/JS `Quad` objects.
5
+
6
+ ## What this package is
7
+
8
+ An RDF Message Log is an RDF document in which `MESSAGE` delimiters group the triples or quads into *messages*, so that a
9
+ stream of RDF can be written to a file (or a socket) and read again as the same stream of messages.
10
+ This package adds that to N3.js **as an extension**: it depends on the public API of the `n3` package (a peer dependency), and
11
+ does not fork, patch or copy N3.js. The syntax of the statements is still parsed and written by N3.js.
12
+
13
+ ## Status
14
+
15
+ RDF Messages is a *Living Document* of the [W3C RDF Stream Processing Community Group](https://www.w3.org/community/rsp/);
16
+ it is **not a W3C Recommendation**, and may still change. This package implements the specification of 25 September 2026
17
+ (upstream commit `9c3b947`) and is versioned `0.x` accordingly.
18
+
19
+ ## Installation
20
+
21
+ ```sh
22
+ npm install n3 n3.js-messages
23
+ ```
24
+
25
+ Requires Node.js ≥ 18 and `n3` ≥ 2.0.0 < 3. The package works with `import` and `require`, and ships TypeScript declarations.
26
+
27
+ ## Parsing
28
+
29
+ ```js
30
+ import { parseMessages } from 'n3.js-messages';
31
+
32
+ const messages = parseMessages(`
33
+ VERSION "1.2-messages"
34
+ PREFIX ex: <http://example.org/>
35
+
36
+ ex:alice ex:says "Hello" .
37
+ MESSAGE
38
+ ex:bob ex:says "Hi" .
39
+ MESSAGE
40
+ MESSAGE
41
+ ex:carol ex:says "Bye" .
42
+ `, { format: 'Turtle' });
43
+
44
+ // An array of messages; a message is an array of RDF/JS quads, in document order
45
+ console.log(messages.length); // 4 (the third message is empty)
46
+ console.log(messages[1][0].object); // Literal "Hi"
47
+ ```
48
+
49
+ `format` is `'Turtle'`, `'TriG'` (the default), `'N-Triples'` or `'N-Quads'` (or the matching media type).
50
+ Other options: `baseIRI`, `factory` (any RDF/JS `DataFactory`), `blankNodePrefix`, and `version` / `messages: true` to announce
51
+ a message log without a `VERSION` line (for example when the media type says so).
52
+ `MessageParser` offers callbacks instead of an array:
53
+
54
+ ```js
55
+ import { MessageParser } from 'n3.js-messages';
56
+
57
+ new MessageParser({ format: 'TriG' }).parse(text, {
58
+ onMessage: (error, message, prefixes, index) => { /* message === null at the end */ },
59
+ onQuad: (error, quad, messageIndex) => { /* optional */ },
60
+ onPrefix: (prefix, iri) => {},
61
+ onVersion: label => {},
62
+ });
63
+ ```
64
+
65
+ Messages are not merged, empty messages are preserved, and quads keep the order of the document.
66
+ A document that does not announce a `-messages` version is one message, and a delimiter in it is an error.
67
+
68
+ ## Streaming
69
+
70
+ `MessageStreamParser` is a Node.js stream: text (or bytes) in, one array of quads per message out, **as soon as the message is
71
+ complete**, with normal backpressure. It never buffers the input, and keeps nothing of messages that it already emitted.
72
+
73
+ ```js
74
+ import { createReadStream } from 'node:fs';
75
+ import { MessageStreamParser } from 'n3.js-messages';
76
+
77
+ const messages = createReadStream('log.trig').pipe(new MessageStreamParser({ format: 'TriG' }));
78
+ for await (const message of messages)
79
+ console.log(`message with ${message.length} quads`);
80
+ // parser.on('prefix', (prefix, iri) => …) and parser.on('version', label => …) are emitted too
81
+ ```
82
+
83
+ Chunk boundaries may be anywhere (inside `MESSAGE`, inside an IRI, between the bytes of a character, after the CR of a CRLF);
84
+ the result is identical to parsing the whole string at once.
85
+
86
+ ## Writing
87
+
88
+ ```js
89
+ import { MessageWriter, MessageStreamWriter, writeMessages } from 'n3.js-messages';
90
+ import { DataFactory } from 'n3';
91
+ const { namedNode, literal, quad } = DataFactory;
92
+
93
+ const writer = new MessageWriter(process.stdout, { format: 'TriG', prefixes: { ex: 'http://example.org/' } });
94
+ await writer.addMessage([quad(namedNode('http://example.org/alice'), namedNode('http://example.org/says'), literal('Hello'))]);
95
+ await writer.addMessage([]); // empty messages are written, too
96
+ await writer.end();
97
+
98
+ const text = writeMessages(messages, { format: 'N-Quads' }); // synchronous, returns a string
99
+ ```
100
+
101
+ - Writes `VERSION "1.2-messages"` (`@version "1.2-messages" .` in Turtle/TriG) first, and a delimiter after each message:
102
+ `MESSAGE` in N-Triples and N-Quads, `@message .` in Turtle and TriG. A parser gets a message when it is written, and a trailing empty
103
+ message survives a round trip.
104
+ - Each message starts from a clean serializer state, so consecutive messages in the same named graph are valid. Prefixes (and `baseIRI` with
105
+ `writeBase: true`) are declared once; `addPrefix` declares more before the next message.
106
+ - `addMessage` and `end` accept a callback or return a promise. They complete when the output stream has accepted the data, not when it was queued.
107
+ - `MessageStreamWriter` is the `Transform` equivalent: messages in (object mode), text out.
108
+
109
+ ## Blank node semantics
110
+
111
+ Blank node labels are **scoped to their message**: `_:a` in message 1 and `_:a` in message 2 are different blank nodes, also when the
112
+ whole log is parsed by one parser instance, from one stream, with any data factory. Within one message a label is one node.
113
+ Consequently, a blank node that the same quad objects share between two messages is *written* with the same label twice, and
114
+ comes back as two nodes. Prefixes and the base IRI, on the other hand, stay in effect across messages (and a later `PREFIX` overrides).
115
+
116
+ ## Supported syntaxes
117
+
118
+ | Syntax | Version announcement | Delimiter | Graphs |
119
+ | --- | --- | --- | --- |
120
+ | Turtle | `VERSION "1.2-messages"` or `@version "1.2-messages" .` | `MESSAGE` or `@message .` | – |
121
+ | TriG | idem | `MESSAGE` or `@message .` | graph blocks |
122
+ | N-Triples | `VERSION "1.2-messages"` | `MESSAGE` (own line) | – |
123
+ | N-Quads | `VERSION "1.2-messages"` | `MESSAGE` (own line) | fourth term |
124
+
125
+ The labels `1.2-messages`, `1.2-basic-messages` and `1.1-messages` are accepted. Not supported: NDJSON-LD, YAML-LD, RDF/XML message logs,
126
+ Jelly, Notation3. Syntax details and ambiguities of the specification are in [IMPLEMENTATION-NOTES.md](IMPLEMENTATION-NOTES.md).
127
+
128
+ ## Specification compliance
129
+
130
+ The [RDF Messages test suite](https://w3c-cg.github.io/rsp/spec/messages-tests) is vendored at a pinned upstream revision and run by
131
+ `npm run test:spec` (see [spec/README.md](spec/README.md)). The suite is a prose document, so the cases are extracted from it
132
+ mechanically; each parsing case runs through the synchronous parser and through the stream parser with one character per chunk.
133
+
134
+ Result of the run for this version (node 25.9.0, n3 2.10.6): **16 passed, 0 failed, 12 skipped** of 28 cases.
135
+ The 12 skipped cases do not apply to RDF syntax parsers and serializers: 7 test protocol adapters (SSE, WebSocket, Kafka, MQTT, other brokers,
136
+ Jelly gRPC), and 5 are about NDJSON-LD, a different serialization. The run prints each skipped case with its reason.
137
+ `npm run test:spec:earl` writes an EARL report.
138
+
139
+ Beyond the suite there are 334 unit, regression, property and differential tests (100% statement, branch, function and line coverage),
140
+ including every possible two-chunk split of the fixtures, deterministic randomized chunking, per-byte streaming, and the regressions of
141
+ the review of N3.js [pull request 586](https://github.com/rdfjs/N3.js/pull/586), which proposed this feature for N3.js itself.
142
+
143
+ ## Relationship to N3.js
144
+
145
+ - `n3` is a **peer dependency** (`>=2.0.0 <3`), so that an application has one N3.js installation. It is tested against n3 2.0.0 (minimum) and
146
+ the latest 2.x, and against N3.js `main` in a scheduled, non-blocking job.
147
+ - This is not a fork. The N3.js maintainers asked for RDF Messages to live in a separate package until the specification matures
148
+ (they proposed it in the discussion of pull request 586, which first added the feature to N3.js itself); this package is that package.
149
+ Where N3.js lacks a hook, [IMPLEMENTATION-NOTES.md](IMPLEMENTATION-NOTES.md) says which, and how it is bridged with public API only.
150
+
151
+ ## Performance
152
+
153
+ `npm run perf` runs the benchmarks (`PERF_QUICK=1` for a smoke run) and writes `.perf-results/latest.json`; `npm run perf:compare` checks
154
+ ratio-based guardrails against each other and compares with `perf/baseline.json`. Method, thresholds and their rationale are in
155
+ [perf/README.md](perf/README.md). Measured on an Intel i7-1265U, node 25.9.0 (median of 5; quads/s):
156
+
157
+ | | plain N3.js | n3.js-messages |
158
+ | --- | --- | --- |
159
+ | parse 100k quads, one message | 688k | 719k (`parseMessages`), 784k (stream, 64 KiB chunks) |
160
+ | parse 10k messages × 5 quads | – | 588k (`parseMessages`), 813k (stream) |
161
+ | stream, 16-byte chunks (10k quads) | – | 464k |
162
+ | write 100k quads in one message (N-Quads) | – | 1.79M |
163
+ | write 10k messages (N-Quads) | – | 1.18M (235k messages/s) |
164
+
165
+ Streaming 100k messages (17 MB) through `MessageStreamParser` grows the heap by 0.12 MB (after gc).
166
+
167
+ ### Comparison with rdf-parser-ts and rdf-writer-ts
168
+
169
+ [rdf-parser-ts](https://github.com/pietercolpaert/rdf-parser-ts) and its sibling [rdf-writer-ts](https://github.com/pietercolpaert/rdf-writer-ts)
170
+ also support RDF Messages, as standalone packages instead of a layer over N3.js. The benchmark suite runs the same generated documents
171
+ through both (`npm run perf`, then `npm run perf:compare`). Throughput in quads per second, median of 5, Intel i7-1265U, Node.js 25.9
172
+ (the higher number is faster; a speed above 1× means n3.js-messages is faster):
173
+
174
+ | Workload | n3.js-messages | rdf-parser-ts / rdf-writer-ts | Speed of n3.js-messages |
175
+ | --- | --- | --- | --- |
176
+ | Parse a string, one message, 100k quads | 719k | 696k | 1.03× |
177
+ | Parse a string, 10k messages × 5 quads | 588k | 691k | 0.85× |
178
+ | Parse a string, 10 messages × 10k quads | 680k | 745k | 0.91× |
179
+ | Stream, 64 KiB chunks, 100k quads | 784k | 680k | 1.15× |
180
+ | Stream, 64 KiB chunks, 10k messages | 813k | 549k | 1.48× |
181
+ | Stream, 16-byte chunks, 10k quads | 464k | 254k | 1.83× |
182
+ | Stream, 1-character chunks, 1k quads | 117k | 76k | 1.53× |
183
+ | Write one message, 100k quads, N-Quads | 1,793k | 1,678k | 1.07× |
184
+ | Write one message, 100k quads, TriG | 1,667k | 1,636k | 1.02× |
185
+ | Write 10k messages, N-Quads | 1,177k | 1,540k | 0.76× |
186
+ | Write 10k messages, TriG | 1,039k | 1,374k | 0.76× |
187
+
188
+ - **Streaming is where this package is ahead**: 1.15× to 1.8×, most for tiny chunks. Our scanner and the N3.js lexer work incrementally; by its
189
+ documentation, rdf-parser-ts runs its parser over every complete statement prefix of the buffered input. I did not profile it to confirm the cause.
190
+ - **Parsing a whole string is faster with rdf-parser-ts** (0.85× to 0.91× for many messages or large ones, parity for one message): `parseMessages` is
191
+ N3.js plus a scanner, so it is bounded by the speed of N3.js, which rdf-parser-ts also beats on ordinary documents (by 1.2×).
192
+ - **Writing**: on par for one large message; rdf-writer-ts is faster for many small messages (0.76×), since this package creates an N3.js writer per message.
193
+ - **Memory**: both stay flat while streaming 100k messages (17 MB): the heap grows by 0.12 MB here and
194
+ 0.08 MB with rdf-parser-ts.
195
+
196
+ Beyond speed, the choice is mostly about the ecosystem. Choose n3.js-messages if you already use N3.js (one parser and one set of syntax quirks, `n3` as a
197
+ shared peer dependency, Node.js 18+, messages as plain `Quad[]`, and a writer that starts every message from a clean state). Choose rdf-parser-ts and rdf-writer-ts if you do not need N3.js, or if parsing whole
198
+ documents and writing many tiny messages are your bottleneck (they need Node.js 22+ and use `rdf-data-factory` by default).
199
+ These are one machine's numbers from a single process: run `npm run perf` on yours; method and caveats are in [perf/README.md](perf/README.md).
200
+
201
+ ## Development
202
+
203
+ ```sh
204
+ npm ci
205
+ npm test # unit, regression, property tests, with coverage (100% required)
206
+ npm run lint
207
+ npm run test:spec # official RDF Messages test suite
208
+ npm run test:types # TypeScript declarations
209
+ npm run test:package # packs the tarball and tests it in a clean consumer project (needs network)
210
+ npm run perf
211
+ npm pack --dry-run
212
+ ```
213
+
214
+ `FUZZ_SEED=… FUZZ_ITERATIONS=…` repeats the property test with another seed; a failure prints its seed and a minimized log.
215
+
216
+ ## License
217
+
218
+ MIT. The vendored test suite in `spec/fixtures` keeps its upstream license (`spec/fixtures/LICENSE-UPSTREAM.md`).
package/index.d.ts ADDED
@@ -0,0 +1,115 @@
1
+ // Type definitions for n3.js-messages
2
+ import * as RDF from '@rdfjs/types';
3
+ import { Transform } from 'stream';
4
+
5
+ export type MessageFormat =
6
+ | 'Turtle' | 'TriG' | 'N-Triples' | 'N-Quads'
7
+ | (string & {});
8
+
9
+ /** One RDF message: the quads between two message delimiters. */
10
+ export type Message<Q extends RDF.BaseQuad = RDF.Quad> = Q[];
11
+
12
+ /**
13
+ * The part of an RDF/JS DataFactory that is used for parsing; any `RDF.DataFactory<Q>` fits.
14
+ * The quads that the factory creates determine the quads that are parsed.
15
+ */
16
+ export interface MessageQuadFactory<Q extends RDF.BaseQuad = RDF.Quad>
17
+ extends Pick<RDF.DataFactory<RDF.Quad, any>, 'namedNode' | 'blankNode' | 'literal' | 'variable' | 'defaultGraph'> {
18
+ quad(subject: Q['subject'], predicate: Q['predicate'], object: Q['object'], graph?: Q['graph']): Q;
19
+ }
20
+
21
+ export interface MessageParserOptions<Q extends RDF.BaseQuad = RDF.Quad> {
22
+ /** Syntax name or media type, case-insensitive. Defaults to `TriG`. */
23
+ format?: MessageFormat;
24
+ baseIRI?: string;
25
+ /** Data factory used to create terms and quads. */
26
+ factory?: MessageQuadFactory<Q>;
27
+ /** Prefix for generated blank node labels, e.g. `b0_`. */
28
+ blankNodePrefix?: string;
29
+ /** RDF version label, e.g. `1.2-messages`; a `-messages` suffix forces message mode. */
30
+ version?: string;
31
+ /** Force message mode even without a `-messages` version declaration. */
32
+ messages?: boolean;
33
+ }
34
+
35
+ export type MessageCallback<Q extends RDF.BaseQuad = RDF.Quad> = (
36
+ error: Error | null,
37
+ message: Q[] | null,
38
+ prefixes?: { [prefix: string]: RDF.NamedNode },
39
+ index?: number,
40
+ ) => void;
41
+
42
+ export interface MessageParseCallbacks<Q extends RDF.BaseQuad = RDF.Quad> {
43
+ onMessage?: MessageCallback<Q>;
44
+ onQuad?: (error: Error | null, quad?: Q, messageIndex?: number) => void;
45
+ onPrefix?: (prefix: string, term: RDF.NamedNode) => void;
46
+ onVersion?: (version: string) => void;
47
+ }
48
+
49
+ export class MessageParser<Q extends RDF.BaseQuad = RDF.Quad> {
50
+ constructor(options?: MessageParserOptions<Q>);
51
+ /** Synchronously parses a string; throws on syntax errors. */
52
+ parse(input: string): Q[][];
53
+ /** Parses a string or stream, reporting messages through a callback. */
54
+ parse(input: string | NodeJS.ReadableStream, callback: MessageCallback<Q>): void;
55
+ /** Parses a string or stream, reporting through object callbacks. */
56
+ parse(input: string | NodeJS.ReadableStream, callbacks: MessageParseCallbacks<Q>): void;
57
+ /** Parses a stream without callbacks (reports nothing). */
58
+ parse(input: NodeJS.ReadableStream): void;
59
+ }
60
+
61
+ export function parseMessages<Q extends RDF.BaseQuad = RDF.Quad>(
62
+ input: string,
63
+ options?: MessageParserOptions<Q>,
64
+ ): Q[][];
65
+
66
+ export class MessageStreamParser<Q extends RDF.BaseQuad = RDF.Quad> extends Transform
67
+ implements AsyncIterable<Q[]> {
68
+ constructor(options?: MessageParserOptions<Q>);
69
+ on(event: 'prefix', listener: (prefix: string, term: RDF.NamedNode) => void): this;
70
+ on(event: 'version', listener: (version: string) => void): this;
71
+ on(event: 'data', listener: (message: Q[]) => void): this;
72
+ on(event: string | symbol, listener: (...args: any[]) => void): this;
73
+ once(event: 'prefix', listener: (prefix: string, term: RDF.NamedNode) => void): this;
74
+ once(event: 'version', listener: (version: string) => void): this;
75
+ once(event: 'data', listener: (message: Q[]) => void): this;
76
+ once(event: string | symbol, listener: (...args: any[]) => void): this;
77
+ [Symbol.asyncIterator](): NodeJS.AsyncIterator<Q[]>;
78
+ }
79
+
80
+ export interface MessageWriterOptions {
81
+ /** Syntax name or media type, case-insensitive. Defaults to `TriG`. */
82
+ format?: MessageFormat;
83
+ /** Prefixes that are declared up front. */
84
+ prefixes?: { [prefix: string]: RDF.NamedNode | string };
85
+ baseIRI?: string;
86
+ writeBase?: boolean;
87
+ lists?: { [listHead: string]: RDF.Term[] };
88
+ /** RDF version label announced in the output. Defaults to `1.2-messages`. */
89
+ version?: string;
90
+ /** Whether to end the output stream on `end()`. Defaults to `true`. */
91
+ end?: boolean;
92
+ }
93
+
94
+ export interface MessageOutputStream {
95
+ write(chunk: string, encoding?: string, callback?: (error?: Error | null) => void): unknown;
96
+ end(callback?: (error?: Error | null) => void): unknown;
97
+ }
98
+
99
+ export class MessageWriter {
100
+ constructor(options?: MessageWriterOptions);
101
+ constructor(outputStream?: MessageOutputStream | NodeJS.WritableStream | null, options?: MessageWriterOptions);
102
+ addMessage(quads: RDF.BaseQuad[], done: (error: Error | null) => void): void;
103
+ addMessage(quads: RDF.BaseQuad[]): Promise<void>;
104
+ addPrefix(prefix: string, iri: RDF.NamedNode | string, done: (error: Error | null) => void): void;
105
+ addPrefix(prefix: string, iri: RDF.NamedNode | string): Promise<void>;
106
+ /** Without an output stream, the result is the written string. */
107
+ end(done: (error: Error | null, result?: string) => void): void;
108
+ end(): Promise<string>;
109
+ }
110
+
111
+ export function writeMessages(messages: Iterable<RDF.BaseQuad[]>, options?: MessageWriterOptions): string;
112
+
113
+ export class MessageStreamWriter extends Transform {
114
+ constructor(options?: Omit<MessageWriterOptions, 'end'>);
115
+ }
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+ exports.parseMessages = parseMessages;
8
+ var _string_decoder = require("string_decoder");
9
+ var _MessageParserCore = _interopRequireDefault(require("./MessageParserCore.js"));
10
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
11
+ // **MessageParser** parses RDF Message Logs (Turtle, TriG, N-Triples and N-Quads).
12
+
13
+ class MessageParser {
14
+ constructor(options) {
15
+ this._options = options || {};
16
+ // Fail early on unsupported options
17
+ this._createCore({});
18
+ }
19
+ _createCore(sink) {
20
+ return new _MessageParserCore.default(this._options, sink);
21
+ }
22
+
23
+ // ### `parse` parses a string or a readable stream of RDF Messages.
24
+ //
25
+ // Without callbacks, a string is parsed synchronously into an array of
26
+ // messages (arrays of quads), and parse errors are thrown.
27
+ // With a callback `(error, message, prefixes)`, or an object with the
28
+ // callbacks `onMessage(error, message, prefixes)`, `onQuad(error, quad, messageIndex)`,
29
+ // `onPrefix(prefix, term)` and `onVersion(label)`, messages are reported as soon
30
+ // as they are complete. The final call has `null` as message.
31
+ parse(input, callbacks) {
32
+ if (typeof callbacks === 'function') callbacks = {
33
+ onMessage: callbacks
34
+ };
35
+ const messages = callbacks ? null : [];
36
+ let error = null;
37
+ callbacks = callbacks || {};
38
+ const sink = {
39
+ message: (quads, index) => callbacks.onMessage ? callbacks.onMessage(null, quads, undefined, index) : messages && messages.push(quads),
40
+ end: prefixes => callbacks.onMessage && callbacks.onMessage(null, null, prefixes),
41
+ error: e => {
42
+ error = e;
43
+ callbacks.onMessage && callbacks.onMessage(e);
44
+ callbacks.onQuad && callbacks.onQuad(e);
45
+ }
46
+ };
47
+ if (callbacks.onQuad) sink.quad = (quad, index) => callbacks.onQuad(null, quad, index);
48
+ if (callbacks.onPrefix) sink.prefix = callbacks.onPrefix;
49
+ if (callbacks.onVersion) sink.version = callbacks.onVersion;
50
+ const core = this._createCore(sink);
51
+
52
+ // Parse a string at once
53
+ if (typeof input === 'string') {
54
+ core.write(input);
55
+ core.end();
56
+ if (error && messages) throw error;
57
+ return messages || undefined;
58
+ }
59
+
60
+ // Parse a stream chunk by chunk
61
+ const decoder = new _string_decoder.StringDecoder('utf8');
62
+ input.on('data', chunk => core.write(typeof chunk === 'string' ? chunk : decoder.write(chunk)));
63
+ input.on('end', () => {
64
+ core.write(decoder.end());
65
+ core.end();
66
+ });
67
+ input.on('error', e => sink.error(e));
68
+ return undefined;
69
+ }
70
+ }
71
+
72
+ // ### `parseMessages` synchronously parses a string into an array of messages
73
+ exports.default = MessageParser;
74
+ function parseMessages(input, options) {
75
+ return new MessageParser(options).parse(input);
76
+ }
@@ -0,0 +1,162 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+ var _events = require("events");
8
+ var _n = require("n3");
9
+ var _MessageScanner = require("./MessageScanner.js");
10
+ var _formats = require("./formats.js");
11
+ // **MessageParserCore** is the push-based engine behind all message parsers.
12
+ // It feeds scanned text to the public N3.js parser and groups the resulting
13
+ // quads into messages.
14
+
15
+ // Unrecognizable by construction: a document cannot contain it by accident
16
+ function createSentinel() {
17
+ return `rdf-messages-${Math.random().toString(36).slice(2)}${Math.random().toString(36).slice(2)}`;
18
+ }
19
+ class MessageParserCore {
20
+ // `sink` receives `message(quads, index)`, `end(prefixes)`, `error(error)`
21
+ // and optionally `quad(quad, messageIndex)`, `prefix(prefix, term)`, `version(label)`
22
+ constructor(options, sink) {
23
+ options = options || {};
24
+ const format = (0, _formats.resolveFormat)(options.format);
25
+ this._sink = sink;
26
+ this._factory = options.factory || _n.DataFactory;
27
+ this._blankNodePrefix = typeof options.blankNodePrefix === 'string' ? options.blankNodePrefix.replace(/^_:/, '') : undefined;
28
+ this._blankNodeCounter = 0;
29
+ this._supportedVersions = _n.Parser.SUPPORTED_VERSIONS;
30
+ this._messages = options.messages === true || (0, _formats.isMessagesVersion)(options.version);
31
+ this._index = 0;
32
+ this._current = null;
33
+ this._scope = null;
34
+ this._seenQuad = false;
35
+ this._failed = false;
36
+ this._finished = false;
37
+ const n3Options = {
38
+ format: format.name,
39
+ factory: this._factory,
40
+ // Version labels are validated here, because N3.js does not know `-messages` labels
41
+ parseUnsupportedVersions: true
42
+ };
43
+ if (options.baseIRI) n3Options.baseIRI = options.baseIRI;
44
+ if (options.version) {
45
+ this._checkVersion(options.version, true);
46
+ n3Options.version = (0, _formats.baseVersion)(options.version);
47
+ }
48
+ const sentinel = createSentinel();
49
+ this._sentinel = sentinel;
50
+ this._scanner = format.lineMode ? new _MessageScanner.LineScanner(sentinel) : new _MessageScanner.TurtleScanner(sentinel);
51
+ this._input = new _events.EventEmitter();
52
+ const callbacks = {
53
+ onQuad: (error, quad, prefixes) => this._onQuad(error, quad, prefixes),
54
+ onPrefix: (prefix, term) => this._failed || sink.prefix && sink.prefix(prefix, term),
55
+ onVersion: label => this._onVersion(label)
56
+ };
57
+ if (format.lineMode) callbacks.onComment = comment => this._onComment(comment);
58
+ new _n.Parser(n3Options).parse(this._input, callbacks);
59
+ }
60
+
61
+ // ### `write` accepts the next chunk of text
62
+ write(text) {
63
+ if (!this._failed && !this._finished && text.length > 0) this._feed(this._scanner.push(text));
64
+ }
65
+
66
+ // ### `end` signals the end of the input
67
+ end() {
68
+ if (!this._failed && !this._finished) {
69
+ this._feed(this._scanner.end());
70
+ this._input.emit('end');
71
+ }
72
+ }
73
+ _feed(text) {
74
+ if (text.length > 0) this._input.emit('data', text);
75
+ }
76
+ _onQuad(error, quad, prefixes) {
77
+ if (this._failed || this._finished) return;
78
+ if (error) this._fail(error);else if (quad) this._addQuad(quad);else {
79
+ this._finished = true;
80
+ if (this._current !== null) this._emitMessage();
81
+ this._sink.end(prefixes);
82
+ }
83
+ }
84
+ _addQuad(quad) {
85
+ this._seenQuad = true;
86
+ // Blank node labels are scoped to their message
87
+ if (this._messages) quad = this._scopeQuad(quad);
88
+ if (this._current === null) this._current = [];
89
+ this._current.push(quad);
90
+ if (this._sink.quad) this._sink.quad(quad, this._index);
91
+ }
92
+ _onVersion(label) {
93
+ if (this._failed) return;
94
+ if (label === this._sentinel) this._delimiter(this._scanner.delimiterLines.shift());else this._announce(label);
95
+ }
96
+
97
+ // Line mode reports delimiters and versions as comments
98
+ _onComment(comment) {
99
+ if (this._failed || !comment.startsWith(this._sentinel)) return;
100
+ const rest = comment.substring(this._sentinel.length);
101
+ if (rest[0] === 'M') this._delimiter(Number(rest.substring(1)));else this._announce(rest.substring(1));
102
+ }
103
+ _announce(label) {
104
+ const error = this._checkVersion(label);
105
+ if (error) return this._fail(error);
106
+ if ((0, _formats.isMessagesVersion)(label) && !this._messages) {
107
+ if (this._seenQuad) return this._fail(new Error(`Version "${label}" must be announced before the first statement.`));
108
+ this._messages = true;
109
+ }
110
+ if (this._sink.version) this._sink.version(label);
111
+ }
112
+
113
+ // Validates a version label, and returns an error for unsupported ones
114
+ _checkVersion(label, throwing) {
115
+ if ((0, _formats.isSupportedVersion)((0, _formats.baseVersion)(label), this._supportedVersions)) return null;
116
+ const error = new Error(`Detected unsupported version: "${label}"`);
117
+ if (throwing) throw error;
118
+ return error;
119
+ }
120
+ _delimiter(line) {
121
+ if (!this._messages) {
122
+ return this._fail(new Error(`Unexpected "MESSAGE" on line ${line}: the document did not announce ` + 'a version with the "-messages" suffix.'));
123
+ }
124
+ this._emitMessage();
125
+ }
126
+
127
+ // Finalizes the current message, which is empty if no quad was seen
128
+ _emitMessage() {
129
+ const quads = this._current || [];
130
+ this._current = null;
131
+ this._scope = null;
132
+ this._sink.message(quads, this._index++);
133
+ }
134
+ _fail(error) {
135
+ this._failed = true;
136
+ this._current = null;
137
+ this._sink.error(error);
138
+ }
139
+
140
+ // ## Blank node scoping
141
+
142
+ _scopeQuad(quad) {
143
+ const subject = this._scopeTerm(quad.subject);
144
+ const predicate = this._scopeTerm(quad.predicate);
145
+ const object = this._scopeTerm(quad.object);
146
+ const graph = this._scopeTerm(quad.graph);
147
+ return subject === quad.subject && predicate === quad.predicate && object === quad.object && graph === quad.graph ? quad : this._factory.quad(subject, predicate, object, graph);
148
+ }
149
+ _scopeTerm(term) {
150
+ if (term.termType === 'BlankNode') {
151
+ const scope = this._scope || (this._scope = new Map());
152
+ let scoped = scope.get(term.value);
153
+ if (!scoped) {
154
+ scoped = this._blankNodePrefix === undefined ? this._factory.blankNode() : this._factory.blankNode(`${this._blankNodePrefix}${this._blankNodeCounter++}`);
155
+ scope.set(term.value, scoped);
156
+ }
157
+ return scoped;
158
+ }
159
+ return term.termType === 'Quad' ? this._scopeQuad(term) : term;
160
+ }
161
+ }
162
+ exports.default = MessageParserCore;