1use rdom_core::{Dom, NodeId};
17
18use crate::error::{ParseError, Result};
19
20const VOID_TAGS: &[&str] = &[
24 "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source",
25 "track", "wbr", "vr",
26];
27
28fn is_void_tag(tag: &str) -> bool {
29 VOID_TAGS.contains(&tag)
30}
31
32pub fn parse<Ext>(template: &str) -> Result<(Dom<Ext>, Vec<NodeId>)>
35where
36 Ext: Default + 'static,
37{
38 let mut dom = Dom::new();
39 let root = dom.root();
40 let ids = parse_into(&mut dom, template, root)?;
41 Ok((dom, ids))
42}
43
44pub fn parse_into<Ext>(dom: &mut Dom<Ext>, template: &str, mount: NodeId) -> Result<Vec<NodeId>>
48where
49 Ext: Default + 'static,
50{
51 let mut p = Parser::new(template);
52 let ids = p.parse_nodes(dom, mount)?;
53 Ok(ids)
54}
55
56struct Parser<'a> {
59 src: &'a str,
60 bytes: &'a [u8],
61 pos: usize,
62 line: u32,
63 col: u32,
64}
65
66impl<'a> Parser<'a> {
67 fn new(src: &'a str) -> Self {
68 Self {
69 src,
70 bytes: src.as_bytes(),
71 pos: 0,
72 line: 1,
73 col: 1,
74 }
75 }
76
77 fn eof(&self) -> bool {
80 self.pos >= self.bytes.len()
81 }
82
83 fn peek(&self) -> Option<u8> {
84 self.bytes.get(self.pos).copied()
85 }
86
87 fn peek_at(&self, offset: usize) -> Option<u8> {
88 self.bytes.get(self.pos + offset).copied()
89 }
90
91 fn starts_with(&self, needle: &str) -> bool {
92 self.src[self.pos..].starts_with(needle)
93 }
94
95 fn advance(&mut self) -> Option<u8> {
96 let b = self.peek()?;
97 self.pos += 1;
98 if b == b'\n' {
99 self.line += 1;
100 self.col = 1;
101 } else {
102 self.col += 1;
103 }
104 Some(b)
105 }
106
107 fn advance_n(&mut self, n: usize) {
108 for _ in 0..n {
109 if self.advance().is_none() {
110 break;
111 }
112 }
113 }
114
115 fn skip_ws(&mut self) {
116 while let Some(b) = self.peek() {
117 if b.is_ascii_whitespace() {
118 self.advance();
119 } else {
120 break;
121 }
122 }
123 }
124
125 fn err(&self, msg: impl Into<String>) -> ParseError {
126 ParseError::new(msg, self.line, self.col, self.pos)
127 }
128
129 fn parse_nodes<Ext>(&mut self, dom: &mut Dom<Ext>, parent: NodeId) -> Result<Vec<NodeId>>
132 where
133 Ext: Default + 'static,
134 {
135 let mut out = Vec::new();
136 loop {
137 if self.eof() {
138 break;
139 }
140 if self.starts_with("</") {
141 break;
143 }
144 if self.starts_with("<!--") {
145 let id = self.parse_comment(dom, parent)?;
146 out.push(id);
147 continue;
148 }
149 if self.peek() == Some(b'<') {
150 let id = self.parse_element(dom, parent)?;
151 out.push(id);
152 continue;
153 }
154 let id = self.parse_text(dom, parent)?;
156 if let Some(id) = id {
157 out.push(id);
158 }
159 }
160 Ok(out)
161 }
162
163 fn parse_comment<Ext>(&mut self, dom: &mut Dom<Ext>, parent: NodeId) -> Result<NodeId>
166 where
167 Ext: Default + 'static,
168 {
169 self.advance_n(4);
171 let start = self.pos;
172 loop {
173 if self.eof() {
174 return Err(self
175 .err("unterminated comment")
176 .with_hint("missing `-->` closing"));
177 }
178 if self.starts_with("-->") {
179 let data = &self.src[start..self.pos];
180 self.advance_n(3);
181 let id = dom.create_comment(data);
182 dom.append_child(parent, id)
183 .map_err(|e| self.err(format!("failed to append comment: {:?}", e)))?;
184 return Ok(id);
185 }
186 self.advance();
187 }
188 }
189
190 fn parse_text<Ext>(&mut self, dom: &mut Dom<Ext>, parent: NodeId) -> Result<Option<NodeId>>
198 where
199 Ext: Default + 'static,
200 {
201 let mut out = String::new();
202 loop {
203 let slice_start = self.pos;
206 while let Some(b) = self.peek() {
207 if b == b'<' || b == b'&' {
208 break;
209 }
210 self.advance();
211 }
212 if slice_start < self.pos {
213 out.push_str(&self.src[slice_start..self.pos]);
214 }
215 match self.peek() {
216 None | Some(b'<') => break,
217 Some(b'&') => {
218 out.push_str(&self.parse_entity()?);
219 }
220 _ => unreachable!(),
221 }
222 }
223 if out.is_empty() {
224 return Ok(None);
225 }
226 let id = dom.create_text_node(&out);
227 dom.append_child(parent, id)
228 .map_err(|e| self.err(format!("failed to append text: {:?}", e)))?;
229 Ok(Some(id))
230 }
231
232 fn parse_entity(&mut self) -> Result<String> {
235 debug_assert_eq!(self.peek(), Some(b'&'));
239 let save = self.snapshot();
240
241 self.advance(); let start = self.pos;
245 let mut end = None;
246 for i in 0..16 {
247 match self.peek_at(i) {
248 Some(b';') => {
249 end = Some(self.pos + i);
250 break;
251 }
252 Some(b) if b.is_ascii_alphanumeric() || b == b'#' || b == b'x' || b == b'X' => {
253 continue;
254 }
255 _ => break,
256 }
257 }
258
259 let Some(end) = end else {
260 self.restore(save);
262 self.advance(); return Ok("&".to_string());
264 };
265
266 let body = &self.src[start..end];
267 let decoded = decode_entity_body(body);
268 if let Some(d) = decoded {
269 let consume = end - self.pos + 1;
271 self.advance_n(consume);
272 Ok(d)
273 } else {
274 self.restore(save);
277 self.advance();
278 Ok("&".to_string())
279 }
280 }
281
282 fn parse_element<Ext>(&mut self, dom: &mut Dom<Ext>, parent: NodeId) -> Result<NodeId>
285 where
286 Ext: Default + 'static,
287 {
288 debug_assert_eq!(self.peek(), Some(b'<'));
289 self.advance(); let tag = self.parse_tag_name()?;
292 let tag_lc = tag.to_ascii_lowercase();
293
294 let element = dom.create_element(&tag_lc);
295
296 loop {
298 self.skip_ws();
299 match self.peek() {
300 None => {
301 return Err(self
302 .err(format!("unexpected EOF inside <{}>", tag_lc))
303 .with_hint("missing closing `>`"));
304 }
305 Some(b'>') => {
306 self.advance();
307 break;
308 }
309 Some(b'/') => {
310 self.advance();
312 self.skip_ws();
313 if self.peek() != Some(b'>') {
314 return Err(self
315 .err(format!("expected `>` after `/` in <{}/>", tag_lc))
316 .with_hint("self-closing syntax is `/>`"));
317 }
318 self.advance();
319 dom.append_child(parent, element)
320 .map_err(|e| self.err(format!("failed to append <{}>: {:?}", tag_lc, e)))?;
321 return Ok(element);
322 }
323 Some(_) => {
324 self.parse_attribute(dom, element)?;
325 }
326 }
327 }
328
329 if is_void_tag(&tag_lc) {
331 dom.append_child(parent, element)
332 .map_err(|e| self.err(format!("failed to append <{}>: {:?}", tag_lc, e)))?;
333 return Ok(element);
334 }
335
336 self.parse_nodes(dom, element)?;
338
339 if !self.starts_with("</") {
340 return Err(self
341 .err(format!("missing closing tag for <{}>", tag_lc))
342 .with_hint(format!("add </{}> to close", tag_lc)));
343 }
344 self.advance_n(2); let close_tag = self.parse_tag_name()?;
347 if close_tag.to_ascii_lowercase() != tag_lc {
348 return Err(self
349 .err(format!(
350 "mismatched closing tag: found </{}>, expected </{}>",
351 close_tag, tag_lc
352 ))
353 .with_hint("tags must be properly nested"));
354 }
355 self.skip_ws();
356 if self.peek() != Some(b'>') {
357 return Err(self
358 .err(format!("expected `>` in </{}>", tag_lc))
359 .with_hint("no attributes on closing tags"));
360 }
361 self.advance();
362
363 dom.append_child(parent, element)
364 .map_err(|e| self.err(format!("failed to append <{}>: {:?}", tag_lc, e)))?;
365 Ok(element)
366 }
367
368 fn parse_tag_name(&mut self) -> Result<String> {
369 let start = self.pos;
370 while let Some(b) = self.peek() {
371 if b.is_ascii_alphanumeric() || b == b'-' || b == b'_' {
372 self.advance();
373 } else {
374 break;
375 }
376 }
377 if start == self.pos {
378 return Err(self
379 .err("expected tag name")
380 .with_hint("tag names start with a letter"));
381 }
382 Ok(self.src[start..self.pos].to_string())
383 }
384
385 fn parse_attribute<Ext>(&mut self, dom: &mut Dom<Ext>, element: NodeId) -> Result<()>
386 where
387 Ext: Default + 'static,
388 {
389 let name = self.parse_attr_name()?;
390 self.skip_ws();
391
392 let value = if self.peek() == Some(b'=') {
393 self.advance();
394 self.skip_ws();
395 Some(self.parse_attr_value()?)
396 } else {
397 None
398 };
399
400 match value {
401 Some(v) => {
402 if name.eq_ignore_ascii_case("class") {
405 for token in v.split_ascii_whitespace() {
406 dom.add_class(element, token)
407 .map_err(|e| self.err(format!("failed to add class: {:?}", e)))?;
408 }
409 } else {
410 dom.set_attribute(element, &name, &v)
411 .map_err(|e| self.err(format!("failed to set attribute: {:?}", e)))?;
412 }
413 }
414 None => {
415 dom.set_attribute(element, &name, "")
417 .map_err(|e| self.err(format!("failed to set attribute: {:?}", e)))?;
418 }
419 }
420 Ok(())
421 }
422
423 fn parse_attr_name(&mut self) -> Result<String> {
424 let start = self.pos;
425 while let Some(b) = self.peek() {
426 if b.is_ascii_alphanumeric() || b == b'-' || b == b'_' || b == b':' {
427 self.advance();
428 } else {
429 break;
430 }
431 }
432 if start == self.pos {
433 return Err(self.err("expected attribute name"));
434 }
435 Ok(self.src[start..self.pos].to_string())
436 }
437
438 fn parse_attr_value(&mut self) -> Result<String> {
439 let first = self.peek();
440 match first {
441 Some(b'"') => self.parse_quoted(b'"'),
442 Some(b'\'') => self.parse_quoted(b'\''),
443 Some(_) => self.parse_unquoted(),
444 None => Err(self.err("unexpected EOF in attribute value")),
445 }
446 }
447
448 fn parse_quoted(&mut self, quote: u8) -> Result<String> {
449 self.advance(); let mut out = String::new();
451 loop {
452 let slice_start = self.pos;
454 while let Some(b) = self.peek() {
455 if b == quote || b == b'&' {
456 break;
457 }
458 self.advance();
459 }
460 if slice_start < self.pos {
461 out.push_str(&self.src[slice_start..self.pos]);
462 }
463 match self.peek() {
464 None => {
465 return Err(self
466 .err(format!(
467 "unterminated attribute value (expected `{}`)",
468 quote as char
469 ))
470 .with_hint("missing closing quote"));
471 }
472 Some(b) if b == quote => {
473 self.advance();
474 return Ok(out);
475 }
476 Some(b'&') => {
477 out.push_str(&self.parse_entity()?);
478 }
479 _ => unreachable!(),
480 }
481 }
482 }
483
484 fn parse_unquoted(&mut self) -> Result<String> {
485 let mut out = String::new();
486 loop {
487 let slice_start = self.pos;
488 while let Some(b) = self.peek() {
489 if b.is_ascii_whitespace() || b == b'>' || b == b'/' || b == b'&' {
490 break;
491 }
492 self.advance();
493 }
494 if slice_start < self.pos {
495 out.push_str(&self.src[slice_start..self.pos]);
496 }
497 match self.peek() {
498 Some(b'&') => out.push_str(&self.parse_entity()?),
499 _ => break,
500 }
501 }
502 if out.is_empty() {
503 return Err(self
504 .err("empty unquoted attribute value")
505 .with_hint("use \"\" or '' for empty value"));
506 }
507 Ok(out)
508 }
509
510 fn snapshot(&self) -> (usize, u32, u32) {
513 (self.pos, self.line, self.col)
514 }
515
516 fn restore(&mut self, (pos, line, col): (usize, u32, u32)) {
517 self.pos = pos;
518 self.line = line;
519 self.col = col;
520 }
521}
522
523fn decode_entity_body(body: &str) -> Option<String> {
528 match body {
529 "amp" => Some("&".to_string()),
530 "lt" => Some("<".to_string()),
531 "gt" => Some(">".to_string()),
532 "quot" => Some("\"".to_string()),
533 "apos" => Some("'".to_string()),
534 "nbsp" => Some("\u{00A0}".to_string()),
535 _ => {
536 if let Some(rest) = body.strip_prefix('#') {
537 if let Some(hex) = rest.strip_prefix('x').or_else(|| rest.strip_prefix('X')) {
538 let n = u32::from_str_radix(hex, 16).ok()?;
539 let c = char::from_u32(n)?;
540 Some(c.to_string())
541 } else {
542 let n: u32 = rest.parse().ok()?;
543 let c = char::from_u32(n)?;
544 Some(c.to_string())
545 }
546 } else {
547 None
548 }
549 }
550 }
551}
552
553#[cfg(test)]
554mod tests {
555 use super::*;
556
557 fn parse_str(s: &str) -> (Dom<()>, Vec<NodeId>) {
558 parse(s).unwrap()
559 }
560
561 #[test]
564 fn empty_element() {
565 let (dom, ids) = parse_str("<div></div>");
566 assert_eq!(ids.len(), 1);
567 let n = dom.node(ids[0]);
568 assert_eq!(n.tag_name(), Some("div"));
569 assert_eq!(n.child_nodes().count(), 0);
570 }
571
572 #[test]
573 fn self_closing_element() {
574 let (dom, ids) = parse_str("<br/>");
575 assert_eq!(ids.len(), 1);
576 assert_eq!(dom.node(ids[0]).tag_name(), Some("br"));
577 }
578
579 #[test]
580 fn self_closing_with_space() {
581 let (dom, ids) = parse_str("<br />");
582 assert_eq!(dom.node(ids[0]).tag_name(), Some("br"));
583 }
584
585 #[test]
586 fn void_element_auto_closes() {
587 let (dom, ids) = parse_str("<br>");
589 assert_eq!(ids.len(), 1);
590 assert_eq!(dom.node(ids[0]).tag_name(), Some("br"));
591 }
592
593 #[test]
594 fn multiple_void_elements() {
595 let (dom, ids) = parse_str("<br><hr><img>");
596 assert_eq!(ids.len(), 3);
597 assert_eq!(dom.node(ids[0]).tag_name(), Some("br"));
598 assert_eq!(dom.node(ids[1]).tag_name(), Some("hr"));
599 assert_eq!(dom.node(ids[2]).tag_name(), Some("img"));
600 }
601
602 #[test]
603 fn case_insensitive_tag_names() {
604 let (dom, ids) = parse_str("<DIV></div>");
605 assert_eq!(dom.node(ids[0]).tag_name(), Some("div"));
606 }
607
608 #[test]
611 fn nested_elements() {
612 let (dom, ids) = parse_str("<div><span></span></div>");
613 let outer = ids[0];
614 assert_eq!(dom.node(outer).child_nodes().count(), 1);
615 let inner = dom.node(outer).first_child().unwrap().id();
616 assert_eq!(dom.node(inner).tag_name(), Some("span"));
617 }
618
619 #[test]
620 fn deeply_nested() {
621 let (dom, ids) = parse_str("<a><b><c><d></d></c></b></a>");
622 let mut cur = ids[0];
623 for tag in &["a", "b", "c", "d"] {
624 assert_eq!(dom.node(cur).tag_name(), Some(*tag));
625 cur = dom.node(cur).first_child().map(|n| n.id()).unwrap_or(cur);
626 }
627 }
628
629 #[test]
632 fn text_node() {
633 let (dom, ids) = parse_str("<div>hello</div>");
634 let child = dom.node(ids[0]).first_child().unwrap();
635 assert_eq!(child.node_value(), Some("hello"));
636 }
637
638 #[test]
639 fn mixed_content() {
640 let (dom, ids) = parse_str("<div>before <b>mid</b> after</div>");
641 let div = ids[0];
642 let children: Vec<_> = dom.node(div).child_nodes().collect();
643 assert_eq!(children.len(), 3);
644 assert_eq!(children[0].node_value(), Some("before "));
645 assert_eq!(children[1].tag_name(), Some("b"));
646 assert_eq!(children[2].node_value(), Some(" after"));
647 }
648
649 #[test]
650 fn text_at_top_level() {
651 let (dom, ids) = parse_str("hello <span>world</span>");
652 assert_eq!(ids.len(), 2);
653 let root = dom.root();
654 let first = dom.node(root).first_child().unwrap();
655 assert_eq!(first.node_value(), Some("hello "));
656 }
657
658 #[test]
661 fn double_quoted_attr() {
662 let (dom, ids) = parse_str(r#"<div id="main"></div>"#);
663 assert_eq!(dom.node(ids[0]).get_attribute("id"), Some("main"));
664 }
665
666 #[test]
667 fn single_quoted_attr() {
668 let (dom, ids) = parse_str("<div id='main'></div>");
669 assert_eq!(dom.node(ids[0]).get_attribute("id"), Some("main"));
670 }
671
672 #[test]
673 fn unquoted_attr() {
674 let (dom, ids) = parse_str("<div id=main></div>");
675 assert_eq!(dom.node(ids[0]).get_attribute("id"), Some("main"));
676 }
677
678 #[test]
679 fn boolean_attr() {
680 let (dom, ids) = parse_str("<input disabled>");
681 assert_eq!(dom.node(ids[0]).get_attribute("disabled"), Some(""));
682 assert!(dom.node(ids[0]).has_attribute("disabled"));
683 }
684
685 #[test]
686 fn multiple_attrs() {
687 let (dom, ids) = parse_str(r#"<div id="x" role="banner" data-n="5"></div>"#);
688 let n = dom.node(ids[0]);
689 assert_eq!(n.get_attribute("id"), Some("x"));
690 assert_eq!(n.get_attribute("role"), Some("banner"));
691 assert_eq!(n.get_attribute("data-n"), Some("5"));
692 }
693
694 #[test]
695 fn class_attr_populates_classlist() {
696 let (dom, ids) = parse_str(r#"<div class="a b c"></div>"#);
697 let n = dom.node(ids[0]);
698 assert!(n.has_class("a"));
699 assert!(n.has_class("b"));
700 assert!(n.has_class("c"));
701 }
702
703 #[test]
704 fn attr_name_case_preserved() {
705 let (dom, ids) = parse_str(r#"<div dataFoo="bar"></div>"#);
707 assert_eq!(dom.node(ids[0]).get_attribute("dataFoo"), Some("bar"));
708 }
709
710 #[test]
711 fn whitespace_around_attrs() {
712 let (dom, ids) = parse_str("<div id=main role=banner ></div>");
713 assert_eq!(dom.node(ids[0]).get_attribute("id"), Some("main"));
714 assert_eq!(dom.node(ids[0]).get_attribute("role"), Some("banner"));
715 }
716
717 #[test]
718 fn attr_name_with_hyphens_and_colons() {
719 let (dom, ids) = parse_str(r#"<div data-x="1" aria:label="y"></div>"#);
720 assert_eq!(dom.node(ids[0]).get_attribute("data-x"), Some("1"));
721 assert_eq!(dom.node(ids[0]).get_attribute("aria:label"), Some("y"));
722 }
723
724 #[test]
727 fn entity_amp() {
728 let (dom, ids) = parse_str("<div>a & b</div>");
729 let child = dom.node(ids[0]).first_child().unwrap();
730 assert_eq!(child.node_value(), Some("a & b"));
731 }
732
733 #[test]
734 fn entity_lt_gt_quot_apos() {
735 let (dom, ids) = parse_str("<div><tag> "q" 'a'</div>");
736 let child = dom.node(ids[0]).first_child().unwrap();
737 assert_eq!(child.node_value(), Some("<tag> \"q\" 'a'"));
738 }
739
740 #[test]
741 fn entity_decimal_numeric() {
742 let (dom, ids) = parse_str("<div>AB</div>");
743 let child = dom.node(ids[0]).first_child().unwrap();
744 assert_eq!(child.node_value(), Some("AB"));
745 }
746
747 #[test]
748 fn entity_hex_numeric() {
749 let (dom, ids) = parse_str("<div>AB</div>");
750 let child = dom.node(ids[0]).first_child().unwrap();
751 assert_eq!(child.node_value(), Some("AB"));
752 }
753
754 #[test]
755 fn entity_in_attr_value() {
756 let (dom, ids) = parse_str(r#"<div title="a & b"></div>"#);
757 assert_eq!(dom.node(ids[0]).get_attribute("title"), Some("a & b"));
758 }
759
760 #[test]
761 fn unknown_entity_preserved_as_literal_amp() {
762 let (dom, ids) = parse_str("<div>&xyz;</div>");
764 let child = dom.node(ids[0]).first_child().unwrap();
765 assert_eq!(child.node_value(), Some("&xyz;"));
767 }
768
769 #[test]
770 fn entity_nbsp() {
771 let (dom, ids) = parse_str("<div>a b</div>");
772 let child = dom.node(ids[0]).first_child().unwrap();
773 assert_eq!(child.node_value(), Some("a\u{A0}b"));
774 }
775
776 #[test]
779 fn comment_preserved() {
780 let (dom, ids) = parse_str("<!-- hello -->");
781 assert_eq!(ids.len(), 1);
782 let c = dom.node(ids[0]);
783 assert_eq!(c.node_type(), rdom_core::NodeType::Comment);
784 assert_eq!(c.data(), Some(" hello "));
785 }
786
787 #[test]
788 fn comment_inside_element() {
789 let (dom, ids) = parse_str("<div><!-- note -->body</div>");
790 let div = ids[0];
791 let children: Vec<_> = dom.node(div).child_nodes().collect();
792 assert_eq!(children.len(), 2);
793 assert_eq!(children[0].node_type(), rdom_core::NodeType::Comment);
794 assert_eq!(children[1].node_value(), Some("body"));
795 }
796
797 #[test]
800 fn error_mismatched_tags() {
801 let err = parse::<()>("<div></span>").unwrap_err();
802 assert!(err.msg.contains("mismatched"));
803 }
804
805 #[test]
806 fn error_missing_close() {
807 let err = parse::<()>("<div>").unwrap_err();
808 assert!(err.msg.contains("missing closing"));
809 }
810
811 #[test]
812 fn error_unterminated_comment() {
813 let err = parse::<()>("<!-- never ends").unwrap_err();
814 assert!(err.msg.contains("unterminated"));
815 }
816
817 #[test]
818 fn error_unterminated_attr_value() {
819 let err = parse::<()>(r#"<div id="abc>"#).unwrap_err();
820 assert!(err.msg.contains("unterminated"));
821 }
822
823 #[test]
824 fn error_position_reported() {
825 let err = parse::<()>("<div>\n<span></p>\n</div>").unwrap_err();
826 assert_eq!(err.line, 2);
828 }
829
830 #[test]
831 fn error_has_hint() {
832 let err = parse::<()>("<div>").unwrap_err();
833 assert!(err.hint.is_some());
834 }
835
836 #[test]
839 fn parse_into_appends_to_mount() {
840 let mut dom: Dom<()> = Dom::new();
841 let mount = dom.create_element("body");
842 let root = dom.root();
843 dom.append_child(root, mount).unwrap();
844
845 let ids = parse_into(&mut dom, "<h1>Title</h1><p>Body</p>", mount).unwrap();
846 assert_eq!(ids.len(), 2);
847 assert_eq!(dom.node(mount).child_nodes().count(), 2);
848 }
849
850 #[test]
853 fn realistic_template() {
854 let t = r#"
855 <div class="card" id="hero">
856 <h1>Welcome</h1>
857 <p>Hello & welcome to <strong>rdom</strong>.</p>
858 <br/>
859 <!-- TODO: add icon -->
860 <button disabled>OK</button>
861 </div>
862 "#;
863 let (dom, ids) = parse::<()>(t).unwrap();
864 let div_id = ids
867 .iter()
868 .find(|&&id| dom.node(id).tag_name() == Some("div"))
869 .copied()
870 .unwrap();
871 let div = dom.node(div_id);
872 assert!(div.has_class("card"));
873 assert_eq!(div.get_attribute("id"), Some("hero"));
874
875 let h1 = div
877 .child_nodes()
878 .find(|c| c.tag_name() == Some("h1"))
879 .unwrap();
880 assert_eq!(
881 dom.node(h1.id()).first_child().unwrap().node_value(),
882 Some("Welcome")
883 );
884
885 let btn = div
887 .child_nodes()
888 .find(|c| c.tag_name() == Some("button"))
889 .unwrap();
890 assert!(dom.node(btn.id()).has_attribute("disabled"));
891 }
892
893 #[test]
896 fn round_trip_simple() {
897 let src = "<div><span>hi</span></div>";
898 let (dom, ids) = parse::<()>(src).unwrap();
899 let out = dom.outer_markup(ids[0]);
900 assert_eq!(out, src);
901 }
902
903 #[test]
904 fn round_trip_with_attrs() {
905 let src = r#"<div data-x="1" id="main"><p></p></div>"#;
906 let (dom, ids) = parse::<()>(src).unwrap();
907 let out = dom.outer_markup(ids[0]);
908 assert_eq!(out, src);
910 }
911
912 #[test]
913 fn round_trip_void_element() {
914 let src = "<hr/>";
915 let (dom, ids) = parse::<()>(src).unwrap();
916 let out = dom.outer_markup(ids[0]);
917 assert_eq!(out, "<hr/>");
918 }
919
920 #[test]
921 fn round_trip_entities_escaped() {
922 let src = "<div>a & b <c></div>";
923 let (dom, ids) = parse::<()>(src).unwrap();
924 let out = dom.outer_markup(ids[0]);
925 assert_eq!(out, src);
926 }
927
928 #[test]
931 fn whitespace_preserved_in_text() {
932 let (dom, ids) = parse_str("<p> hello world </p>");
933 let child = dom.node(ids[0]).first_child().unwrap();
934 assert_eq!(child.node_value(), Some(" hello world "));
935 }
936
937 #[test]
938 fn newlines_preserved() {
939 let (dom, ids) = parse_str("<pre>line1\nline2</pre>");
940 let child = dom.node(ids[0]).first_child().unwrap();
941 assert_eq!(child.node_value(), Some("line1\nline2"));
942 }
943
944 #[test]
947 fn many_children() {
948 let src: String = (0..50).map(|_| "<li>x</li>").collect();
949 let (dom, ids) = parse::<()>(&format!("<ul>{}</ul>", src)).unwrap();
950 let ul = ids[0];
951 assert_eq!(dom.node(ul).child_element_count(), 50);
952 }
953
954 #[test]
957 fn empty_template() {
958 let (_, ids) = parse_str("");
959 assert!(ids.is_empty());
960 }
961
962 #[test]
963 fn whitespace_only_template() {
964 let (dom, ids) = parse_str(" \n ");
965 assert_eq!(ids.len(), 1);
967 let c = dom.node(ids[0]);
968 assert_eq!(c.node_type(), rdom_core::NodeType::Text);
969 }
970
971 #[test]
974 fn hyphenated_tag() {
975 let (dom, ids) = parse_str("<tree-item></tree-item>");
976 assert_eq!(dom.node(ids[0]).tag_name(), Some("tree-item"));
977 }
978
979 #[test]
980 fn underscore_tag() {
981 let (dom, ids) = parse_str("<my_element></my_element>");
982 assert_eq!(dom.node(ids[0]).tag_name(), Some("my_element"));
983 }
984
985 #[test]
988 fn adjacent_elements() {
989 let (dom, ids) = parse_str("<a></a><b></b>");
990 assert_eq!(ids.len(), 2);
991 assert_eq!(dom.node(ids[0]).tag_name(), Some("a"));
992 assert_eq!(dom.node(ids[1]).tag_name(), Some("b"));
993 }
994}