1use crate::turtle_lex::{advance_turtle_scan, is_in_comment_or_string_at, TurtleScanState};
2use std::collections::BTreeMap;
3use strixonomy_core::{Annotation, Axiom, Entity, SourceLocation};
4
5pub fn short_name_from_iri(iri: &str) -> String {
6 if let Some((_, name)) = iri.rsplit_once('#') {
7 return name.to_string();
8 }
9 if let Some((_, name)) = iri.rsplit_once('/') {
10 return name.to_string();
11 }
12 iri.to_string()
13}
14
15pub fn prefixes_from_turtle(source_text: &str) -> BTreeMap<String, String> {
17 let mut map = BTreeMap::new();
18 for line in source_text.lines() {
19 let trimmed = line.trim();
20 if trimmed.is_empty() || trimmed.starts_with('#') {
21 continue;
22 }
23 let mut parts = trimmed.split_whitespace();
24 let Some(keyword) = parts.next() else {
25 continue;
26 };
27 if !(keyword.eq_ignore_ascii_case("@prefix") || keyword.eq_ignore_ascii_case("PREFIX")) {
28 continue;
29 }
30 let Some(prefix_raw) = parts.next() else { continue };
31 let Some(uri_raw) = parts.next() else { continue };
32 let prefix = prefix_raw.trim_end_matches(':').to_string();
33 if let Some(uri) = parse_turtle_prefix_iri(uri_raw) {
34 map.insert(prefix, uri);
35 }
36 }
37 map
38}
39
40fn parse_turtle_prefix_iri(token: &str) -> Option<String> {
41 let token = token.trim_end_matches('.');
42 if !token.starts_with('<') || !token.ends_with('>') {
43 return None;
44 }
45 let inner = &token[1..token.len() - 1];
46 if inner.is_empty() || inner.contains('<') || inner.contains('>') {
47 return None;
48 }
49 Some(inner.to_string())
50}
51
52pub fn namespaces_for_text(
53 source_text: &str,
54 declared: &BTreeMap<String, String>,
55) -> BTreeMap<String, String> {
56 let mut merged = declared.clone();
57 merged.extend(prefixes_from_turtle(source_text));
58 merged
59}
60
61fn subject_needles(
62 iri: &str,
63 _short_name: &str,
64 namespaces: &BTreeMap<String, String>,
65) -> Vec<String> {
66 let mut needles = vec![format!("<{iri}>")];
67 if let Some(default_ns) = namespaces.get("") {
69 if iri.starts_with(default_ns.as_str()) {
70 let local = &iri[default_ns.len()..];
71 if crate::patch::is_valid_pn_local(local) {
72 needles.push(format!(":{local}"));
73 }
74 }
75 }
76 if let Some((prefix, ns)) = crate::patch::best_namespace_match(iri, namespaces) {
78 let local = &iri[ns.len()..];
79 if crate::patch::is_valid_pn_local(local) {
80 needles.push(format!("{prefix}:{local}"));
81 }
82 }
83 needles
84}
85
86fn line_byte_offset_after(text: &str, line_start: usize, line: &str) -> usize {
88 let after_content = line_start + line.len();
89 let bytes = text.as_bytes();
90 if bytes.get(after_content) == Some(&b'\r') && bytes.get(after_content + 1) == Some(&b'\n') {
91 after_content + 2
92 } else if bytes.get(after_content) == Some(&b'\n') {
93 after_content + 1
94 } else {
95 after_content
96 }
97}
98
99#[allow(dead_code)]
101pub fn find_subject_statement(
102 source_text: &str,
103 iri: &str,
104 short_name: &str,
105 namespaces: &BTreeMap<String, String>,
106) -> Option<ByteRange> {
107 all_subject_statements(source_text, iri, short_name, namespaces).into_iter().next()
108}
109
110pub fn all_subject_statements(
112 source_text: &str,
113 iri: &str,
114 short_name: &str,
115 namespaces: &BTreeMap<String, String>,
116) -> Vec<ByteRange> {
117 let needles = subject_needles(iri, short_name, namespaces);
118 let mut byte_offset = 0usize;
119 let mut out = Vec::new();
120 for line in source_text.lines() {
121 let trimmed = line.trim_start();
122 if trimmed.starts_with('@') {
123 byte_offset = line_byte_offset_after(source_text, byte_offset, line);
124 continue;
125 }
126 if let Some(col_in_trimmed) = subject_column_at_line_start(trimmed, &needles) {
127 let ws = line.len() - trimmed.len();
128 let start = byte_offset + ws + col_in_trimmed;
129 out.push(ByteRange { start: start as u64, end: start as u64 });
130 }
131 byte_offset = line_byte_offset_after(source_text, byte_offset, line);
132 }
133 out
134}
135
136pub fn all_entity_statement_ranges(
138 source_text: &str,
139 iri: &str,
140 short_name: &str,
141 namespaces: &BTreeMap<String, String>,
142) -> Vec<ByteRange> {
143 all_subject_statements(source_text, iri, short_name, namespaces)
144 .into_iter()
145 .filter_map(|start_range| {
146 let end = statement_end_byte(source_text, start_range.start as usize)?;
147 if end > start_range.start as usize {
148 Some(ByteRange { start: start_range.start, end: end as u64 })
149 } else {
150 None
151 }
152 })
153 .collect()
154}
155
156fn primary_entity_statement_start(
158 source_text: &str,
159 iri: &str,
160 short_name: &str,
161 namespaces: &BTreeMap<String, String>,
162) -> Option<ByteRange> {
163 let starts = all_subject_statements(source_text, iri, short_name, namespaces);
164 for start in &starts {
165 if let Some(end) = statement_end_byte(source_text, start.start as usize) {
166 let stmt = &source_text[start.start as usize..end];
167 if crate::turtle_lex::statement_has_type_predicate(stmt) {
168 return Some(*start);
169 }
170 }
171 }
172 starts.first().copied()
173}
174
175fn subject_column_at_line_start(trimmed: &str, needles: &[String]) -> Option<usize> {
177 for needle in needles {
178 if !trimmed.starts_with(needle) {
179 continue;
180 }
181 let rest = &trimmed[needle.len()..];
182 if rest.is_empty() || rest.starts_with(|c: char| c.is_whitespace() || c == ';' || c == '.')
183 {
184 return Some(0);
185 }
186 }
187 None
188}
189
190pub fn find_entity_block(
192 source_text: &str,
193 iri: &str,
194 short_name: &str,
195 namespaces: &BTreeMap<String, String>,
196) -> SourceLocation {
197 if let Some(range) = primary_entity_statement_start(source_text, iri, short_name, namespaces) {
198 let start = range.start as usize;
199 let line_idx = source_text[..start].matches('\n').count();
200 let line_start = source_text[..start].rfind('\n').map(|p| p + 1).unwrap_or(0);
201 let col = start - line_start;
202 return SourceLocation {
203 line: Some((line_idx + 1) as u64),
204 column: Some(col as u64),
205 start_byte: Some(range.start),
206 end_byte: None,
207 };
208 }
209 SourceLocation::default()
210}
211
212pub fn annotate_spans(
214 source_text: &str,
215 entities: &mut [Entity],
216 annotations: &mut [Annotation],
217 axioms: &mut [Axiom],
218) {
219 for entity in entities.iter_mut() {
220 if let Some(block) = entity_block_range(source_text, entity, &BTreeMap::new()) {
221 if entity.source_location.end_byte.is_none() {
222 entity.source_location.end_byte = Some(block.end);
223 }
224 }
225 }
226
227 for ann in annotations.iter_mut() {
228 if let Some(span) = find_predicate_literal_span(
229 source_text,
230 &ann.subject,
231 predicate_local_name(&ann.predicate),
232 &ann.object,
233 ) {
234 ann.source_location = span;
235 }
236 }
237
238 for axiom in axioms.iter_mut() {
239 if axiom.axiom_kind == strixonomy_core::AXIOM_KIND_SUB_CLASS_OF {
240 if let Some(span) = find_subclass_span(source_text, &axiom.subject, &axiom.object) {
241 axiom.source_location = span;
242 }
243 }
244 }
245}
246
247pub fn entity_block_range(
248 source_text: &str,
249 entity: &Entity,
250 declared_namespaces: &BTreeMap<String, String>,
251) -> Option<ByteRange> {
252 let namespaces = namespaces_for_text(source_text, declared_namespaces);
253 let start = if let Some(s) = entity.source_location.start_byte {
254 s as usize
255 } else {
256 primary_entity_statement_start(source_text, &entity.iri, &entity.short_name, &namespaces)
257 .map(|r| r.start as usize)?
258 };
259 let end = statement_end_byte(source_text, start)?;
260 if end > start {
261 Some(ByteRange { start: start as u64, end: end as u64 })
262 } else {
263 None
264 }
265}
266
267pub fn entity_primary_block_range(
269 source_text: &str,
270 entity_iri: &str,
271 namespaces: &BTreeMap<String, String>,
272) -> Option<ByteRange> {
273 let short_name = short_name_from_iri(entity_iri);
274 let start = primary_entity_statement_start(source_text, entity_iri, &short_name, namespaces)?;
275 let end = statement_end_byte(source_text, start.start as usize)?;
276 if end > start.start as usize {
277 Some(ByteRange { start: start.start, end: end as u64 })
278 } else {
279 None
280 }
281}
282
283pub(crate) fn is_turtle_name_char(b: u8) -> bool {
285 b.is_ascii_alphanumeric() || matches!(b, b'_' | b'-' | b':' | b'~' | b'%' | b'\\' | b'.')
286}
287
288pub(crate) fn is_turtle_terminating_dot(bytes: &[u8], i: usize) -> bool {
293 if bytes.get(i) != Some(&b'.') {
294 return false;
295 }
296 let prev_name = i > 0 && is_turtle_name_char(bytes[i - 1]) && bytes[i - 1] != b'.';
297 let next_name = bytes.get(i + 1).is_some_and(|b| is_turtle_name_char(*b) && *b != b'.');
298 !(prev_name && next_name)
299}
300
301pub fn is_in_comment_or_string(text: &str, byte_offset: usize) -> bool {
306 is_in_comment_or_string_at(text, byte_offset)
307}
308
309pub(crate) fn statement_end_byte(source_text: &str, start: usize) -> Option<usize> {
315 let bytes = source_text.as_bytes();
316 if start >= bytes.len() {
317 return None;
318 }
319 let mut i = start;
320 let mut bracket_depth = 0i32;
321 let mut paren_depth = 0i32;
322 let mut state = TurtleScanState::default();
323
324 while i < bytes.len() {
325 if state.in_comment {
326 if bytes[i] == b'\n' {
327 state.in_comment = false;
328 }
329 i += 1;
330 continue;
331 }
332 if state.in_string() || state.in_iri {
333 i = advance_turtle_scan(bytes, i, &mut state);
334 continue;
335 }
336
337 if bytes.get(i..i + 3) == Some(br#"""""#) {
338 state.string_kind = Some(crate::turtle_lex::TurtleStringKind::LongDouble);
339 i += 3;
340 continue;
341 }
342 if bytes.get(i..i + 3) == Some(br"'''") {
343 state.string_kind = Some(crate::turtle_lex::TurtleStringKind::LongSingle);
344 i += 3;
345 continue;
346 }
347
348 let b = bytes[i];
349 match b {
350 b'#' => {
351 state.in_comment = true;
352 i += 1;
353 }
354 b'"' => {
355 state.string_kind = Some(crate::turtle_lex::TurtleStringKind::ShortDouble);
356 i += 1;
357 }
358 b'\'' => {
359 state.string_kind = Some(crate::turtle_lex::TurtleStringKind::ShortSingle);
360 i += 1;
361 }
362 b'<' => {
363 state.in_iri = true;
364 i += 1;
365 }
366 b'[' => {
367 bracket_depth += 1;
368 i += 1;
369 }
370 b']' => {
371 bracket_depth = bracket_depth.saturating_sub(1);
372 i += 1;
373 }
374 b'(' => {
375 paren_depth += 1;
376 i += 1;
377 }
378 b')' => {
379 paren_depth = paren_depth.saturating_sub(1);
380 i += 1;
381 }
382 b'.' if bracket_depth == 0
383 && paren_depth == 0
384 && is_turtle_terminating_dot(bytes, i) =>
385 {
386 return Some(i + 1);
387 }
388 _ => i += 1,
389 }
390 }
391 None
392}
393
394#[derive(Debug, Clone, Copy)]
395pub struct ByteRange {
396 pub start: u64,
397 pub end: u64,
398}
399
400fn line_start_byte(source_text: &str, line_idx: usize) -> u64 {
401 let mut offset = 0usize;
402 for (i, line) in source_text.lines().enumerate() {
403 if i == line_idx {
404 return offset as u64;
405 }
406 offset = line_byte_offset_after(source_text, offset, line);
407 }
408 0
409}
410
411fn predicate_local_name(predicate: &str) -> &str {
412 if predicate.contains("label") {
413 "label"
414 } else if predicate.contains("comment") {
415 "comment"
416 } else if predicate.contains("subClassOf") {
417 "subClassOf"
418 } else {
419 predicate
420 }
421}
422
423fn find_predicate_literal_span(
424 source_text: &str,
425 subject: &str,
426 predicate: &str,
427 object: &str,
428) -> Option<SourceLocation> {
429 let object_needle = object.trim_matches('"');
430 for (line_idx, line) in source_text.lines().enumerate() {
431 if !line.contains(subject) && !line.contains(&short_name_from_iri(subject)) {
432 continue;
433 }
434 if line.contains(predicate) && line.contains(object_needle) {
435 let col = line.find(predicate).unwrap_or(0);
436 let start_byte = line_start_byte(source_text, line_idx) + col as u64;
437 return Some(SourceLocation {
438 line: Some((line_idx + 1) as u64),
439 column: Some(col as u64),
440 start_byte: Some(start_byte),
441 end_byte: Some(start_byte + line.len() as u64),
442 });
443 }
444 }
445 None
446}
447
448fn find_subclass_span(source_text: &str, subject: &str, parent: &str) -> Option<SourceLocation> {
449 let parent_short = short_name_from_iri(parent);
450 for (line_idx, line) in source_text.lines().enumerate() {
451 if !line.contains("subClassOf") {
452 continue;
453 }
454 if (line.contains(subject) || line.contains(&short_name_from_iri(subject)))
455 && (line.contains(parent) || line.contains(&parent_short))
456 {
457 let start_byte = line_start_byte(source_text, line_idx);
458 return Some(SourceLocation {
459 line: Some((line_idx + 1) as u64),
460 column: Some(0),
461 start_byte: Some(start_byte),
462 end_byte: Some(start_byte + line.len() as u64),
463 });
464 }
465 }
466 None
467}
468
469#[cfg(test)]
470mod tests {
471 use super::*;
472 use strixonomy_core::{Entity, EntityKind};
473
474 fn ex_ns() -> BTreeMap<String, String> {
475 BTreeMap::from([
476 ("ex".into(), "http://example.org/people#".into()),
477 ("owl".into(), "http://www.w3.org/2002/07/owl#".into()),
478 ])
479 }
480
481 fn clinic_ns() -> BTreeMap<String, String> {
482 BTreeMap::from([("ex".into(), "http://example.org/clinic#".into())])
483 }
484
485 #[test]
486 fn finds_entity_line() {
487 let ttl = include_str!("../../../fixtures/example.ttl");
488 let loc = find_entity_block(ttl, "http://example.org/people#Person", "Person", &ex_ns());
489 assert!(loc.line.is_some());
490 let line = ttl.lines().nth(loc.line.unwrap() as usize - 1).unwrap();
491 assert!(line.contains("ex:Person a owl:Class"));
492 }
493
494 #[test]
495 fn subject_not_domain_mention() {
496 let ttl = include_str!("../../../fixtures/example.ttl");
497 let range =
498 find_subject_statement(ttl, "http://example.org/people#Person", "Person", &ex_ns())
499 .expect("subject");
500 let line = ttl[..range.start as usize].rfind('\n').map(|p| p + 1).unwrap_or(0);
501 let subject_line = &ttl[line..];
502 assert!(subject_line.starts_with("ex:Person a"));
503 }
504
505 #[test]
506 fn patient_block_includes_multiline_restriction() {
507 let ttl = include_str!("../../../fixtures/complex-classes.ttl");
508 let entity = Entity {
509 iri: "http://example.org/clinic#Patient".into(),
510 short_name: "Patient".into(),
511 kind: EntityKind::Class,
512 ontology_id: String::new(),
513 source_location: find_entity_block(
514 ttl,
515 "http://example.org/clinic#Patient",
516 "Patient",
517 &clinic_ns(),
518 ),
519 labels: vec![],
520 comments: vec![],
521 deprecated: false,
522 obo_id: None,
523 ..Default::default()
524 };
525 let range = entity_block_range(ttl, &entity, &clinic_ns()).expect("block");
526 let block = &ttl[range.start as usize..range.end as usize];
527 assert!(block.contains("owl:Restriction"));
528 assert!(block.contains("owl:someValuesFrom"));
529 assert!(block.trim_end().ends_with('.'));
530 }
531
532 #[test]
533 fn add_label_targets_class_block_not_trailing_triple() {
534 use crate::patch::{apply_patches_to_text, PatchOp};
535 let ttl = include_str!("../../../fixtures/example.ttl");
536 let patches = vec![PatchOp::AddLabel {
537 entity_iri: "http://example.org/people#Person".into(),
538 value: "Human".into(),
539 }];
540 let result = apply_patches_to_text(ttl, &patches, true, &ex_ns()).expect("patch");
541 let preview = result.preview_text.expect("preview");
542 let person_block_start = preview.find("ex:Person a owl:Class").expect("class decl");
543 let human_pos = preview.find("Human").expect("label");
544 assert!(human_pos > person_block_start);
545 let trailing = preview.find("ex:Person rdfs:subClassOf ex:Thing");
546 if let Some(t) = trailing {
547 assert!(human_pos < t, "label must be in class block, not trailing triple");
548 }
549 }
550
551 #[test]
552 fn statement_end_respects_dots_in_absolute_iris() {
553 let ttl = "<http://example.org/people#Person> a owl:Class ;\n rdfs:label \"Person\" .\n";
554 let end = statement_end_byte(ttl, 0).expect("end");
555 let block = &ttl[..end];
556 assert!(block.contains("rdfs:label"));
557 assert!(block.trim_end().ends_with('.'));
558 assert!(!block.ends_with("example."));
559 }
560
561 #[test]
562 fn statement_end_respects_dots_in_comments() {
563 let ttl = "ex:Person a owl:Class ; # see docs.\n rdfs:label \"Person\" .\n";
564 let end = statement_end_byte(ttl, 0).expect("end");
565 let block = &ttl[..end];
566 assert!(block.contains("rdfs:label"));
567 assert!(block.contains("# see docs."));
568 }
569
570 #[test]
571 fn statement_end_respects_dots_in_local_names() {
572 let ttl = "ex:foo.bar a owl:Class .\n";
573 let end = statement_end_byte(ttl, 0).expect("end");
574 assert_eq!(&ttl[..end], "ex:foo.bar a owl:Class .");
575 }
576
577 #[test]
578 fn is_in_comment_or_string_detects_all_turtle_string_forms() {
579 let double = "rdfs:label \"http://example.org#Person\" .";
580 let person_in_double = double.find("http://example.org#Person").unwrap();
581 assert!(is_in_comment_or_string(double, person_in_double));
582
583 let single = "rdfs:comment 'http://example.org#Person' .";
584 let person_in_single = single.find("http://example.org#Person").unwrap();
585 assert!(is_in_comment_or_string(single, person_in_single));
586
587 let long_double = "rdfs:comment \"\"\"http://example.org#Person\"\"\" .";
588 let person_in_long_double = long_double.find("http://example.org#Person").unwrap();
589 assert!(is_in_comment_or_string(long_double, person_in_long_double));
590
591 let long_single = "rdfs:comment '''http://example.org#Person''' .";
592 let person_in_long_single = long_single.find("http://example.org#Person").unwrap();
593 assert!(is_in_comment_or_string(long_single, person_in_long_single));
594
595 let outside = "ex:Person a owl:Class .";
596 let person_outside = outside.find("ex:Person").unwrap();
597 assert!(!is_in_comment_or_string(outside, person_outside));
598 }
599
600 #[test]
601 fn statement_end_respects_dots_in_long_strings() {
602 let ttl = "ex:Person a owl:Class ;\n rdfs:comment '''See Dr. Smith.''' .\n";
603 let end = statement_end_byte(ttl, 0).expect("end");
604 let block = &ttl[..end];
605 assert!(block.contains("Dr. Smith"));
606 assert!(block.trim_end().ends_with('.'));
607 }
608
609 #[test]
610 fn subject_needles_use_longest_namespace_local_not_short_name() {
611 let ns = BTreeMap::from([("ex".into(), "http://example.org/".into())]);
612 let iri = "http://example.org/foo/Person";
613 let needles = subject_needles(iri, "Person", &ns);
614 assert!(needles.contains(&format!("<{iri}>")));
615 assert!(
616 !needles.iter().any(|n| n == "ex:Person"),
617 "must not emit CURIE that expands to a different IRI: {needles:?}"
618 );
619 assert!(!needles.iter().any(|n| n.starts_with("ex:")));
621 }
622
623 #[test]
624 fn subject_needles_prefer_longest_prefix_match() {
625 let ns = BTreeMap::from([
626 ("ex".into(), "http://example.org/".into()),
627 ("foo".into(), "http://example.org/foo/".into()),
628 ]);
629 let iri = "http://example.org/foo/Person";
630 let needles = subject_needles(iri, "Person", &ns);
631 assert!(needles.contains(&"foo:Person".to_string()));
632 assert!(
633 !needles.iter().any(|n| n == "ex:Person"),
634 "shorter prefix must not produce a colliding CURIE: {needles:?}"
635 );
636 }
637
638 #[test]
639 fn find_subject_does_not_match_wrong_curie_under_short_prefix() {
640 let ttl = r#"@prefix ex: <http://example.org/> .
641@prefix owl: <http://www.w3.org/2002/07/owl#> .
642
643ex:Person a owl:Class .
644<http://example.org/foo/Person> a owl:Class .
645"#;
646 let ns = BTreeMap::from([
647 ("ex".into(), "http://example.org/".into()),
648 ("owl".into(), "http://www.w3.org/2002/07/owl#".into()),
649 ]);
650 let range = find_subject_statement(ttl, "http://example.org/foo/Person", "Person", &ns)
651 .expect("nested path subject");
652 let line_start = ttl[..range.start as usize].rfind('\n').map(|p| p + 1).unwrap_or(0);
653 let subject_line = &ttl[line_start..];
654 assert!(
655 subject_line.trim_start().starts_with("<http://example.org/foo/Person>"),
656 "must locate the absolute IRI subject, not ex:Person: {subject_line}"
657 );
658 }
659
660 #[test]
661 fn statement_end_includes_single_quoted_label() {
662 let ttl = r#"ex:Person a owl:Class ;
663 rdfs:label 'Human' ."#;
664 let start = ttl.find("ex:Person").unwrap();
665 let end = statement_end_byte(ttl, start).expect("end");
666 assert!(ttl[start..end].contains("'Human'"));
667 }
668
669 #[test]
670 fn statement_end_respects_escaped_triple_quotes_in_long_string() {
671 let ttl = r#"@prefix ex: <http://example.org/> .
672@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
673ex:A a owl:Class ;
674 rdfs:comment """contains \""" quote""" .
675"#;
676 let start = ttl.find("ex:A").unwrap();
677 let end = statement_end_byte(ttl, start).expect("end");
678 assert!(ttl[start..end].contains(r#"\""" quote"""#));
679 }
680
681 #[test]
682 fn prefixes_from_turtle_accepts_prefix_keyword() {
683 let ttl = "PREFIX ex: <http://example.org/people#>\n";
684 let map = prefixes_from_turtle(ttl);
685 assert_eq!(map.get("ex"), Some(&"http://example.org/people#".to_string()));
686 }
687
688 #[test]
689 fn prefixes_from_turtle_accepts_uppercase_at_prefix() {
690 let ttl = "@PREFIX owl: <http://www.w3.org/2002/07/owl#> .\n";
691 let map = prefixes_from_turtle(ttl);
692 assert_eq!(map.get("owl"), Some(&"http://www.w3.org/2002/07/owl#".to_string()));
693 }
694
695 #[test]
696 fn prefixes_from_turtle_scans_after_prefix_form_and_default_prefix() {
697 let ttl = concat!(
698 "PREFIX ex: <http://example.org/people#>\n",
699 "@prefix owl: <http://www.w3.org/2002/07/owl#> .\n",
700 "@prefix : <http://example.org/org#> .\n",
701 ":Person a owl:Class .\n"
702 );
703 let map = prefixes_from_turtle(ttl);
704 assert_eq!(map.get("ex"), Some(&"http://example.org/people#".to_string()));
705 assert_eq!(map.get("owl"), Some(&"http://www.w3.org/2002/07/owl#".to_string()));
706 assert_eq!(map.get(""), Some(&"http://example.org/org#".to_string()));
707 }
708}