1use std::collections::HashMap;
20use std::fmt;
21
22const RDF_TYPE: &str = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type";
27const RDF_PREFIX: &str = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
28#[allow(dead_code)]
29const XSD_STRING: &str = "http://www.w3.org/2001/XMLSchema#string";
30
31fn default_prefixes() -> HashMap<String, String> {
33 let mut m = HashMap::new();
34 m.insert("rdf".into(), RDF_PREFIX.into());
35 m.insert(
36 "rdfs".into(),
37 "http://www.w3.org/2000/01/rdf-schema#".into(),
38 );
39 m.insert("xsd".into(), "http://www.w3.org/2001/XMLSchema#".into());
40 m.insert("owl".into(), "http://www.w3.org/2002/07/owl#".into());
41 m.insert("dc".into(), "http://purl.org/dc/elements/1.1/".into());
42 m.insert("dcterms".into(), "http://purl.org/dc/terms/".into());
43 m.insert("foaf".into(), "http://xmlns.com/foaf/0.1/".into());
44 m.insert("schema".into(), "https://schema.org/".into());
45 m
46}
47
48#[derive(Debug, Clone, PartialEq, Eq)]
54pub struct Attribute {
55 pub name: String,
57 pub value: String,
59}
60
61impl Attribute {
62 pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
64 Self {
65 name: name.into(),
66 value: value.into(),
67 }
68 }
69}
70
71#[derive(Debug, Clone)]
73pub struct Element {
74 pub tag: String,
76 pub attributes: Vec<Attribute>,
78 pub text: String,
80 pub children: Vec<Element>,
82}
83
84impl Element {
85 pub fn new(tag: impl Into<String>) -> Self {
87 Self {
88 tag: tag.into(),
89 attributes: Vec::new(),
90 text: String::new(),
91 children: Vec::new(),
92 }
93 }
94
95 pub fn attr(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
97 self.attributes.push(Attribute::new(name, value));
98 self
99 }
100
101 pub fn text(mut self, text: impl Into<String>) -> Self {
103 self.text = text.into();
104 self
105 }
106
107 pub fn child(mut self, child: Element) -> Self {
109 self.children.push(child);
110 self
111 }
112
113 pub fn get_attr(&self, name: &str) -> Option<&str> {
115 self.attributes
116 .iter()
117 .find(|a| a.name == name)
118 .map(|a| a.value.as_str())
119 }
120}
121
122#[derive(Debug, Clone, PartialEq, Eq)]
131pub struct RdfaTriple {
132 pub subject: String,
134 pub predicate: String,
136 pub object: RdfaObject,
138}
139
140#[derive(Debug, Clone, PartialEq, Eq)]
142pub enum RdfaObject {
143 Iri(String),
145 Literal(String),
147 TypedLiteral {
149 value: String,
151 datatype: String,
153 },
154 LangLiteral {
156 value: String,
158 lang: String,
160 },
161}
162
163impl fmt::Display for RdfaObject {
164 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
165 match self {
166 RdfaObject::Iri(iri) => write!(f, "<{iri}>"),
167 RdfaObject::Literal(v) => write!(f, "\"{v}\""),
168 RdfaObject::TypedLiteral { value, datatype } => {
169 write!(f, "\"{value}\"^^<{datatype}>")
170 }
171 RdfaObject::LangLiteral { value, lang } => write!(f, "\"{value}\"@{lang}"),
172 }
173 }
174}
175
176#[derive(Debug, Clone)]
182struct EvalContext {
183 subject: Option<String>,
185 base: String,
187 prefixes: HashMap<String, String>,
189 vocab: Option<String>,
191 lang: Option<String>,
193}
194
195impl EvalContext {
196 fn new(base: &str) -> Self {
197 Self {
198 subject: None,
199 base: base.to_string(),
200 prefixes: default_prefixes(),
201 vocab: None,
202 lang: None,
203 }
204 }
205
206 fn resolve(&self, term: &str) -> Option<String> {
208 let trimmed = term.trim();
209 if trimmed.is_empty() {
210 return None;
211 }
212 if trimmed.starts_with('<') && trimmed.ends_with('>') {
214 return Some(trimmed[1..trimmed.len() - 1].to_string());
215 }
216 if trimmed.contains("://") {
217 return Some(trimmed.to_string());
218 }
219 if let Some(colon) = trimmed.find(':') {
221 let prefix = &trimmed[..colon];
222 let local = &trimmed[colon + 1..];
223 if let Some(ns) = self.prefixes.get(prefix) {
224 return Some(format!("{ns}{local}"));
225 }
226 }
227 if let Some(vocab) = &self.vocab {
229 return Some(format!("{vocab}{trimmed}"));
230 }
231 if !self.base.is_empty() {
233 return Some(format!("{}{}", self.base, trimmed));
234 }
235 None
236 }
237
238 fn register_prefix_attr(&mut self, prefix_attr: &str) {
242 let tokens: Vec<&str> = prefix_attr.split_whitespace().collect();
243 let mut i = 0;
244 while i + 1 < tokens.len() {
245 let pfx = tokens[i];
246 let ns = tokens[i + 1];
247 if let Some(prefix_label) = pfx.strip_suffix(':') {
248 self.prefixes
249 .insert(prefix_label.to_string(), ns.to_string());
250 }
251 i += 2;
252 }
253 }
254}
255
256#[derive(Debug, Clone)]
262pub struct RdfaConfig {
263 pub base: String,
265 pub process_typeof: bool,
267 pub process_rel_rev: bool,
269}
270
271impl Default for RdfaConfig {
272 fn default() -> Self {
273 Self {
274 base: String::new(),
275 process_typeof: true,
276 process_rel_rev: true,
277 }
278 }
279}
280
281pub struct RdfaParser {
283 config: RdfaConfig,
284}
285
286impl RdfaParser {
287 pub fn new(config: RdfaConfig) -> Self {
289 Self { config }
290 }
291
292 pub fn parse(&self, root: &Element) -> Vec<RdfaTriple> {
294 let ctx = EvalContext::new(&self.config.base);
295 let mut triples = Vec::new();
296 self.process_element(root, &ctx, &mut triples);
297 triples
298 }
299
300 fn process_element(
301 &self,
302 element: &Element,
303 parent_ctx: &EvalContext,
304 triples: &mut Vec<RdfaTriple>,
305 ) {
306 let mut ctx = parent_ctx.clone();
307
308 if let Some(pfx_attr) = element.get_attr("prefix") {
310 ctx.register_prefix_attr(pfx_attr);
311 }
312
313 if let Some(vocab) = element.get_attr("vocab") {
315 ctx.vocab = Some(vocab.to_string());
316 }
317
318 if let Some(lang) = element
320 .get_attr("lang")
321 .or_else(|| element.get_attr("xml:lang"))
322 {
323 ctx.lang = if lang.is_empty() {
324 None
325 } else {
326 Some(lang.to_string())
327 };
328 }
329
330 let new_subject: Option<String> = element
333 .get_attr("about")
334 .and_then(|v| ctx.resolve(v))
335 .or_else(|| ctx.subject.clone());
336
337 let resource_iri: Option<String> =
340 element.get_attr("resource").and_then(|v| ctx.resolve(v));
341
342 let effective_subject: Option<String> =
344 new_subject.clone().or_else(|| resource_iri.clone());
345
346 if self.config.process_typeof {
348 if let (Some(subj), Some(typeof_attr)) =
349 (&effective_subject, element.get_attr("typeof"))
350 {
351 for type_term in typeof_attr.split_whitespace() {
352 if let Some(type_iri) = ctx.resolve(type_term) {
353 triples.push(RdfaTriple {
354 subject: subj.clone(),
355 predicate: RDF_TYPE.to_string(),
356 object: RdfaObject::Iri(type_iri),
357 });
358 }
359 }
360 }
361 }
362
363 if let (Some(subj), Some(property_attr)) =
365 (&effective_subject, element.get_attr("property"))
366 {
367 for prop_term in property_attr.split_whitespace() {
368 if let Some(predicate) = ctx.resolve(prop_term) {
369 let object = self.extract_object(element, &ctx, &resource_iri);
371 if let Some(obj) = object {
372 triples.push(RdfaTriple {
373 subject: subj.clone(),
374 predicate,
375 object: obj,
376 });
377 }
378 }
379 }
380 }
381
382 if self.config.process_rel_rev {
384 if let (Some(subj), Some(rel_attr), Some(obj_iri)) = (
385 &effective_subject,
386 element.get_attr("rel"),
387 resource_iri.as_ref().or(element
388 .get_attr("href")
389 .and_then(|h| ctx.resolve(h))
390 .as_ref()),
391 ) {
392 for rel_term in rel_attr.split_whitespace() {
393 if let Some(predicate) = ctx.resolve(rel_term) {
394 triples.push(RdfaTriple {
395 subject: subj.clone(),
396 predicate,
397 object: RdfaObject::Iri(obj_iri.clone()),
398 });
399 }
400 }
401 }
402
403 if let (Some(subj), Some(rev_attr), Some(obj_iri)) = (
405 &effective_subject,
406 element.get_attr("rev"),
407 resource_iri.as_ref().or(element
408 .get_attr("href")
409 .and_then(|h| ctx.resolve(h))
410 .as_ref()),
411 ) {
412 for rev_term in rev_attr.split_whitespace() {
413 if let Some(predicate) = ctx.resolve(rev_term) {
414 triples.push(RdfaTriple {
416 subject: obj_iri.clone(),
417 predicate,
418 object: RdfaObject::Iri(subj.clone()),
419 });
420 }
421 }
422 }
423 }
424
425 let child_subject = if element.get_attr("property").is_none() {
430 resource_iri.or(effective_subject)
431 } else {
432 effective_subject
433 };
434 ctx.subject = child_subject;
435
436 for child in &element.children {
437 self.process_element(child, &ctx, triples);
438 }
439 }
440
441 fn extract_object(
450 &self,
451 element: &Element,
452 ctx: &EvalContext,
453 resource_iri: &Option<String>,
454 ) -> Option<RdfaObject> {
455 if let Some(iri) = resource_iri {
457 if element.get_attr("content").is_none() && element.get_attr("datatype").is_none() {
458 return Some(RdfaObject::Iri(iri.clone()));
459 }
460 }
461
462 let raw_value: String = element
463 .get_attr("content")
464 .map(|s| s.to_string())
465 .unwrap_or_else(|| element.text.clone());
466
467 let datatype = element.get_attr("datatype").and_then(|dt| ctx.resolve(dt));
468 let lang = element
469 .get_attr("lang")
470 .or_else(|| element.get_attr("xml:lang"))
471 .and_then(|l| {
472 if l.is_empty() {
473 None
474 } else {
475 Some(l.to_string())
476 }
477 })
478 .or_else(|| {
479 if element.get_attr("lang").is_some() || element.get_attr("xml:lang").is_some() {
481 None
482 } else {
483 ctx.lang.clone()
484 }
485 });
486
487 if let Some(dt) = datatype {
488 return Some(RdfaObject::TypedLiteral {
489 value: raw_value,
490 datatype: dt,
491 });
492 }
493 if let Some(l) = lang {
494 return Some(RdfaObject::LangLiteral {
495 value: raw_value,
496 lang: l,
497 });
498 }
499 if raw_value.is_empty() {
501 None
502 } else {
503 Some(RdfaObject::Literal(raw_value))
504 }
505 }
506}
507
508pub fn extract_head_prefixes(head: &Element) -> HashMap<String, String> {
517 let mut prefixes = default_prefixes();
518 if let Some(pfx) = head.get_attr("prefix") {
519 let mut ctx = EvalContext::new("");
520 ctx.register_prefix_attr(pfx);
521 prefixes.extend(ctx.prefixes);
522 }
523 for child in &head.children {
524 if let Some(pfx) = child.get_attr("prefix") {
525 let mut ctx = EvalContext::new("");
526 ctx.register_prefix_attr(pfx);
527 for (k, v) in ctx.prefixes {
528 prefixes.insert(k, v);
529 }
530 }
531 }
532 prefixes
533}
534
535#[cfg(test)]
540mod tests {
541 use super::*;
542
543 fn parser() -> RdfaParser {
544 RdfaParser::new(RdfaConfig {
545 base: "http://example.org/".to_string(),
546 ..Default::default()
547 })
548 }
549
550 #[test]
555 fn test_typeof_single() {
556 let el = Element::new("div")
557 .attr("about", "http://example.org/alice")
558 .attr("typeof", "foaf:Person");
559 let triples = parser().parse(&el);
560 assert_eq!(triples.len(), 1);
561 assert_eq!(triples[0].predicate, RDF_TYPE);
562 assert_eq!(
563 triples[0].object,
564 RdfaObject::Iri("http://xmlns.com/foaf/0.1/Person".to_string())
565 );
566 }
567
568 #[test]
569 fn test_typeof_multiple_types() {
570 let el = Element::new("div")
571 .attr("about", "http://example.org/alice")
572 .attr("typeof", "foaf:Person schema:Person");
573 let triples = parser().parse(&el);
574 assert_eq!(triples.len(), 2);
575 }
576
577 #[test]
582 fn test_property_plain_literal() {
583 let el = Element::new("span")
584 .attr("about", "http://example.org/alice")
585 .attr("property", "foaf:name")
586 .text("Alice");
587 let triples = parser().parse(&el);
588 assert_eq!(triples.len(), 1);
589 assert_eq!(triples[0].object, RdfaObject::Literal("Alice".to_string()));
590 }
591
592 #[test]
593 fn test_property_content_attribute() {
594 let el = Element::new("span")
595 .attr("about", "http://example.org/alice")
596 .attr("property", "foaf:name")
597 .attr("content", "Alice Smith")
598 .text("displayed text");
599 let triples = parser().parse(&el);
600 assert_eq!(
601 triples[0].object,
602 RdfaObject::Literal("Alice Smith".to_string())
603 );
604 }
605
606 #[test]
607 fn test_property_typed_literal() {
608 let el = Element::new("span")
609 .attr("about", "http://example.org/event")
610 .attr("property", "schema:startDate")
611 .attr("datatype", "xsd:date")
612 .text("2026-01-01");
613 let triples = parser().parse(&el);
614 assert_eq!(triples.len(), 1);
615 match &triples[0].object {
616 RdfaObject::TypedLiteral { value, datatype } => {
617 assert_eq!(value, "2026-01-01");
618 assert!(datatype.contains("date"));
619 }
620 other => panic!("Expected TypedLiteral, got {other:?}"),
621 }
622 }
623
624 #[test]
625 fn test_property_lang_literal() {
626 let el = Element::new("span")
627 .attr("about", "http://example.org/doc")
628 .attr("property", "dc:title")
629 .attr("lang", "en")
630 .text("Hello");
631 let triples = parser().parse(&el);
632 assert_eq!(
633 triples[0].object,
634 RdfaObject::LangLiteral {
635 value: "Hello".to_string(),
636 lang: "en".to_string()
637 }
638 );
639 }
640
641 #[test]
646 fn test_about_absolute_iri() {
647 let el = Element::new("div")
648 .attr("about", "http://example.org/resource")
649 .attr("property", "rdfs:label")
650 .text("Test");
651 let triples = parser().parse(&el);
652 assert_eq!(triples[0].subject, "http://example.org/resource");
653 }
654
655 #[test]
656 fn test_about_curie() {
657 let el = Element::new("div")
658 .attr("prefix", "ex: http://example.org/")
659 .attr("about", "ex:alice")
660 .attr("property", "rdfs:label")
661 .text("Alice");
662 let triples = parser().parse(&el);
663 assert_eq!(triples[0].subject, "http://example.org/alice");
664 }
665
666 #[test]
671 fn test_resource_as_object_iri() {
672 let el = Element::new("span")
673 .attr("about", "http://example.org/alice")
674 .attr("property", "foaf:knows")
675 .attr("resource", "http://example.org/bob");
676 let triples = parser().parse(&el);
677 assert_eq!(triples.len(), 1);
678 assert_eq!(
679 triples[0].object,
680 RdfaObject::Iri("http://example.org/bob".to_string())
681 );
682 }
683
684 #[test]
689 fn test_prefix_declaration() {
690 let el = Element::new("div")
691 .attr(
692 "prefix",
693 "ex: http://example.org/ dc: http://purl.org/dc/elements/1.1/",
694 )
695 .attr("about", "ex:book1")
696 .attr("property", "dc:title")
697 .text("The Book");
698 let triples = parser().parse(&el);
699 assert_eq!(triples[0].subject, "http://example.org/book1");
700 assert_eq!(
701 triples[0].predicate,
702 "http://purl.org/dc/elements/1.1/title"
703 );
704 }
705
706 #[test]
711 fn test_rel_attribute() {
712 let el = Element::new("a")
713 .attr("about", "http://example.org/alice")
714 .attr("rel", "foaf:knows")
715 .attr("resource", "http://example.org/bob");
716 let triples = parser().parse(&el);
717 assert!(triples
718 .iter()
719 .any(|t| t.predicate == "http://xmlns.com/foaf/0.1/knows"));
720 }
721
722 #[test]
723 fn test_rev_attribute() {
724 let el = Element::new("a")
725 .attr("about", "http://example.org/bob")
726 .attr("rev", "foaf:knows")
727 .attr("resource", "http://example.org/alice");
728 let triples = parser().parse(&el);
729 let rev_triple = triples
731 .iter()
732 .find(|t| t.predicate == "http://xmlns.com/foaf/0.1/knows");
733 assert!(rev_triple.is_some());
734 let rv = rev_triple.expect("rev triple should exist");
735 assert_eq!(rv.subject, "http://example.org/alice");
737 }
738
739 #[test]
744 fn test_subject_inheritance_from_parent() {
745 let child = Element::new("span")
746 .attr("property", "foaf:name")
747 .text("Alice");
748 let parent = Element::new("div")
749 .attr("about", "http://example.org/alice")
750 .child(child);
751 let triples = parser().parse(&parent);
752 assert!(triples
753 .iter()
754 .any(|t| t.subject == "http://example.org/alice"));
755 }
756
757 #[test]
758 fn test_lang_inheritance_from_parent() {
759 let child = Element::new("span")
760 .attr("property", "rdfs:label")
761 .text("Bonjour");
762 let parent = Element::new("div")
763 .attr("about", "http://example.org/res")
764 .attr("lang", "fr")
765 .child(child);
766 let triples = parser().parse(&parent);
767 let label_triple = triples.iter().find(|t| t.predicate.contains("label"));
768 assert!(label_triple.is_some());
769 match &label_triple.expect("label triple should exist").object {
770 RdfaObject::LangLiteral { lang, .. } => assert_eq!(lang, "fr"),
771 other => panic!("Expected LangLiteral, got {other:?}"),
772 }
773 }
774
775 #[test]
780 fn test_vocab_attribute() {
781 let el = Element::new("div")
782 .attr("vocab", "https://schema.org/")
783 .attr("about", "http://example.org/person")
784 .attr("typeof", "Person");
785 let triples = parser().parse(&el);
786 assert!(triples.iter().any(|t| {
787 t.predicate == RDF_TYPE
788 && t.object == RdfaObject::Iri("https://schema.org/Person".to_string())
789 }));
790 }
791
792 #[test]
797 fn test_extract_head_prefixes() {
798 let head =
799 Element::new("head").attr("prefix", "ex: http://example.org/ my: http://my.org/");
800 let prefixes = extract_head_prefixes(&head);
801 assert_eq!(
802 prefixes.get("ex").map(|s| s.as_str()),
803 Some("http://example.org/")
804 );
805 assert_eq!(
806 prefixes.get("my").map(|s| s.as_str()),
807 Some("http://my.org/")
808 );
809 }
810
811 #[test]
816 fn test_rdfa_object_iri_display() {
817 let obj = RdfaObject::Iri("http://example.org/".to_string());
818 assert_eq!(obj.to_string(), "<http://example.org/>");
819 }
820
821 #[test]
822 fn test_rdfa_object_literal_display() {
823 let obj = RdfaObject::Literal("hello".to_string());
824 assert_eq!(obj.to_string(), "\"hello\"");
825 }
826
827 #[test]
828 fn test_rdfa_object_typed_literal_display() {
829 let obj = RdfaObject::TypedLiteral {
830 value: "42".to_string(),
831 datatype: XSD_STRING.to_string(),
832 };
833 assert!(obj.to_string().contains("42"));
834 }
835
836 #[test]
837 fn test_rdfa_object_lang_literal_display() {
838 let obj = RdfaObject::LangLiteral {
839 value: "hello".to_string(),
840 lang: "en".to_string(),
841 };
842 assert_eq!(obj.to_string(), "\"hello\"@en");
843 }
844
845 #[test]
850 fn test_multiple_properties_space_separated() {
851 let el = Element::new("span")
852 .attr("about", "http://example.org/doc")
853 .attr("property", "dc:title rdfs:label")
854 .text("My Doc");
855 let triples = parser().parse(&el);
856 assert_eq!(triples.len(), 2);
857 }
858
859 #[test]
864 fn test_empty_text_no_triple() {
865 let el = Element::new("span")
866 .attr("about", "http://example.org/doc")
867 .attr("property", "dc:description");
868 let triples = parser().parse(&el);
869 assert!(triples.is_empty());
871 }
872
873 #[test]
878 fn test_no_subject_no_triple() {
879 let el = Element::new("span")
881 .attr("property", "foaf:name")
882 .text("Orphan");
883 let triples = parser().parse(&el);
884 assert!(triples.is_empty());
885 }
886
887 #[test]
892 fn test_element_builder_get_attr() {
893 let el = Element::new("div").attr("id", "main").attr("class", "hero");
894 assert_eq!(el.get_attr("id"), Some("main"));
895 assert_eq!(el.get_attr("class"), Some("hero"));
896 assert_eq!(el.get_attr("missing"), None);
897 }
898
899 #[test]
900 fn test_attribute_struct() {
901 let a = Attribute::new("rel", "stylesheet");
902 assert_eq!(a.name, "rel");
903 assert_eq!(a.value, "stylesheet");
904 }
905
906 #[test]
907 fn test_typeof_no_about() {
908 let el = Element::new("div").attr("typeof", "foaf:Person");
910 let triples = parser().parse(&el);
911 assert!(triples.is_empty());
912 }
913
914 #[test]
915 fn test_property_iri_object_via_resource() {
916 let el = Element::new("a")
917 .attr("about", "http://example.org/s")
918 .attr("property", "foaf:homepage")
919 .attr("resource", "http://example.org/page");
920 let triples = parser().parse(&el);
921 assert!(triples
922 .iter()
923 .any(|t| matches!(&t.object, RdfaObject::Iri(iri) if iri.contains("page"))));
924 }
925
926 #[test]
927 fn test_default_prefix_rdf() {
928 let el = Element::new("span")
930 .attr("about", "http://example.org/r")
931 .attr("typeof", "rdf:Resource");
932 let triples = parser().parse(&el);
933 assert!(!triples.is_empty());
934 assert!(triples[0]
935 .object
936 .to_string()
937 .contains("rdf-syntax-ns#Resource"));
938 }
939
940 #[test]
941 fn test_xml_lang_attribute() {
942 let el = Element::new("span")
943 .attr("about", "http://example.org/r")
944 .attr("property", "dc:title")
945 .attr("xml:lang", "de")
946 .text("Hallo");
947 let triples = parser().parse(&el);
948 assert_eq!(
949 triples[0].object,
950 RdfaObject::LangLiteral {
951 value: "Hallo".to_string(),
952 lang: "de".to_string()
953 }
954 );
955 }
956
957 #[test]
958 fn test_multiple_children_multiple_triples() {
959 let child1 = Element::new("span")
960 .attr("property", "foaf:name")
961 .text("Alice");
962 let child2 = Element::new("span")
963 .attr("property", "foaf:mbox")
964 .attr("resource", "mailto:alice@example.org");
965 let parent = Element::new("div")
966 .attr("about", "http://example.org/alice")
967 .child(child1)
968 .child(child2);
969 let triples = parser().parse(&parent);
970 assert!(triples.len() >= 2);
971 }
972
973 #[test]
974 fn test_nested_resource_sets_child_subject() {
975 let child = Element::new("span")
976 .attr("property", "foaf:name")
977 .text("Bob");
978 let parent = Element::new("div")
979 .attr("about", "http://example.org/alice")
980 .attr("resource", "http://example.org/bob")
981 .child(child);
982 let triples = parser().parse(&parent);
983 let name_triple = triples.iter().find(|t| t.predicate.contains("name"));
985 assert!(name_triple.is_some());
986 assert_eq!(
987 name_triple.expect("name triple should exist").subject,
988 "http://example.org/bob"
989 );
990 }
991
992 #[test]
993 fn test_extract_head_prefixes_with_child() {
994 let meta = Element::new("meta").attr("prefix", "schema: https://schema.org/");
995 let head = Element::new("head").child(meta);
996 let prefixes = extract_head_prefixes(&head);
997 assert_eq!(
998 prefixes.get("schema").map(|s| s.as_str()),
999 Some("https://schema.org/")
1000 );
1001 }
1002
1003 #[test]
1004 fn test_rdfa_config_default() {
1005 let cfg = RdfaConfig::default();
1006 assert!(cfg.process_typeof);
1007 assert!(cfg.process_rel_rev);
1008 assert!(cfg.base.is_empty());
1009 }
1010
1011 #[test]
1012 fn test_parser_no_process_typeof() {
1013 let cfg = RdfaConfig {
1014 process_typeof: false,
1015 base: "http://example.org/".to_string(),
1016 ..Default::default()
1017 };
1018 let el = Element::new("div")
1019 .attr("about", "http://example.org/alice")
1020 .attr("typeof", "foaf:Person");
1021 let triples = RdfaParser::new(cfg).parse(&el);
1022 assert!(triples.is_empty());
1023 }
1024
1025 #[test]
1026 fn test_parser_no_process_rel_rev() {
1027 let cfg = RdfaConfig {
1028 process_rel_rev: false,
1029 base: "http://example.org/".to_string(),
1030 ..Default::default()
1031 };
1032 let el = Element::new("a")
1033 .attr("about", "http://example.org/s")
1034 .attr("rel", "foaf:knows")
1035 .attr("resource", "http://example.org/o");
1036 let triples = RdfaParser::new(cfg).parse(&el);
1037 assert!(triples.is_empty());
1038 }
1039
1040 #[test]
1041 fn test_triple_subject_from_parent_and_child_typeof() {
1042 let child = Element::new("div")
1043 .attr("typeof", "schema:Book")
1044 .attr("about", "http://example.org/book1");
1045 let parent = Element::new("div")
1046 .attr("about", "http://example.org/collection")
1047 .child(child);
1048 let triples = parser().parse(&parent);
1049 assert!(triples
1050 .iter()
1051 .any(|t| t.subject == "http://example.org/book1"));
1052 }
1053
1054 #[test]
1055 fn test_about_relative_iri_resolved_with_base() {
1056 let el = Element::new("span")
1057 .attr("about", "alice")
1058 .attr("property", "foaf:name")
1059 .text("Alice");
1060 let triples = parser().parse(&el);
1062 assert_eq!(triples[0].subject, "http://example.org/alice");
1063 }
1064
1065 #[test]
1066 fn test_property_with_datatype_overrides_resource() {
1067 let el = Element::new("span")
1069 .attr("about", "http://example.org/e")
1070 .attr("property", "schema:startDate")
1071 .attr("datatype", "xsd:date")
1072 .attr("resource", "http://example.org/ignored")
1073 .text("2026-03-04");
1074 let triples = parser().parse(&el);
1075 assert!(matches!(
1076 &triples[0].object,
1077 RdfaObject::TypedLiteral { .. }
1078 ));
1079 }
1080
1081 #[test]
1082 fn test_default_prefixes_rdfs() {
1083 let el = Element::new("div")
1084 .attr("about", "http://example.org/r")
1085 .attr("property", "rdfs:comment")
1086 .text("A thing");
1087 let triples = parser().parse(&el);
1088 assert!(triples[0].predicate.contains("comment"));
1089 }
1090
1091 #[test]
1092 fn test_default_prefixes_owl() {
1093 let el = Element::new("div")
1094 .attr("about", "http://example.org/r")
1095 .attr("typeof", "owl:Class");
1096 let triples = parser().parse(&el);
1097 assert!(triples[0].object.to_string().contains("owl"));
1098 }
1099
1100 #[test]
1101 fn test_element_children_count() {
1102 let el = Element::new("div")
1103 .child(Element::new("span"))
1104 .child(Element::new("span"));
1105 assert_eq!(el.children.len(), 2);
1106 }
1107
1108 #[test]
1109 fn test_element_text_method() {
1110 let el = Element::new("span").text("hello");
1111 assert_eq!(el.text, "hello");
1112 }
1113
1114 #[test]
1115 fn test_rdfa_triple_fields() {
1116 let triple = RdfaTriple {
1117 subject: "http://example.org/s".to_string(),
1118 predicate: "http://example.org/p".to_string(),
1119 object: RdfaObject::Literal("value".to_string()),
1120 };
1121 assert_eq!(triple.subject, "http://example.org/s");
1122 assert_eq!(triple.predicate, "http://example.org/p");
1123 }
1124
1125 #[test]
1126 fn test_lang_empty_string_clears_lang() {
1127 let child = Element::new("span")
1128 .attr("property", "dc:title")
1129 .attr("lang", "")
1130 .text("Title");
1131 let parent = Element::new("div")
1132 .attr("about", "http://example.org/doc")
1133 .attr("lang", "fr")
1134 .child(child);
1135 let triples = parser().parse(&parent);
1136 let title = triples.iter().find(|t| t.predicate.contains("title"));
1137 if let Some(t) = title {
1139 assert!(matches!(&t.object, RdfaObject::Literal(_)));
1140 }
1141 }
1142
1143 #[test]
1144 fn test_curie_unknown_prefix_not_resolved() {
1145 let el = Element::new("span")
1146 .attr("about", "http://example.org/r")
1147 .attr("property", "unknown:prop")
1148 .text("value");
1149 let _triples = parser().parse(&el);
1152 }
1154}