@@ -7,9 +7,6 @@ import {IJsonLdContext, IJsonLdContextNormalizedRaw, IPrefixValue, JsonLdContext
77import { JsonLdContextNormalized , defaultExpandOptions , IExpandOptions } from "./JsonLdContextNormalized" ;
88import { Util } from "./Util" ;
99
10- // tslint:disable-next-line:no-var-requires
11- const canonicalizeJson = require ( 'canonicalize' ) ;
12-
1310/**
1411 * Parses JSON-LD contexts.
1512 */
@@ -93,13 +90,14 @@ export class ContextParser {
9390 */
9491 public idifyReverseTerms ( context : IJsonLdContextNormalizedRaw ) : IJsonLdContextNormalizedRaw {
9592 for ( const key of Object . keys ( context ) ) {
96- const value : IPrefixValue = context [ key ] ;
93+ let value = context [ key ] ;
9794 if ( value && typeof value === 'object' ) {
9895 if ( value [ '@reverse' ] && ! value [ '@id' ] ) {
9996 if ( typeof value [ '@reverse' ] !== 'string' || Util . isValidKeyword ( value [ '@reverse' ] ) ) {
10097 throw new ErrorCoded ( `Invalid @reverse value, must be absolute IRI or blank node: '${ value [ '@reverse' ] } '` ,
10198 ERROR_CODES . INVALID_IRI_MAPPING ) ;
10299 }
100+ value = context [ key ] = { ...value , '@id' : value [ '@reverse' ] } ;
103101 value [ '@id' ] = < string > value [ '@reverse' ] ;
104102 if ( Util . isPotentialKeyword ( value [ '@reverse' ] ) ) {
105103 delete value [ '@reverse' ] ;
@@ -118,10 +116,12 @@ export class ContextParser {
118116 * @param {IJsonLdContextNormalizedRaw } context A context.
119117 * @param {boolean } expandContentTypeToBase If @type inside the context may be expanded
120118 * 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.
121121 */
122- public expandPrefixedTerms ( context : JsonLdContextNormalized , expandContentTypeToBase : boolean ) {
122+ public expandPrefixedTerms ( context : JsonLdContextNormalized , expandContentTypeToBase : boolean , keys ?: string [ ] ) {
123123 const contextRaw = context . getContextRaw ( ) ;
124- for ( const key of Object . keys ( contextRaw ) ) {
124+ for ( const key of ( keys || Object . keys ( contextRaw ) ) ) {
125125 // Only expand allowed keys
126126 if ( Util . EXPAND_KEYS_BLACKLIST . indexOf ( key ) < 0 && ! Util . isReservedInternalKeyword ( key ) ) {
127127 // 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
162162 if ( '@id' in value ) {
163163 // Use @id value for expansion
164164 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 ) } ;
166166 changed = changed || id !== contextRaw [ key ] [ '@id' ] ;
167167 }
168168 } else if ( ! Util . isPotentialKeyword ( key ) && canAddIdEntry ) {
169169 // Add an explicit @id value based on the expanded key value
170170 const newId = context . expandTerm ( key , true ) ;
171171 if ( newId !== key ) {
172172 // Don't set @id if expansion failed
173- contextRaw [ key ] [ '@id' ] = newId ;
173+ contextRaw [ key ] = { ... contextRaw [ key ] , '@id' : newId } ;
174174 changed = true ;
175175 }
176176 }
177177 if ( type && typeof type === 'string' && type !== '@vocab'
178178 && ( ! value [ '@container' ] || ! ( < any > value [ '@container' ] ) [ '@type' ] )
179179 && canAddIdEntry ) {
180180 // 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 } ;
184188 }
185- changed = changed || type !== contextRaw [ key ] [ '@type' ] ;
186189 }
187190 }
188191 if ( ! changed ) {
@@ -209,7 +212,10 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR
209212 const value = context [ key ] ;
210213 if ( value && typeof value === 'object' ) {
211214 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+ }
213219 }
214220 }
215221 }
@@ -226,13 +232,13 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR
226232 const value = context [ key ] ;
227233 if ( value && typeof value === 'object' ) {
228234 if ( typeof value [ '@container' ] === 'string' ) {
229- value [ '@container' ] = { [ value [ '@container' ] ] : true } ;
235+ context [ key ] = { ... value , '@container' : { [ value [ '@container' ] ] : true } } ;
230236 } else if ( Array . isArray ( value [ '@container' ] ) ) {
231237 const newValue : { [ key : string ] : boolean } = { } ;
232238 for ( const containerValue of value [ '@container' ] ) {
233239 newValue [ containerValue ] = true ;
234240 }
235- value [ '@container' ] = newValue ;
241+ context [ key ] = { ... value , '@container' : newValue } ;
236242 }
237243 }
238244 }
@@ -256,7 +262,7 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR
256262 if ( value && typeof value === 'object' ) {
257263 if ( ! ( '@protected' in context [ key ] ) ) {
258264 // 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 } ;
260266 }
261267 } else {
262268 // 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
265271 '@protected' : true ,
266272 } ;
267273 if ( Util . isSimpleTermDefinitionPrefix ( value , expandOptions ) ) {
268- context [ key ] [ '@prefix' ] = true
274+ context [ key ] = { ... context [ key ] , '@prefix' : true } ;
269275 }
270276 }
271277 }
@@ -280,29 +286,29 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR
280286 * @param {IJsonLdContextNormalizedRaw } contextBefore The context that may contain some protected terms.
281287 * @param {IJsonLdContextNormalizedRaw } contextAfter A new context that is being applied on the first one.
282288 * @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.
283291 */
284292 public validateKeywordRedefinitions ( contextBefore : IJsonLdContextNormalizedRaw ,
285293 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 ) ) ) {
288297 if ( Util . isTermProtected ( contextBefore , key ) ) {
289298 // The entry in the context before will always be in object-mode
290299 // If the new entry is in string-mode, convert it to object-mode
291300 // before checking if it is identical.
292301 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 {
298304 // We modify this deliberately,
299305 // as we need it for the value comparison (they must be identical modulo '@protected')),
300306 // 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+ }
303309
304310 // Error if they are not identical
305- if ( valueBefore !== valueAfter ) {
311+ if ( ! Util . deepEqual ( contextBefore [ key ] , contextAfter [ key ] ) ) {
306312 throw new ErrorCoded ( `Attempted to override the protected keyword ${ key } from ${
307313 JSON . stringify ( Util . getContextValueId ( contextBefore [ key ] ) ) } to ${
308314 JSON . stringify ( Util . getContextValueId ( contextAfter [ key ] ) ) } `,
@@ -593,10 +599,11 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP
593599 * @param {IJsonLdContextNormalizedRaw } context A context.
594600 * @param {IParseOptions } options Parsing options.
595601 * @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.
596604 */
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 ) ) ) {
600607 const value = context [ key ] ;
601608 if ( value && typeof value === 'object' ) {
602609 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
607614 // https://w3c.github.io/json-ld-api/#h-note-10
608615 if ( this . validateContext ) {
609616 try {
610- const parentContext = { ...context } ;
611- parentContext [ key ] = { ...parentContext [ key ] } ;
617+ const parentContext = { ...context , [ key ] : { ...context [ key ] } } ;
612618 delete parentContext [ key ] [ '@context' ] ;
613619 await this . parse ( value [ '@context' ] ,
614620 { ...options , external : false , parentContext, ignoreProtection : true , ignoreRemoteScopedContexts : true , ignoreScopedContexts : true } ) ;
615621 } catch ( e ) {
616622 throw new ErrorCoded ( e . message , ERROR_CODES . INVALID_SCOPED_CONTEXT ) ;
617623 }
618624 }
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 ( ) }
623628 }
624629 }
625630 }
@@ -632,18 +637,21 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP
632637 * @param {IParseOptions } options Optional parsing options.
633638 * @return {Promise<JsonLdContextNormalized> } A promise resolving to the context.
634639 */
640+ public async parse ( context : JsonLdContext , options ?: IParseOptions ) : Promise < JsonLdContextNormalized >
635641 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 > {
637646 const {
638647 baseIRI,
639- parentContext : parentContextInitial ,
648+ parentContext,
640649 external,
641650 processingMode = ContextParser . DEFAULT_PROCESSING_MODE ,
642651 normalizeLanguageTags,
643652 ignoreProtection,
644653 minimalProcessing,
645654 } = options ;
646- let parentContext = parentContextInitial ;
647655 const remoteContexts = options . remoteContexts || { } ;
648656
649657 // Avoid remote context overflows
@@ -705,7 +713,11 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP
705713 external : ! ! contextIris [ i ] || options . external ,
706714 parentContext : accContext . getContextRaw ( ) ,
707715 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+ } ) ) ,
709721 Promise . resolve ( new JsonLdContextNormalized ( parentContext || { } ) ) ) ;
710722
711723 // Override the base IRI if provided.
@@ -718,10 +730,7 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP
718730 }
719731
720732 // 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 } ;
725734
726735 // According to the JSON-LD spec, @base must be ignored from external contexts.
727736 if ( external ) {
@@ -760,46 +769,55 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP
760769 }
761770
762771 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 [ ] = [ ] ;
764783 if ( typeof parentContext === 'object' ) {
765784 // 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+ }
768792 }
769793
770- const newContextWrapped = new JsonLdContextNormalized ( newContext ) ;
771-
772794 // 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 ) ;
774798
775799 // In JSON-LD 1.1, @vocab can be relative to @vocab in the parent context, or a compact IRI.
776800 if ( ( newContext && newContext [ '@version' ] || ContextParser . DEFAULT_PROCESSING_MODE ) >= 1.1
777801 && ( ( context [ '@vocab' ] && typeof context [ '@vocab' ] === 'string' ) || context [ '@vocab' ] === '' ) ) {
778802 if ( parentContext && '@vocab' in parentContext && context [ '@vocab' ] . indexOf ( ':' ) < 0 ) {
779803 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 ) {
782805 // @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+
785808 }
786809 }
787810
788- // Handle terms (before protection checks)
789- this . idifyReverseTerms ( newContext ) ;
790- this . expandPrefixedTerms ( newContextWrapped , this . expandContentTypeToBase ) ;
811+ this . expandPrefixedTerms ( newContextWrapped , this . expandContentTypeToBase , keys ) ;
791812
792813 // In JSON-LD 1.1, check if we are not redefining any protected keywords
793814 if ( ! ignoreProtection && parentContext && processingMode >= 1.1 ) {
794- this . validateKeywordRedefinitions ( parentContext , newContext , defaultExpandOptions ) ;
815+ this . validateKeywordRedefinitions ( parentContext , newContext , defaultExpandOptions , overlappingKeys ) ;
795816 }
796817
797- this . normalize ( newContext , { processingMode, normalizeLanguageTags } ) ;
798- this . applyScopedProtected ( newContext , { processingMode } , defaultExpandOptions ) ;
799- if ( this . validateContext ) {
818+ if ( this . validateContext && ! internalOptions . skipValidation ) {
800819 this . validate ( newContext , { processingMode } ) ;
801820 }
802-
803821 return newContextWrapped ;
804822 } else {
805823 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
816834 // First try to retrieve the context from cache
817835 const cached = this . documentCache [ url ] ;
818836 if ( cached ) {
819- return typeof cached === 'string' ? cached : Array . isArray ( cached ) ? cached . slice ( ) : { ... cached } ;
837+ return cached ;
820838 }
821839
822840 // If not in cache, load it
@@ -863,8 +881,8 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP
863881 * @param importContextIri The full URI of an @import value.
864882 */
865883 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 ) ;
868886
869887 // Require the context to be a non-array object
870888 if ( typeof importContext !== 'object' || Array . isArray ( importContext ) ) {
@@ -877,6 +895,7 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP
877895 throw new ErrorCoded ( 'An imported context can not import another context: ' + importContextIri ,
878896 ERROR_CODES . INVALID_CONTEXT_ENTRY ) ;
879897 }
898+ importContext = { ...importContext } ;
880899
881900 // Containers have to be converted into hash values the same way as for the importing context
882901 // Otherwise context validation will fail for container values
@@ -972,4 +991,3 @@ export interface IParseOptions {
972991 */
973992 ignoreScopedContexts ?: boolean ;
974993}
975-
0 commit comments