Skip to main content

oxjsonld/
context.rs

1use crate::error::{JsonLdErrorCode, JsonLdSyntaxError};
2use crate::{JsonLdProcessingMode, JsonLdProfile, JsonLdProfileSet};
3use json_event_parser::{JsonEvent, JsonSyntaxError, SliceJsonParser};
4use oxiri::Iri;
5use std::borrow::Cow;
6use std::collections::HashMap;
7use std::error::Error;
8use std::panic::{RefUnwindSafe, UnwindSafe};
9use std::slice;
10use std::sync::{Arc, Mutex};
11
12type LoadDocumentCallback = dyn Fn(
13        &str,
14        &JsonLdLoadDocumentOptions,
15    ) -> Result<JsonLdRemoteDocument, Box<dyn Error + Send + Sync>>
16    + Send
17    + Sync
18    + UnwindSafe
19    + RefUnwindSafe;
20
21#[derive(Eq, PartialEq, Debug, Clone)]
22pub enum JsonNode {
23    String(String),
24    Number(String),
25    Boolean(bool),
26    Null,
27    Array(Vec<JsonNode>),
28    Object(HashMap<String, JsonNode>),
29}
30
31#[derive(Default, Clone)]
32pub struct JsonLdContext {
33    pub base_iri: Option<Iri<String>>,
34    pub original_base_url: Option<Iri<String>>,
35    pub vocabulary_mapping: Option<String>,
36    pub default_language: Option<String>,
37    pub default_direction: Option<&'static str>,
38    pub term_definitions: HashMap<String, JsonLdTermDefinition>,
39    pub previous_context: Option<Arc<JsonLdContext>>,
40}
41
42impl JsonLdContext {
43    pub fn new_empty(original_base_url: Option<Iri<String>>) -> Self {
44        JsonLdContext {
45            base_iri: original_base_url.clone(),
46            original_base_url,
47            vocabulary_mapping: None,
48            default_language: None,
49            default_direction: None,
50            term_definitions: HashMap::new(),
51            previous_context: None,
52        }
53    }
54}
55
56#[derive(Clone)]
57pub struct JsonLdTermDefinition {
58    // In the fields, None is unset Some(None) is set to null
59    pub iri_mapping: Option<Option<String>>,
60    pub prefix_flag: bool,
61    pub protected: bool,
62    pub reverse_property: bool,
63    pub base_url: Option<Iri<String>>,
64    pub context: Option<JsonNode>,
65    pub container_mapping: &'static [&'static str],
66    pub direction_mapping: Option<Option<&'static str>>,
67    pub index_mapping: Option<String>,
68    pub language_mapping: Option<Option<String>>,
69    pub nest_value: Option<String>,
70    pub type_mapping: Option<String>,
71}
72
73pub struct JsonLdContextProcessor {
74    pub processing_mode: JsonLdProcessingMode,
75    pub lenient: bool, // Custom option to ignore invalid base IRIs
76    pub max_context_recursion: usize,
77    pub remote_context_cache: Arc<Mutex<HashMap<String, (Option<Iri<String>>, JsonNode)>>>,
78    pub load_document_callback: Option<Arc<LoadDocumentCallback>>,
79}
80
81/// Used to pass various options to the LoadDocumentCallback.
82pub struct JsonLdLoadDocumentOptions {
83    /// One or more IRIs to use in the request as a profile parameter.
84    pub request_profile: JsonLdProfileSet,
85}
86
87/// Returned information about a remote JSON-LD document or context.
88pub struct JsonLdRemoteDocument {
89    /// The retrieved document
90    pub document: Vec<u8>,
91    /// The final URL of the loaded document. This is important to handle HTTP redirects properly
92    pub document_url: String,
93}
94
95impl JsonLdContextProcessor {
96    /// [Context Processing Algorithm](https://www.w3.org/TR/json-ld-api/#algorithm)
97    pub fn process_context(
98        &self,
99        active_context: &JsonLdContext,
100        local_context: JsonNode,
101        base_url: Option<&Iri<String>>,
102        remote_contexts: &mut Vec<String>,
103        override_protected: bool,
104        mut propagate: bool,
105        validate_scoped_context: bool,
106        errors: &mut Vec<JsonLdSyntaxError>,
107    ) -> JsonLdContext {
108        // 1)
109        let mut result = active_context.clone();
110        // 2)
111        if let JsonNode::Object(local_context) = &local_context {
112            if let Some(propagate_node) = local_context.get("@propagate") {
113                if let JsonNode::Boolean(new) = propagate_node {
114                    propagate = *new;
115                } else {
116                    errors.push(JsonLdSyntaxError::msg_and_code(
117                        "@propagate value must be a boolean",
118                        JsonLdErrorCode::InvalidPropagateValue,
119                    ))
120                }
121            }
122        }
123        // 3)
124        if !propagate && result.previous_context.is_none() {
125            result.previous_context = Some(Arc::new(active_context.clone()));
126        }
127        // 4)
128        let local_context = if let JsonNode::Array(c) = local_context {
129            c
130        } else {
131            vec![local_context]
132        };
133        // 5)
134        for context in local_context {
135            let mut context = match context {
136                // 5.1)
137                JsonNode::Null => {
138                    // 5.1.1)
139                    if !override_protected {
140                        for (name, def) in &active_context.term_definitions {
141                            if def.protected {
142                                errors.push(JsonLdSyntaxError::msg_and_code(format!("Definition of {name} will be overridden even if it's protected"), JsonLdErrorCode::InvalidContextNullification));
143                            }
144                        }
145                    }
146                    // 5.1.2)
147                    let mut new_result =
148                        JsonLdContext::new_empty(active_context.original_base_url.clone());
149                    if !propagate {
150                        new_result.previous_context = Some(Arc::new(result));
151                    }
152                    result = new_result;
153                    // 5.1.3)
154                    continue;
155                }
156                // 5.2)
157                JsonNode::String(context) => {
158                    // 5.2.1)
159                    let context = match if let Some(base_url) = base_url {
160                        base_url.resolve(&context)
161                    } else {
162                        Iri::parse(context.clone())
163                    } {
164                        Ok(url) => url.into_inner(),
165                        Err(e) => {
166                            errors.push(JsonLdSyntaxError::msg_and_code(
167                                format!("Invalid remote context URL '{context}': {e}"),
168                                JsonLdErrorCode::LoadingDocumentFailed,
169                            ));
170                            continue;
171                        }
172                    };
173                    // 5.2.2)
174                    if !validate_scoped_context && remote_contexts.contains(&context) {
175                        continue;
176                    }
177                    // 5.2.3)
178                    if remote_contexts.len() >= self.max_context_recursion {
179                        errors.push(JsonLdSyntaxError::msg_and_code(
180                            format!(
181                                "This processor only allows {} remote context, threshold exceeded",
182                                self.max_context_recursion
183                            ),
184                            JsonLdErrorCode::ContextOverflow,
185                        ));
186                        continue;
187                    }
188                    remote_contexts.push(context.clone());
189                    let (loaded_context_base, loaded_context_content) =
190                        match self.load_remote_context(&context) {
191                            Ok(r) => r,
192                            Err(e) => {
193                                errors.push(e);
194                                continue;
195                            }
196                        };
197                    // 5.2.6)
198                    result = self.process_context(
199                        &result,
200                        loaded_context_content,
201                        loaded_context_base.as_ref(),
202                        remote_contexts,
203                        false,
204                        true,
205                        validate_scoped_context,
206                        errors,
207                    );
208                    assert_eq!(
209                        remote_contexts.pop(),
210                        Some(context),
211                        "The remote context stack is invalid"
212                    );
213                    continue;
214                }
215                // 5.3)
216                JsonNode::Array(_) | JsonNode::Number(_) | JsonNode::Boolean(_) => {
217                    errors.push(JsonLdSyntaxError::msg_and_code(
218                        "@context value must be null, a string or an object",
219                        JsonLdErrorCode::InvalidLocalContext,
220                    ));
221                    continue;
222                }
223                // 5.4)
224                JsonNode::Object(context) => context,
225            };
226            // 5.5)
227            if let Some(value) = context.remove("@version") {
228                // 5.5.1)
229                if let JsonNode::Number(version) = value {
230                    if version != "1.1" {
231                        errors.push(JsonLdSyntaxError::msg_and_code(
232                            format!("The only supported @version value is 1.1, found {version}"),
233                            JsonLdErrorCode::InvalidVersionValue,
234                        ));
235                    }
236                } else {
237                    errors.push(JsonLdSyntaxError::msg_and_code(
238                        "@version value must be a number",
239                        JsonLdErrorCode::InvalidVersionValue,
240                    ));
241                }
242                // 5.5.2)
243                if self.processing_mode == JsonLdProcessingMode::JsonLd1_0 {
244                    errors.push(JsonLdSyntaxError::msg_and_code(
245                        "@version is only supported in JSON-LD 1.1",
246                        JsonLdErrorCode::ProcessingModeConflict,
247                    ));
248                }
249            }
250            // 5.6)
251            if let Some(value) = context.remove("@import") {
252                // 5.6.1)
253                if self.processing_mode == JsonLdProcessingMode::JsonLd1_0 {
254                    errors.push(JsonLdSyntaxError::msg_and_code(
255                        "@import is only supported in JSON-LD 1.1",
256                        JsonLdErrorCode::InvalidContextEntry,
257                    ));
258                }
259                // 5.6.2)
260                let JsonNode::String(import) = value else {
261                    errors.push(JsonLdSyntaxError::msg_and_code(
262                        "@import must be a string",
263                        JsonLdErrorCode::InvalidImportValue,
264                    ));
265                    continue;
266                };
267                // 5.6.3)
268                let import = match if let Some(base_url) = base_url {
269                    base_url.resolve(&import)
270                } else {
271                    Iri::parse(import.clone())
272                } {
273                    Ok(import) => import,
274                    Err(e) => {
275                        errors.push(JsonLdSyntaxError::msg_and_code(
276                            format!("Invalid @import iri {import}: {e}"),
277                            JsonLdErrorCode::InvalidImportValue,
278                        ));
279                        continue;
280                    }
281                };
282                // 5.6.4)
283                let (_, loaded_context_content) = match self.load_remote_context(&import) {
284                    Ok(r) => r,
285                    Err(e) => {
286                        errors.push(e);
287                        continue;
288                    }
289                };
290                // 5.6.6)
291                let JsonNode::Object(loaded_context_content) = loaded_context_content else {
292                    errors.push(JsonLdSyntaxError::msg_and_code(
293                        format!("Imported context {import} must be an object"),
294                        JsonLdErrorCode::InvalidRemoteContext,
295                    ));
296                    continue;
297                };
298                // 5.6.7)
299                if loaded_context_content.contains_key("@import") {
300                    errors.push(JsonLdSyntaxError::msg_and_code(
301                        format!("Imported context {import} must not contain an @import key"),
302                        JsonLdErrorCode::InvalidContextEntry,
303                    ));
304                    continue;
305                }
306                // 5.6.8)
307                for (key, value) in loaded_context_content {
308                    context.entry(key).or_insert(value);
309                }
310            }
311            // 5.7)
312            if let Some(value) = context.remove("@base") {
313                if remote_contexts.is_empty() {
314                    match value {
315                        // 5.7.2)
316                        JsonNode::Null => {
317                            result.base_iri = None;
318                        }
319                        // 5.7.3) and 5.7.4)
320                        JsonNode::String(value) => {
321                            if self.lenient {
322                                result.base_iri = Some(if let Some(base_iri) = &result.base_iri {
323                                    base_iri.resolve_unchecked(&value)
324                                } else {
325                                    Iri::parse_unchecked(value.clone())
326                                })
327                            } else {
328                                match if let Some(base_iri) = &result.base_iri {
329                                    base_iri.resolve(&value)
330                                } else {
331                                    Iri::parse(value.clone())
332                                } {
333                                    Ok(iri) => result.base_iri = Some(iri),
334                                    Err(e) => errors.push(JsonLdSyntaxError::msg_and_code(
335                                        format!("Invalid @base '{value}': {e}"),
336                                        JsonLdErrorCode::InvalidBaseIri,
337                                    )),
338                                }
339                            }
340                        }
341                        _ => errors.push(JsonLdSyntaxError::msg_and_code(
342                            "@base value must be a string",
343                            JsonLdErrorCode::InvalidBaseIri,
344                        )),
345                    }
346                }
347            }
348            // 5.8)
349            if let Some(value) = context.remove("@vocab") {
350                match value {
351                    // 5.8.2)
352                    JsonNode::Null => {
353                        result.vocabulary_mapping = None;
354                    }
355                    // 5.8.3)
356                    JsonNode::String(value) => {
357                        if let Some(vocab) = self
358                            .expand_iri(
359                                &mut result,
360                                value.as_str().into(),
361                                true,
362                                true,
363                                None,
364                                &mut HashMap::new(),
365                                errors,
366                            )
367                            .filter(|iri| !has_keyword_form(iri))
368                        {
369                            result.vocabulary_mapping = Some(vocab.into());
370                        } else {
371                            errors.push(JsonLdSyntaxError::msg_and_code(
372                                format!("Invalid @vocab '{value}'"),
373                                JsonLdErrorCode::InvalidVocabMapping,
374                            ));
375                        }
376                    }
377                    _ => errors.push(JsonLdSyntaxError::msg_and_code(
378                        "@vocab value must be a string",
379                        JsonLdErrorCode::InvalidVocabMapping,
380                    )),
381                }
382            }
383            // 5.9)
384            if let Some(value) = context.remove("@language") {
385                match value {
386                    // 5.9.2)
387                    JsonNode::Null => {
388                        result.default_language = None;
389                    }
390                    // 5.9.3)
391                    JsonNode::String(value) => result.default_language = Some(value),
392                    _ => errors.push(JsonLdSyntaxError::msg_and_code(
393                        "@language value must be a string or null",
394                        JsonLdErrorCode::InvalidDefaultLanguage,
395                    )),
396                }
397            }
398            // 5.10)
399            if let Some(value) = context.remove("@direction") {
400                // 5.10.1)
401                if self.processing_mode == JsonLdProcessingMode::JsonLd1_0 {
402                    errors.push(JsonLdSyntaxError::msg_and_code(
403                        "@direction is only supported in JSON-LD 1.1",
404                        JsonLdErrorCode::InvalidContextEntry,
405                    ));
406                }
407                match value {
408                    // 5.10.3)
409                    JsonNode::Null => {
410                        result.default_direction = None;
411                    }
412                    // 5.10.4)
413                    JsonNode::String(value) => match value.as_str() {
414                        "ltr" => result.default_direction = Some("ltr"),
415                        "rtl" => result.default_direction = Some("rtl"),
416                        _ => errors.push(JsonLdSyntaxError::msg_and_code(
417                            format!("@direction value must be 'ltr' or 'rtl', found '{value}'"),
418                            JsonLdErrorCode::InvalidBaseDirection,
419                        )),
420                    },
421                    _ => errors.push(JsonLdSyntaxError::msg_and_code(
422                        "@direction value must be a string or null",
423                        JsonLdErrorCode::InvalidBaseDirection,
424                    )),
425                }
426            }
427            // 5.11)
428            if let Some(value) = context.remove("@propagate") {
429                // 5.11.1)
430                if self.processing_mode == JsonLdProcessingMode::JsonLd1_0 {
431                    errors.push(JsonLdSyntaxError::msg_and_code(
432                        "@propagate is only supported in JSON-LD 1.1",
433                        JsonLdErrorCode::InvalidContextEntry,
434                    ));
435                }
436                // 5.11.2)
437                if !matches!(value, JsonNode::Boolean(_)) {
438                    errors.push(JsonLdSyntaxError::msg_and_code(
439                        "@propagate value must be a boolean",
440                        JsonLdErrorCode::InvalidPropagateValue,
441                    ));
442                    continue;
443                }
444            }
445            // 5.13)
446            let mut protected = false;
447            if let Some(value) = context.remove("@protected") {
448                if self.processing_mode == JsonLdProcessingMode::JsonLd1_0 {
449                    errors.push(JsonLdSyntaxError::msg_and_code(
450                        "@protected is only supported in JSON-LD 1.1",
451                        JsonLdErrorCode::InvalidContextEntry,
452                    ));
453                }
454                if let JsonNode::Boolean(value) = value {
455                    protected = value
456                } else {
457                    errors.push(JsonLdSyntaxError::msg_and_code(
458                        "@protected value must be a boolean",
459                        JsonLdErrorCode::InvalidProtectedValue,
460                    ))
461                }
462            }
463            let mut defined = HashMap::new();
464            for term in context.keys() {
465                self.create_term_definition(
466                    &mut result,
467                    &context,
468                    term,
469                    &mut defined,
470                    base_url,
471                    protected,
472                    override_protected,
473                    remote_contexts,
474                    errors,
475                )
476            }
477        }
478        // 6)
479        result
480    }
481
482    /// [Create Term Definition](https://www.w3.org/TR/json-ld-api/#create-term-definition)
483    fn create_term_definition(
484        &self,
485        active_context: &mut JsonLdContext,
486        local_context: &HashMap<String, JsonNode>,
487        term: &str,
488        defined: &mut HashMap<String, bool>,
489        base_url: Option<&Iri<String>>,
490        protected: bool,
491        override_protected: bool,
492        remote_contexts: &mut Vec<String>,
493        errors: &mut Vec<JsonLdSyntaxError>,
494    ) {
495        // 1)
496        if let Some(defined_value) = defined.get(term) {
497            if !defined_value {
498                errors.push(JsonLdSyntaxError::msg_and_code(
499                    "Cyclic IRI mapping",
500                    JsonLdErrorCode::CyclicIriMapping,
501                ))
502            }
503            return;
504        }
505        // 2)
506        if term.is_empty() {
507            errors.push(JsonLdSyntaxError::msg_and_code(
508                "@context terms must not be the empty strings",
509                JsonLdErrorCode::InvalidTermDefinition,
510            ));
511            return;
512        }
513        defined.insert(term.into(), false);
514        // 3)
515        let Some(value) = local_context.get(term) else {
516            unreachable!();
517        };
518        // 4)
519        if term == "@type" {
520            if self.processing_mode == JsonLdProcessingMode::JsonLd1_0 {
521                errors.push(JsonLdSyntaxError::msg_and_code(
522                    "@type keyword can't be redefined in JSON-LD 1.0 @context",
523                    JsonLdErrorCode::KeywordRedefinition,
524                ));
525            }
526            if let JsonNode::Object(value) = value {
527                if value.is_empty() {
528                    errors.push(JsonLdSyntaxError::msg_and_code(
529                        "@type keyword definition can't be empty",
530                        JsonLdErrorCode::KeywordRedefinition,
531                    ));
532                    return;
533                }
534                for (key, key_value) in value {
535                    match key.as_str() {
536                        "@protected" => (),
537                        "@container" => match key_value {
538                            JsonNode::String(s) if s == "@set" => (),
539                            JsonNode::Array(s)
540                                if s.iter().all(|v| {
541                                    if let JsonNode::String(s) = v {
542                                        s == "@set"
543                                    } else {
544                                        false
545                                    }
546                                }) => {}
547                            _ => {
548                                errors.push(JsonLdSyntaxError::msg_and_code(
549                                    "@type definition only allowed @container is @set",
550                                    JsonLdErrorCode::KeywordRedefinition,
551                                ));
552                                return;
553                            }
554                        },
555                        _ => {
556                            errors.push(JsonLdSyntaxError::msg_and_code(
557                                format!("@type definition can only contain @protected and @container keywords, {key} found"),
558                                JsonLdErrorCode::KeywordRedefinition,
559                            ));
560                            return;
561                        }
562                    }
563                }
564            } else {
565                errors.push(JsonLdSyntaxError::msg_and_code(
566                    "@type definition must be an object",
567                    JsonLdErrorCode::KeywordRedefinition,
568                ));
569                return;
570            }
571        } else if has_keyword_form(term) {
572            // 5)
573            if is_keyword(term) {
574                errors.push(JsonLdSyntaxError::msg_and_code(
575                    format!("{term} keyword can't be redefined in context"),
576                    JsonLdErrorCode::KeywordRedefinition,
577                ));
578            }
579            return;
580        }
581        // 6)
582        let previous_definition = active_context.term_definitions.remove(term);
583        let value = match value {
584            // 7)
585            JsonNode::Null => Cow::Owned([("@id".to_owned(), JsonNode::Null)].into()),
586            // 8)
587            JsonNode::String(id) => {
588                Cow::Owned([("@id".to_owned(), JsonNode::String(id.clone()))].into())
589            }
590            // 9)
591            JsonNode::Object(map) => Cow::Borrowed(map),
592            _ => {
593                errors.push(JsonLdSyntaxError::msg_and_code(
594                    "Term definition value must be null, a string or a map",
595                    JsonLdErrorCode::InvalidTermDefinition,
596                ));
597                return;
598            }
599        };
600        // 10)
601        let mut definition = JsonLdTermDefinition {
602            iri_mapping: None,
603            prefix_flag: false,
604            protected,
605            reverse_property: false,
606            base_url: None,
607            context: None,
608            container_mapping: &[],
609            direction_mapping: None,
610            index_mapping: None,
611            language_mapping: None,
612            nest_value: None,
613            type_mapping: None,
614        };
615        // 11)
616        if let Some(key_value) = value.get("@protected") {
617            if self.processing_mode == JsonLdProcessingMode::JsonLd1_0 {
618                errors.push(JsonLdSyntaxError::msg_and_code(
619                    "@protected keyword can't be used in JSON-LD 1.0 @context",
620                    JsonLdErrorCode::InvalidTermDefinition,
621                ));
622            }
623            let JsonNode::Boolean(key_value) = key_value else {
624                errors.push(JsonLdSyntaxError::msg_and_code(
625                    "@protected value must be a boolean",
626                    JsonLdErrorCode::InvalidProtectedValue,
627                ));
628                return;
629            };
630            definition.protected = *key_value;
631        }
632        // 12)
633        if let Some(key_value) = value.get("@type") {
634            // 12.1)
635            let JsonNode::String(r#type) = key_value else {
636                errors.push(JsonLdSyntaxError::msg_and_code(
637                    "The value of @type in a context must be a string",
638                    JsonLdErrorCode::InvalidTypeMapping,
639                ));
640                return;
641            };
642            // 12.2)
643            let Some(r#type) = self.expand_iri(
644                active_context,
645                r#type.as_str().into(),
646                false,
647                true,
648                Some(local_context),
649                defined,
650                errors,
651            ) else {
652                errors.push(JsonLdSyntaxError::msg_and_code(
653                    format!("Invalid @type value in context: {type}"),
654                    JsonLdErrorCode::InvalidTypeMapping,
655                ));
656                return;
657            };
658            // 12.3)
659            if matches!(r#type.as_ref(), "@json" | "@none")
660                && self.processing_mode == JsonLdProcessingMode::JsonLd1_0
661            {
662                errors.push(JsonLdSyntaxError::msg_and_code(
663                    format!("@type value {type} in a context is only supported in JSON-LD 1.1"),
664                    JsonLdErrorCode::InvalidTypeMapping,
665                ));
666            }
667            // 12.4)
668            let is_keyword = has_keyword_form(&r#type);
669            if is_keyword && !matches!(r#type.as_ref(), "@id" | "@json" | "@none" | "@vocab")
670                || r#type.starts_with("_:")
671            {
672                errors.push(JsonLdSyntaxError::msg_and_code(
673                    format!("Invalid @type value in context: {type}"),
674                    JsonLdErrorCode::InvalidTypeMapping,
675                ));
676            }
677            if !self.lenient && !is_keyword {
678                if let Err(e) = Iri::parse(r#type.as_ref()) {
679                    errors.push(JsonLdSyntaxError::msg_and_code(
680                        format!("Invalid @type iri '{type}': {e}"),
681                        JsonLdErrorCode::InvalidTypeMapping,
682                    ));
683                }
684            }
685            // 12.5)
686            definition.type_mapping = Some(r#type.into());
687        }
688        // 13)
689        if let Some(key_value) = value.get("@reverse") {
690            // 13.1)
691            if value.contains_key("@id") {
692                errors.push(JsonLdSyntaxError::msg_and_code(
693                    "@reverse and @id cannot be used together in a context",
694                    JsonLdErrorCode::InvalidReverseProperty,
695                ));
696                return;
697            }
698            if value.contains_key("@nest") {
699                errors.push(JsonLdSyntaxError::msg_and_code(
700                    "@reverse and @nest cannot be used together in a context",
701                    JsonLdErrorCode::InvalidReverseProperty,
702                ));
703                return;
704            }
705            // 13.2)
706            let JsonNode::String(key_value) = key_value else {
707                errors.push(JsonLdSyntaxError::msg_and_code(
708                    "@reverse value must be a string in a context",
709                    JsonLdErrorCode::InvalidIriMapping,
710                ));
711                return;
712            };
713            // 13.3)
714            if has_keyword_form(key_value) {
715                return;
716            }
717            // 13.4)
718            if let Some(iri) = self.expand_iri(
719                active_context,
720                key_value.into(),
721                false,
722                true,
723                Some(local_context),
724                defined,
725                errors,
726            ) {
727                if self.lenient && !has_keyword_form(&iri)
728                    || !self.lenient && (iri.starts_with("_:") || Iri::parse(iri.as_ref()).is_ok())
729                {
730                    definition.iri_mapping = Some(Some(iri.into()));
731                } else {
732                    errors.push(JsonLdSyntaxError::msg_and_code(
733                        format!("{iri} is not a valid IRI or blank node"),
734                        JsonLdErrorCode::InvalidIriMapping,
735                    ));
736                    definition.iri_mapping = Some(None);
737                }
738            } else {
739                definition.iri_mapping = Some(None);
740            }
741            definition.iri_mapping = Some(
742                self.expand_iri(
743                    active_context,
744                    key_value.into(),
745                    false,
746                    true,
747                    Some(local_context),
748                    defined,
749                    errors,
750                )
751                .map(Into::into),
752            );
753            // 13.5)
754            if let Some(container_entry) = value.get("@container") {
755                match container_entry {
756                    JsonNode::Null => (),
757                    JsonNode::String(container_entry) => {
758                        if !matches!(container_entry.as_str(), "@index" | "@set") {
759                            errors.push(JsonLdSyntaxError::msg_and_code(
760                                "@reverse is only compatible with @index and @set containers",
761                                JsonLdErrorCode::InvalidReverseProperty,
762                            ));
763                        }
764                    }
765                    _ => {
766                        errors.push(JsonLdSyntaxError::msg_and_code(
767                            "@container value must be a string or null",
768                            JsonLdErrorCode::InvalidReverseProperty,
769                        ));
770                    }
771                }
772            }
773            // 13.6)
774            definition.reverse_property = true;
775        } else if let Some(key_value) = value.get("@id").filter(|v| {
776            if let JsonNode::String(v) = v {
777                v != term
778            } else {
779                true
780            }
781        }) {
782            // 14)
783            match key_value {
784                // 14.1)
785                JsonNode::Null => {
786                    definition.iri_mapping = Some(None);
787                }
788                JsonNode::String(id) => {
789                    if id == term {
790                        return;
791                    }
792                    let Some(expanded) = self.expand_iri(
793                        active_context,
794                        id.into(),
795                        false,
796                        true,
797                        Some(local_context),
798                        defined,
799                        errors,
800                    ) else {
801                        // 14.2.2)
802                        definition.iri_mapping = Some(None);
803                        return;
804                    };
805                    // 14.2.3)
806                    if expanded == "@context" {
807                        errors.push(JsonLdSyntaxError::msg_and_code(
808                            "@context cannot be aliased with @id: @context",
809                            JsonLdErrorCode::InvalidKeywordAlias,
810                        ));
811                        return;
812                    }
813                    definition.iri_mapping = Some(Some(expanded.into()));
814                    // 14.2.4)
815                    if term
816                        .as_bytes()
817                        .get(1..term.len() - 1)
818                        .is_some_and(|t| t.contains(&b':'))
819                        || term.contains('/')
820                    {
821                        // 14.2.4.1)
822                        defined.insert(term.into(), true);
823                        // 14.2.4.2)
824                        let expended_term = self.expand_iri(
825                            active_context,
826                            term.into(),
827                            false,
828                            true,
829                            Some(local_context),
830                            defined,
831                            errors,
832                        );
833                        if expended_term.as_deref()
834                            != definition.iri_mapping.as_ref().and_then(|o| o.as_deref())
835                        {
836                            errors.push(JsonLdSyntaxError::msg_and_code(
837                                if let (Some(expended_term), Some(Some(iri_mapping))) = (&expended_term, &definition.iri_mapping) {
838                                    format!("Inconsistent expansion of {term} between {expended_term} and {iri_mapping}")
839                                } else {
840                                    format!("Inconsistent expansion of {term}")
841                                },
842                                JsonLdErrorCode::InvalidIriMapping,
843                            ))
844                        }
845                    }
846                    // 14.2.5)
847                    if !term.contains(':')
848                        && !term.contains('/')
849                        && definition.iri_mapping.as_ref().is_some_and(|iri| {
850                            iri.as_ref().is_some_and(|iri| {
851                                iri.ends_with(|c| {
852                                    matches!(c, ':' | '/' | '?' | '#' | '[' | ']' | '@')
853                                }) || iri.starts_with("_:")
854                            })
855                        })
856                    {
857                        definition.prefix_flag = true;
858                    }
859                }
860                // 14.2.1)
861                _ => {
862                    definition.iri_mapping = Some(None);
863                    errors.push(JsonLdSyntaxError::msg_and_code(
864                        "@id value must be a string",
865                        JsonLdErrorCode::InvalidIriMapping,
866                    ))
867                }
868            }
869        } else if let Some((prefix, suffix)) = term.split_once(':').and_then(|(prefix, suffix)| {
870            if prefix.is_empty() {
871                // We ignore the empty prefixes
872                suffix.split_once(':')
873            } else {
874                Some((prefix, suffix))
875            }
876        }) {
877            // 15)
878            if local_context.contains_key(prefix) {
879                // 15.1)
880                self.create_term_definition(
881                    active_context,
882                    local_context,
883                    prefix,
884                    defined,
885                    base_url,
886                    false,
887                    false,
888                    remote_contexts,
889                    errors,
890                )
891            }
892            if let Some(term_definition) = active_context.term_definitions.get(prefix) {
893                // 15.2)
894                if let Some(Some(iri_mapping)) = &term_definition.iri_mapping {
895                    definition.iri_mapping = Some(Some(format!("{iri_mapping}{suffix}")));
896                }
897            } else {
898                // 15.3)
899                definition.iri_mapping = Some(Some(term.into()));
900            }
901        } else if term.contains('/') {
902            // 16)
903            let iri = match if let Some(base_url) = base_url {
904                base_url.resolve(term)
905            } else {
906                Iri::parse(term.to_owned())
907            } {
908                Ok(iri) => iri.into_inner(),
909                Err(e) => {
910                    errors.push(JsonLdSyntaxError::msg_and_code(
911                        format!("Invalid term relative IRI '{term}': {e}"),
912                        JsonLdErrorCode::InvalidIriMapping,
913                    ));
914                    return;
915                }
916            };
917            definition.iri_mapping = Some(Some(iri));
918        } else if term == "@type" {
919            // 17)
920            definition.iri_mapping = Some(Some("@type".into()));
921        } else {
922            // 18)
923            if let Some(vocabulary_mapping) = &active_context.vocabulary_mapping {
924                definition.iri_mapping = Some(Some(format!("{vocabulary_mapping}{term}")));
925            } else {
926                errors.push(JsonLdSyntaxError::msg_and_code(
927                    format!("No @vocab key to build an IRI from context {term} term definition"),
928                    JsonLdErrorCode::InvalidIriMapping,
929                ))
930            }
931        }
932        // 19)
933        if let Some(key_value) = value.get("@container") {
934            const ALLOWED_CONTAINER_MAPPINGS: &[&[&str]] = &[
935                &["@index"],
936                &["@index", "@set"],
937                &["@language"],
938                &["@language", "@set"],
939                &["@graph"],
940                &["@graph", "@set"],
941                &["@graph", "@id"],
942                &["@graph", "@id", "@set"],
943                &["@graph", "@index"],
944                &["@graph", "@index", "@set"],
945                &["@id"],
946                &["@id", "@set"],
947                &["@list"],
948                &["@set"],
949                &["@type"],
950                &["@type", "@set"],
951            ];
952
953            // 19.1)
954            let mut container_mapping = Vec::new();
955            for value in if let JsonNode::Array(value) = key_value {
956                if self.processing_mode == JsonLdProcessingMode::JsonLd1_0 {
957                    errors.push(JsonLdSyntaxError::msg_and_code(
958                                    "@container definition with multiple values is not supported in JSON-LD 1.0",
959                                    JsonLdErrorCode::InvalidContainerMapping,
960                                ));
961                }
962                value.as_slice()
963            } else {
964                slice::from_ref(key_value)
965            } {
966                if let JsonNode::String(container) = value {
967                    container_mapping.push(container.as_str());
968                } else {
969                    errors.push(JsonLdSyntaxError::msg_and_code(
970                        "@container value must be a string or an array of strings",
971                        JsonLdErrorCode::InvalidContainerMapping,
972                    ));
973                }
974            }
975            container_mapping.sort_unstable();
976            let Some(container_mapping) = ALLOWED_CONTAINER_MAPPINGS
977                .iter()
978                .find_map(|c| (*c == container_mapping).then_some(*c))
979            else {
980                errors.push(JsonLdSyntaxError::msg_and_code(
981                    "Not supported @container value combination",
982                    JsonLdErrorCode::InvalidContainerMapping,
983                ));
984                return;
985            };
986            // 19.2)
987            if self.processing_mode == JsonLdProcessingMode::JsonLd1_0 {
988                if let Some(bad) = ["@graph", "@id", "@type"]
989                    .into_iter()
990                    .find(|k| container_mapping.contains(k))
991                {
992                    errors.push(JsonLdSyntaxError::msg_and_code(
993                        format!("{bad} container is not supported in JSON-LD 1.0"),
994                        JsonLdErrorCode::InvalidContainerMapping,
995                    ));
996                }
997            }
998            // 19.3)
999            definition.container_mapping = container_mapping;
1000            // 19.4)
1001            if container_mapping.contains(&"@type") {
1002                if let Some(type_mapping) = &definition.type_mapping {
1003                    if !["@id", "@vocab"].contains(&type_mapping.as_str()) {
1004                        errors.push(JsonLdSyntaxError::msg_and_code(
1005                                    format!("Type mapping must be @id or @vocab, not {type_mapping} when used with @type container"),
1006                                    JsonLdErrorCode::InvalidTypeMapping,
1007                                ));
1008                    }
1009                } else {
1010                    definition.type_mapping = Some("@id".into());
1011                }
1012            }
1013        }
1014        // 20)
1015        if let Some(key_value) = value.get("@index") {
1016            // 20.1)
1017            if self.processing_mode == JsonLdProcessingMode::JsonLd1_0 {
1018                errors.push(JsonLdSyntaxError::msg_and_code(
1019                    "@index inside of term definitions is only supported in JSON-LD 1.1",
1020                    JsonLdErrorCode::InvalidTermDefinition,
1021                ));
1022            }
1023            if !definition.container_mapping.contains(&"@index") {
1024                errors.push(JsonLdSyntaxError::msg_and_code(
1025                    "@index inside of term definitions is only allowed when @container is set to @index",
1026                    JsonLdErrorCode::InvalidTermDefinition,
1027                ));
1028            }
1029            // 20.2)
1030            let JsonNode::String(index) = key_value else {
1031                errors.push(JsonLdSyntaxError::msg_and_code(
1032                    "@index value must be a string",
1033                    JsonLdErrorCode::InvalidTermDefinition,
1034                ));
1035                return;
1036            };
1037            let Some(expanded_index) = self.expand_iri(
1038                active_context,
1039                index.into(),
1040                false,
1041                true,
1042                Some(local_context),
1043                defined,
1044                errors,
1045            ) else {
1046                errors.push(JsonLdSyntaxError::msg_and_code(
1047                    "@index value must be a valid IRI",
1048                    JsonLdErrorCode::InvalidTermDefinition,
1049                ));
1050                return;
1051            };
1052            if self.lenient
1053                && (has_keyword_form(&expanded_index) || expanded_index.starts_with("_:"))
1054                || !self.lenient && Iri::parse(expanded_index.as_ref()).is_err()
1055            {
1056                errors.push(JsonLdSyntaxError::msg_and_code(
1057                    "@index value must be a valid IRI",
1058                    JsonLdErrorCode::InvalidTermDefinition,
1059                ));
1060                return;
1061            }
1062            // 20.3)
1063            definition.index_mapping = Some(index.into());
1064        }
1065        // 21)
1066        if let Some(key_value) = value.get("@context") {
1067            // 21.1)
1068            if self.processing_mode == JsonLdProcessingMode::JsonLd1_0 {
1069                errors.push(JsonLdSyntaxError::msg_and_code(
1070                    "@context inside of term definitions is only supported in JSON-LD 1.1",
1071                    JsonLdErrorCode::InvalidTermDefinition,
1072                ));
1073            }
1074            // 21.2)
1075            let context = key_value;
1076            // 21.3)
1077            let error_count = errors.len();
1078            self.process_context(
1079                active_context,
1080                context.clone(),
1081                base_url,
1082                remote_contexts,
1083                true,
1084                true,
1085                false,
1086                errors,
1087            );
1088            for error in errors.drain(error_count..).collect::<Vec<_>>() {
1089                errors.push(JsonLdSyntaxError::msg_and_code(
1090                    format!("Invalid scoped context: {error}"),
1091                    JsonLdErrorCode::InvalidScopedContext,
1092                ));
1093            }
1094            // 21.4)
1095            definition.context = Some(context.clone());
1096            definition.base_url = base_url.cloned();
1097        }
1098        // 22)
1099        if let Some(key_value) = value.get("@language") {
1100            if value.contains_key("@type") {
1101                errors.push(JsonLdSyntaxError::msg_and_code(
1102                    "Both @language and @type can't be set at the same time",
1103                    JsonLdErrorCode::InvalidLanguageMapping,
1104                ));
1105            }
1106            definition.language_mapping = Some(match key_value {
1107                JsonNode::String(language) => Some(language.clone()),
1108                JsonNode::Null => None,
1109                _ => {
1110                    errors.push(JsonLdSyntaxError::msg_and_code(
1111                        "@language value must be a string or null",
1112                        JsonLdErrorCode::InvalidLanguageMapping,
1113                    ));
1114                    return;
1115                }
1116            })
1117        }
1118        // 23)
1119        if let Some(key_value) = value.get("@direction") {
1120            // 23.1)
1121            if self.processing_mode == JsonLdProcessingMode::JsonLd1_0 {
1122                errors.push(JsonLdSyntaxError::msg_and_code(
1123                    "@direction is only supported in JSON-LD 1.1",
1124                    JsonLdErrorCode::InvalidTermDefinition,
1125                ));
1126            }
1127            match key_value {
1128                // 5.10.3)
1129                JsonNode::Null => {
1130                    definition.direction_mapping = Some(None);
1131                }
1132                // 5.10.4)
1133                JsonNode::String(value) => match value.as_str() {
1134                    "ltr" => definition.direction_mapping = Some(Some("ltr")),
1135                    "rtl" => definition.direction_mapping = Some(Some("rtl")),
1136                    _ => errors.push(JsonLdSyntaxError::msg_and_code(
1137                        format!("@direction value must be 'ltr' or 'rtl', found '{value}'"),
1138                        JsonLdErrorCode::InvalidBaseDirection,
1139                    )),
1140                },
1141                _ => errors.push(JsonLdSyntaxError::msg_and_code(
1142                    "@direction value must be a string or null",
1143                    JsonLdErrorCode::InvalidBaseDirection,
1144                )),
1145            }
1146        }
1147        // 24)
1148        if let Some(key_value) = value.get("@nest") {
1149            // 24.1)
1150            if self.processing_mode == JsonLdProcessingMode::JsonLd1_0 {
1151                errors.push(JsonLdSyntaxError::msg_and_code(
1152                    "@nest is only supported in JSON-LD 1.1",
1153                    JsonLdErrorCode::InvalidTermDefinition,
1154                ));
1155            }
1156            // 24.2)
1157            let JsonNode::String(value) = key_value else {
1158                errors.push(JsonLdSyntaxError::msg_and_code(
1159                    "@nest value must be a string",
1160                    JsonLdErrorCode::InvalidNestValue,
1161                ));
1162                return;
1163            };
1164            if is_keyword(value) && value != "@nest" {
1165                errors.push(JsonLdSyntaxError::msg_and_code(
1166                    "@nest value must not be a keyword other than @nest",
1167                    JsonLdErrorCode::InvalidNestValue,
1168                ));
1169                return;
1170            }
1171            definition.nest_value = Some(value.into());
1172        }
1173        // 25)
1174        if let Some(key_value) = value.get("@prefix") {
1175            // 25.1)
1176            if self.processing_mode == JsonLdProcessingMode::JsonLd1_0 {
1177                errors.push(JsonLdSyntaxError::msg_and_code(
1178                    "@prefix is only supported in JSON-LD 1.1",
1179                    JsonLdErrorCode::InvalidTermDefinition,
1180                ));
1181            }
1182            if term.contains(':') {
1183                errors.push(JsonLdSyntaxError::msg_and_code(
1184                    format!("@prefix cannot be set on terms like {term} that contains a :"),
1185                    JsonLdErrorCode::InvalidTermDefinition,
1186                ));
1187                return;
1188            }
1189            if term.contains('/') {
1190                errors.push(JsonLdSyntaxError::msg_and_code(
1191                    format!("@prefix cannot be set on terms like {term} that contains a /"),
1192                    JsonLdErrorCode::InvalidTermDefinition,
1193                ));
1194                return;
1195            }
1196            // 25.2)
1197            let JsonNode::Boolean(value) = key_value else {
1198                errors.push(JsonLdSyntaxError::msg_and_code(
1199                    "@prefix value must be a boolean",
1200                    JsonLdErrorCode::InvalidPrefixValue,
1201                ));
1202                return;
1203            };
1204            definition.prefix_flag = *value;
1205            // 25.3)
1206            if definition.prefix_flag
1207                && definition
1208                    .iri_mapping
1209                    .as_ref()
1210                    .is_some_and(|d| d.as_ref().is_some_and(|d| is_keyword(d)))
1211            {
1212                errors.push(JsonLdSyntaxError::msg_and_code(
1213                    format!("@prefix cannot be set on terms like {term} that are keywords"),
1214                    JsonLdErrorCode::InvalidTermDefinition,
1215                ));
1216                return;
1217            }
1218        }
1219        // 26)
1220        if let Some(key) = value.keys().find(|k| {
1221            !matches!(
1222                k.as_str(),
1223                "@id"
1224                    | "@reverse"
1225                    | "@container"
1226                    | "@context"
1227                    | "@direction"
1228                    | "@index"
1229                    | "@language"
1230                    | "@nest"
1231                    | "@prefix"
1232                    | "@protected"
1233                    | "@type"
1234            )
1235        }) {
1236            errors.push(JsonLdSyntaxError::msg_and_code(
1237                format!("Unexpected key in term definition '{key}'"),
1238                JsonLdErrorCode::InvalidTermDefinition,
1239            ));
1240        }
1241        // 27)
1242        if !override_protected {
1243            if let Some(previous_definition) = previous_definition {
1244                if previous_definition.protected {
1245                    // 27.1)
1246                    if definition.iri_mapping != previous_definition.iri_mapping
1247                        || definition.prefix_flag != previous_definition.prefix_flag
1248                        || definition.reverse_property != previous_definition.reverse_property
1249                        || definition.base_url != previous_definition.base_url
1250                        || definition.context != previous_definition.context
1251                        || definition.container_mapping != previous_definition.container_mapping
1252                        || definition.direction_mapping != previous_definition.direction_mapping
1253                        || definition.index_mapping != previous_definition.index_mapping
1254                        || definition.language_mapping != previous_definition.language_mapping
1255                        || definition.nest_value != previous_definition.nest_value
1256                        || definition.type_mapping != previous_definition.type_mapping
1257                    {
1258                        errors.push(JsonLdSyntaxError::msg_and_code(
1259                            format!("Overriding the protected term {term}"),
1260                            JsonLdErrorCode::ProtectedTermRedefinition,
1261                        ));
1262                    }
1263                    // 27.2)
1264                    definition = previous_definition;
1265                }
1266            }
1267        }
1268        // 28)
1269        active_context
1270            .term_definitions
1271            .insert(term.into(), definition);
1272        defined.insert(term.into(), true);
1273    }
1274
1275    /// [IRI Expansion](https://www.w3.org/TR/json-ld-api/#iri-expansion)
1276    ///
1277    /// Warning: take care of synchronizing this implementation with the full one in [`JsonLdExpansionConverter`].
1278    pub fn expand_iri<'a>(
1279        &self,
1280        active_context: &mut JsonLdContext,
1281        value: Cow<'a, str>,
1282        document_relative: bool,
1283        vocab: bool,
1284        local_context: Option<&HashMap<String, JsonNode>>,
1285        defined: &mut HashMap<String, bool>,
1286        errors: &mut Vec<JsonLdSyntaxError>,
1287    ) -> Option<Cow<'a, str>> {
1288        if has_keyword_form(&value) {
1289            // 1)
1290            return is_keyword(&value).then_some(value);
1291        }
1292        // 3)
1293        if let Some(local_context) = local_context {
1294            if local_context.contains_key(value.as_ref())
1295                && defined.get(value.as_ref()) != Some(&true)
1296            {
1297                self.create_term_definition(
1298                    active_context,
1299                    local_context,
1300                    &value,
1301                    defined,
1302                    None,
1303                    false,
1304                    false,
1305                    &mut Vec::new(),
1306                    errors,
1307                )
1308            }
1309        }
1310        if let Some(term_definition) = active_context.term_definitions.get(value.as_ref()) {
1311            if let Some(iri_mapping) = &term_definition.iri_mapping {
1312                let iri_mapping = iri_mapping.as_ref()?;
1313                // 4)
1314                if is_keyword(iri_mapping) {
1315                    return Some(iri_mapping.clone().into());
1316                }
1317                // 5)
1318                if vocab {
1319                    return Some(iri_mapping.clone().into());
1320                }
1321            }
1322        }
1323        // 6.1)
1324        if let Some((prefix, suffix)) = value.split_once(':') {
1325            // 6.2)
1326            if prefix == "_" || suffix.starts_with("//") {
1327                return Some(value);
1328            }
1329            // 6.3)
1330            if let Some(local_context) = local_context {
1331                if local_context.contains_key(prefix) && defined.get(prefix) != Some(&true) {
1332                    self.create_term_definition(
1333                        active_context,
1334                        local_context,
1335                        prefix,
1336                        defined,
1337                        None,
1338                        false,
1339                        false,
1340                        &mut Vec::new(),
1341                        errors,
1342                    )
1343                }
1344            }
1345            // 6.4)
1346            if let Some(term_definition) = active_context.term_definitions.get(prefix) {
1347                if let Some(Some(iri_mapping)) = &term_definition.iri_mapping {
1348                    if term_definition.prefix_flag {
1349                        return Some(format!("{iri_mapping}{suffix}").into());
1350                    }
1351                }
1352            }
1353            // 6.5)
1354            if Iri::parse(value.as_ref()).is_ok() {
1355                return Some(value);
1356            }
1357        }
1358        // 7)
1359        if vocab {
1360            if let Some(vocabulary_mapping) = &active_context.vocabulary_mapping {
1361                return Some(format!("{vocabulary_mapping}{value}").into());
1362            }
1363        }
1364        // 8)
1365        if document_relative {
1366            if let Some(base_iri) = &active_context.base_iri {
1367                if self.lenient {
1368                    return Some(base_iri.resolve_unchecked(&value).into_inner().into());
1369                } else if let Ok(value) = base_iri.resolve(&value) {
1370                    return Some(base_iri.resolve_unchecked(&value).into_inner().into());
1371                }
1372            }
1373        }
1374
1375        Some(value)
1376    }
1377
1378    fn load_remote_context(
1379        &self,
1380        url: &str,
1381    ) -> Result<(Option<Iri<String>>, JsonNode), JsonLdSyntaxError> {
1382        #[expect(clippy::unwrap_in_result)]
1383        let mut remote_context_cache = self.remote_context_cache.lock().unwrap();
1384        if let Some(loaded_context) = remote_context_cache.get(url) {
1385            // 5.2.4)
1386            return Ok(loaded_context.clone());
1387        }
1388
1389        // 5.2.5)
1390        let Some(load_document_callback) = &self.load_document_callback else {
1391            return Err(JsonLdSyntaxError::msg_and_code(
1392                "No LoadDocumentCallback has been set to load remote contexts",
1393                JsonLdErrorCode::LoadingRemoteContextFailed,
1394            ));
1395        };
1396        let context_document = match load_document_callback(
1397            url,
1398            &JsonLdLoadDocumentOptions {
1399                request_profile: JsonLdProfile::Context.into(),
1400            },
1401        ) {
1402            Ok(document) => document,
1403            Err(e) => {
1404                return Err(JsonLdSyntaxError::msg_and_code(
1405                    format!("Failed to load remote context {url}: {e}"),
1406                    JsonLdErrorCode::LoadingRemoteContextFailed,
1407                ));
1408            }
1409        };
1410        let parsed_document = match json_slice_to_node(&context_document.document) {
1411            Ok(d) => d,
1412            Err(e) => {
1413                return Err(JsonLdSyntaxError::msg_and_code(
1414                    format!("Failed to parse remote context {url}: {e}"),
1415                    JsonLdErrorCode::LoadingRemoteContextFailed,
1416                ));
1417            }
1418        };
1419        let JsonNode::Object(parsed_document) = parsed_document else {
1420            return Err(JsonLdSyntaxError::msg_and_code(
1421                format!("Remote context {url} must be a map"),
1422                JsonLdErrorCode::InvalidRemoteContext,
1423            ));
1424        };
1425        let Some(loaded_context) = parsed_document
1426            .into_iter()
1427            .find_map(|(k, v)| (k == "@context").then_some(v))
1428        else {
1429            return Err(JsonLdSyntaxError::msg_and_code(
1430                format!("Remote context {url} must be contain a @context key"),
1431                JsonLdErrorCode::InvalidRemoteContext,
1432            ));
1433        };
1434        let document_url = Iri::parse(context_document.document_url).ok();
1435        remote_context_cache.insert(url.into(), (document_url.clone(), loaded_context.clone()));
1436        Ok((document_url, loaded_context))
1437    }
1438}
1439
1440pub fn has_keyword_form(value: &str) -> bool {
1441    value
1442        .strip_prefix('@')
1443        .is_some_and(|suffix| !suffix.is_empty() && suffix.bytes().all(|b| b.is_ascii_alphabetic()))
1444}
1445
1446pub fn is_keyword(value: &str) -> bool {
1447    matches!(
1448        value,
1449        "@base"
1450            | "@container"
1451            | "@context"
1452            | "@direction"
1453            | "@graph"
1454            | "@id"
1455            | "@import"
1456            | "@included"
1457            | "@index"
1458            | "@json"
1459            | "@language"
1460            | "@list"
1461            | "@nest"
1462            | "@none"
1463            | "@prefix"
1464            | "@propagate"
1465            | "@protected"
1466            | "@reverse"
1467            | "@set"
1468            | "@type"
1469            | "@value"
1470            | "@version"
1471            | "@vocab"
1472    )
1473}
1474
1475fn json_slice_to_node(data: &[u8]) -> Result<JsonNode, JsonSyntaxError> {
1476    let mut parser = SliceJsonParser::new(data);
1477    json_node_from_events(std::iter::from_fn(|| match parser.parse_next() {
1478        Ok(JsonEvent::Eof) => None,
1479        Ok(event) => Some(Ok(event)),
1480        Err(e) => Some(Err(e)),
1481    }))
1482}
1483
1484enum BuildingObjectOrArrayNode {
1485    Object(HashMap<String, JsonNode>),
1486    ObjectWithPendingKey(HashMap<String, JsonNode>, String),
1487    Array(Vec<JsonNode>),
1488}
1489
1490pub fn json_node_from_events<'a>(
1491    events: impl IntoIterator<Item = Result<JsonEvent<'a>, JsonSyntaxError>>,
1492) -> Result<JsonNode, JsonSyntaxError> {
1493    let mut stack = Vec::new();
1494    for event in events {
1495        if let Some(result) = match event? {
1496            JsonEvent::String(value) => {
1497                after_to_node_event(&mut stack, JsonNode::String(value.into()))
1498            }
1499            JsonEvent::Number(value) => {
1500                after_to_node_event(&mut stack, JsonNode::Number(value.into()))
1501            }
1502            JsonEvent::Boolean(value) => after_to_node_event(&mut stack, JsonNode::Boolean(value)),
1503            JsonEvent::Null => after_to_node_event(&mut stack, JsonNode::Null),
1504            JsonEvent::EndArray | JsonEvent::EndObject => {
1505                let value = match stack.pop() {
1506                    Some(BuildingObjectOrArrayNode::Object(object)) => JsonNode::Object(object),
1507                    Some(BuildingObjectOrArrayNode::Array(array)) => JsonNode::Array(array),
1508                    _ => unreachable!(),
1509                };
1510                after_to_node_event(&mut stack, value)
1511            }
1512            JsonEvent::StartArray => {
1513                stack.push(BuildingObjectOrArrayNode::Array(Vec::new()));
1514                None
1515            }
1516            JsonEvent::StartObject => {
1517                stack.push(BuildingObjectOrArrayNode::Object(HashMap::new()));
1518                None
1519            }
1520            JsonEvent::ObjectKey(key) => {
1521                if let Some(BuildingObjectOrArrayNode::Object(object)) = stack.pop() {
1522                    stack.push(BuildingObjectOrArrayNode::ObjectWithPendingKey(
1523                        object,
1524                        key.into(),
1525                    ));
1526                }
1527                None
1528            }
1529            JsonEvent::Eof => unreachable!(),
1530        } {
1531            return Ok(result);
1532        }
1533    }
1534    unreachable!("The JSON emitted by the parser mut be valid")
1535}
1536
1537fn after_to_node_event(
1538    stack: &mut Vec<BuildingObjectOrArrayNode>,
1539    new_value: JsonNode,
1540) -> Option<JsonNode> {
1541    match stack.pop() {
1542        Some(BuildingObjectOrArrayNode::ObjectWithPendingKey(mut object, key)) => {
1543            object.insert(key, new_value);
1544            stack.push(BuildingObjectOrArrayNode::Object(object));
1545            None
1546        }
1547        Some(BuildingObjectOrArrayNode::Object(object)) => {
1548            stack.push(BuildingObjectOrArrayNode::Object(object));
1549            None
1550        }
1551        Some(BuildingObjectOrArrayNode::Array(mut array)) => {
1552            array.push(new_value);
1553            stack.push(BuildingObjectOrArrayNode::Array(array));
1554            None
1555        }
1556        None => Some(new_value),
1557    }
1558}