n3 2.2.10 → 2.2.12

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/lib/N3Lexer.js CHANGED
@@ -13,9 +13,10 @@ const {
13
13
  xsd
14
14
  } = _IRIs.default;
15
15
 
16
- // Regular expression and replacement string to escape N3 strings
16
+ // Regular expression and replacement strings to unescape N3 strings
17
17
  const escapeSequence = /\\u([a-fA-F0-9]{4})|\\U([a-fA-F0-9]{8})|\\([^])/g;
18
- const escapeReplacements = {
18
+ // Fixed escape sequences allowed in string literals (ECHAR)
19
+ const stringEscapeReplacements = {
19
20
  '\\': '\\',
20
21
  "'": "'",
21
22
  '"': '"',
@@ -23,7 +24,10 @@ const escapeReplacements = {
23
24
  'r': '\r',
24
25
  't': '\t',
25
26
  'f': '\f',
26
- 'b': '\b',
27
+ 'b': '\b'
28
+ };
29
+ // Fixed escape sequences allowed in local names of prefixed names (PN_LOCAL_ESC)
30
+ const localNameEscapeReplacements = {
27
31
  '_': '_',
28
32
  '~': '~',
29
33
  '.': '.',
@@ -31,6 +35,7 @@ const escapeReplacements = {
31
35
  '!': '!',
32
36
  '$': '$',
33
37
  '&': '&',
38
+ "'": "'",
34
39
  '(': '(',
35
40
  ')': ')',
36
41
  '*': '*',
@@ -181,7 +186,7 @@ class N3Lexer {
181
186
  if (match = this._unescapedIri.exec(input)) type = 'IRI', value = match[1];
182
187
  // Try to find a full IRI with escape sequences
183
188
  else if (match = this._iri.exec(input)) {
184
- value = this._unescape(match[1]);
189
+ value = this._unescape(match[1], stringEscapeReplacements);
185
190
  if (value === null || illegalIriChars.test(value)) return reportSyntaxError(this);
186
191
  type = 'IRI';
187
192
  }
@@ -364,7 +369,7 @@ class N3Lexer {
364
369
  // Try to find a prefixed name. Since it can contain (but not end with) a dot,
365
370
  // we always need a non-dot character before deciding it is a prefixed name.
366
371
  // Therefore, try inserting a space if we're at the end of the input.
367
- else if ((match = this._prefixed.exec(input)) || inputFinished && (match = this._prefixed.exec(`${input} `))) type = 'prefixed', prefix = match[1] || '', value = this._unescape(match[2]);
372
+ else if ((match = this._prefixed.exec(input)) || inputFinished && (match = this._prefixed.exec(`${input} `))) type = 'prefixed', prefix = match[1] || '', value = this._unescape(match[2], localNameEscapeReplacements);
368
373
  }
369
374
 
370
375
  // A type token is special: it can only be emitted after an IRI or prefixed name is read
@@ -420,8 +425,9 @@ class N3Lexer {
420
425
  }
421
426
  }
422
427
 
423
- // ### `_unescape` replaces N3 escape codes by their corresponding characters
424
- _unescape(item) {
428
+ // ### `_unescape` replaces N3 escape codes by their corresponding characters,
429
+ // allowing only the fixed escape sequences from the given replacement table
430
+ _unescape(item, replacements) {
425
431
  let invalid = false;
426
432
  const replaced = item.replace(escapeSequence, (sequence, unicode4, unicode8, escapedChar) => {
427
433
  // 4-digit unicode character
@@ -443,7 +449,7 @@ class N3Lexer {
443
449
  return charCode <= 0xFFFF ? String.fromCharCode(Number.parseInt(unicode8, 16)) : String.fromCharCode(0xD800 + ((charCode -= 0x10000) >> 10), 0xDC00 + (charCode & 0x3FF));
444
450
  }
445
451
  // fixed escape sequence
446
- if (escapedChar in escapeReplacements) return escapeReplacements[escapedChar];
452
+ if (escapedChar in replacements) return replacements[escapedChar];
447
453
  // invalid escape sequence
448
454
  invalid = true;
449
455
  return '';
@@ -477,7 +483,7 @@ class N3Lexer {
477
483
  if (openingLength === 1 && lines !== 0 || openingLength === 3 && this._lineMode) break;
478
484
  this._line += lines;
479
485
  return {
480
- value: this._unescape(raw),
486
+ value: this._unescape(raw, stringEscapeReplacements),
481
487
  matchLength
482
488
  };
483
489
  }
package/lib/N3Parser.js CHANGED
@@ -508,7 +508,23 @@ class N3Parser {
508
508
  case '{':
509
509
  // Start a new formula
510
510
  if (!this._n3Mode) return this._error('Unexpected graph', token);
511
- this._saveContext('formula', this._graph, this._subject, this._predicate, this._graph = this._factory.blankNode());
511
+ // The formula is an item of the list,
512
+ // so it must be linked in the list's graph before the graph changes
513
+ list = this._factory.blankNode();
514
+ item = this._factory.blankNode();
515
+ // Is this the first element of the list?
516
+ if (previousList === null) {
517
+ // This list is either the subject or the object of its parent
518
+ if (parent.predicate === null) parent.subject = list;else parent.object = list;
519
+ } else {
520
+ // Continue the previous list with the current list
521
+ this._emit(previousList, this.RDF_REST, list, this._graph);
522
+ }
523
+ // Output the item
524
+ this._emit(list, this.RDF_FIRST, item, this._graph);
525
+ // Stack the current list quad and start the formula
526
+ this._saveContext('formula', this._graph, list, this.RDF_FIRST, this._graph = item);
527
+ this._subject = null;
512
528
  return this._readSubject;
513
529
  case '<<(':
514
530
  this._saveContext('<<(', this._graph, null, null, null);
@@ -9,6 +9,9 @@ var _N3Writer = _interopRequireDefault(require("./N3Writer"));
9
9
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
10
10
  // **N3StreamWriter** serializes a quad stream into a text stream.
11
11
 
12
+ const MIN_CHUNK_SIZE = 16 * 1024;
13
+ const DEFAULT_FLUSH_DELAY_MS = 20;
14
+
12
15
  // ## Constructor
13
16
  class N3StreamWriter extends _readableStream.Transform {
14
17
  constructor(options) {
@@ -17,27 +20,77 @@ class N3StreamWriter extends _readableStream.Transform {
17
20
  writableObjectMode: true
18
21
  });
19
22
 
23
+ // Coalesce serialized fragments into larger stream chunks
24
+ this._buffer = '';
25
+
26
+ // Flush partial chunks after a bounded delay
27
+ this._flushTimer = null;
28
+ this._flushDelay = options && options.flushDelay !== undefined ? options.flushDelay : DEFAULT_FLUSH_DELAY_MS;
29
+
20
30
  // Set up writer with a dummy stream object
21
31
  const writer = this._writer = new _N3Writer.default({
22
- write: (quad, encoding, callback) => {
23
- this.push(quad);
32
+ write: (chunk, encoding, callback) => {
33
+ this._buffer += chunk;
34
+ if (this._buffer.length >= MIN_CHUNK_SIZE) this._pushBuffer();else if (this._flushTimer === null) this._armFlushTimer();
24
35
  callback && callback();
25
36
  },
26
37
  end: callback => {
38
+ this._pushBuffer();
27
39
  this.push(null);
28
40
  callback && callback();
29
41
  }
30
42
  }, options);
31
43
 
32
- // Implement Transform methods on top of writer
44
+ // Flush buffered output before serialization errors
45
+ let pendingDone = null;
46
+ const quadDone = error => {
47
+ const done = pendingDone;
48
+ pendingDone = null;
49
+ if (error) this._pushBuffer();
50
+ done(error);
51
+ };
33
52
  this._transform = (quad, encoding, done) => {
34
- writer.addQuad(quad, done);
53
+ pendingDone = done;
54
+ writer.addQuad(quad, quadDone);
35
55
  };
36
56
  this._flush = done => {
37
57
  writer.end(done);
38
58
  };
39
59
  }
40
60
 
61
+ // ### `_pushBuffer` flushes coalesced output to the stream queue
62
+ _pushBuffer() {
63
+ this._clearFlushTimer();
64
+ if (this._buffer !== '') {
65
+ this.push(this._buffer);
66
+ this._buffer = '';
67
+ }
68
+ }
69
+
70
+ // ### `_armFlushTimer` schedules a partial-chunk flush
71
+ _armFlushTimer() {
72
+ this._flushTimer = setTimeout(() => {
73
+ this._flushTimer = null;
74
+ this._pushBuffer();
75
+ }, this._flushDelay);
76
+ // Browser timers do not implement `unref`
77
+ this._flushTimer.unref && this._flushTimer.unref();
78
+ }
79
+
80
+ // ### `_clearFlushTimer` cancels a scheduled flush
81
+ _clearFlushTimer() {
82
+ if (this._flushTimer !== null) {
83
+ clearTimeout(this._flushTimer);
84
+ this._flushTimer = null;
85
+ }
86
+ }
87
+
88
+ // ### `_destroy` cancels a scheduled flush, so it cannot fire afterwards
89
+ _destroy(error, callback) {
90
+ this._clearFlushTimer();
91
+ super._destroy(error, callback);
92
+ }
93
+
41
94
  // ### Serializes a stream of quads
42
95
  import(stream) {
43
96
  stream.on('data', quad => {
@@ -47,6 +100,7 @@ class N3StreamWriter extends _readableStream.Transform {
47
100
  this.end();
48
101
  });
49
102
  stream.on('error', error => {
103
+ this._pushBuffer();
50
104
  this.emit('error', error);
51
105
  });
52
106
  stream.on('prefix', (prefix, iri) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n3",
3
- "version": "2.2.10",
3
+ "version": "2.2.12",
4
4
  "description": "Lightning fast, asynchronous, streaming Turtle / N3 / RDF library.",
5
5
  "author": "Ruben Verborgh <ruben.verborgh@gmail.com>",
6
6
  "keywords": [
package/src/N3Lexer.js CHANGED
@@ -4,14 +4,18 @@ import namespaces from './IRIs';
4
4
 
5
5
  const { xsd } = namespaces;
6
6
 
7
- // Regular expression and replacement string to escape N3 strings
7
+ // Regular expression and replacement strings to unescape N3 strings
8
8
  const escapeSequence = /\\u([a-fA-F0-9]{4})|\\U([a-fA-F0-9]{8})|\\([^])/g;
9
- const escapeReplacements = {
9
+ // Fixed escape sequences allowed in string literals (ECHAR)
10
+ const stringEscapeReplacements = {
10
11
  '\\': '\\', "'": "'", '"': '"',
11
12
  'n': '\n', 'r': '\r', 't': '\t', 'f': '\f', 'b': '\b',
13
+ };
14
+ // Fixed escape sequences allowed in local names of prefixed names (PN_LOCAL_ESC)
15
+ const localNameEscapeReplacements = {
12
16
  '_': '_', '~': '~', '.': '.', '-': '-', '!': '!', '$': '$', '&': '&',
13
- '(': '(', ')': ')', '*': '*', '+': '+', ',': ',', ';': ';', '=': '=',
14
- '/': '/', '?': '?', '#': '#', '@': '@', '%': '%',
17
+ "'": "'", '(': '(', ')': ')', '*': '*', '+': '+', ',': ',', ';': ';',
18
+ '=': '=', '/': '/', '?': '?', '#': '#', '@': '@', '%': '%',
15
19
  };
16
20
  const illegalIriChars = /[\x00-\x20<>\\"\{\}\|\^\`]/;
17
21
 
@@ -152,7 +156,7 @@ export default class N3Lexer {
152
156
  type = 'IRI', value = match[1];
153
157
  // Try to find a full IRI with escape sequences
154
158
  else if (match = this._iri.exec(input)) {
155
- value = this._unescape(match[1]);
159
+ value = this._unescape(match[1], stringEscapeReplacements);
156
160
  if (value === null || illegalIriChars.test(value))
157
161
  return reportSyntaxError(this);
158
162
  type = 'IRI';
@@ -382,7 +386,7 @@ export default class N3Lexer {
382
386
  // Therefore, try inserting a space if we're at the end of the input.
383
387
  else if ((match = this._prefixed.exec(input)) ||
384
388
  inputFinished && (match = this._prefixed.exec(`${input} `)))
385
- type = 'prefixed', prefix = match[1] || '', value = this._unescape(match[2]);
389
+ type = 'prefixed', prefix = match[1] || '', value = this._unescape(match[2], localNameEscapeReplacements);
386
390
  }
387
391
 
388
392
  // A type token is special: it can only be emitted after an IRI or prefixed name is read
@@ -427,8 +431,9 @@ export default class N3Lexer {
427
431
  function reportSyntaxError(self) { callback(self._syntaxError(/^\S*/.exec(input)[0])); }
428
432
  }
429
433
 
430
- // ### `_unescape` replaces N3 escape codes by their corresponding characters
431
- _unescape(item) {
434
+ // ### `_unescape` replaces N3 escape codes by their corresponding characters,
435
+ // allowing only the fixed escape sequences from the given replacement table
436
+ _unescape(item, replacements) {
432
437
  let invalid = false;
433
438
  const replaced = item.replace(escapeSequence, (sequence, unicode4, unicode8, escapedChar) => {
434
439
  // 4-digit unicode character
@@ -451,8 +456,8 @@ export default class N3Lexer {
451
456
  String.fromCharCode(0xD800 + ((charCode -= 0x10000) >> 10), 0xDC00 + (charCode & 0x3FF));
452
457
  }
453
458
  // fixed escape sequence
454
- if (escapedChar in escapeReplacements)
455
- return escapeReplacements[escapedChar];
459
+ if (escapedChar in replacements)
460
+ return replacements[escapedChar];
456
461
  // invalid escape sequence
457
462
  invalid = true;
458
463
  return '';
@@ -488,7 +493,7 @@ export default class N3Lexer {
488
493
  openingLength === 3 && this._lineMode)
489
494
  break;
490
495
  this._line += lines;
491
- return { value: this._unescape(raw), matchLength };
496
+ return { value: this._unescape(raw, stringEscapeReplacements), matchLength };
492
497
  }
493
498
  closingPos++;
494
499
  }
package/src/N3Parser.js CHANGED
@@ -538,8 +538,28 @@ export default class N3Parser {
538
538
  // Start a new formula
539
539
  if (!this._n3Mode)
540
540
  return this._error('Unexpected graph', token);
541
- this._saveContext('formula', this._graph, this._subject, this._predicate,
542
- this._graph = this._factory.blankNode());
541
+ // The formula is an item of the list,
542
+ // so it must be linked in the list's graph before the graph changes
543
+ list = this._factory.blankNode();
544
+ item = this._factory.blankNode();
545
+ // Is this the first element of the list?
546
+ if (previousList === null) {
547
+ // This list is either the subject or the object of its parent
548
+ if (parent.predicate === null)
549
+ parent.subject = list;
550
+ else
551
+ parent.object = list;
552
+ }
553
+ else {
554
+ // Continue the previous list with the current list
555
+ this._emit(previousList, this.RDF_REST, list, this._graph);
556
+ }
557
+ // Output the item
558
+ this._emit(list, this.RDF_FIRST, item, this._graph);
559
+ // Stack the current list quad and start the formula
560
+ this._saveContext('formula', this._graph, list, this.RDF_FIRST,
561
+ this._graph = item);
562
+ this._subject = null;
543
563
  return this._readSubject;
544
564
  case '<<(':
545
565
  this._saveContext('<<(', this._graph, null, null, null);
@@ -2,27 +2,89 @@
2
2
  import { Transform } from 'readable-stream';
3
3
  import N3Writer from './N3Writer';
4
4
 
5
+ const MIN_CHUNK_SIZE = 16 * 1024;
6
+ const DEFAULT_FLUSH_DELAY_MS = 20;
7
+
5
8
  // ## Constructor
6
9
  export default class N3StreamWriter extends Transform {
7
10
  constructor(options) {
8
11
  super({ encoding: 'utf8', writableObjectMode: true });
9
12
 
13
+ // Coalesce serialized fragments into larger stream chunks
14
+ this._buffer = '';
15
+
16
+ // Flush partial chunks after a bounded delay
17
+ this._flushTimer = null;
18
+ this._flushDelay = options && options.flushDelay !== undefined ?
19
+ options.flushDelay : DEFAULT_FLUSH_DELAY_MS;
20
+
10
21
  // Set up writer with a dummy stream object
11
22
  const writer = this._writer = new N3Writer({
12
- write: (quad, encoding, callback) => { this.push(quad); callback && callback(); },
13
- end: callback => { this.push(null); callback && callback(); },
23
+ write: (chunk, encoding, callback) => {
24
+ this._buffer += chunk;
25
+ if (this._buffer.length >= MIN_CHUNK_SIZE)
26
+ this._pushBuffer();
27
+ else if (this._flushTimer === null)
28
+ this._armFlushTimer();
29
+ callback && callback();
30
+ },
31
+ end: callback => { this._pushBuffer(); this.push(null); callback && callback(); },
14
32
  }, options);
15
33
 
16
- // Implement Transform methods on top of writer
17
- this._transform = (quad, encoding, done) => { writer.addQuad(quad, done); };
34
+ // Flush buffered output before serialization errors
35
+ let pendingDone = null;
36
+ const quadDone = error => {
37
+ const done = pendingDone;
38
+ pendingDone = null;
39
+ if (error)
40
+ this._pushBuffer();
41
+ done(error);
42
+ };
43
+ this._transform = (quad, encoding, done) => {
44
+ pendingDone = done;
45
+ writer.addQuad(quad, quadDone);
46
+ };
18
47
  this._flush = done => { writer.end(done); };
19
48
  }
20
49
 
50
+ // ### `_pushBuffer` flushes coalesced output to the stream queue
51
+ _pushBuffer() {
52
+ this._clearFlushTimer();
53
+ if (this._buffer !== '') {
54
+ this.push(this._buffer);
55
+ this._buffer = '';
56
+ }
57
+ }
58
+
59
+ // ### `_armFlushTimer` schedules a partial-chunk flush
60
+ _armFlushTimer() {
61
+ this._flushTimer = setTimeout(() => {
62
+ this._flushTimer = null;
63
+ this._pushBuffer();
64
+ }, this._flushDelay);
65
+ // Browser timers do not implement `unref`
66
+ this._flushTimer.unref && this._flushTimer.unref();
67
+ }
68
+
69
+ // ### `_clearFlushTimer` cancels a scheduled flush
70
+ _clearFlushTimer() {
71
+ if (this._flushTimer !== null) {
72
+ clearTimeout(this._flushTimer);
73
+ this._flushTimer = null;
74
+ }
75
+ }
76
+
77
+ // ### `_destroy` cancels a scheduled flush, so it cannot fire afterwards
78
+ _destroy(error, callback) {
79
+ this._clearFlushTimer();
80
+ super._destroy(error, callback);
81
+ }
82
+
21
83
  // ### Serializes a stream of quads
22
84
  import(stream) {
23
85
  stream.on('data', quad => { this.write(quad); });
24
86
  stream.on('end', () => { this.end(); });
25
- stream.on('error', error => { this.emit('error', error); });
87
+ stream.on('error', error => { this._pushBuffer(); this.emit('error', error); });
26
88
  stream.on('prefix', (prefix, iri) => { this._writer.addPrefix(prefix, iri); });
27
89
  return this;
28
90
  }