Skip to content

Commit 0de9783

Browse files
committed
Merge branch 'feature/rdfstar'
2 parents e9c14a1 + 74e0e83 commit 0de9783

18 files changed

+16295
-990
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ jobs:
2222
matrix:
2323
os: [ubuntu-latest]
2424
node-version:
25-
- 12.x
2625
- 14.x
2726
- 16.x
2827
- 18.x
@@ -68,9 +67,9 @@ jobs:
6867
matrix:
6968
os: [ubuntu-latest]
7069
node-version:
71-
- 12.x
7270
- 14.x
7371
- 16.x
72+
- 18.x
7473
steps:
7574
- name: Use Node.js ${{ matrix.node-version }}
7675
uses: actions/setup-node@v3

README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,8 @@ Optionally, the following parameters can be set in the `JsonLdParser` constructo
162162
* `normalizeLanguageTags`: Whether or not language tags should be normalized to lowercase. _(Default: `false` for JSON-LD 1.1 (and higher), `true` for JSON-LD 1.0)_
163163
* `streamingProfileAllowOutOfOrderPlainType`: When the streaming profile flag is enabled, `@type` entries MUST come before other properties since they may defined a type-scoped context. However, when this flag is enabled, `@type` entries that do NOT define a type-scoped context may appear anywhere just like a regular property.. _(Default: `false`)_
164164
* `skipContextValidation`: If JSON-LD context validation should be skipped. This is useful when parsing large contexts that are known to be valid. _(Default: `false`)_
165+
* `rdfstar`: If embedded nodes and annotated objects should be parsed according to the [JSON-LD star specification](https://json-ld.github.io/json-ld-star/). _(Default: `true`)_
166+
* `rdfstarReverseInEmbedded`: If embedded nodes in JSON-LD star can have reverse properties. _(Default: `false`)_
165167

166168
```javascript
167169
new JsonLdParser({
@@ -179,6 +181,7 @@ new JsonLdParser({
179181
defaultGraph: namedNode('http://example.org/graph'),
180182
rdfDirection: 'i18n-datatype',
181183
normalizeLanguageTags: true,
184+
rdfstar: true,
182185
});
183186
```
184187

@@ -239,8 +242,9 @@ Other documents will still be parsed correctly as well, with a slightly lower ef
239242

240243
## Streaming Profile
241244

242-
This parser adheres to both the [JSON-LD 1.1](https://www.w3.org/TR/json-ld/) specification
243-
and the [JSON-LD 1.1 Streaming specification](https://w3c.github.io/json-ld-streaming/).
245+
This parser adheres to the [JSON-LD 1.1](https://www.w3.org/TR/json-ld/) specification,
246+
the [JSON-LD 1.1 Streaming](https://w3c.github.io/json-ld-streaming/) specification,
247+
and the [JSON-LD star](https://json-ld.github.io/json-ld-star/) specification.
244248

245249
By default, this parser assumes that JSON-LD document
246250
are *not* in the [streaming document form](https://w3c.github.io/json-ld-streaming/#streaming-document-form).
@@ -261,6 +265,8 @@ This parser implements the following [JSON-LD specifications](https://json-ld.or
261265
* JSON-LD 1.1 - Transform JSON-LD to RDF
262266
* JSON-LD 1.1 - Error handling
263267
* JSON-LD 1.1 - Streaming Transform JSON-LD to RDF
268+
* [JSON-LD star](https://json-ld.github.io/json-ld-star/) - Transform JSON-LD star to RDF
269+
* [JSON-LD star](https://json-ld.github.io/json-ld-star/) - Error handling
264270

265271
## Performance
266272

lib/JsonLdParser.ts

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {EntryHandlerKeywordValue} from "./entryhandler/keyword/EntryHandlerKeywo
2020
import {ParsingContext} from "./ParsingContext";
2121
import {Util} from "./Util";
2222
import {parse as parseLinkHeader} from "http-link-header";
23+
import { EntryHandlerKeywordAnnotation } from './entryhandler/keyword/EntryHandlerKeywordAnnotation';
2324

2425
/**
2526
* A stream transformer that parses JSON-LD (text) streams to an {@link RDF.Stream}.
@@ -36,6 +37,7 @@ export class JsonLdParser extends Transform implements RDF.Sink<EventEmitter, RD
3637
new EntryHandlerKeywordNest(),
3738
new EntryHandlerKeywordType(),
3839
new EntryHandlerKeywordValue(),
40+
new EntryHandlerKeywordAnnotation(),
3941
new EntryHandlerContainer(),
4042
new EntryHandlerKeywordUnknownFallback(),
4143
new EntryHandlerPredicate(),
@@ -251,7 +253,7 @@ export class JsonLdParser extends Transform implements RDF.Sink<EventEmitter, RD
251253
}
252254

253255
// Skip further processing if this node is part of a literal
254-
if (this.util.isLiteral(depth)) {
256+
if (await this.util.isLiteral(keys, depth)) {
255257
handleKey = false;
256258
}
257259

@@ -304,6 +306,7 @@ export class JsonLdParser extends Transform implements RDF.Sink<EventEmitter, RD
304306
this.parsingContext.jsonLiteralStack.splice(depth, 1);
305307
this.parsingContext.validationStack.splice(depth - 1, 2);
306308
this.parsingContext.literalStack.splice(depth, this.parsingContext.literalStack.length - depth);
309+
this.parsingContext.annotationsBuffer.splice(depth, 1);
307310
// TODO: just like the literal stack, splice all other stack until the end as well?
308311
}
309312

@@ -318,12 +321,13 @@ export class JsonLdParser extends Transform implements RDF.Sink<EventEmitter, RD
318321
*/
319322
public async flushBuffer(depth: number, keys: any[]) {
320323
let subjects: RDF.Term[] = this.parsingContext.idStack[depth];
321-
if (!subjects) {
324+
const subjectsWasDefined = !!subjects;
325+
if (!subjectsWasDefined) {
322326
subjects = this.parsingContext.idStack[depth] = [ this.util.dataFactory.blankNode() ];
323327
}
324328

325329
// Flush values at this level
326-
const valueBuffer: { predicate: RDF.Term, object: RDF.Term, reverse: boolean }[] =
330+
const valueBuffer: { predicate: RDF.Term, object: RDF.Term, reverse: boolean, isEmbedded: boolean }[] =
327331
this.parsingContext.unidentifiedValuesBuffer[depth];
328332
if (valueBuffer) {
329333
for (const subject of subjects) {
@@ -336,13 +340,7 @@ export class JsonLdParser extends Transform implements RDF.Sink<EventEmitter, RD
336340
// Flush values to stream if the graph @id is known
337341
this.parsingContext.emittedStack[depth] = true;
338342
for (const bufferedValue of valueBuffer) {
339-
if (bufferedValue.reverse) {
340-
this.parsingContext.emitQuad(depth, this.util.dataFactory.quad(
341-
bufferedValue.object, bufferedValue.predicate, subject, graph));
342-
} else {
343-
this.parsingContext.emitQuad(depth, this.util.dataFactory.quad(
344-
subject, bufferedValue.predicate, bufferedValue.object, graph));
345-
}
343+
this.util.emitQuadChecked(depth, subject, bufferedValue.predicate, bufferedValue.object, graph, bufferedValue.reverse, bufferedValue.isEmbedded);
346344
}
347345
}
348346
} else {
@@ -355,12 +353,14 @@ export class JsonLdParser extends Transform implements RDF.Sink<EventEmitter, RD
355353
object: subject,
356354
predicate: bufferedValue.predicate,
357355
subject: bufferedValue.object,
356+
isEmbedded: bufferedValue.isEmbedded,
358357
});
359358
} else {
360359
subGraphBuffer.push({
361360
object: bufferedValue.object,
362361
predicate: bufferedValue.predicate,
363362
subject,
363+
isEmbedded: bufferedValue.isEmbedded,
364364
});
365365
}
366366
}
@@ -389,6 +389,23 @@ export class JsonLdParser extends Transform implements RDF.Sink<EventEmitter, RD
389389
}
390390
this.parsingContext.unidentifiedGraphsBuffer.splice(depth, 1);
391391
}
392+
393+
// Push unhandled annotations up the stack as nested annotations
394+
const annotationsBuffer = this.parsingContext.annotationsBuffer[depth];
395+
if (annotationsBuffer) {
396+
// Throw an error if we reach the top, and still have annotations
397+
if (annotationsBuffer.length > 0 && depth === 1) {
398+
this.parsingContext.emitError(new ErrorCoded(`Annotations can not be made on top-level nodes`,
399+
ERROR_CODES.INVALID_ANNOTATION));
400+
}
401+
402+
// Pass the annotations buffer up one level in the stack
403+
const annotationsBufferParent = this.parsingContext.getAnnotationsBufferSafe(depth - 1);
404+
for (const annotation of annotationsBuffer) {
405+
annotationsBufferParent.push(annotation);
406+
}
407+
delete this.parsingContext.annotationsBuffer[depth];
408+
}
392409
}
393410

394411
/**
@@ -637,4 +654,14 @@ export interface IJsonLdParserOptions {
637654
* This is useful when parsing large contexts that are known to be valid.
638655
*/
639656
skipContextValidation?: boolean;
657+
/**
658+
* If embedded nodes and annotated objects should be parsed according to the JSON-LD star specification.
659+
* Defaults to true
660+
*/
661+
rdfstar?: boolean;
662+
/**
663+
* If embedded nodes may use reverse properties
664+
* Defaults to false.
665+
*/
666+
rdfstarReverseInEmbedded?: boolean;
640667
}

lib/ParsingContext.ts

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import * as RDF from "@rdfjs/types";
55
import {ContextTree} from "./ContextTree";
66
import {IJsonLdParserOptions, JsonLdParser} from "./JsonLdParser";
77

8+
export type AnnotationsBufferEntry = { predicate: RDF.Term, object: RDF.Term, reverse: boolean, nestedAnnotations: AnnotationsBufferEntry[], depth: number };
9+
810
/**
911
* Data holder for parsing information.
1012
*/
@@ -36,6 +38,8 @@ export class ParsingContext {
3638
public readonly rdfDirection?: 'i18n-datatype' | 'compound-literal';
3739
public readonly normalizeLanguageTags?: boolean;
3840
public readonly streamingProfileAllowOutOfOrderPlainType?: boolean;
41+
public readonly rdfstar: boolean;
42+
public readonly rdfstarReverseInEmbedded?: boolean;
3943

4044
// Stack of indicating if a depth has been touched.
4145
public readonly processingStack: boolean[];
@@ -63,10 +67,12 @@ export class ParsingContext {
6367
public readonly jsonLiteralStack: boolean[];
6468
// Triples that don't know their subject @id yet.
6569
// L0: stack depth; L1: values
66-
public readonly unidentifiedValuesBuffer: { predicate: RDF.Term, object: RDF.Term, reverse: boolean }[][];
70+
public readonly unidentifiedValuesBuffer: { predicate: RDF.Term, object: RDF.Term, reverse: boolean, isEmbedded: boolean }[][];
6771
// Quads that don't know their graph @id yet.
6872
// L0: stack depth; L1: values
69-
public readonly unidentifiedGraphsBuffer: { subject: RDF.Term, predicate: RDF.Term, object: RDF.Term }[][];
73+
public readonly unidentifiedGraphsBuffer: { subject: RDF.Term, predicate: RDF.Term, object: RDF.Term, isEmbedded: boolean }[][];
74+
// Stack of annotation objects on incomplete nodes.
75+
public readonly annotationsBuffer: AnnotationsBufferEntry[][];
7076

7177
// Depths that should be still flushed
7278
public pendingContainerFlushBuffers: { depth: number, keys: any[] }[];
@@ -92,6 +98,8 @@ export class ParsingContext {
9298
this.rdfDirection = options.rdfDirection;
9399
this.normalizeLanguageTags = options.normalizeLanguageTags;
94100
this.streamingProfileAllowOutOfOrderPlainType = options.streamingProfileAllowOutOfOrderPlainType;
101+
this.rdfstar = options.rdfstar !== false;
102+
this.rdfstarReverseInEmbedded = options.rdfstarReverseInEmbedded;
95103

96104
this.topLevelProperties = false;
97105
this.activeProcessingMode = parseFloat(this.processingMode);
@@ -111,6 +119,7 @@ export class ParsingContext {
111119
this.jsonLiteralStack = [];
112120
this.unidentifiedValuesBuffer = [];
113121
this.unidentifiedGraphsBuffer = [];
122+
this.annotationsBuffer = [];
114123

115124
this.pendingContainerFlushBuffers = [];
116125

@@ -342,7 +351,7 @@ export class ParsingContext {
342351
* @return {{predicate: Term; object: Term; reverse: boolean}[]} An element of
343352
* {@link ParsingContext.unidentifiedValuesBuffer}.
344353
*/
345-
public getUnidentifiedValueBufferSafe(depth: number): { predicate: RDF.Term, object: RDF.Term, reverse: boolean }[] {
354+
public getUnidentifiedValueBufferSafe(depth: number): { predicate: RDF.Term, object: RDF.Term, reverse: boolean, isEmbedded: boolean }[] {
346355
let buffer = this.unidentifiedValuesBuffer[depth];
347356
if (!buffer) {
348357
buffer = [];
@@ -357,7 +366,7 @@ export class ParsingContext {
357366
* @return {{predicate: Term; object: Term; reverse: boolean}[]} An element of
358367
* {@link ParsingContext.unidentifiedGraphsBuffer}.
359368
*/
360-
public getUnidentifiedGraphBufferSafe(depth: number): { subject: RDF.Term, predicate: RDF.Term, object: RDF.Term }[] {
369+
public getUnidentifiedGraphBufferSafe(depth: number): { subject: RDF.Term, predicate: RDF.Term, object: RDF.Term, isEmbedded: boolean }[] {
361370
let buffer = this.unidentifiedGraphsBuffer[depth];
362371
if (!buffer) {
363372
buffer = [];
@@ -366,6 +375,20 @@ export class ParsingContext {
366375
return buffer;
367376
}
368377

378+
/**
379+
* Safely get or create the depth value of {@link ParsingContext.annotationsBuffer}.
380+
* @param {number} depth A depth.
381+
* @return {} An element of {@link ParsingContext.annotationsBuffer}.
382+
*/
383+
public getAnnotationsBufferSafe(depth: number): AnnotationsBufferEntry[] {
384+
let buffer = this.annotationsBuffer[depth];
385+
if (!buffer) {
386+
buffer = [];
387+
this.annotationsBuffer[depth] = buffer;
388+
}
389+
return buffer;
390+
}
391+
369392
/**
370393
* @return IExpandOptions The expand options for the active processing mode.
371394
*/
@@ -406,6 +429,16 @@ export class ParsingContext {
406429
this.unidentifiedValuesBuffer[depth] = this.unidentifiedValuesBuffer[depth + depthOffset];
407430
delete this.unidentifiedValuesBuffer[depth + depthOffset];
408431
}
432+
if (this.annotationsBuffer[depth + depthOffset - 1]) {
433+
if (!this.annotationsBuffer[depth - 1]) {
434+
this.annotationsBuffer[depth - 1] = [];
435+
}
436+
this.annotationsBuffer[depth - 1] = [
437+
...this.annotationsBuffer[depth - 1],
438+
...this.annotationsBuffer[depth + depthOffset - 1],
439+
];
440+
delete this.annotationsBuffer[depth + depthOffset - 1];
441+
}
409442

410443
// TODO: also do the same for other stacks
411444
}

0 commit comments

Comments
 (0)