n3.js-messages 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -28
- package/lib/MessageParserCore.js +27 -36
- package/lib/MessageWriter.js +30 -29
- package/lib/index.js +2 -1
- package/package.json +7 -7
- package/src/MessageParserCore.js +27 -46
- package/src/MessageWriter.js +31 -28
- package/src/MessageScanner.js +0 -390
package/README.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# n3.js-messages
|
|
2
2
|
|
|
3
|
+
[](https://github.com/pietercolpaert/n3.js-messages/actions/workflows/ci.yml)
|
|
4
|
+
[](https://coveralls.io/github/pietercolpaert/n3.js-messages?branch=main)
|
|
5
|
+
[](https://www.npmjs.com/package/n3.js-messages)
|
|
6
|
+
[](https://github.com/pietercolpaert/n3.js-messages/blob/main/LICENSE)
|
|
7
|
+
|
|
3
8
|
[RDF Messages](https://w3c-cg.github.io/rsp/spec/messages) for [N3.js](https://github.com/rdfjs/N3.js):
|
|
4
9
|
parse, stream and write message-delimited **Turtle, TriG, N-Triples and N-Quads**, with ordinary RDF/JS `Quad` objects.
|
|
5
10
|
|
|
@@ -22,7 +27,7 @@ it is **not a W3C Recommendation**, and may still change. This package implement
|
|
|
22
27
|
npm install n3 n3.js-messages
|
|
23
28
|
```
|
|
24
29
|
|
|
25
|
-
Requires Node.js ≥ 18 and `n3` ≥ 2.
|
|
30
|
+
Requires Node.js ≥ 18 and `n3` ≥ 2.13.7 < 3. The package works with `import` and `require`, and ships TypeScript declarations.
|
|
26
31
|
|
|
27
32
|
## Parsing
|
|
28
33
|
|
|
@@ -98,7 +103,7 @@ await writer.end();
|
|
|
98
103
|
const text = writeMessages(messages, { format: 'N-Quads' }); // synchronous, returns a string
|
|
99
104
|
```
|
|
100
105
|
|
|
101
|
-
- Writes `VERSION "1.2-messages"` (`@version "1.2-messages"
|
|
106
|
+
- Writes `VERSION "1.2-messages"` (`@version "1.2-messages".` in Turtle/TriG) first, and a delimiter after each message:
|
|
102
107
|
`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
108
|
message survives a round trip.
|
|
104
109
|
- Each message starts from a clean serializer state, so consecutive messages in the same named graph are valid. Prefixes (and `baseIRI` with
|
|
@@ -136,17 +141,20 @@ The 12 skipped cases do not apply to RDF syntax parsers and serializers: 7 test
|
|
|
136
141
|
Jelly gRPC), and 5 are about NDJSON-LD, a different serialization. The run prints each skipped case with its reason.
|
|
137
142
|
`npm run test:spec:earl` writes an EARL report.
|
|
138
143
|
|
|
139
|
-
Beyond the suite there are
|
|
144
|
+
Beyond the suite there are 312 unit, regression, property and differential tests (100% statement, branch, function and line coverage),
|
|
140
145
|
including every possible two-chunk split of the fixtures, deterministic randomized chunking, per-byte streaming, and the regressions of
|
|
141
146
|
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
147
|
|
|
143
148
|
## Relationship to N3.js
|
|
144
149
|
|
|
145
|
-
- `n3` is a **peer dependency** (`>=2.
|
|
150
|
+
- `n3` is a **peer dependency** (`>=2.13.7 <3`), so that an application has one N3.js installation. It is tested against n3 2.13.7 (minimum) and
|
|
146
151
|
the latest 2.x, and against N3.js `main` in a scheduled, non-blocking job.
|
|
147
152
|
- This is not a fork. The N3.js maintainers asked for RDF Messages to live in a separate package until the specification matures
|
|
148
153
|
(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
|
-
|
|
154
|
+
- N3.js parses the delimiters itself: the parser is created with `directives: ['message']`, and reports `MESSAGE` and `@message .`
|
|
155
|
+
through `onDirective`, in order with the quads. The writer is a small subclass of `N3.Writer` that announces the log with the
|
|
156
|
+
`version` option and ends every message with `_endStatement()` before it writes the delimiter.
|
|
157
|
+
Blank node scoping per message is done by this package.
|
|
150
158
|
|
|
151
159
|
## Performance
|
|
152
160
|
|
|
@@ -156,11 +164,11 @@ ratio-based guardrails against each other and compares with `perf/baseline.json`
|
|
|
156
164
|
|
|
157
165
|
| | plain N3.js | n3.js-messages |
|
|
158
166
|
| --- | --- | --- |
|
|
159
|
-
| parse 100k quads, one message |
|
|
160
|
-
| parse 10k messages × 5 quads | – |
|
|
161
|
-
| stream, 16-byte chunks (10k quads) | – |
|
|
162
|
-
| write 100k quads in one message (N-Quads) | – | 1.
|
|
163
|
-
| write 10k messages (N-Quads) | – | 1.
|
|
167
|
+
| parse 100k quads, one message | 649k | 691k (`parseMessages`), 735k (stream, 64 KiB chunks) |
|
|
168
|
+
| parse 10k messages × 5 quads | – | 564k (`parseMessages`), 655k (stream) |
|
|
169
|
+
| stream, 16-byte chunks (10k quads) | – | 324k |
|
|
170
|
+
| write 100k quads in one message (N-Quads) | – | 1.34M |
|
|
171
|
+
| write 10k messages (N-Quads) | – | 1.10M (221k messages/s) |
|
|
164
172
|
|
|
165
173
|
Streaming 100k messages (17 MB) through `MessageStreamParser` grows the heap by 0.12 MB (after gc).
|
|
166
174
|
|
|
@@ -173,29 +181,30 @@ through both (`npm run perf`, then `npm run perf:compare`). Throughput in quads
|
|
|
173
181
|
|
|
174
182
|
| Workload | n3.js-messages | rdf-parser-ts / rdf-writer-ts | Speed of n3.js-messages |
|
|
175
183
|
| --- | --- | --- | --- |
|
|
176
|
-
| Parse a string, one message, 100k quads |
|
|
177
|
-
| Parse a string, 10k messages × 5 quads |
|
|
178
|
-
| Parse a string, 10 messages × 10k quads |
|
|
179
|
-
| Stream, 64 KiB chunks, 100k quads |
|
|
180
|
-
| Stream, 64 KiB chunks, 10k messages |
|
|
181
|
-
| Stream, 16-byte chunks, 10k quads |
|
|
182
|
-
| Stream, 1-character chunks, 1k quads |
|
|
183
|
-
| Write one message, 100k quads, N-Quads | 1,
|
|
184
|
-
| Write one message, 100k quads, TriG | 1,
|
|
185
|
-
| Write 10k messages, N-Quads | 1,
|
|
186
|
-
| Write 10k messages, TriG |
|
|
187
|
-
|
|
188
|
-
- **Streaming is where this package is ahead**: 1.15× to 1.
|
|
184
|
+
| Parse a string, one message, 100k quads | 691k | 551k | 1.25× |
|
|
185
|
+
| Parse a string, 10k messages × 5 quads | 564k | 546k | 1.03× |
|
|
186
|
+
| Parse a string, 10 messages × 10k quads | 732k | 596k | 1.23× |
|
|
187
|
+
| Stream, 64 KiB chunks, 100k quads | 735k | 638k | 1.15× |
|
|
188
|
+
| Stream, 64 KiB chunks, 10k messages | 655k | 511k | 1.28× |
|
|
189
|
+
| Stream, 16-byte chunks, 10k quads | 324k | 198k | 1.64× |
|
|
190
|
+
| Stream, 1-character chunks, 1k quads | 79k | 61k | 1.29× |
|
|
191
|
+
| Write one message, 100k quads, N-Quads | 1,342k | 1,352k | 0.99× |
|
|
192
|
+
| Write one message, 100k quads, TriG | 1,079k | 1,281k | 0.84× |
|
|
193
|
+
| Write 10k messages, N-Quads | 1,103k | 689k | 1.60× |
|
|
194
|
+
| Write 10k messages, TriG | 829k | 595k | 1.39× |
|
|
195
|
+
|
|
196
|
+
- **Streaming is where this package is furthest ahead**: 1.15× to 1.64×, most for tiny chunks. The N3.js lexer works incrementally; by its
|
|
189
197
|
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
|
|
191
|
-
N3.js
|
|
192
|
-
- **Writing**: on par for one large message; rdf-writer-ts is faster for
|
|
198
|
+
- **Parsing a whole string** is faster here too (1.03× for many small messages, 1.23× to 1.25× for large ones): `parseMessages` is
|
|
199
|
+
N3.js with an extra directive, and N3.js itself is also ahead of rdf-parser-ts on an ordinary document (1.07×).
|
|
200
|
+
- **Writing**: on par for one large N-Quads message (0.99×); rdf-writer-ts is faster for one large TriG message (0.84×); this package is faster for
|
|
201
|
+
many small messages (1.4× to 1.6×).
|
|
193
202
|
- **Memory**: both stay flat while streaming 100k messages (17 MB): the heap grows by 0.12 MB here and
|
|
194
203
|
0.08 MB with rdf-parser-ts.
|
|
195
204
|
|
|
196
205
|
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
|
|
198
|
-
|
|
206
|
+
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 writing
|
|
207
|
+
large TriG messages is your bottleneck (they need Node.js 22+ and use `rdf-data-factory` by default).
|
|
199
208
|
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
209
|
|
|
201
210
|
## Development
|
package/lib/MessageParserCore.js
CHANGED
|
@@ -6,16 +6,11 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
6
6
|
exports.default = void 0;
|
|
7
7
|
var _events = require("events");
|
|
8
8
|
var _n = require("n3");
|
|
9
|
-
var _MessageScanner = require("./MessageScanner.js");
|
|
10
9
|
var _formats = require("./formats.js");
|
|
11
10
|
// **MessageParserCore** is the push-based engine behind all message parsers.
|
|
12
|
-
// It feeds
|
|
13
|
-
// quads into messages.
|
|
11
|
+
// It feeds text to the N3.js parser, which recognizes the message delimiters
|
|
12
|
+
// through its `directives` option, and groups the resulting quads into messages.
|
|
14
13
|
|
|
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
14
|
class MessageParserCore {
|
|
20
15
|
// `sink` receives `message(quads, index)`, `end(prefixes)`, `error(error)`
|
|
21
16
|
// and optionally `quad(quad, messageIndex)`, `prefix(prefix, term)`, `version(label)`
|
|
@@ -38,40 +33,47 @@ class MessageParserCore {
|
|
|
38
33
|
format: format.name,
|
|
39
34
|
factory: this._factory,
|
|
40
35
|
// Version labels are validated here, because N3.js does not know `-messages` labels
|
|
41
|
-
parseUnsupportedVersions: true
|
|
36
|
+
parseUnsupportedVersions: true,
|
|
37
|
+
// N3.js reports `MESSAGE` and `@message .` between statements through `onDirective`,
|
|
38
|
+
// in order with the quads, and rejects them anywhere else
|
|
39
|
+
directives: ['message']
|
|
42
40
|
};
|
|
43
41
|
if (options.baseIRI) n3Options.baseIRI = options.baseIRI;
|
|
44
42
|
if (options.version) {
|
|
45
43
|
this._checkVersion(options.version, true);
|
|
46
44
|
n3Options.version = (0, _formats.baseVersion)(options.version);
|
|
47
45
|
}
|
|
48
|
-
const sentinel = createSentinel();
|
|
49
|
-
this._sentinel = sentinel;
|
|
50
|
-
this._scanner = format.lineMode ? new _MessageScanner.LineScanner(sentinel) : new _MessageScanner.TurtleScanner(sentinel);
|
|
51
46
|
this._input = new _events.EventEmitter();
|
|
52
|
-
|
|
47
|
+
this._lineMode = format.lineMode;
|
|
48
|
+
// The line of the current token, and of the last delimiter
|
|
49
|
+
this._line = 0;
|
|
50
|
+
this._delimiterLine = 0;
|
|
51
|
+
new _n.Parser(n3Options).parse(this._input, {
|
|
53
52
|
onQuad: (error, quad, prefixes) => this._onQuad(error, quad, prefixes),
|
|
54
53
|
onPrefix: (prefix, term) => this._failed || sink.prefix && sink.prefix(prefix, term),
|
|
55
|
-
onVersion: label => this.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
onVersion: label => this._failed || this._announce(label),
|
|
55
|
+
onDirective: () => this._failed || this._delimiter(),
|
|
56
|
+
onToken: token => this._onToken(token)
|
|
57
|
+
});
|
|
59
58
|
}
|
|
60
59
|
|
|
61
60
|
// ### `write` accepts the next chunk of text
|
|
62
61
|
write(text) {
|
|
63
|
-
if (!this._failed && !this._finished && text.length > 0) this.
|
|
62
|
+
if (!this._failed && !this._finished && text.length > 0) this._input.emit('data', text);
|
|
64
63
|
}
|
|
65
64
|
|
|
66
65
|
// ### `end` signals the end of the input
|
|
67
66
|
end() {
|
|
68
|
-
if (!this._failed && !this._finished)
|
|
69
|
-
this._feed(this._scanner.end());
|
|
70
|
-
this._input.emit('end');
|
|
71
|
-
}
|
|
67
|
+
if (!this._failed && !this._finished) this._input.emit('end');
|
|
72
68
|
}
|
|
73
|
-
|
|
74
|
-
|
|
69
|
+
|
|
70
|
+
// Keeps track of lines; in N-Triples and N-Quads, a delimiter must be on its own line
|
|
71
|
+
_onToken(token) {
|
|
72
|
+
const line = token.line,
|
|
73
|
+
delimiter = token.type === 'MESSAGE';
|
|
74
|
+
if (this._lineMode && !this._failed && (delimiter ? line === this._line : line === this._delimiterLine && token.type !== 'eof')) this._fail(new Error(`Unexpected "MESSAGE" on line ${line}.`));
|
|
75
|
+
if (delimiter) this._delimiterLine = line;
|
|
76
|
+
this._line = line;
|
|
75
77
|
}
|
|
76
78
|
_onQuad(error, quad, prefixes) {
|
|
77
79
|
if (this._failed || this._finished) return;
|
|
@@ -89,17 +91,6 @@ class MessageParserCore {
|
|
|
89
91
|
this._current.push(quad);
|
|
90
92
|
if (this._sink.quad) this._sink.quad(quad, this._index);
|
|
91
93
|
}
|
|
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
94
|
_announce(label) {
|
|
104
95
|
const error = this._checkVersion(label);
|
|
105
96
|
if (error) return this._fail(error);
|
|
@@ -117,9 +108,9 @@ class MessageParserCore {
|
|
|
117
108
|
if (throwing) throw error;
|
|
118
109
|
return error;
|
|
119
110
|
}
|
|
120
|
-
_delimiter(
|
|
111
|
+
_delimiter() {
|
|
121
112
|
if (!this._messages) {
|
|
122
|
-
return this._fail(new Error(`Unexpected "MESSAGE" on line ${
|
|
113
|
+
return this._fail(new Error(`Unexpected "MESSAGE" on line ${this._line}: the document did not announce ` + 'a version with the "-messages" suffix.'));
|
|
123
114
|
}
|
|
124
115
|
this._emitMessage();
|
|
125
116
|
}
|
package/lib/MessageWriter.js
CHANGED
|
@@ -9,12 +9,20 @@ var _n = require("n3");
|
|
|
9
9
|
var _formats = require("./formats.js");
|
|
10
10
|
// **MessageWriter** writes RDF Message Logs (Turtle, TriG, N-Triples and N-Quads).
|
|
11
11
|
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
// a message as soon as it is written,
|
|
16
|
-
//
|
|
12
|
+
// One N3.js writer writes the whole log: its `version` option announces the log,
|
|
13
|
+
// and after each message, `_endStatement` finishes the pending statement (and graph block)
|
|
14
|
+
// before a delimiter is written (`MESSAGE` in line formats, `@message .` in Turtle and TriG).
|
|
15
|
+
// That way, consumers receive a message as soon as it is written, no serialization state
|
|
16
|
+
// (current graph, subject, predicate) leaks from one message into the next, and trailing
|
|
17
|
+
// empty messages survive a round trip (a delimiter before the end of the input does not create a message).
|
|
17
18
|
|
|
19
|
+
class LogWriter extends _n.Writer {
|
|
20
|
+
// ### `addDelimiter` ends the current message
|
|
21
|
+
addDelimiter() {
|
|
22
|
+
this._endStatement();
|
|
23
|
+
this._write(this._lineMode ? 'MESSAGE\n' : '@message .\n');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
18
26
|
class MessageWriter {
|
|
19
27
|
// `outputStream` is optional: without it, `end` returns the output as a string
|
|
20
28
|
constructor(outputStream, options) {
|
|
@@ -30,15 +38,15 @@ class MessageWriter {
|
|
|
30
38
|
this._pending = 0;
|
|
31
39
|
this._waiting = [];
|
|
32
40
|
this._error = null;
|
|
33
|
-
this._muted = false;
|
|
34
41
|
this._started = false;
|
|
35
42
|
this._ended = false;
|
|
36
43
|
this._declared = Object.assign({}, options.prefixes);
|
|
37
44
|
this._newPrefixes = {};
|
|
38
|
-
// The N3.js
|
|
45
|
+
// The N3.js writer writes to this sink, which it never ends
|
|
39
46
|
this._sink = {
|
|
40
47
|
write: (chunk, encoding, done) => this._write(chunk, done)
|
|
41
48
|
};
|
|
49
|
+
this._writer = null;
|
|
42
50
|
}
|
|
43
51
|
|
|
44
52
|
// ### `addMessage` writes the quads of one message, followed by a delimiter.
|
|
@@ -106,8 +114,8 @@ class MessageWriter {
|
|
|
106
114
|
done(error || null);
|
|
107
115
|
}
|
|
108
116
|
_write(chunk, done) {
|
|
109
|
-
if (
|
|
110
|
-
|
|
117
|
+
if (!this._stream) {
|
|
118
|
+
this._output += chunk;
|
|
111
119
|
return done && done();
|
|
112
120
|
}
|
|
113
121
|
this._pending++;
|
|
@@ -126,30 +134,24 @@ class MessageWriter {
|
|
|
126
134
|
if (this._pending === 0) this._waiting.splice(0).forEach(callback => this._settle(callback));
|
|
127
135
|
}
|
|
128
136
|
|
|
129
|
-
// Announces the RDF Messages version, which makes parsers expect delimiters
|
|
137
|
+
// Announces the RDF Messages version, which makes parsers expect delimiters,
|
|
138
|
+
// and declares the base and prefixes, which stay in effect for all messages
|
|
130
139
|
_start() {
|
|
131
140
|
if (!this._started) {
|
|
132
141
|
this._started = true;
|
|
133
|
-
|
|
142
|
+
const options = this._options;
|
|
143
|
+
this._writer = new LogWriter(this._sink, {
|
|
144
|
+
format: this._format.name,
|
|
145
|
+
end: false,
|
|
146
|
+
version: this._version,
|
|
147
|
+
baseIRI: options.baseIRI,
|
|
148
|
+
writeBase: options.writeBase,
|
|
149
|
+
prefixes: this._declared
|
|
150
|
+
});
|
|
134
151
|
}
|
|
135
152
|
}
|
|
136
153
|
_writeMessage(quads) {
|
|
137
|
-
const
|
|
138
|
-
const options = {
|
|
139
|
-
format: this._format.name,
|
|
140
|
-
end: false
|
|
141
|
-
};
|
|
142
|
-
if (this._options.baseIRI) {
|
|
143
|
-
options.baseIRI = this._options.baseIRI;
|
|
144
|
-
// The base is declared by us, once, as it stays in effect (and older N3.js writers lack `writeBase`)
|
|
145
|
-
if (first && this._options.writeBase && !this._format.lineMode) this._write(`@base <${this._options.baseIRI}>.\n`);
|
|
146
|
-
}
|
|
147
|
-
// Prefixes and base are declared once, as they stay in effect
|
|
148
|
-
options.prefixes = this._declared;
|
|
149
|
-
this._muted = !first;
|
|
150
|
-
const writer = new _n.Writer(this._sink, options);
|
|
151
|
-
this._muted = false;
|
|
152
|
-
this._messageWritten = true;
|
|
154
|
+
const writer = this._writer;
|
|
153
155
|
for (const prefix of Object.keys(this._newPrefixes)) {
|
|
154
156
|
writer.addPrefix(prefix, this._newPrefixes[prefix]);
|
|
155
157
|
this._declared[prefix] = this._newPrefixes[prefix];
|
|
@@ -157,8 +159,7 @@ class MessageWriter {
|
|
|
157
159
|
this._newPrefixes = {};
|
|
158
160
|
const record = error => error && (this._error = this._error || error);
|
|
159
161
|
for (const quad of quads) writer.addQuad(quad.subject, quad.predicate, quad.object, quad.graph, record);
|
|
160
|
-
writer.
|
|
161
|
-
this._write(this._format.lineMode ? 'MESSAGE\n' : '@message .\n');
|
|
162
|
+
writer.addDelimiter();
|
|
162
163
|
}
|
|
163
164
|
}
|
|
164
165
|
|
package/lib/index.js
CHANGED
|
@@ -44,4 +44,5 @@ var _MessageStreamParser = _interopRequireDefault(require("./MessageStreamParser
|
|
|
44
44
|
var _MessageWriter = _interopRequireWildcard(require("./MessageWriter.js"));
|
|
45
45
|
var _MessageStreamWriter = _interopRequireDefault(require("./MessageStreamWriter.js"));
|
|
46
46
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
47
|
-
|
|
47
|
+
var cache;
|
|
48
|
+
function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var r, c, a = Object.defineProperty, n = { __proto__: null, default: e }; if (Object(e) !== e) return n; if (cache || "function" != typeof WeakMap || (cache = new WeakMap()), cache) { if (cache.has(e)) return cache.get(e); cache.set(e, n); } for (c in e) "default" !== c && {}.hasOwnProperty.call(e, c) && ((r = a && Object.getOwnPropertyDescriptor(e, c)) && (r.get || r.set) ? a(n, c, r) : n[c] = e[c]); return n; }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "n3.js-messages",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "RDF Messages (message-delimited Turtle, TriG, N-Triples and N-Quads) for N3.js: message-aware parser, streaming parser and writer.",
|
|
5
5
|
"author": "Pieter Colpaert",
|
|
6
6
|
"license": "MIT",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"index.d.ts"
|
|
38
38
|
],
|
|
39
39
|
"peerDependencies": {
|
|
40
|
-
"n3": ">=2.
|
|
40
|
+
"n3": ">=2.13.7 <3"
|
|
41
41
|
},
|
|
42
42
|
"repository": {
|
|
43
43
|
"type": "git",
|
|
@@ -62,9 +62,9 @@
|
|
|
62
62
|
"spec:update": "node scripts/update-spec.js"
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
65
|
-
"@babel/cli": "^
|
|
66
|
-
"@babel/core": "^
|
|
67
|
-
"@babel/preset-env": "^
|
|
65
|
+
"@babel/cli": "^8.0.6",
|
|
66
|
+
"@babel/core": "^8.0.7",
|
|
67
|
+
"@babel/preset-env": "^8.0.7",
|
|
68
68
|
"@types/n3": "^1.26.4",
|
|
69
69
|
"@types/node": "^26.6.4",
|
|
70
70
|
"babel-jest": "^30.5.2",
|
|
@@ -73,11 +73,11 @@
|
|
|
73
73
|
"eslint-plugin-jest": "^29.16.6",
|
|
74
74
|
"globals": "^17.13.0",
|
|
75
75
|
"jest": "^30.5.2",
|
|
76
|
-
"n3": "^2.
|
|
76
|
+
"n3": "^2.13.8",
|
|
77
77
|
"rdf-isomorphic": "^2.0.1",
|
|
78
78
|
"rdf-parser-ts": "^0.4.0",
|
|
79
79
|
"rdf-writer-ts": "^0.2.1",
|
|
80
|
-
"typescript": "^
|
|
80
|
+
"typescript": "^7.0.2"
|
|
81
81
|
},
|
|
82
82
|
"jest": {
|
|
83
83
|
"testMatch": [
|
package/src/MessageParserCore.js
CHANGED
|
@@ -1,16 +1,10 @@
|
|
|
1
1
|
// **MessageParserCore** is the push-based engine behind all message parsers.
|
|
2
|
-
// It feeds
|
|
3
|
-
// quads into messages.
|
|
2
|
+
// It feeds text to the N3.js parser, which recognizes the message delimiters
|
|
3
|
+
// through its `directives` option, and groups the resulting quads into messages.
|
|
4
4
|
import { EventEmitter } from 'events';
|
|
5
5
|
import { Parser, DataFactory } from 'n3';
|
|
6
|
-
import { TurtleScanner, LineScanner } from './MessageScanner.js';
|
|
7
6
|
import { resolveFormat, isMessagesVersion, baseVersion, isSupportedVersion } from './formats.js';
|
|
8
7
|
|
|
9
|
-
// Unrecognizable by construction: a document cannot contain it by accident
|
|
10
|
-
function createSentinel() {
|
|
11
|
-
return `rdf-messages-${Math.random().toString(36).slice(2)}${Math.random().toString(36).slice(2)}`;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
8
|
export default class MessageParserCore {
|
|
15
9
|
// `sink` receives `message(quads, index)`, `end(prefixes)`, `error(error)`
|
|
16
10
|
// and optionally `quad(quad, messageIndex)`, `prefix(prefix, term)`, `version(label)`
|
|
@@ -36,6 +30,9 @@ export default class MessageParserCore {
|
|
|
36
30
|
factory: this._factory,
|
|
37
31
|
// Version labels are validated here, because N3.js does not know `-messages` labels
|
|
38
32
|
parseUnsupportedVersions: true,
|
|
33
|
+
// N3.js reports `MESSAGE` and `@message .` between statements through `onDirective`,
|
|
34
|
+
// in order with the quads, and rejects them anywhere else
|
|
35
|
+
directives: ['message'],
|
|
39
36
|
};
|
|
40
37
|
if (options.baseIRI)
|
|
41
38
|
n3Options.baseIRI = options.baseIRI;
|
|
@@ -44,37 +41,41 @@ export default class MessageParserCore {
|
|
|
44
41
|
n3Options.version = baseVersion(options.version);
|
|
45
42
|
}
|
|
46
43
|
|
|
47
|
-
const sentinel = createSentinel();
|
|
48
|
-
this._sentinel = sentinel;
|
|
49
|
-
this._scanner = format.lineMode ? new LineScanner(sentinel) : new TurtleScanner(sentinel);
|
|
50
44
|
this._input = new EventEmitter();
|
|
51
|
-
|
|
45
|
+
this._lineMode = format.lineMode;
|
|
46
|
+
// The line of the current token, and of the last delimiter
|
|
47
|
+
this._line = 0;
|
|
48
|
+
this._delimiterLine = 0;
|
|
49
|
+
new Parser(n3Options).parse(this._input, {
|
|
52
50
|
onQuad: (error, quad, prefixes) => this._onQuad(error, quad, prefixes),
|
|
53
51
|
onPrefix: (prefix, term) => this._failed || sink.prefix && sink.prefix(prefix, term),
|
|
54
|
-
onVersion: label => this.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
new Parser(n3Options).parse(this._input, callbacks);
|
|
52
|
+
onVersion: label => this._failed || this._announce(label),
|
|
53
|
+
onDirective: () => this._failed || this._delimiter(),
|
|
54
|
+
onToken: token => this._onToken(token),
|
|
55
|
+
});
|
|
59
56
|
}
|
|
60
57
|
|
|
61
58
|
// ### `write` accepts the next chunk of text
|
|
62
59
|
write(text) {
|
|
63
60
|
if (!this._failed && !this._finished && text.length > 0)
|
|
64
|
-
this.
|
|
61
|
+
this._input.emit('data', text);
|
|
65
62
|
}
|
|
66
63
|
|
|
67
64
|
// ### `end` signals the end of the input
|
|
68
65
|
end() {
|
|
69
|
-
if (!this._failed && !this._finished)
|
|
70
|
-
this._feed(this._scanner.end());
|
|
66
|
+
if (!this._failed && !this._finished)
|
|
71
67
|
this._input.emit('end');
|
|
72
|
-
}
|
|
73
68
|
}
|
|
74
69
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
70
|
+
// Keeps track of lines; in N-Triples and N-Quads, a delimiter must be on its own line
|
|
71
|
+
_onToken(token) {
|
|
72
|
+
const line = token.line, delimiter = token.type === 'MESSAGE';
|
|
73
|
+
if (this._lineMode && !this._failed &&
|
|
74
|
+
(delimiter ? line === this._line : line === this._delimiterLine && token.type !== 'eof'))
|
|
75
|
+
this._fail(new Error(`Unexpected "MESSAGE" on line ${line}.`));
|
|
76
|
+
if (delimiter)
|
|
77
|
+
this._delimiterLine = line;
|
|
78
|
+
this._line = line;
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
_onQuad(error, quad, prefixes) {
|
|
@@ -104,26 +105,6 @@ export default class MessageParserCore {
|
|
|
104
105
|
this._sink.quad(quad, this._index);
|
|
105
106
|
}
|
|
106
107
|
|
|
107
|
-
_onVersion(label) {
|
|
108
|
-
if (this._failed)
|
|
109
|
-
return;
|
|
110
|
-
if (label === this._sentinel)
|
|
111
|
-
this._delimiter(this._scanner.delimiterLines.shift());
|
|
112
|
-
else
|
|
113
|
-
this._announce(label);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Line mode reports delimiters and versions as comments
|
|
117
|
-
_onComment(comment) {
|
|
118
|
-
if (this._failed || !comment.startsWith(this._sentinel))
|
|
119
|
-
return;
|
|
120
|
-
const rest = comment.substring(this._sentinel.length);
|
|
121
|
-
if (rest[0] === 'M')
|
|
122
|
-
this._delimiter(Number(rest.substring(1)));
|
|
123
|
-
else
|
|
124
|
-
this._announce(rest.substring(1));
|
|
125
|
-
}
|
|
126
|
-
|
|
127
108
|
_announce(label) {
|
|
128
109
|
const error = this._checkVersion(label);
|
|
129
110
|
if (error)
|
|
@@ -147,9 +128,9 @@ export default class MessageParserCore {
|
|
|
147
128
|
return error;
|
|
148
129
|
}
|
|
149
130
|
|
|
150
|
-
_delimiter(
|
|
131
|
+
_delimiter() {
|
|
151
132
|
if (!this._messages) {
|
|
152
|
-
return this._fail(new Error(`Unexpected "MESSAGE" on line ${
|
|
133
|
+
return this._fail(new Error(`Unexpected "MESSAGE" on line ${this._line}: the document did not announce ` +
|
|
153
134
|
'a version with the "-messages" suffix.'));
|
|
154
135
|
}
|
|
155
136
|
this._emitMessage();
|
package/src/MessageWriter.js
CHANGED
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
// **MessageWriter** writes RDF Message Logs (Turtle, TriG, N-Triples and N-Quads).
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// a message as soon as it is written,
|
|
7
|
-
//
|
|
3
|
+
// One N3.js writer writes the whole log: its `version` option announces the log,
|
|
4
|
+
// and after each message, `_endStatement` finishes the pending statement (and graph block)
|
|
5
|
+
// before a delimiter is written (`MESSAGE` in line formats, `@message .` in Turtle and TriG).
|
|
6
|
+
// That way, consumers receive a message as soon as it is written, no serialization state
|
|
7
|
+
// (current graph, subject, predicate) leaks from one message into the next, and trailing
|
|
8
|
+
// empty messages survive a round trip (a delimiter before the end of the input does not create a message).
|
|
8
9
|
import { Writer } from 'n3';
|
|
9
10
|
import { resolveFormat, isMessagesVersion, baseVersion, isSupportedVersion } from './formats.js';
|
|
10
11
|
|
|
12
|
+
class LogWriter extends Writer {
|
|
13
|
+
// ### `addDelimiter` ends the current message
|
|
14
|
+
addDelimiter() {
|
|
15
|
+
this._endStatement();
|
|
16
|
+
this._write(this._lineMode ? 'MESSAGE\n' : '@message .\n');
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
11
20
|
export default class MessageWriter {
|
|
12
21
|
// `outputStream` is optional: without it, `end` returns the output as a string
|
|
13
22
|
constructor(outputStream, options) {
|
|
@@ -26,13 +35,13 @@ export default class MessageWriter {
|
|
|
26
35
|
this._pending = 0;
|
|
27
36
|
this._waiting = [];
|
|
28
37
|
this._error = null;
|
|
29
|
-
this._muted = false;
|
|
30
38
|
this._started = false;
|
|
31
39
|
this._ended = false;
|
|
32
40
|
this._declared = Object.assign({}, options.prefixes);
|
|
33
41
|
this._newPrefixes = {};
|
|
34
|
-
// The N3.js
|
|
42
|
+
// The N3.js writer writes to this sink, which it never ends
|
|
35
43
|
this._sink = { write: (chunk, encoding, done) => this._write(chunk, done) };
|
|
44
|
+
this._writer = null;
|
|
36
45
|
}
|
|
37
46
|
|
|
38
47
|
// ### `addMessage` writes the quads of one message, followed by a delimiter.
|
|
@@ -117,9 +126,8 @@ export default class MessageWriter {
|
|
|
117
126
|
}
|
|
118
127
|
|
|
119
128
|
_write(chunk, done) {
|
|
120
|
-
if (
|
|
121
|
-
|
|
122
|
-
this._output += chunk;
|
|
129
|
+
if (!this._stream) {
|
|
130
|
+
this._output += chunk;
|
|
123
131
|
return done && done();
|
|
124
132
|
}
|
|
125
133
|
this._pending++;
|
|
@@ -140,29 +148,25 @@ export default class MessageWriter {
|
|
|
140
148
|
this._waiting.splice(0).forEach(callback => this._settle(callback));
|
|
141
149
|
}
|
|
142
150
|
|
|
143
|
-
// Announces the RDF Messages version, which makes parsers expect delimiters
|
|
151
|
+
// Announces the RDF Messages version, which makes parsers expect delimiters,
|
|
152
|
+
// and declares the base and prefixes, which stay in effect for all messages
|
|
144
153
|
_start() {
|
|
145
154
|
if (!this._started) {
|
|
146
155
|
this._started = true;
|
|
147
|
-
|
|
156
|
+
const options = this._options;
|
|
157
|
+
this._writer = new LogWriter(this._sink, {
|
|
158
|
+
format: this._format.name,
|
|
159
|
+
end: false,
|
|
160
|
+
version: this._version,
|
|
161
|
+
baseIRI: options.baseIRI,
|
|
162
|
+
writeBase: options.writeBase,
|
|
163
|
+
prefixes: this._declared,
|
|
164
|
+
});
|
|
148
165
|
}
|
|
149
166
|
}
|
|
150
167
|
|
|
151
168
|
_writeMessage(quads) {
|
|
152
|
-
const
|
|
153
|
-
const options = { format: this._format.name, end: false };
|
|
154
|
-
if (this._options.baseIRI) {
|
|
155
|
-
options.baseIRI = this._options.baseIRI;
|
|
156
|
-
// The base is declared by us, once, as it stays in effect (and older N3.js writers lack `writeBase`)
|
|
157
|
-
if (first && this._options.writeBase && !this._format.lineMode)
|
|
158
|
-
this._write(`@base <${this._options.baseIRI}>.\n`);
|
|
159
|
-
}
|
|
160
|
-
// Prefixes and base are declared once, as they stay in effect
|
|
161
|
-
options.prefixes = this._declared;
|
|
162
|
-
this._muted = !first;
|
|
163
|
-
const writer = new Writer(this._sink, options);
|
|
164
|
-
this._muted = false;
|
|
165
|
-
this._messageWritten = true;
|
|
169
|
+
const writer = this._writer;
|
|
166
170
|
for (const prefix of Object.keys(this._newPrefixes)) {
|
|
167
171
|
writer.addPrefix(prefix, this._newPrefixes[prefix]);
|
|
168
172
|
this._declared[prefix] = this._newPrefixes[prefix];
|
|
@@ -171,8 +175,7 @@ export default class MessageWriter {
|
|
|
171
175
|
const record = error => error && (this._error = this._error || error);
|
|
172
176
|
for (const quad of quads)
|
|
173
177
|
writer.addQuad(quad.subject, quad.predicate, quad.object, quad.graph, record);
|
|
174
|
-
writer.
|
|
175
|
-
this._write(this._format.lineMode ? 'MESSAGE\n' : '@message .\n');
|
|
178
|
+
writer.addDelimiter();
|
|
176
179
|
}
|
|
177
180
|
}
|
|
178
181
|
|
package/src/MessageScanner.js
DELETED
|
@@ -1,390 +0,0 @@
|
|
|
1
|
-
// **MessageScanner** finds RDF Message delimiters in a text stream.
|
|
2
|
-
//
|
|
3
|
-
// N3.js has no extension point for new directives, and its lexer rejects
|
|
4
|
-
// `MESSAGE` and `@message`. The scanners in this file therefore sit in front of
|
|
5
|
-
// the N3.js lexer: they track just enough lexical context (IRIs, strings,
|
|
6
|
-
// long strings, comments, words, braces) to recognize *real* delimiters, and
|
|
7
|
-
// rewrite them into text that N3.js can lex and that is easy to recognize
|
|
8
|
-
// again in N3.js' public parser callbacks:
|
|
9
|
-
//
|
|
10
|
-
// - Turtle and TriG: a delimiter becomes `@version "<sentinel>" .`.
|
|
11
|
-
// N3.js only accepts version directives at statement boundaries, so the
|
|
12
|
-
// N3.js parser itself rejects delimiters in other grammar positions, and
|
|
13
|
-
// reports the delimiter through its public `onVersion` callback, exactly in
|
|
14
|
-
// order with the emitted quads. The dot matters: the N3.js lexer reads an
|
|
15
|
-
// `@` after a literal as a language tag, so a literal that ends a SPARQL-style
|
|
16
|
-
// `VERSION "label"` directive would swallow a following `@prefix`.
|
|
17
|
-
// For the same reason, SPARQL-style `VERSION "label"` directives in the input
|
|
18
|
-
// are rewritten to `@version "label" .`.
|
|
19
|
-
// - N-Triples and N-Quads: N3.js has no version directive in line mode.
|
|
20
|
-
// The grammar is line-based, so a delimiter or version line becomes the
|
|
21
|
-
// comment `#<sentinel>M<line>` / `#<sentinel>V<label>`, which is reported through
|
|
22
|
-
// the public `onComment` callback, again in order with the quads.
|
|
23
|
-
//
|
|
24
|
-
// Delimiters that are not recognized (inside strings, IRIs, comments, prefixed
|
|
25
|
-
// names or graph blocks) are passed through unchanged. The scanners never
|
|
26
|
-
// throw: whatever they do not rewrite is rejected by N3.js as a syntax error.
|
|
27
|
-
|
|
28
|
-
const TOP = 0, IRI = 1, LT = 2, COMMENT = 3, STRING = 4, QUOTE1 = 5, QUOTE2 = 6, LONG = 7, CANDIDATE = 8;
|
|
29
|
-
|
|
30
|
-
const MESSAGE = 'MESSAGE', AT_MESSAGE = '@message', VERSION = 'VERSION';
|
|
31
|
-
|
|
32
|
-
function isWhitespace(code) {
|
|
33
|
-
return code === 32 || code === 9 || code === 10 || code === 13;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Compares a character to a position of a keyword; `VERSION` is case-insensitive
|
|
37
|
-
function matches(keyword, index, code) {
|
|
38
|
-
return keyword === VERSION ? String.fromCharCode(code).toUpperCase() === keyword[index] :
|
|
39
|
-
code === keyword.charCodeAt(index);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Characters that may follow `MESSAGE`: whitespace, a comment, or an IRI
|
|
43
|
-
function endsMessageKeyword(code) {
|
|
44
|
-
return isWhitespace(code) || code === 35 || code === 60;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Characters that may follow `@message`: whitespace, a comment, or the dot
|
|
48
|
-
function endsAtMessageKeyword(code) {
|
|
49
|
-
return isWhitespace(code) || code === 35 || code === 46;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// ## Turtle and TriG
|
|
53
|
-
export class TurtleScanner {
|
|
54
|
-
constructor(sentinel) {
|
|
55
|
-
this._sentinel = sentinel;
|
|
56
|
-
this._state = TOP;
|
|
57
|
-
// Whether the next character can start a new token
|
|
58
|
-
this._tokenStart = true;
|
|
59
|
-
// Whether we are inside a bare word (prefixed name, number, keyword)
|
|
60
|
-
this._inWord = false;
|
|
61
|
-
this._wordEscape = false;
|
|
62
|
-
this._quote = 0;
|
|
63
|
-
this._escaped = false;
|
|
64
|
-
this._quoteRun = 0;
|
|
65
|
-
this._depth = 0;
|
|
66
|
-
this._candidate = '';
|
|
67
|
-
this._target = '';
|
|
68
|
-
this._candidateLine = 1;
|
|
69
|
-
this._beforeCandidate = 32;
|
|
70
|
-
this._versionPending = false;
|
|
71
|
-
this._line = 1;
|
|
72
|
-
this._afterCR = false;
|
|
73
|
-
this._started = false;
|
|
74
|
-
this._lastOut = 32;
|
|
75
|
-
// The line of every rewritten delimiter that was not yet consumed
|
|
76
|
-
this.delimiterLines = [];
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// ### `push` scans a chunk of text and returns the text to hand to N3.js
|
|
80
|
-
push(text) {
|
|
81
|
-
let out = '';
|
|
82
|
-
let segmentStart = 0;
|
|
83
|
-
for (let i = 0; i < text.length; i++) {
|
|
84
|
-
const code = text.charCodeAt(i);
|
|
85
|
-
// Keep track of the line number (CRLF counts once, also across chunks)
|
|
86
|
-
if (code === 10) {
|
|
87
|
-
if (!this._afterCR)
|
|
88
|
-
this._line++;
|
|
89
|
-
this._afterCR = false;
|
|
90
|
-
}
|
|
91
|
-
else if (code === 13) {
|
|
92
|
-
this._line++;
|
|
93
|
-
this._afterCR = true;
|
|
94
|
-
}
|
|
95
|
-
else
|
|
96
|
-
this._afterCR = false;
|
|
97
|
-
|
|
98
|
-
// A state change may require the same character to be handled again
|
|
99
|
-
let again;
|
|
100
|
-
do {
|
|
101
|
-
again = false;
|
|
102
|
-
switch (this._state) {
|
|
103
|
-
case TOP:
|
|
104
|
-
if (this._scanTop(code)) {
|
|
105
|
-
// A possible delimiter starts here: hold it back until it is resolved
|
|
106
|
-
out += text.substring(segmentStart, i);
|
|
107
|
-
segmentStart = i + 1;
|
|
108
|
-
this._candidate = text[i];
|
|
109
|
-
this._candidateLine = this._line;
|
|
110
|
-
this._beforeCandidate = i > 0 ? text.charCodeAt(i - 1) : this._lastOut;
|
|
111
|
-
this._target = code === 77 ? MESSAGE : code === 64 ? AT_MESSAGE : VERSION;
|
|
112
|
-
this._state = CANDIDATE;
|
|
113
|
-
}
|
|
114
|
-
break;
|
|
115
|
-
case CANDIDATE: {
|
|
116
|
-
const index = this._candidate.length, target = this._target;
|
|
117
|
-
if (index < target.length && matches(target, index, code)) {
|
|
118
|
-
this._candidate += text[i];
|
|
119
|
-
segmentStart = i + 1;
|
|
120
|
-
}
|
|
121
|
-
else {
|
|
122
|
-
const complete = index === target.length &&
|
|
123
|
-
(target === AT_MESSAGE ? endsAtMessageKeyword(code) : endsMessageKeyword(code));
|
|
124
|
-
out += this._resolveCandidate(complete);
|
|
125
|
-
segmentStart = i;
|
|
126
|
-
again = true;
|
|
127
|
-
}
|
|
128
|
-
break;
|
|
129
|
-
}
|
|
130
|
-
case IRI:
|
|
131
|
-
if (code === 62)
|
|
132
|
-
this._endToken(true);
|
|
133
|
-
break;
|
|
134
|
-
case LT:
|
|
135
|
-
// `<<` starts a reified triple, everything else an IRI
|
|
136
|
-
if (code === 60)
|
|
137
|
-
this._endToken(true);
|
|
138
|
-
else {
|
|
139
|
-
this._state = IRI;
|
|
140
|
-
again = true;
|
|
141
|
-
}
|
|
142
|
-
break;
|
|
143
|
-
case COMMENT:
|
|
144
|
-
if (code === 10 || code === 13)
|
|
145
|
-
this._endToken(true);
|
|
146
|
-
break;
|
|
147
|
-
case STRING:
|
|
148
|
-
if (this._escaped)
|
|
149
|
-
this._escaped = false;
|
|
150
|
-
else if (code === 92)
|
|
151
|
-
this._escaped = true;
|
|
152
|
-
else if (code === this._quote) {
|
|
153
|
-
this._endToken(false);
|
|
154
|
-
if (this._versionPending) {
|
|
155
|
-
// Terminate a SPARQL-style version directive, which we turned into an `@version` directive
|
|
156
|
-
this._versionPending = false;
|
|
157
|
-
out += `${text.substring(segmentStart, i + 1)} .`;
|
|
158
|
-
segmentStart = i + 1;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
break;
|
|
162
|
-
case QUOTE1:
|
|
163
|
-
// A second quote either starts a long string or ends an empty string
|
|
164
|
-
if (code === this._quote)
|
|
165
|
-
this._state = QUOTE2;
|
|
166
|
-
else {
|
|
167
|
-
this._state = STRING;
|
|
168
|
-
again = true;
|
|
169
|
-
}
|
|
170
|
-
break;
|
|
171
|
-
case QUOTE2:
|
|
172
|
-
if (code === this._quote) {
|
|
173
|
-
this._state = LONG;
|
|
174
|
-
this._escaped = false;
|
|
175
|
-
this._quoteRun = 0;
|
|
176
|
-
}
|
|
177
|
-
else {
|
|
178
|
-
this._endToken(false);
|
|
179
|
-
again = true;
|
|
180
|
-
}
|
|
181
|
-
break;
|
|
182
|
-
default: // LONG
|
|
183
|
-
if (this._escaped) {
|
|
184
|
-
this._escaped = false;
|
|
185
|
-
this._quoteRun = 0;
|
|
186
|
-
}
|
|
187
|
-
else if (code === 92) {
|
|
188
|
-
this._escaped = true;
|
|
189
|
-
this._quoteRun = 0;
|
|
190
|
-
}
|
|
191
|
-
else if (code === this._quote) {
|
|
192
|
-
if (++this._quoteRun === 3)
|
|
193
|
-
this._endToken(false);
|
|
194
|
-
}
|
|
195
|
-
else
|
|
196
|
-
this._quoteRun = 0;
|
|
197
|
-
}
|
|
198
|
-
} while (again);
|
|
199
|
-
}
|
|
200
|
-
out += text.substring(segmentStart);
|
|
201
|
-
return this._remember(out);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// ### `end` resolves any held-back text at the end of the input
|
|
205
|
-
end() {
|
|
206
|
-
let out = '';
|
|
207
|
-
if (this._state === CANDIDATE)
|
|
208
|
-
out = this._resolveCandidate(this._candidate.length === this._target.length);
|
|
209
|
-
return this._remember(out);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// ### `_scanTop` handles a character between tokens or inside a word.
|
|
213
|
-
// It returns true if the character may start a message delimiter.
|
|
214
|
-
_scanTop(code) {
|
|
215
|
-
// An escape inside a word makes the next character part of the word
|
|
216
|
-
if (this._wordEscape) {
|
|
217
|
-
this._wordEscape = false;
|
|
218
|
-
return false;
|
|
219
|
-
}
|
|
220
|
-
switch (code) {
|
|
221
|
-
case 32: case 9: case 10: case 13:
|
|
222
|
-
this._structural();
|
|
223
|
-
break;
|
|
224
|
-
case 0xFEFF:
|
|
225
|
-
// A byte order mark at the very start is not part of any token
|
|
226
|
-
if (this._started)
|
|
227
|
-
this._word();
|
|
228
|
-
break;
|
|
229
|
-
case 35: // #
|
|
230
|
-
this._state = COMMENT;
|
|
231
|
-
this._inWord = false;
|
|
232
|
-
break;
|
|
233
|
-
case 60: // <
|
|
234
|
-
this._state = LT;
|
|
235
|
-
this._inWord = false;
|
|
236
|
-
break;
|
|
237
|
-
case 34: case 39: // " '
|
|
238
|
-
this._state = QUOTE1;
|
|
239
|
-
this._quote = code;
|
|
240
|
-
this._inWord = false;
|
|
241
|
-
break;
|
|
242
|
-
case 123: // {
|
|
243
|
-
this._depth++;
|
|
244
|
-
this._structural();
|
|
245
|
-
break;
|
|
246
|
-
case 125: // }
|
|
247
|
-
this._depth = Math.max(0, this._depth - 1);
|
|
248
|
-
this._structural();
|
|
249
|
-
break;
|
|
250
|
-
case 40: case 41: case 91: case 93: case 44: case 59: // ( ) [ ] , ;
|
|
251
|
-
this._structural();
|
|
252
|
-
break;
|
|
253
|
-
case 46: // . ends a statement, unless it is part of a word
|
|
254
|
-
if (!this._inWord)
|
|
255
|
-
this._structural();
|
|
256
|
-
break;
|
|
257
|
-
case 92: // \
|
|
258
|
-
this._word();
|
|
259
|
-
this._wordEscape = true;
|
|
260
|
-
break;
|
|
261
|
-
case 77: case 64: case 86: case 118: // M @ V v
|
|
262
|
-
if (this._tokenStart) {
|
|
263
|
-
this._started = true;
|
|
264
|
-
return true;
|
|
265
|
-
}
|
|
266
|
-
this._word();
|
|
267
|
-
break;
|
|
268
|
-
default:
|
|
269
|
-
this._word();
|
|
270
|
-
}
|
|
271
|
-
this._started = true;
|
|
272
|
-
return false;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
_structural() {
|
|
276
|
-
this._inWord = false;
|
|
277
|
-
this._tokenStart = true;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
_word() {
|
|
281
|
-
this._inWord = true;
|
|
282
|
-
this._tokenStart = false;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// ### `_endToken` returns to the top level after an IRI, string or comment
|
|
286
|
-
_endToken(tokenStart) {
|
|
287
|
-
this._state = TOP;
|
|
288
|
-
this._inWord = false;
|
|
289
|
-
this._tokenStart = tokenStart;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// ### `_resolveCandidate` decides whether a held-back candidate is a delimiter
|
|
293
|
-
// and returns the text to emit for it
|
|
294
|
-
_resolveCandidate(complete) {
|
|
295
|
-
const candidate = this._candidate;
|
|
296
|
-
this._state = TOP;
|
|
297
|
-
this._candidate = '';
|
|
298
|
-
// A delimiter in a graph block is not rewritten, so that N3.js rejects it
|
|
299
|
-
if (!complete || (this._depth > 0 && this._target !== VERSION)) {
|
|
300
|
-
this._word();
|
|
301
|
-
return candidate;
|
|
302
|
-
}
|
|
303
|
-
this._structural();
|
|
304
|
-
const previous = this._beforeCandidate;
|
|
305
|
-
const space = isWhitespace(previous) || previous === 0xFEFF ? '' : ' ';
|
|
306
|
-
if (this._target === VERSION) {
|
|
307
|
-
this._versionPending = true;
|
|
308
|
-
return `${space}@version`;
|
|
309
|
-
}
|
|
310
|
-
this.delimiterLines.push(this._candidateLine);
|
|
311
|
-
return `${space}@version "${this._sentinel}"${candidate === MESSAGE ? ' .' : ''}`;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
_remember(out) {
|
|
315
|
-
if (out.length > 0)
|
|
316
|
-
this._lastOut = out.charCodeAt(out.length - 1);
|
|
317
|
-
return out;
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
// ## N-Triples and N-Quads
|
|
322
|
-
// Lines of the form `MESSAGE` or `VERSION "label"` are the only directives.
|
|
323
|
-
// They cannot be confused with strings or IRIs, as those never span lines.
|
|
324
|
-
export class LineScanner {
|
|
325
|
-
constructor(sentinel) {
|
|
326
|
-
this._sentinel = sentinel;
|
|
327
|
-
this._buffer = '';
|
|
328
|
-
this._line = 1;
|
|
329
|
-
this._messageLine = new RegExp(`^[ \\t]*${MESSAGE}[ \\t]*(?:#.*)?$`);
|
|
330
|
-
this._versionLine = /^[ \t]*VERSION[ \t]*(?:"([^"\\]*)"|'([^'\\]*)')[ \t]*(?:#.*)?$/i;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// ### `push` scans a chunk of text and returns the complete lines, rewritten
|
|
334
|
-
push(text) {
|
|
335
|
-
const data = this._buffer + text;
|
|
336
|
-
// A final carriage return waits for a possible line feed, so that CRLF counts as one line break
|
|
337
|
-
const limit = data.endsWith('\r') ? data.length - 1 : data.length;
|
|
338
|
-
const lastBreak = Math.max(data.lastIndexOf('\n', limit - 1), data.lastIndexOf('\r', limit - 1));
|
|
339
|
-
if (lastBreak < 0) {
|
|
340
|
-
this._buffer = data;
|
|
341
|
-
return '';
|
|
342
|
-
}
|
|
343
|
-
this._buffer = data.substring(lastBreak + 1);
|
|
344
|
-
return this._rewrite(data.substring(0, lastBreak + 1));
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
// ### `end` returns the last line, which has no line break
|
|
348
|
-
end() {
|
|
349
|
-
const rest = this._buffer;
|
|
350
|
-
this._buffer = '';
|
|
351
|
-
return rest.length > 0 ? this._rewrite(rest) : '';
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
_rewrite(block) {
|
|
355
|
-
let out = '';
|
|
356
|
-
let copyFrom = 0, lineStart = 0;
|
|
357
|
-
const breaks = /\r\n|\n|\r/g;
|
|
358
|
-
let match;
|
|
359
|
-
do {
|
|
360
|
-
match = breaks.exec(block);
|
|
361
|
-
const lineEnd = match ? match.index : block.length;
|
|
362
|
-
const replacement = this._rewriteLine(block, lineStart, lineEnd);
|
|
363
|
-
if (replacement !== null) {
|
|
364
|
-
out += block.substring(copyFrom, lineStart) + replacement;
|
|
365
|
-
copyFrom = lineEnd;
|
|
366
|
-
}
|
|
367
|
-
lineStart = match ? breaks.lastIndex : block.length + 1;
|
|
368
|
-
if (match)
|
|
369
|
-
this._line++;
|
|
370
|
-
} while (lineStart <= block.length);
|
|
371
|
-
return out + block.substring(copyFrom);
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
// Only lines that start with `MESSAGE` or `VERSION` need a closer look
|
|
375
|
-
_rewriteLine(block, start, end) {
|
|
376
|
-
let first = start;
|
|
377
|
-
while (first < end && (block[first] === ' ' || block[first] === '\t'))
|
|
378
|
-
first++;
|
|
379
|
-
const code = block.charCodeAt(first);
|
|
380
|
-
if (first === end || (code !== 77 && code !== 86 && code !== 118))
|
|
381
|
-
return null;
|
|
382
|
-
const line = block.substring(first, end);
|
|
383
|
-
if (this._messageLine.test(line))
|
|
384
|
-
return `#${this._sentinel}M${this._line}`;
|
|
385
|
-
const version = this._versionLine.exec(line);
|
|
386
|
-
if (version)
|
|
387
|
-
return `#${this._sentinel}V${version[1] === undefined ? version[2] : version[1]}`;
|
|
388
|
-
return null;
|
|
389
|
-
}
|
|
390
|
-
}
|