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 +21 -0
- package/README.md +218 -0
- package/index.d.ts +115 -0
- package/lib/MessageParser.js +76 -0
- package/lib/MessageParserCore.js +162 -0
- package/lib/MessageScanner.js +395 -0
- package/lib/MessageStreamParser.js +44 -0
- package/lib/MessageStreamWriter.js +35 -0
- package/lib/MessageWriter.js +178 -0
- package/lib/formats.js +55 -0
- package/lib/index.js +47 -0
- package/lib/package.json +1 -0
- package/package.json +107 -0
- package/src/MessageParser.js +71 -0
- package/src/MessageParserCore.js +197 -0
- package/src/MessageScanner.js +390 -0
- package/src/MessageStreamParser.js +33 -0
- package/src/MessageStreamWriter.js +22 -0
- package/src/MessageWriter.js +194 -0
- package/src/formats.js +39 -0
- package/src/index.js +4 -0
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;
|