@noeldemartin/solid-utils 0.1.1-next.8c064afc67573f7cf20fa4cbeecfad10a949a9ef → 0.1.1-next.9e1ba757a71d124d509b14f4ed0de95a13e0c196

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.
@@ -1,6 +1,8 @@
1
1
  export * from './auth';
2
+ export * from './identifiers';
2
3
  export * from './interop';
3
4
  export * from './io';
4
5
  export * from './jsonld';
5
6
  export * from './testing';
6
7
  export * from './vocabs';
8
+ export * from './wac';
@@ -1,57 +1,95 @@
1
1
  import { uuid } from '@noeldemartin/utils';
2
2
 
3
3
  import { createSolidDocument, fetchSolidDocument, solidDocumentExists, updateSolidDocument } from '@/helpers/io';
4
- import type SolidThing from '@/models/SolidThing';
5
4
  import type { Fetch } from '@/helpers/io';
6
5
  import type { SolidUserProfile } from '@/helpers/auth';
7
6
 
8
- async function mintPrivateTypeIndexUrl(user: SolidUserProfile, fetch?: Fetch): Promise<string> {
9
- fetch = fetch ?? window.fetch;
7
+ type TypeIndexType = 'public' | 'private';
8
+
9
+ async function mintTypeIndexUrl(user: SolidUserProfile, type: TypeIndexType, fetch?: Fetch): Promise<string> {
10
+ fetch = fetch ?? window.fetch.bind(fetch);
10
11
 
11
12
  const storageUrl = user.storageUrls[0];
12
- const typeIndexUrl = `${storageUrl}settings/privateTypeIndex`;
13
+ const typeIndexUrl = `${storageUrl}settings/${type}TypeIndex`;
13
14
 
14
15
  return await solidDocumentExists(typeIndexUrl, fetch)
15
- ? `${storageUrl}settings/privateTypeIndex-${uuid()}`
16
+ ? `${storageUrl}settings/${type}TypeIndex-${uuid()}`
16
17
  : typeIndexUrl;
17
18
  }
18
19
 
19
- export async function createPrivateTypeIndex(user: SolidUserProfile, fetch?: Fetch): Promise<string> {
20
- fetch = fetch ?? window.fetch;
20
+ async function createTypeIndex(user: SolidUserProfile, type: TypeIndexType, fetch?: Fetch) {
21
+ if (user.writableProfileUrl === null) {
22
+ throw new Error('Can\'t create type index without a writable profile document');
23
+ }
21
24
 
22
- const typeIndexUrl = await mintPrivateTypeIndexUrl(user, fetch);
23
- const typeIndexBody = `
24
- <> a
25
- <http://www.w3.org/ns/solid/terms#TypeIndex>,
26
- <http://www.w3.org/ns/solid/terms#UnlistedDocument> .
27
- `;
25
+ fetch = fetch ?? window.fetch.bind(fetch);
26
+
27
+ const typeIndexUrl = await mintTypeIndexUrl(user, type, fetch);
28
+ const typeIndexBody = type === 'public'
29
+ ? '<> a <http://www.w3.org/ns/solid/terms#TypeIndex> .'
30
+ : `
31
+ <> a
32
+ <http://www.w3.org/ns/solid/terms#TypeIndex>,
33
+ <http://www.w3.org/ns/solid/terms#UnlistedDocument> .
34
+ `;
28
35
  const profileUpdateBody = `
29
36
  INSERT DATA {
30
- <${user.webId}> <http://www.w3.org/ns/solid/terms#privateTypeIndex> <${typeIndexUrl}> .
37
+ <${user.webId}> <http://www.w3.org/ns/solid/terms#${type}TypeIndex> <${typeIndexUrl}> .
31
38
  }
32
39
  `;
33
40
 
34
- createSolidDocument(typeIndexUrl, typeIndexBody, fetch);
35
- updateSolidDocument(user.webId, profileUpdateBody, fetch);
41
+ await Promise.all([
42
+ createSolidDocument(typeIndexUrl, typeIndexBody, fetch),
43
+ updateSolidDocument(user.writableProfileUrl, profileUpdateBody, fetch),
44
+ ]);
45
+
46
+ if (type === 'public') {
47
+ // TODO Implement updating ACLs for the listing itself to public
48
+ }
36
49
 
37
50
  return typeIndexUrl;
38
51
  }
39
52
 
40
- export async function findContainerRegistration(
53
+ async function findRegistrations(
41
54
  typeIndexUrl: string,
42
- childrenType: string,
55
+ type: string | string[],
56
+ predicate: string,
43
57
  fetch?: Fetch,
44
- ): Promise<SolidThing | null> {
58
+ ): Promise<string[]> {
45
59
  const typeIndex = await fetchSolidDocument(typeIndexUrl, fetch);
46
- const containerQuad = typeIndex
47
- .statements(undefined, 'rdfs:type', 'solid:TypeRegistration')
48
- .find(
49
- statement =>
50
- typeIndex.contains(statement.subject.value, 'solid:forClass', childrenType) &&
51
- typeIndex.contains(statement.subject.value, 'solid:instanceContainer'),
52
- );
53
-
54
- return containerQuad
55
- ? typeIndex.getThing(containerQuad.subject.value) ?? null
56
- : null;
60
+ const types = Array.isArray(type) ? type : [type];
61
+
62
+ return types.map(
63
+ type => typeIndex
64
+ .statements(undefined, 'rdf:type', 'solid:TypeRegistration')
65
+ .filter(statement => typeIndex.contains(statement.subject.value, 'solid:forClass', type))
66
+ .map(statement => typeIndex.statements(statement.subject.value, predicate))
67
+ .flat()
68
+ .map(statement => statement.object.value)
69
+ .filter(url => !!url),
70
+ ).flat();
71
+ }
72
+
73
+ export async function createPublicTypeIndex(user: SolidUserProfile, fetch?: Fetch): Promise<string> {
74
+ return createTypeIndex(user, 'public', fetch);
75
+ }
76
+
77
+ export async function createPrivateTypeIndex(user: SolidUserProfile, fetch?: Fetch): Promise<string> {
78
+ return createTypeIndex(user, 'private', fetch);
79
+ }
80
+
81
+ export async function findContainerRegistrations(
82
+ typeIndexUrl: string,
83
+ type: string | string[],
84
+ fetch?: Fetch,
85
+ ): Promise<string[]> {
86
+ return findRegistrations(typeIndexUrl, type, 'solid:instanceContainer', fetch);
87
+ }
88
+
89
+ export async function findInstanceRegistrations(
90
+ typeIndexUrl: string,
91
+ type: string | string[],
92
+ fetch?: Fetch,
93
+ ): Promise<string[]> {
94
+ return findRegistrations(typeIndexUrl, type, 'solid:instance', fetch);
57
95
  }
package/src/helpers/io.ts CHANGED
@@ -1,10 +1,15 @@
1
- import { arr, arrayFilter, arrayReplace,objectWithoutEmpty } from '@noeldemartin/utils';
2
- import { BlankNode as N3BlankNode, Quad as N3Quad, Parser as TurtleParser, Writer as TurtleWriter } from 'n3';
3
- import type { JsonLdDocument } from 'jsonld';
4
- import { toRDF } from 'jsonld';
5
1
  import md5 from 'md5';
2
+ import {
3
+ TurtleParser,
4
+ TurtleWriter,
5
+ createBlankNode,
6
+ createQuad,
7
+ jsonLDFromRDF,
8
+ jsonLDToRDF,
9
+ } from '@noeldemartin/solid-utils-external';
10
+ import { arr, arrayFilter, arrayReplace, objectWithoutEmpty, stringMatchAll, tap } from '@noeldemartin/utils';
6
11
  import type { Quad } from 'rdf-js';
7
- import type { Term as N3Term } from 'n3';
12
+ import type { JsonLdDocument, Term } from '@noeldemartin/solid-utils-external';
8
13
 
9
14
  import SolidDocument from '@/models/SolidDocument';
10
15
 
@@ -13,10 +18,10 @@ import NetworkRequestError from '@/errors/NetworkRequestError';
13
18
  import NotFoundError from '@/errors/NotFoundError';
14
19
  import UnauthorizedError from '@/errors/UnauthorizedError';
15
20
  import { isJsonLDGraph } from '@/helpers/jsonld';
16
- import type { JsonLD } from '@/helpers/jsonld';
21
+ import type { JsonLD, JsonLDGraph, JsonLDResource } from '@/helpers/jsonld';
17
22
 
18
23
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
19
- export declare type AnyFetch = (input: any, options?: any) => Promise<any>;
24
+ export declare type AnyFetch = (input: any, options?: any) => Promise<Response>;
20
25
  export declare type TypedFetch = (input: RequestInfo, options?: RequestInit) => Promise<Response>;
21
26
  export declare type Fetch = TypedFetch | AnyFetch;
22
27
 
@@ -44,7 +49,10 @@ async function fetchRawSolidDocument(url: string, fetch: Fetch): Promise<{ body:
44
49
  if (error instanceof UnauthorizedError)
45
50
  throw error;
46
51
 
47
- throw new NetworkRequestError(url);
52
+ if (error instanceof NotFoundError)
53
+ throw error;
54
+
55
+ throw new NetworkRequestError(url, { cause: error });
48
56
  }
49
57
  }
50
58
 
@@ -52,26 +60,23 @@ function normalizeBlankNodes(quads: Quad[]): Quad[] {
52
60
  const normalizedQuads = quads.slice(0);
53
61
  const quadsIndexes: Record<string, Set<number>> = {};
54
62
  const blankNodeIds = arr(quads)
55
- .flatMap((quad, index) => {
56
- const ids = arrayFilter([
57
- quad.object.termType === 'BlankNode' ? quad.object.value : null,
58
- quad.subject.termType === 'BlankNode' ? quad.subject.value : null,
59
- ]);
60
-
61
- for (const id of ids) {
62
- quadsIndexes[id] = quadsIndexes[id] ?? new Set();
63
- quadsIndexes[id].add(index);
64
- }
65
-
66
- return ids;
67
- })
63
+ .flatMap(
64
+ (quad, index) => tap(
65
+ arrayFilter([
66
+ quad.object.termType === 'BlankNode' ? quad.object.value : null,
67
+ quad.subject.termType === 'BlankNode' ? quad.subject.value : null,
68
+ ]),
69
+ ids => ids.forEach(id => (quadsIndexes[id] ??= new Set()).add(index)),
70
+ ),
71
+ )
68
72
  .filter()
69
73
  .unique();
70
74
 
71
75
  for (const originalId of blankNodeIds) {
76
+ const quadIndexes = quadsIndexes[originalId] as Set<number>;
72
77
  const normalizedId = md5(
73
- arr(quadsIndexes[originalId])
74
- .map(index => quads[index])
78
+ arr(quadIndexes)
79
+ .map(index => quads[index] as Quad)
75
80
  .filter(({ subject: { termType, value } }) => termType === 'BlankNode' && value === originalId)
76
81
  .map(
77
82
  ({ predicate, object }) => object.termType === 'BlankNode'
@@ -82,18 +87,29 @@ function normalizeBlankNodes(quads: Quad[]): Quad[] {
82
87
  .join(),
83
88
  );
84
89
 
85
- for (const index of quadsIndexes[originalId]) {
86
- const quad = normalizedQuads[index];
87
- const terms: Record<string, N3Term> = { subject: quad.subject as N3Term, object: quad.object as N3Term };
90
+ for (const index of quadIndexes) {
91
+ const quad = normalizedQuads[index] as Quad;
92
+ const terms: Record<string, Term> = {
93
+ subject: quad.subject as Term,
94
+ object: quad.object as Term,
95
+ };
88
96
 
89
97
  for (const [termName, termValue] of Object.entries(terms)) {
90
98
  if (termValue.termType !== 'BlankNode' || termValue.value !== originalId)
91
99
  continue;
92
100
 
93
- terms[termName] = new N3BlankNode(normalizedId);
101
+ terms[termName] = createBlankNode(normalizedId) as Term;
94
102
  }
95
103
 
96
- arrayReplace(normalizedQuads, quad, new N3Quad(terms.subject, quad.predicate as N3Term, terms.object));
104
+ arrayReplace(
105
+ normalizedQuads,
106
+ quad,
107
+ createQuad(
108
+ terms.subject as Term,
109
+ quad.predicate as Term,
110
+ terms.object as Term,
111
+ ),
112
+ );
97
113
  }
98
114
  }
99
115
 
@@ -101,12 +117,17 @@ function normalizeBlankNodes(quads: Quad[]): Quad[] {
101
117
  }
102
118
 
103
119
  export interface ParsingOptions {
104
- documentUrl: string;
120
+ baseIRI: string;
105
121
  normalizeBlankNodes: boolean;
106
122
  }
107
123
 
124
+ export interface RDFGraphData {
125
+ quads: Quad[];
126
+ containsRelativeIRIs: boolean;
127
+ }
128
+
108
129
  export async function createSolidDocument(url: string, body: string, fetch?: Fetch): Promise<SolidDocument> {
109
- fetch = fetch ?? window.fetch;
130
+ fetch = fetch ?? window.fetch.bind(window);
110
131
 
111
132
  const statements = await turtleToQuads(body);
112
133
 
@@ -121,19 +142,32 @@ export async function createSolidDocument(url: string, body: string, fetch?: Fet
121
142
 
122
143
  export async function fetchSolidDocument(url: string, fetch?: Fetch): Promise<SolidDocument> {
123
144
  const { body: data, headers } = await fetchRawSolidDocument(url, fetch ?? window.fetch);
124
- const statements = await turtleToQuads(data, { documentUrl: url });
145
+ const statements = await turtleToQuads(data, { baseIRI: url });
125
146
 
126
147
  return new SolidDocument(url, statements, headers);
127
148
  }
128
149
 
129
- export async function jsonldToQuads(jsonld: JsonLD): Promise<Quad[]> {
150
+ export async function fetchSolidDocumentIfFound(url: string, fetch?: Fetch): Promise<SolidDocument | null> {
151
+ try {
152
+ const document = await fetchSolidDocument(url, fetch);
153
+
154
+ return document;
155
+ } catch (error) {
156
+ if (!(error instanceof NotFoundError))
157
+ throw error;
158
+
159
+ return null;
160
+ }
161
+ }
162
+
163
+ export async function jsonldToQuads(jsonld: JsonLD, baseIRI?: string): Promise<Quad[]> {
130
164
  if (isJsonLDGraph(jsonld)) {
131
- const graphQuads = await Promise.all(jsonld['@graph'].map(jsonldToQuads));
165
+ const graphQuads = await Promise.all(jsonld['@graph'].map(resource => jsonldToQuads(resource, baseIRI)));
132
166
 
133
167
  return graphQuads.flat();
134
168
  }
135
169
 
136
- return toRDF(jsonld as JsonLdDocument) as Promise<Quad[]>;
170
+ return jsonLDToRDF(jsonld as JsonLdDocument, { base: baseIRI }) as Promise<Quad[]>;
137
171
  }
138
172
 
139
173
  export function normalizeSparql(sparql: string): string {
@@ -149,6 +183,60 @@ export function normalizeSparql(sparql: string): string {
149
183
  .join(' ;\n');
150
184
  }
151
185
 
186
+ export function parseTurtle(turtle: string, options: Partial<ParsingOptions> = {}): Promise<RDFGraphData> {
187
+ const parserOptions = objectWithoutEmpty({ baseIRI: options.baseIRI });
188
+ const parser = new TurtleParser(parserOptions);
189
+ const data: RDFGraphData = {
190
+ quads: [],
191
+ containsRelativeIRIs: false,
192
+ };
193
+
194
+ return new Promise((resolve, reject) => {
195
+ const resolveRelativeIRI = parser._resolveRelativeIRI;
196
+
197
+ parser._resolveRelativeIRI = (...args) => {
198
+ data.containsRelativeIRIs = true;
199
+ parser._resolveRelativeIRI = resolveRelativeIRI;
200
+
201
+ return parser._resolveRelativeIRI(...args);
202
+ };
203
+
204
+ parser.parse(turtle, (error, quad) => {
205
+ if (error) {
206
+ reject(
207
+ new MalformedSolidDocumentError(
208
+ options.baseIRI ?? null,
209
+ SolidDocumentFormat.Turtle,
210
+ error.message,
211
+ ),
212
+ );
213
+
214
+ return;
215
+ }
216
+
217
+ if (!quad) {
218
+ // data.quads = options.normalizeBlankNodes
219
+ // ? normalizeBlankNodes(data.quads)
220
+ // : data.quads;
221
+
222
+ resolve(data);
223
+
224
+ return;
225
+ }
226
+
227
+ data.quads.push(quad);
228
+ });
229
+ });
230
+ }
231
+
232
+ export async function quadsToJsonLD(quads: Quad[]): Promise<JsonLDGraph> {
233
+ const graph = await jsonLDFromRDF(quads);
234
+
235
+ return {
236
+ '@graph': graph as JsonLDResource[],
237
+ };
238
+ }
239
+
152
240
  export function quadsToTurtle(quads: Quad[]): string {
153
241
  const writer = new TurtleWriter;
154
242
 
@@ -175,7 +263,7 @@ export async function sparqlToQuads(
175
263
  sparql: string,
176
264
  options: Partial<ParsingOptions> = {},
177
265
  ): Promise<Record<string, Quad[]>> {
178
- const operations = sparql.matchAll(/(\w+) DATA {([^}]+)}/g);
266
+ const operations = stringMatchAll<3>(sparql, /(\w+) DATA {([^}]+)}/g);
179
267
  const quads: Record<string, Quad[]> = {};
180
268
 
181
269
  await Promise.all([...operations].map(async operation => {
@@ -189,7 +277,7 @@ export async function sparqlToQuads(
189
277
  }
190
278
 
191
279
  export function sparqlToQuadsSync(sparql: string, options: Partial<ParsingOptions> = {}): Record<string, Quad[]> {
192
- const operations = sparql.matchAll(/(\w+) DATA {([^}]+)}/g);
280
+ const operations = stringMatchAll<3>(sparql, /(\w+) DATA {([^}]+)}/g);
193
281
  const quads: Record<string, Quad[]> = {};
194
282
 
195
283
  for (const operation of operations) {
@@ -203,38 +291,13 @@ export function sparqlToQuadsSync(sparql: string, options: Partial<ParsingOption
203
291
  }
204
292
 
205
293
  export async function turtleToQuads(turtle: string, options: Partial<ParsingOptions> = {}): Promise<Quad[]> {
206
- const parserOptions = objectWithoutEmpty({ baseIRI: options.documentUrl });
207
- const parser = new TurtleParser(parserOptions);
208
- const quads: Quad[] = [];
294
+ const { quads } = await parseTurtle(turtle, options);
209
295
 
210
- return new Promise((resolve, reject) => {
211
- parser.parse(turtle, (error, quad) => {
212
- if (error) {
213
- reject(
214
- new MalformedSolidDocumentError(
215
- options.documentUrl ?? null,
216
- SolidDocumentFormat.Turtle,
217
- error.message,
218
- ),
219
- );
220
- return;
221
- }
222
-
223
- if (!quad) {
224
- options.normalizeBlankNodes
225
- ? resolve(normalizeBlankNodes(quads))
226
- : resolve(quads);
227
-
228
- return;
229
- }
230
-
231
- quads.push(quad);
232
- });
233
- });
296
+ return quads;
234
297
  }
235
298
 
236
299
  export function turtleToQuadsSync(turtle: string, options: Partial<ParsingOptions> = {}): Quad[] {
237
- const parserOptions = objectWithoutEmpty({ baseIRI: options.documentUrl });
300
+ const parserOptions = objectWithoutEmpty({ baseIRI: options.baseIRI });
238
301
  const parser = new TurtleParser(parserOptions);
239
302
 
240
303
  try {
@@ -244,12 +307,16 @@ export function turtleToQuadsSync(turtle: string, options: Partial<ParsingOption
244
307
  ? normalizeBlankNodes(quads)
245
308
  : quads;
246
309
  } catch (error) {
247
- throw new MalformedSolidDocumentError(options.documentUrl ?? null, SolidDocumentFormat.Turtle, error.message);
310
+ throw new MalformedSolidDocumentError(
311
+ options.baseIRI ?? null,
312
+ SolidDocumentFormat.Turtle,
313
+ (error as Error).message ?? '',
314
+ );
248
315
  }
249
316
  }
250
317
 
251
318
  export async function updateSolidDocument(url: string, body: string, fetch?: Fetch): Promise<void> {
252
- fetch = fetch ?? window.fetch;
319
+ fetch = fetch ?? window.fetch.bind(window);
253
320
 
254
321
  await fetch(url, {
255
322
  method: 'PATCH',
@@ -1,3 +1,6 @@
1
+ import { compactJsonLD } from '@noeldemartin/solid-utils-external';
2
+ import type { JsonLdDocument } from '@noeldemartin/solid-utils-external';
3
+
1
4
  export type JsonLD = Partial<{
2
5
  '@context': Record<string, unknown>;
3
6
  '@id': string;
@@ -7,6 +10,20 @@ export type JsonLD = Partial<{
7
10
  export type JsonLDResource = Omit<JsonLD, '@id'> & { '@id': string };
8
11
  export type JsonLDGraph = { '@graph': JsonLDResource[] };
9
12
 
13
+ export async function compactJsonLDGraph(jsonld: JsonLDGraph): Promise<JsonLDGraph> {
14
+ const compactedJsonLD = await compactJsonLD(jsonld as JsonLdDocument, {});
15
+
16
+ if ('@graph' in compactedJsonLD) {
17
+ return compactedJsonLD as JsonLDGraph;
18
+ }
19
+
20
+ if ('@id' in compactedJsonLD) {
21
+ return { '@graph': [compactedJsonLD] } as JsonLDGraph;
22
+ }
23
+
24
+ return { '@graph': [] };
25
+ }
26
+
10
27
  export function isJsonLDGraph(jsonld: JsonLD): jsonld is JsonLDGraph {
11
28
  return '@graph' in jsonld;
12
29
  }
@@ -1,46 +1,67 @@
1
+ import { JSError, arrayRemove, pull, stringMatchAll } from '@noeldemartin/utils';
1
2
  import type { JsonLD } from '@/helpers/jsonld';
2
- import { pull } from '@noeldemartin/utils';
3
3
  import type { Quad, Quad_Object } from 'rdf-js';
4
4
 
5
5
  import { jsonldToQuads, quadToTurtle, quadsToTurtle, sparqlToQuadsSync, turtleToQuadsSync } from './io';
6
6
 
7
7
  let patternsRegExpsIndex: Record<string, RegExp> = {};
8
+ const builtInPatterns: Record<string, string> = {
9
+ '%uuid%': '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}',
10
+ };
11
+
12
+ class ExpectedQuadAssertionError extends JSError {
13
+
14
+ constructor(public readonly expectedQuad: Quad) {
15
+ super(`Couldn't find the following triple: ${quadToTurtle(expectedQuad)}`);
16
+ }
8
17
 
9
- function containsPatterns(value: string): boolean {
10
- return /\[\[(.*\]\[)?([^\]]+)\]\]/.test(value);
11
18
  }
12
19
 
13
- function quadValueEquals(expected: string, actual: string): boolean {
14
- if (!containsPatterns(expected))
15
- return expected === actual;
20
+ function assertExpectedQuadsExist(expectedQuads: Quad[], actualQuads: Quad[]): void {
21
+ for (const expectedQuad of expectedQuads) {
22
+ const matchingQuad = actualQuads.find(actualQuad => quadEquals(expectedQuad, actualQuad));
16
23
 
17
- const patternAliases = [];
24
+ if (!matchingQuad)
25
+ throw new ExpectedQuadAssertionError(expectedQuad);
26
+
27
+ arrayRemove(actualQuads, matchingQuad);
28
+ }
29
+ }
18
30
 
19
- if (!(expected in patternsRegExpsIndex)) {
20
- const patternMatches = expected.matchAll(/\[\[((.*)\]\[)?([^\]]+)\]\]/g);
21
- const patterns: string[] = [];
22
- let expectedRegExp = expected;
31
+ function containsPatterns(value: string): boolean {
32
+ return /\[\[(.*\]\[)?([^\]]+)\]\]/.test(value);
33
+ }
23
34
 
24
- for (const patternMatch of patternMatches) {
25
- if (patternMatch[2]) {
26
- patternAliases.push(patternMatch[2]);
27
- }
35
+ function createPatternRegexp(expected: string): RegExp {
36
+ const patternAliases = [];
37
+ const patternMatches = stringMatchAll<4, 1 | 2>(
38
+ expected,
39
+ /\[\[((.*?)\]\[)?([^\]]+)\]\]/g,
40
+ );
41
+ const patterns: string[] = [];
42
+ let expectedRegExp = expected;
28
43
 
29
- patterns.push(patternMatch[3]);
44
+ for (const patternMatch of patternMatches) {
45
+ patternMatch[2] && patternAliases.push(patternMatch[2]);
30
46
 
31
- expectedRegExp = expectedRegExp.replace(patternMatch[0], `%PATTERN${patterns.length - 1}%`);
32
- }
47
+ patterns.push(patternMatch[3]);
33
48
 
34
- expectedRegExp = expectedRegExp.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
49
+ expectedRegExp = expectedRegExp.replace(patternMatch[0], `%PATTERN${patterns.length - 1}%`);
50
+ }
35
51
 
36
- for (const [patternIndex, pattern] of Object.entries(patterns)) {
37
- expectedRegExp = expectedRegExp.replace(`%PATTERN${patternIndex}%`, pattern);
38
- }
52
+ expectedRegExp = expectedRegExp.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
39
53
 
40
- patternsRegExpsIndex[expected] = new RegExp(expectedRegExp);
54
+ for (const [patternIndex, pattern] of Object.entries(patterns)) {
55
+ expectedRegExp = expectedRegExp.replace(`%PATTERN${patternIndex}%`, builtInPatterns[pattern] ?? pattern);
41
56
  }
42
57
 
43
- return patternsRegExpsIndex[expected].test(actual);
58
+ return new RegExp(expectedRegExp);
59
+ }
60
+
61
+ function quadValueEquals(expected: string, actual: string): boolean {
62
+ return containsPatterns(expected)
63
+ ? (patternsRegExpsIndex[expected] ??= createPatternRegexp(expected)).test(actual)
64
+ : expected === actual;
44
65
  }
45
66
 
46
67
  function quadObjectEquals(expected: Quad_Object, actual: Quad_Object): boolean {
@@ -95,9 +116,13 @@ export async function jsonldEquals(expected: JsonLD, actual: JsonLD): Promise<Eq
95
116
  if (expectedQuads.length !== actualQuads.length)
96
117
  return result(false, `Expected ${expectedQuads.length} triples, found ${actualQuads.length}.`);
97
118
 
98
- for (const expectedQuad of expectedQuads) {
99
- if (!actualQuads.some(actualQuad => quadEquals(expectedQuad, actualQuad)))
100
- return result(false, `Couldn't find the following triple: ${quadToTurtle(expectedQuad)}`);
119
+ try {
120
+ assertExpectedQuadsExist(expectedQuads, actualQuads);
121
+ } catch (error) {
122
+ if (!(error instanceof ExpectedQuadAssertionError))
123
+ throw error;
124
+
125
+ return result(false, error.message);
101
126
  }
102
127
 
103
128
  return result(true, 'jsonld matches');
@@ -121,9 +146,16 @@ export function sparqlEquals(expected: string, actual: string): EqualityResult {
121
146
  if (expectedQuads.length !== actualQuads.length)
122
147
  return result(false, `Expected ${expectedQuads.length} ${operation} triples, found ${actualQuads.length}.`);
123
148
 
124
- for (const expectedQuad of expectedQuads) {
125
- if (!actualQuads.some(actualQuad => quadEquals(expectedQuad, actualQuad)))
126
- return result(false, `Couldn't find the following ${operation} triple: ${quadToTurtle(expectedQuad)}`);
149
+ try {
150
+ assertExpectedQuadsExist(expectedQuads, actualQuads);
151
+ } catch (error) {
152
+ if (!(error instanceof ExpectedQuadAssertionError))
153
+ throw error;
154
+
155
+ return result(
156
+ false,
157
+ `Couldn't find the following ${operation} triple: ${quadToTurtle(error.expectedQuad)}`,
158
+ );
127
159
  }
128
160
  }
129
161
 
@@ -145,9 +177,13 @@ export function turtleEquals(expected: string, actual: string): EqualityResult {
145
177
  if (expectedQuads.length !== actualQuads.length)
146
178
  return result(false, `Expected ${expectedQuads.length} triples, found ${actualQuads.length}.`);
147
179
 
148
- for (const expectedQuad of expectedQuads) {
149
- if (!actualQuads.some(actualQuad => quadEquals(expectedQuad, actualQuad)))
150
- return result(false, `Couldn't find the following triple: ${quadToTurtle(expectedQuad)}`);
180
+ try {
181
+ assertExpectedQuadsExist(expectedQuads, actualQuads);
182
+ } catch (error) {
183
+ if (!(error instanceof ExpectedQuadAssertionError))
184
+ throw error;
185
+
186
+ return result(false, error.message);
151
187
  }
152
188
 
153
189
  return result(true, 'turtle matches');
@@ -6,10 +6,12 @@ export interface ExpandIRIOptions {
6
6
  }
7
7
 
8
8
  const knownPrefixes: RDFContext = {
9
+ acl: 'http://www.w3.org/ns/auth/acl#',
9
10
  foaf: 'http://xmlns.com/foaf/0.1/',
10
11
  pim: 'http://www.w3.org/ns/pim/space#',
11
12
  purl: 'http://purl.org/dc/terms/',
12
- rdfs: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
13
+ rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
14
+ rdfs: 'http://www.w3.org/2000/01/rdf-schema#',
13
15
  schema: 'https://schema.org/',
14
16
  solid: 'http://www.w3.org/ns/solid/terms#',
15
17
  vcard: 'http://www.w3.org/2006/vcard/ns#',
@@ -25,7 +27,7 @@ export function expandIRI(iri: string, options: Partial<ExpandIRIOptions> = {}):
25
27
 
26
28
  const [prefix, name] = iri.split(':');
27
29
 
28
- if (name) {
30
+ if (prefix && name) {
29
31
  const expandedPrefix = knownPrefixes[prefix] ?? options.extraContext?.[prefix] ?? null;
30
32
 
31
33
  if (!expandedPrefix)