@@ -7,9 +7,6 @@ import {IJsonLdContext, IJsonLdContextNormalizedRaw, IPrefixValue, JsonLdContext
7
7
import { JsonLdContextNormalized , defaultExpandOptions , IExpandOptions } from "./JsonLdContextNormalized" ;
8
8
import { Util } from "./Util" ;
9
9
10
- // tslint:disable-next-line:no-var-requires
11
- const canonicalizeJson = require ( 'canonicalize' ) ;
12
-
13
10
/**
14
11
* Parses JSON-LD contexts.
15
12
*/
@@ -93,13 +90,14 @@ export class ContextParser {
93
90
*/
94
91
public idifyReverseTerms ( context : IJsonLdContextNormalizedRaw ) : IJsonLdContextNormalizedRaw {
95
92
for ( const key of Object . keys ( context ) ) {
96
- const value : IPrefixValue = context [ key ] ;
93
+ let value = context [ key ] ;
97
94
if ( value && typeof value === 'object' ) {
98
95
if ( value [ '@reverse' ] && ! value [ '@id' ] ) {
99
96
if ( typeof value [ '@reverse' ] !== 'string' || Util . isValidKeyword ( value [ '@reverse' ] ) ) {
100
97
throw new ErrorCoded ( `Invalid @reverse value, must be absolute IRI or blank node: '${ value [ '@reverse' ] } '` ,
101
98
ERROR_CODES . INVALID_IRI_MAPPING ) ;
102
99
}
100
+ value = context [ key ] = { ...value , '@id' : value [ '@reverse' ] } ;
103
101
value [ '@id' ] = < string > value [ '@reverse' ] ;
104
102
if ( Util . isPotentialKeyword ( value [ '@reverse' ] ) ) {
105
103
delete value [ '@reverse' ] ;
@@ -118,10 +116,12 @@ export class ContextParser {
118
116
* @param {IJsonLdContextNormalizedRaw } context A context.
119
117
* @param {boolean } expandContentTypeToBase If @type inside the context may be expanded
120
118
* via @base if @vocab is set to null.
119
+ * @param {string[] } keys Optional set of keys from the context to expand. If left undefined, all
120
+ * keys in the context will be expanded.
121
121
*/
122
- public expandPrefixedTerms ( context : JsonLdContextNormalized , expandContentTypeToBase : boolean ) {
122
+ public expandPrefixedTerms ( context : JsonLdContextNormalized , expandContentTypeToBase : boolean , keys ?: string [ ] ) {
123
123
const contextRaw = context . getContextRaw ( ) ;
124
- for ( const key of Object . keys ( contextRaw ) ) {
124
+ for ( const key of ( keys || Object . keys ( contextRaw ) ) ) {
125
125
// Only expand allowed keys
126
126
if ( Util . EXPAND_KEYS_BLACKLIST . indexOf ( key ) < 0 && ! Util . isReservedInternalKeyword ( key ) ) {
127
127
// Error if we try to alias a keyword to something else.
@@ -162,27 +162,30 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR
162
162
if ( '@id' in value ) {
163
163
// Use @id value for expansion
164
164
if ( id !== undefined && id !== null && typeof id === 'string' ) {
165
- contextRaw [ key ] [ '@id' ] = context . expandTerm ( id , true ) ;
165
+ contextRaw [ key ] = { ... contextRaw [ key ] , '@id' : context . expandTerm ( id , true ) } ;
166
166
changed = changed || id !== contextRaw [ key ] [ '@id' ] ;
167
167
}
168
168
} else if ( ! Util . isPotentialKeyword ( key ) && canAddIdEntry ) {
169
169
// Add an explicit @id value based on the expanded key value
170
170
const newId = context . expandTerm ( key , true ) ;
171
171
if ( newId !== key ) {
172
172
// Don't set @id if expansion failed
173
- contextRaw [ key ] [ '@id' ] = newId ;
173
+ contextRaw [ key ] = { ... contextRaw [ key ] , '@id' : newId } ;
174
174
changed = true ;
175
175
}
176
176
}
177
177
if ( type && typeof type === 'string' && type !== '@vocab'
178
178
&& ( ! value [ '@container' ] || ! ( < any > value [ '@container' ] ) [ '@type' ] )
179
179
&& canAddIdEntry ) {
180
180
// First check @vocab , then fallback to @base
181
- contextRaw [ key ] [ '@type' ] = context . expandTerm ( type , true ) ;
182
- if ( expandContentTypeToBase && type === contextRaw [ key ] [ '@type' ] ) {
183
- contextRaw [ key ] [ '@type' ] = context . expandTerm ( type , false ) ;
181
+ let expandedType = context . expandTerm ( type , true ) ;
182
+ if ( expandContentTypeToBase && type === expandedType ) {
183
+ expandedType = context . expandTerm ( type , false ) ;
184
+ }
185
+ if ( expandedType !== type ) {
186
+ changed = true ;
187
+ contextRaw [ key ] = { ...contextRaw [ key ] , '@type' : expandedType } ;
184
188
}
185
- changed = changed || type !== contextRaw [ key ] [ '@type' ] ;
186
189
}
187
190
}
188
191
if ( ! changed ) {
@@ -209,7 +212,10 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR
209
212
const value = context [ key ] ;
210
213
if ( value && typeof value === 'object' ) {
211
214
if ( typeof value [ '@language' ] === 'string' ) {
212
- value [ '@language' ] = value [ '@language' ] . toLowerCase ( ) ;
215
+ const lowercase = value [ '@language' ] . toLowerCase ( ) ;
216
+ if ( lowercase !== value [ '@language' ] ) {
217
+ context [ key ] = { ...value , '@language' : lowercase } ;
218
+ }
213
219
}
214
220
}
215
221
}
@@ -226,13 +232,13 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR
226
232
const value = context [ key ] ;
227
233
if ( value && typeof value === 'object' ) {
228
234
if ( typeof value [ '@container' ] === 'string' ) {
229
- value [ '@container' ] = { [ value [ '@container' ] ] : true } ;
235
+ context [ key ] = { ... value , '@container' : { [ value [ '@container' ] ] : true } } ;
230
236
} else if ( Array . isArray ( value [ '@container' ] ) ) {
231
237
const newValue : { [ key : string ] : boolean } = { } ;
232
238
for ( const containerValue of value [ '@container' ] ) {
233
239
newValue [ containerValue ] = true ;
234
240
}
235
- value [ '@container' ] = newValue ;
241
+ context [ key ] = { ... value , '@container' : newValue } ;
236
242
}
237
243
}
238
244
}
@@ -256,7 +262,7 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR
256
262
if ( value && typeof value === 'object' ) {
257
263
if ( ! ( '@protected' in context [ key ] ) ) {
258
264
// Mark terms with object values as protected if they don't have an @protected: false annotation
259
- context [ key ] [ '@protected' ] = true ;
265
+ context [ key ] = { ... context [ key ] , '@protected' : true } ;
260
266
}
261
267
} else {
262
268
// Convert string-based term values to object-based values with @protected: true
@@ -265,7 +271,7 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR
265
271
'@protected' : true ,
266
272
} ;
267
273
if ( Util . isSimpleTermDefinitionPrefix ( value , expandOptions ) ) {
268
- context [ key ] [ '@prefix' ] = true
274
+ context [ key ] = { ... context [ key ] , '@prefix' : true } ;
269
275
}
270
276
}
271
277
}
@@ -280,29 +286,29 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR
280
286
* @param {IJsonLdContextNormalizedRaw } contextBefore The context that may contain some protected terms.
281
287
* @param {IJsonLdContextNormalizedRaw } contextAfter A new context that is being applied on the first one.
282
288
* @param {IExpandOptions } expandOptions Options that are needed for any expansions during this validation.
289
+ * @param {string[] } keys Optional set of keys from the context to validate. If left undefined, all
290
+ * keys defined in contextAfter will be checked.
283
291
*/
284
292
public validateKeywordRedefinitions ( contextBefore : IJsonLdContextNormalizedRaw ,
285
293
contextAfter : IJsonLdContextNormalizedRaw ,
286
- expandOptions : IExpandOptions ) {
287
- for ( const key of Object . keys ( contextAfter ) ) {
294
+ expandOptions ?: IExpandOptions ,
295
+ keys ?: string [ ] ) {
296
+ for ( const key of ( keys ?? Object . keys ( contextAfter ) ) ) {
288
297
if ( Util . isTermProtected ( contextBefore , key ) ) {
289
298
// The entry in the context before will always be in object-mode
290
299
// If the new entry is in string-mode, convert it to object-mode
291
300
// before checking if it is identical.
292
301
if ( typeof contextAfter [ key ] === 'string' ) {
293
- contextAfter [ key ] = { '@id' : contextAfter [ key ] } ;
294
- }
295
-
296
- // Convert term values to strings for each comparison
297
- const valueBefore = canonicalizeJson ( contextBefore [ key ] ) ;
302
+ contextAfter [ key ] = { '@id' : contextAfter [ key ] , '@protected' : true } ;
303
+ } else {
298
304
// We modify this deliberately,
299
305
// as we need it for the value comparison (they must be identical modulo '@protected')),
300
306
// and for the fact that this new value will override the first one.
301
- contextAfter [ key ] [ '@protected' ] = true ;
302
- const valueAfter = canonicalizeJson ( contextAfter [ key ] ) ;
307
+ contextAfter [ key ] = { ... contextAfter [ key ] , '@protected' : true } ;
308
+ }
303
309
304
310
// Error if they are not identical
305
- if ( valueBefore !== valueAfter ) {
311
+ if ( ! Util . deepEqual ( contextBefore [ key ] , contextAfter [ key ] ) ) {
306
312
throw new ErrorCoded ( `Attempted to override the protected keyword ${ key } from ${
307
313
JSON . stringify ( Util . getContextValueId ( contextBefore [ key ] ) ) } to ${
308
314
JSON . stringify ( Util . getContextValueId ( contextAfter [ key ] ) ) } `,
@@ -593,10 +599,11 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP
593
599
* @param {IJsonLdContextNormalizedRaw } context A context.
594
600
* @param {IParseOptions } options Parsing options.
595
601
* @return {IJsonLdContextNormalizedRaw } The mutated input context.
602
+ * @param {string[] } keys Optional set of keys from the context to parseInnerContexts of. If left undefined, all
603
+ * keys in the context will be iterated over.
596
604
*/
597
- public async parseInnerContexts ( context : IJsonLdContextNormalizedRaw , options : IParseOptions )
598
- : Promise < IJsonLdContextNormalizedRaw > {
599
- for ( const key of Object . keys ( context ) ) {
605
+ public async parseInnerContexts ( context : IJsonLdContextNormalizedRaw , options : IParseOptions , keys ?: string [ ] ) : Promise < IJsonLdContextNormalizedRaw > {
606
+ for ( const key of ( keys ?? Object . keys ( context ) ) ) {
600
607
const value = context [ key ] ;
601
608
if ( value && typeof value === 'object' ) {
602
609
if ( '@context' in value && value [ '@context' ] !== null && ! options . ignoreScopedContexts ) {
@@ -607,19 +614,17 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP
607
614
// https://w3c.github.io/json-ld-api/#h-note-10
608
615
if ( this . validateContext ) {
609
616
try {
610
- const parentContext = { ...context } ;
611
- parentContext [ key ] = { ...parentContext [ key ] } ;
617
+ const parentContext = { ...context , [ key ] : { ...context [ key ] } } ;
612
618
delete parentContext [ key ] [ '@context' ] ;
613
619
await this . parse ( value [ '@context' ] ,
614
620
{ ...options , external : false , parentContext, ignoreProtection : true , ignoreRemoteScopedContexts : true , ignoreScopedContexts : true } ) ;
615
621
} catch ( e ) {
616
622
throw new ErrorCoded ( e . message , ERROR_CODES . INVALID_SCOPED_CONTEXT ) ;
617
623
}
618
624
}
619
-
620
- value [ '@context' ] = ( await this . parse ( value [ '@context' ] ,
621
- { ...options , external : false , minimalProcessing : true , ignoreRemoteScopedContexts : true , parentContext : context } ) )
622
- . getContextRaw ( ) ;
625
+ context [ key ] = { ...value , '@context' : ( await this . parse ( value [ '@context' ] ,
626
+ { ...options , external : false , minimalProcessing : true , ignoreRemoteScopedContexts : true , parentContext : context } ) )
627
+ . getContextRaw ( ) }
623
628
}
624
629
}
625
630
}
@@ -632,18 +637,21 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP
632
637
* @param {IParseOptions } options Optional parsing options.
633
638
* @return {Promise<JsonLdContextNormalized> } A promise resolving to the context.
634
639
*/
640
+ public async parse ( context : JsonLdContext , options ?: IParseOptions ) : Promise < JsonLdContextNormalized >
635
641
public async parse ( context : JsonLdContext ,
636
- options : IParseOptions = { } ) : Promise < JsonLdContextNormalized > {
642
+ options : IParseOptions = { } ,
643
+ // These options are only for internal use on recursive calls and should not be used by
644
+ // libraries consuming this function
645
+ internalOptions : { skipValidation ?: boolean } = { } ) : Promise < JsonLdContextNormalized > {
637
646
const {
638
647
baseIRI,
639
- parentContext : parentContextInitial ,
648
+ parentContext,
640
649
external,
641
650
processingMode = ContextParser . DEFAULT_PROCESSING_MODE ,
642
651
normalizeLanguageTags,
643
652
ignoreProtection,
644
653
minimalProcessing,
645
654
} = options ;
646
- let parentContext = parentContextInitial ;
647
655
const remoteContexts = options . remoteContexts || { } ;
648
656
649
657
// Avoid remote context overflows
@@ -705,7 +713,11 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP
705
713
external : ! ! contextIris [ i ] || options . external ,
706
714
parentContext : accContext . getContextRaw ( ) ,
707
715
remoteContexts : contextIris [ i ] ? { ...remoteContexts , [ contextIris [ i ] ] : true } : remoteContexts ,
708
- } ) ) ,
716
+ } ,
717
+ // @ts -expect-error: This third argument causes a type error because we have hidden it from consumers
718
+ {
719
+ skipValidation : i < contexts . length - 1 ,
720
+ } ) ) ,
709
721
Promise . resolve ( new JsonLdContextNormalized ( parentContext || { } ) ) ) ;
710
722
711
723
// Override the base IRI if provided.
@@ -718,10 +730,7 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP
718
730
}
719
731
720
732
// Make a deep clone of the given context, to avoid modifying it.
721
- context = < IJsonLdContextNormalizedRaw > JSON . parse ( JSON . stringify ( context ) ) ; // No better way in JS at the moment.
722
- if ( parentContext && ! minimalProcessing ) {
723
- parentContext = < IJsonLdContextNormalizedRaw > JSON . parse ( JSON . stringify ( parentContext ) ) ;
724
- }
733
+ context = < IJsonLdContextNormalizedRaw > { ...context } ;
725
734
726
735
// According to the JSON-LD spec, @base must be ignored from external contexts.
727
736
if ( external ) {
@@ -760,46 +769,55 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP
760
769
}
761
770
762
771
this . applyScopedProtected ( importContext , { processingMode } , defaultExpandOptions ) ;
763
- let newContext : IJsonLdContextNormalizedRaw = { ...importContext , ...context } ;
772
+
773
+ const newContext : IJsonLdContextNormalizedRaw = Object . assign ( importContext , context ) ;
774
+
775
+ // Handle terms (before protection checks)
776
+ this . idifyReverseTerms ( newContext ) ;
777
+ this . normalize ( newContext , { processingMode, normalizeLanguageTags } ) ;
778
+ this . applyScopedProtected ( newContext , { processingMode } , defaultExpandOptions ) ;
779
+
780
+ const keys = Object . keys ( newContext ) ;
781
+
782
+ const overlappingKeys : string [ ] = [ ] ;
764
783
if ( typeof parentContext === 'object' ) {
765
784
// Merge different parts of the final context in order
766
- this . applyScopedProtected ( newContext , { processingMode } , defaultExpandOptions ) ;
767
- newContext = { ...parentContext , ...newContext } ;
785
+ for ( const key in parentContext ) {
786
+ if ( key in newContext ) {
787
+ overlappingKeys . push ( key ) ;
788
+ } else {
789
+ newContext [ key ] = parentContext [ key ] ;
790
+ }
791
+ }
768
792
}
769
793
770
- const newContextWrapped = new JsonLdContextNormalized ( newContext ) ;
771
-
772
794
// Parse inner contexts with minimal processing
773
- await this . parseInnerContexts ( newContext , options ) ;
795
+ await this . parseInnerContexts ( newContext , options , keys ) ;
796
+
797
+ const newContextWrapped = new JsonLdContextNormalized ( newContext ) ;
774
798
775
799
// In JSON-LD 1.1, @vocab can be relative to @vocab in the parent context, or a compact IRI.
776
800
if ( ( newContext && newContext [ '@version' ] || ContextParser . DEFAULT_PROCESSING_MODE ) >= 1.1
777
801
&& ( ( context [ '@vocab' ] && typeof context [ '@vocab' ] === 'string' ) || context [ '@vocab' ] === '' ) ) {
778
802
if ( parentContext && '@vocab' in parentContext && context [ '@vocab' ] . indexOf ( ':' ) < 0 ) {
779
803
newContext [ '@vocab' ] = parentContext [ '@vocab' ] + context [ '@vocab' ] ;
780
- } else {
781
- if ( Util . isCompactIri ( context [ '@vocab' ] ) || context [ '@vocab' ] in newContextWrapped . getContextRaw ( ) ) {
804
+ } else if ( Util . isCompactIri ( context [ '@vocab' ] ) || context [ '@vocab' ] in newContext ) {
782
805
// @vocab is a compact IRI or refers exactly to a prefix
783
- newContext [ '@vocab' ] = newContextWrapped . expandTerm ( context [ '@vocab' ] , true ) ;
784
- }
806
+ newContext [ '@vocab' ] = newContextWrapped . expandTerm ( context [ '@vocab' ] , true ) ;
807
+
785
808
}
786
809
}
787
810
788
- // Handle terms (before protection checks)
789
- this . idifyReverseTerms ( newContext ) ;
790
- this . expandPrefixedTerms ( newContextWrapped , this . expandContentTypeToBase ) ;
811
+ this . expandPrefixedTerms ( newContextWrapped , this . expandContentTypeToBase , keys ) ;
791
812
792
813
// In JSON-LD 1.1, check if we are not redefining any protected keywords
793
814
if ( ! ignoreProtection && parentContext && processingMode >= 1.1 ) {
794
- this . validateKeywordRedefinitions ( parentContext , newContext , defaultExpandOptions ) ;
815
+ this . validateKeywordRedefinitions ( parentContext , newContext , defaultExpandOptions , overlappingKeys ) ;
795
816
}
796
817
797
- this . normalize ( newContext , { processingMode, normalizeLanguageTags } ) ;
798
- this . applyScopedProtected ( newContext , { processingMode } , defaultExpandOptions ) ;
799
- if ( this . validateContext ) {
818
+ if ( this . validateContext && ! internalOptions . skipValidation ) {
800
819
this . validate ( newContext , { processingMode } ) ;
801
820
}
802
-
803
821
return newContextWrapped ;
804
822
} else {
805
823
throw new ErrorCoded ( `Tried parsing a context that is not a string, array or object, but got ${ context } ` ,
@@ -816,7 +834,7 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP
816
834
// First try to retrieve the context from cache
817
835
const cached = this . documentCache [ url ] ;
818
836
if ( cached ) {
819
- return typeof cached === 'string' ? cached : Array . isArray ( cached ) ? cached . slice ( ) : { ... cached } ;
837
+ return cached ;
820
838
}
821
839
822
840
// If not in cache, load it
@@ -863,8 +881,8 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP
863
881
* @param importContextIri The full URI of an @import value.
864
882
*/
865
883
public async loadImportContext ( importContextIri : string ) : Promise < IJsonLdContextNormalizedRaw > {
866
- // Load the context
867
- const importContext = await this . load ( importContextIri ) ;
884
+ // Load the context - and do a deep clone since we are about to mutate it
885
+ let importContext = await this . load ( importContextIri ) ;
868
886
869
887
// Require the context to be a non-array object
870
888
if ( typeof importContext !== 'object' || Array . isArray ( importContext ) ) {
@@ -877,6 +895,7 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP
877
895
throw new ErrorCoded ( 'An imported context can not import another context: ' + importContextIri ,
878
896
ERROR_CODES . INVALID_CONTEXT_ENTRY ) ;
879
897
}
898
+ importContext = { ...importContext } ;
880
899
881
900
// Containers have to be converted into hash values the same way as for the importing context
882
901
// Otherwise context validation will fail for container values
@@ -972,4 +991,3 @@ export interface IParseOptions {
972
991
*/
973
992
ignoreScopedContexts ?: boolean ;
974
993
}
975
-
0 commit comments