n3.js-messages 0.1.4 → 0.2.1

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.
@@ -5,29 +5,43 @@ import { EventEmitter } from 'events';
5
5
  import { Parser, DataFactory } from 'n3';
6
6
  import { resolveFormat, isMessagesVersion, baseVersion, isSupportedVersion } from './formats.js';
7
7
 
8
+ /** @typedef {import('@rdfjs/types').Term} BaseTerm */
9
+ /** @template {import('@rdfjs/types').BaseQuad} [Q=import('@rdfjs/types').Quad] */
8
10
  export default class MessageParserCore {
9
11
  // `sink` receives `message(quads, index)`, `end(prefixes)`, `error(error)`
10
12
  // and optionally `quad(quad, messageIndex)`, `prefix(prefix, term)`, `version(label)`
13
+ /**
14
+ * @param {import('../common.js').MessageParserOptions<Q> | undefined} options
15
+ * @param {import('./internal.js').ParserSink<Q>} sink
16
+ */
11
17
  constructor(options, sink) {
12
18
  options = options || {};
13
19
  const format = resolveFormat(options.format);
20
+ this._version = options.version ?? format.version;
14
21
  this._sink = sink;
15
- this._factory = options.factory || DataFactory;
22
+
23
+ /** @type {import('../common.js').MessageQuadFactory<Q>} */
24
+ this._factory = options.factory || /** @type {import('../common.js').MessageQuadFactory<Q>} */ (DataFactory);
16
25
  this._blankNodePrefix = typeof options.blankNodePrefix === 'string' ?
17
26
  options.blankNodePrefix.replace(/^_:/, '') : undefined;
18
27
  this._blankNodeCounter = 0;
19
28
  this._supportedVersions = Parser.SUPPORTED_VERSIONS;
20
- this._messages = options.messages === true || isMessagesVersion(options.version);
29
+ this._messages = options.messages === true || isMessagesVersion(this._version);
21
30
  this._index = 0;
31
+
32
+ /** @type {Q[] | null} */
22
33
  this._current = null;
34
+
35
+ /** @type {Map<string, import('@rdfjs/types').BlankNode> | null} */
23
36
  this._scope = null;
24
37
  this._seenQuad = false;
25
38
  this._failed = false;
26
39
  this._finished = false;
27
40
 
41
+ /** @type {import('n3').ParserOptions} */
28
42
  const n3Options = {
29
43
  format: format.name,
30
- factory: this._factory,
44
+ factory: /** @type {import('@rdfjs/types').DataFactory} */ (/** @type {unknown} */ (this._factory)),
31
45
  // Version labels are validated here, because N3.js does not know `-messages` labels
32
46
  parseUnsupportedVersions: true,
33
47
  // N3.js reports `MESSAGE` and `@message .` between statements through `onDirective`,
@@ -36,9 +50,9 @@ export default class MessageParserCore {
36
50
  };
37
51
  if (options.baseIRI)
38
52
  n3Options.baseIRI = options.baseIRI;
39
- if (options.version) {
40
- this._checkVersion(options.version, true);
41
- n3Options.version = baseVersion(options.version);
53
+ if (this._version !== undefined) {
54
+ this._checkVersion(this._version, true);
55
+ n3Options.version = baseVersion(this._version);
42
56
  }
43
57
 
44
58
  this._input = new EventEmitter();
@@ -47,7 +61,7 @@ export default class MessageParserCore {
47
61
  this._line = 0;
48
62
  this._delimiterLine = 0;
49
63
  new Parser(n3Options).parse(this._input, {
50
- onQuad: (error, quad, prefixes) => this._onQuad(error, quad, prefixes),
64
+ onQuad: (error, quad, prefixes) => this._onQuad(error, /** @type {Q | null} */ (quad), prefixes),
51
65
  onPrefix: (prefix, term) => this._failed || sink.prefix && sink.prefix(prefix, term),
52
66
  onVersion: label => this._failed || this._announce(label),
53
67
  onDirective: () => this._failed || this._delimiter(),
@@ -56,6 +70,7 @@ export default class MessageParserCore {
56
70
  }
57
71
 
58
72
  // ### `write` accepts the next chunk of text
73
+ /** @param {string} text */
59
74
  write(text) {
60
75
  if (!this._failed && !this._finished && text.length > 0)
61
76
  this._input.emit('data', text);
@@ -68,6 +83,7 @@ export default class MessageParserCore {
68
83
  }
69
84
 
70
85
  // Keeps track of lines; in N-Triples and N-Quads, a delimiter must be on its own line
86
+ /** @param {{ line: number; type: string }} token */
71
87
  _onToken(token) {
72
88
  const line = token.line, delimiter = token.type === 'MESSAGE';
73
89
  if (this._lineMode && !this._failed &&
@@ -78,6 +94,9 @@ export default class MessageParserCore {
78
94
  this._line = line;
79
95
  }
80
96
 
97
+ /** @param {Error | null} error
98
+ * @param {Q | null} quad
99
+ * @param {Record<string, import('@rdfjs/types').NamedNode>} [prefixes] */
81
100
  _onQuad(error, quad, prefixes) {
82
101
  if (this._failed || this._finished)
83
102
  return;
@@ -93,6 +112,7 @@ export default class MessageParserCore {
93
112
  }
94
113
  }
95
114
 
115
+ /** @param {Q} quad */
96
116
  _addQuad(quad) {
97
117
  this._seenQuad = true;
98
118
  // Blank node labels are scoped to their message
@@ -105,11 +125,14 @@ export default class MessageParserCore {
105
125
  this._sink.quad(quad, this._index);
106
126
  }
107
127
 
128
+ /** @param {string} label */
108
129
  _announce(label) {
109
- const error = this._checkVersion(label);
130
+ // An externally supplied version takes precedence over in-line announcements.
131
+ const version = this._version ?? label;
132
+ const error = this._checkVersion(version);
110
133
  if (error)
111
134
  return this._fail(error);
112
- if (isMessagesVersion(label) && !this._messages) {
135
+ if (isMessagesVersion(version) && !this._messages) {
113
136
  if (this._seenQuad)
114
137
  return this._fail(new Error(`Version "${label}" must be announced before the first statement.`));
115
138
  this._messages = true;
@@ -119,6 +142,8 @@ export default class MessageParserCore {
119
142
  }
120
143
 
121
144
  // Validates a version label, and returns an error for unsupported ones
145
+ /** @param {string} label
146
+ * @param {boolean} [throwing] */
122
147
  _checkVersion(label, throwing) {
123
148
  if (isSupportedVersion(baseVersion(label), this._supportedVersions))
124
149
  return null;
@@ -144,6 +169,7 @@ export default class MessageParserCore {
144
169
  this._sink.message(quads, this._index++);
145
170
  }
146
171
 
172
+ /** @param {Error} error */
147
173
  _fail(error) {
148
174
  this._failed = true;
149
175
  this._current = null;
@@ -152,6 +178,8 @@ export default class MessageParserCore {
152
178
 
153
179
  // ## Blank node scoping
154
180
 
181
+ /** @param {Q} quad
182
+ * @returns {Q} */
155
183
  _scopeQuad(quad) {
156
184
  const subject = this._scopeTerm(quad.subject);
157
185
  const predicate = this._scopeTerm(quad.predicate);
@@ -162,6 +190,9 @@ export default class MessageParserCore {
162
190
  quad : this._factory.quad(subject, predicate, object, graph);
163
191
  }
164
192
 
193
+ /** @template {BaseTerm} T
194
+ * @param {T} term
195
+ * @returns {T} */
165
196
  _scopeTerm(term) {
166
197
  if (term.termType === 'BlankNode') {
167
198
  const scope = this._scope || (this._scope = new Map());
@@ -171,8 +202,9 @@ export default class MessageParserCore {
171
202
  this._factory.blankNode(`${this._blankNodePrefix}${this._blankNodeCounter++}`);
172
203
  scope.set(term.value, scoped);
173
204
  }
174
- return scoped;
205
+ return /** @type {T} */ (/** @type {unknown} */ (scoped));
175
206
  }
176
- return term.termType === 'Quad' ? this._scopeQuad(term) : term;
207
+ return term.termType === 'Quad' ? /** @type {T} */ (/** @type {unknown} */ (
208
+ this._scopeQuad(/** @type {Q} */ (/** @type {unknown} */ (term))))) : term;
177
209
  }
178
210
  }
@@ -6,10 +6,14 @@ import { Transform } from 'stream';
6
6
  import { StringDecoder } from 'string_decoder';
7
7
  import MessageParserCore from './MessageParserCore.js';
8
8
 
9
+ /** @template {import('@rdfjs/types').BaseQuad} [Q=import('@rdfjs/types').Quad] */
9
10
  export default class MessageStreamParser extends Transform {
11
+ /** @param {import('../common.js').MessageParserOptions<Q>} [options] */
10
12
  constructor(options) {
11
13
  super({ decodeStrings: false, readableObjectMode: true });
12
14
  this._decoder = new StringDecoder('utf8');
15
+
16
+ /** @type {Error | null} */
13
17
  this._error = null;
14
18
  this._core = new MessageParserCore(options, {
15
19
  message: quads => this.push(quads),
@@ -20,11 +24,15 @@ export default class MessageStreamParser extends Transform {
20
24
  });
21
25
  }
22
26
 
27
+ /** @param {string | Buffer} chunk
28
+ * @param {BufferEncoding} encoding
29
+ * @param {import('node:stream').TransformCallback} done */
23
30
  _transform(chunk, encoding, done) {
24
31
  this._core.write(typeof chunk === 'string' ? chunk : this._decoder.write(chunk));
25
32
  done(this._error);
26
33
  }
27
34
 
35
+ /** @param {import('node:stream').TransformCallback} done */
28
36
  _flush(done) {
29
37
  this._core.write(this._decoder.end());
30
38
  this._core.end();
@@ -5,19 +5,25 @@ import { Transform } from 'stream';
5
5
  import MessageWriter from './MessageWriter.js';
6
6
 
7
7
  export default class MessageStreamWriter extends Transform {
8
+ /** @param {import('../common.js').MessageWriterOptions} [options] */
8
9
  constructor(options) {
9
10
  super({ writableObjectMode: true, encoding: 'utf8' });
10
11
  // Writing into this stream never blocks, and output is pushed synchronously
12
+ /** @type {Pick<import('./internal.js').Output, 'write'>} */
11
13
  const output = { write: (chunk, encoding, done) => (this.push(chunk), done && done()) };
12
14
  this._writer = new MessageWriter(output, Object.assign({}, options, { end: false }));
13
15
  }
14
16
 
17
+ /** @param {import('../browser.js').MessageWriterInput} message
18
+ * @param {BufferEncoding} encoding
19
+ * @param {import('node:stream').TransformCallback} done */
15
20
  _transform(message, encoding, done) {
16
21
  // A message (array of quads), or a quad of a message: `{ quad, messageIndex }`
17
22
  if (Array.isArray(message)) this._writer.addMessage(message, done);
18
23
  else this._writer.addQuad(message.quad, message.messageIndex, done);
19
24
  }
20
25
 
26
+ /** @param {import('node:stream').TransformCallback} done */
21
27
  _flush(done) {
22
28
  this._writer.end(done);
23
29
  }
@@ -9,6 +9,7 @@
9
9
  import { Writer } from 'n3';
10
10
  import { resolveFormat, isMessagesVersion, baseVersion, isSupportedVersion } from './formats.js';
11
11
 
12
+ /** @extends {Writer<import('@rdfjs/types').BaseQuad>} */
12
13
  class LogWriter extends Writer {
13
14
  // ### `addDelimiter` ends the current message
14
15
  addDelimiter() {
@@ -17,37 +18,59 @@ class LogWriter extends Writer {
17
18
  }
18
19
  }
19
20
 
21
+ /** @typedef {import('./internal.js').Done} Done */
22
+ /** @typedef {import('./internal.js').End} End */
23
+ /** @typedef {import('../common.js').MessageWriterOptions} Options */
24
+ /** @typedef {Pick<import('./internal.js').Output, 'write'> & Partial<Pick<import('./internal.js').Output, 'end'>>} Output */
20
25
  export default class MessageWriter {
21
26
  // `outputStream` is optional: without it, `end` returns the output as a string
27
+ /** @overload
28
+ * @param {Options} [options] */
29
+ /** @overload
30
+ * @param {Output | null} outputStream
31
+ * @param {Options} [options] */
32
+ /** @param {Output | Options | null} [outputStream]
33
+ * @param {Options} [options] */
22
34
  constructor(outputStream, options) {
23
- if (outputStream && typeof outputStream.write !== 'function')
24
- options = outputStream, outputStream = null;
35
+ if (outputStream && typeof /** @type {Output} */ (outputStream).write !== 'function')
36
+ options = /** @type {Options} */ (outputStream), outputStream = null;
25
37
  options = options || {};
26
38
  this._format = resolveFormat(options.format);
27
39
  this._options = options;
28
- this._version = options.version === undefined ? '1.2-messages' : options.version;
40
+ this._version = options.version ?? this._format.version ?? '1.2-messages';
29
41
  if (!isMessagesVersion(this._version) || !isSupportedVersion(baseVersion(this._version)))
30
42
  throw new TypeError(`Unsupported RDF Messages version: "${this._version}"`);
31
43
 
32
44
  this._output = '';
33
- this._stream = outputStream;
45
+ this._stream = /** @type {Output | null | undefined} */ (outputStream);
34
46
  this._endStream = !outputStream || options.end !== false;
35
47
  this._pending = 0;
48
+
49
+ /** @type {Done[]} */
36
50
  this._waiting = [];
51
+
52
+ /** @type {Error | null} */
37
53
  this._error = null;
38
54
  this._started = false;
39
55
  this._ended = false;
40
56
  this._open = false;
41
57
  this._count = 0; // messages begun so far: the index of the next message
42
58
  this._declared = Object.assign({}, options.prefixes);
59
+
60
+ /** @type {Record<string, string>} */
43
61
  this._newPrefixes = {};
44
62
  // The N3.js writer writes to this sink, which it never ends
63
+ /** @type {Pick<Output, 'write'>} */
45
64
  this._sink = { write: (chunk, encoding, done) => this._write(chunk, done) };
65
+
66
+ /** @type {LogWriter | null} */
46
67
  this._writer = null;
47
68
  }
48
69
 
49
70
  // ### `addMessage` writes the quads of one message, followed by a delimiter.
50
71
  // The callback (or returned promise) completes once the output is flushed.
72
+ /** @param {import('@rdfjs/types').BaseQuad[]} quads
73
+ * @param {Done} [done] */
51
74
  addMessage(quads, done) {
52
75
  return this._complete(done, () => {
53
76
  if (this._open) throw new Error('A message is already open');
@@ -58,6 +81,7 @@ export default class MessageWriter {
58
81
  }
59
82
 
60
83
  // ### `beginMessage` opens a message. It is optional: the first `addQuad` opens one implicitly.
84
+ /** @param {Done} [done] */
61
85
  beginMessage(done) {
62
86
  return this._complete(done, () => {
63
87
  if (this._open) throw new Error('A message is already open');
@@ -68,6 +92,9 @@ export default class MessageWriter {
68
92
  // ### `addQuad` writes a quad into the open message right away (a message is opened if there is none).
69
93
  // With a `messageIndex` (0-based), a change of index ends the current message and writes the empty
70
94
  // messages in between, as rdf-writer-ts does; a lower index than the current one is an error.
95
+ /** @param {import('@rdfjs/types').BaseQuad} quad
96
+ * @param {number | Done} [messageIndex]
97
+ * @param {Done} [done] */
71
98
  addQuad(quad, messageIndex, done) {
72
99
  if (typeof messageIndex === 'function') done = messageIndex, messageIndex = undefined;
73
100
  return this._complete(done, () => {
@@ -78,6 +105,7 @@ export default class MessageWriter {
78
105
  }
79
106
 
80
107
  // ### `endMessage` ends the open message with a delimiter. Without an open message, it writes an empty one.
108
+ /** @param {Done} [done] */
81
109
  endMessage(done) {
82
110
  return this._complete(done, () => {
83
111
  if (!this._open) this._begin();
@@ -87,15 +115,25 @@ export default class MessageWriter {
87
115
 
88
116
  // ### `addPrefix` declares a prefix, which is written before the next message
89
117
  // and stays in effect for all messages after it
118
+ /** @param {string} prefix
119
+ * @param {string | import('@rdfjs/types').NamedNode} iri
120
+ * @param {Done} [done] */
90
121
  addPrefix(prefix, iri, done) {
91
122
  return this._complete(done, () => {
92
- if (!this._format.lineMode && this._declared[prefix] !== (iri.value || iri))
93
- this._newPrefixes[prefix] = iri.value || iri;
123
+ if (!this._format.lineMode && this._declared[prefix] !== (typeof iri === 'string' ? iri : iri.value))
124
+ this._newPrefixes[prefix] = typeof iri === 'string' ? iri : iri.value;
94
125
  });
95
126
  }
96
127
 
97
128
  // ### `end` ends the log and, unless disabled, the output stream.
98
129
  // The callback receives the output as a string if no output stream was given.
130
+ /** @overload
131
+ * @returns {Promise<string | void>} */
132
+ /** @overload
133
+ * @param {End} done
134
+ * @returns {void} */
135
+ /** @param {End} [done]
136
+ * @returns {Promise<string | void> | void} */
99
137
  end(done) {
100
138
  // Ending twice is harmless
101
139
  if (this._ended)
@@ -111,6 +149,9 @@ export default class MessageWriter {
111
149
  // ## Private methods
112
150
 
113
151
  // Runs an action and reports completion with a callback or a promise
152
+ /** @param {Done | undefined} done
153
+ * @param {() => void} action
154
+ * @returns {Promise<void> | void} */
114
155
  _complete(done, action) {
115
156
  if (this._ended)
116
157
  this._error = this._error || new Error('Cannot write after end');
@@ -119,7 +160,7 @@ export default class MessageWriter {
119
160
  action();
120
161
  }
121
162
  catch (error) {
122
- this._error = this._error || error;
163
+ this._error = this._error || /** @type {Error} */ (error);
123
164
  }
124
165
  if (typeof done === 'function')
125
166
  return this._whenFlushed(done);
@@ -127,25 +168,29 @@ export default class MessageWriter {
127
168
  }
128
169
 
129
170
  // Ends the output once all queued writes were accepted
171
+ /** @param {End} callback */
130
172
  _finish(callback) {
131
173
  this._ended = true;
132
174
  this._whenFlushed(error => this._endOutput(error, callback));
133
175
  }
134
176
 
177
+ /** @param {Error | null | undefined} error
178
+ * @param {End} callback */
135
179
  _endOutput(error, callback) {
136
180
  if (error || !this._endStream)
137
181
  return callback(error);
138
182
  if (!this._stream)
139
183
  return callback(null, this._output);
140
184
  try {
141
- this._stream.end(endError => callback(endError));
185
+ (/** @type {Required<Output>} */ (this._stream)).end(endError => callback(endError));
142
186
  }
143
187
  catch (endError) {
144
- callback(endError);
188
+ callback(/** @type {Error} */ (endError));
145
189
  }
146
190
  }
147
191
 
148
192
  // Calls back once all queued writes were accepted by the output
193
+ /** @param {Done} done */
149
194
  _whenFlushed(done) {
150
195
  if (this._pending === 0)
151
196
  this._settle(done);
@@ -153,12 +198,15 @@ export default class MessageWriter {
153
198
  this._waiting.push(done);
154
199
  }
155
200
 
201
+ /** @param {Done} done */
156
202
  _settle(done) {
157
203
  const error = this._error;
158
204
  this._error = null;
159
205
  done(error || null);
160
206
  }
161
207
 
208
+ /** @param {string} chunk
209
+ * @param {Done} [done] */
162
210
  _write(chunk, done) {
163
211
  if (!this._stream) {
164
212
  this._output += chunk;
@@ -169,11 +217,13 @@ export default class MessageWriter {
169
217
  this._stream.write(chunk, 'utf8', error => this._written(error, done));
170
218
  }
171
219
  catch (error) {
172
- this._written(error, done);
220
+ this._written(/** @type {Error} */ (error), done);
173
221
  }
174
222
  }
175
223
 
176
224
  // Registers that the output accepted a write, and wakes up those waiting for the output
225
+ /** @param {Error | null | undefined} error
226
+ * @param {Done} [done] */
177
227
  _written(error, done) {
178
228
  this._pending--;
179
229
  this._error = this._error || error || null;
@@ -191,7 +241,7 @@ export default class MessageWriter {
191
241
  this._writer = new LogWriter(this._sink, {
192
242
  format: this._format.name,
193
243
  end: false,
194
- version: this._version,
244
+ version: options.writeVersion === false ? undefined : this._version,
195
245
  baseIRI: options.baseIRI,
196
246
  writeBase: options.writeBase,
197
247
  prefixes: this._declared,
@@ -202,25 +252,30 @@ export default class MessageWriter {
202
252
  // Opens a message: declares the prefixes that were added since the previous one
203
253
  _begin() {
204
254
  this._start();
205
- const writer = this._writer;
255
+ const writer = /** @type {LogWriter} */ (this._writer);
206
256
  for (const prefix of Object.keys(this._newPrefixes)) {
207
- writer.addPrefix(prefix, this._newPrefixes[prefix]);
208
- this._declared[prefix] = this._newPrefixes[prefix];
257
+ const iri = /** @type {string} */ (this._newPrefixes[prefix]);
258
+ writer.addPrefix(prefix, iri);
259
+ this._declared[prefix] = iri;
209
260
  }
210
261
  this._newPrefixes = {};
211
262
  this._open = true;
212
263
  this._count++;
213
264
  }
265
+
266
+ /** @param {import('@rdfjs/types').BaseQuad} quad */
214
267
  _add(quad) {
268
+ /** @type {Done} */
215
269
  const record = error => error && (this._error = this._error || error);
216
- this._writer.addQuad(quad.subject, quad.predicate, quad.object, quad.graph, record);
270
+ (/** @type {LogWriter} */ (this._writer)).addQuad(quad.subject, quad.predicate, quad.object, quad.graph, record);
217
271
  }
218
272
  _close() {
219
273
  this._open = false;
220
- this._writer.addDelimiter();
274
+ (/** @type {LogWriter} */ (this._writer)).addDelimiter();
221
275
  }
222
276
 
223
277
  // Makes message `index` the open message, writing empty messages for the gap
278
+ /** @param {number} index */
224
279
  _moveTo(index) {
225
280
  const current = this._count - 1;
226
281
  if (!Number.isInteger(index) || index < 0) throw new RangeError(`Invalid message index: ${index}`);
@@ -235,18 +290,25 @@ export default class MessageWriter {
235
290
  }
236
291
  }
237
292
 
293
+ /** @param {Iterable<import('@rdfjs/types').BaseQuad[]>} messages
294
+ * @param {Options} [options]
295
+ * @returns {string} */
238
296
  // ### `writeMessages` synchronously serializes an array of messages to a string
239
297
  export function writeMessages(messages, options) {
240
298
  const writer = new MessageWriter(options);
299
+
300
+ /** @type {Error[]} */
241
301
  const errors = [];
242
- let output;
302
+ let output = '';
303
+
304
+ /** @param {Error | null | undefined} error */
243
305
  function collect(error) {
244
306
  if (error)
245
307
  errors.push(error);
246
308
  }
247
309
  for (const message of messages)
248
310
  writer.addMessage(message, collect);
249
- writer.end((error, result) => (collect(error), output = result));
311
+ writer.end((error, result) => (collect(error), output = /** @type {string} */ (result)));
250
312
  if (errors.length > 0)
251
313
  throw errors[0];
252
314
  return output;
package/src/browser.js ADDED
@@ -0,0 +1,61 @@
1
+ import MessageParserCore from './MessageParserCore.js';
2
+ import MessageWriter from './MessageWriter.js';
3
+
4
+ export { default as MessageParser, parseMessages } from './MessageParser.js';
5
+ export { default as MessageWriter, writeMessages } from './MessageWriter.js';
6
+
7
+ /**
8
+ * A Web Streams parser; strings and UTF-8 bytes can be piped directly from fetch.
9
+ * @template {import('@rdfjs/types').BaseQuad} [Q=import('@rdfjs/types').Quad]
10
+ * @extends {TransformStream<import('../browser.js').MessageChunk, Q[]>}
11
+ */
12
+ export class MessageStreamParser extends TransformStream {
13
+ /** @param {import('../browser.js').BrowserParserOptions<Q>} [options] */
14
+ constructor(options = {}) {
15
+ const decoder = new TextDecoder();
16
+
17
+ /** @type {MessageParserCore<Q>} */
18
+ let core;
19
+ super({
20
+ start(controller) {
21
+ core = new MessageParserCore(options, {
22
+ message: quads => controller.enqueue(quads),
23
+ end: () => {},
24
+ error: error => { throw error; },
25
+ prefix: options.onPrefix,
26
+ version: options.onVersion,
27
+ });
28
+ },
29
+ transform(chunk) {
30
+ core.write(typeof chunk === 'string' ? chunk : decoder.decode(chunk, { stream: true }));
31
+ },
32
+ flush() {
33
+ core.write(decoder.decode());
34
+ core.end();
35
+ },
36
+ });
37
+ }
38
+ }
39
+
40
+ /** A Web Streams writer; accepts messages or individual indexed quads.
41
+ * @extends {TransformStream<import('../browser.js').MessageWriterInput, string>} */
42
+ export class MessageStreamWriter extends TransformStream {
43
+ /** @param {import('../common.js').MessageWriterOptions} [options] */
44
+ constructor(options) {
45
+ /** @type {MessageWriter} */
46
+ let writer;
47
+ super({
48
+ start(controller) {
49
+ /** @type {Pick<import('./internal.js').Output, 'write'>} */
50
+ const output = { write: (chunk, encoding, done) => { controller.enqueue(chunk); done(); } };
51
+ writer = new MessageWriter(output, { ...options, end: false });
52
+ },
53
+ transform(message) {
54
+ return Array.isArray(message) ? writer.addMessage(message) : writer.addQuad(message.quad, message.messageIndex);
55
+ },
56
+ flush() {
57
+ return writer.end().then(() => {});
58
+ },
59
+ });
60
+ }
61
+ }
package/src/formats.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // **formats** describes the RDF syntaxes that support RDF Messages.
2
2
 
3
+ /** @type {Record<string, { name: string; lineMode: boolean }>} */
3
4
  const FORMATS = {
4
5
  turtle: { name: 'Turtle', lineMode: false },
5
6
  trig: { name: 'TriG', lineMode: false },
@@ -11,8 +12,9 @@ const FORMATS = {
11
12
  const BASE_VERSIONS = ['1.2', '1.2-basic', '1.1'];
12
13
 
13
14
  // ### `resolveFormat` maps a format name or media type to a supported syntax
15
+ /** @returns {{ name: string; lineMode: boolean; version?: string }} */
14
16
  export function resolveFormat(format = 'TriG') {
15
- const key = String(format).toLowerCase().split(';')[0].trim()
17
+ const key = String(format).toLowerCase().replace(/;[\s\S]*$/, '').trim()
16
18
  .replace(/^(?:text|application)\//, '')
17
19
  .replace(/^x-/, '')
18
20
  .replace(/[-_ ]/g, '');
@@ -20,19 +22,32 @@ export function resolveFormat(format = 'TriG') {
20
22
  if (!resolved)
21
23
  throw new TypeError(`Unsupported RDF Messages format: "${format}". ` +
22
24
  'Supported formats are Turtle, TriG, N-Triples and N-Quads.');
23
- return resolved;
25
+ let version;
26
+ // Consume quoted values whole, so a semicolon inside another parameter cannot
27
+ // accidentally introduce a version parameter. Parameter names are case-insensitive.
28
+ for (const parameter of String(format).matchAll(/;\s*([^;=\s]+)\s*=\s*("(?:[^"\\]|\\.)*"|[^;]*)\s*(?=;|$)/g)) {
29
+ if (/** @type {string} */ (parameter[1]).toLowerCase() === 'version') {
30
+ if (version !== undefined)
31
+ throw new TypeError('Duplicate RDF version parameter.');
32
+ version = /** @type {string} */ (parameter[2]).trim().replace(/^"(.*)"$/, '$1').replace(/\\(.)/g, '$1');
33
+ }
34
+ }
35
+ return version === undefined ? resolved : { ...resolved, version };
24
36
  }
25
37
 
38
+ /** @param {unknown} version */
26
39
  // ### `isMessagesVersion` checks whether a version label announces RDF Messages
27
40
  export function isMessagesVersion(version) {
28
41
  return typeof version === 'string' && version.endsWith('-messages');
29
42
  }
30
43
 
44
+ /** @param {string} version */
31
45
  // ### `baseVersion` strips the `-messages` suffix of a version label
32
46
  export function baseVersion(version) {
33
47
  return isMessagesVersion(version) ? version.slice(0, -'-messages'.length) : version;
34
48
  }
35
49
 
50
+ /** @param {string} version */
36
51
  // ### `isSupportedVersion` checks whether a (base) version label is supported
37
52
  export function isSupportedVersion(version, supported = BASE_VERSIONS) {
38
53
  return supported.includes(version);
@@ -0,0 +1,15 @@
1
+ import type * as RDF from '@rdfjs/types';
2
+ export interface ParserSink<Q extends RDF.BaseQuad> {
3
+ message(quads: Q[], index: number): unknown;
4
+ end(prefixes?: Record<string, RDF.NamedNode>): unknown;
5
+ error(error: Error): unknown;
6
+ quad?(quad: Q, index: number): unknown;
7
+ prefix?(prefix: string, term: RDF.NamedNode): unknown;
8
+ version?(label: string): unknown;
9
+ }
10
+ export type Done = (error?: Error | null) => void;
11
+ export type End = (error?: Error | null, result?: string) => void;
12
+ export interface Output {
13
+ write(chunk: string, encoding: string, done: Done): unknown;
14
+ end(done: Done): unknown;
15
+ }