1use rdom_core::{Dom, NodeId, is_void_element};
28
29use crate::entities::{LONGEST_LEGACY_NAME, LONGEST_NAME, NAMED_REFERENCES};
30
31use crate::error::{ParseError, Result};
32
33#[derive(Clone, Copy, PartialEq, Eq)]
37enum RefContext {
38 Text,
39 Attribute,
40}
41
42pub fn parse<Ext>(template: &str) -> Result<(Dom<Ext>, Vec<NodeId>)>
45where
46 Ext: Default + 'static,
47{
48 let mut dom = Dom::new();
49 let root = dom.root();
50 let ids = parse_into(&mut dom, template, root)?;
51 Ok((dom, ids))
52}
53
54pub fn parse_into<Ext>(dom: &mut Dom<Ext>, template: &str, mount: NodeId) -> Result<Vec<NodeId>>
58where
59 Ext: Default + 'static,
60{
61 let mut p = Parser::new(template);
62 let ids = p.parse_nodes(dom, mount)?;
63 if !p.eof() {
64 return Err(p
67 .err("unexpected closing tag at top level")
68 .with_hint("nothing is open here — remove the end tag or open its element"));
69 }
70 Ok(ids)
71}
72
73struct Parser<'a> {
76 src: &'a str,
77 bytes: &'a [u8],
78 pos: usize,
79 line: u32,
80 col: u32,
81}
82
83impl<'a> Parser<'a> {
84 fn new(src: &'a str) -> Self {
85 Self {
86 src,
87 bytes: src.as_bytes(),
88 pos: 0,
89 line: 1,
90 col: 1,
91 }
92 }
93
94 fn eof(&self) -> bool {
97 self.pos >= self.bytes.len()
98 }
99
100 fn peek(&self) -> Option<u8> {
101 self.bytes.get(self.pos).copied()
102 }
103
104 fn peek_at(&self, offset: usize) -> Option<u8> {
105 self.bytes.get(self.pos + offset).copied()
106 }
107
108 fn starts_with(&self, needle: &str) -> bool {
109 self.src[self.pos..].starts_with(needle)
110 }
111
112 fn advance(&mut self) -> Option<u8> {
113 let b = self.peek()?;
114 self.pos += 1;
115 if b == b'\n' {
116 self.line += 1;
117 self.col = 1;
118 } else {
119 self.col += 1;
120 }
121 Some(b)
122 }
123
124 fn advance_n(&mut self, n: usize) {
125 for _ in 0..n {
126 if self.advance().is_none() {
127 break;
128 }
129 }
130 }
131
132 fn skip_ws(&mut self) {
133 while let Some(b) = self.peek() {
134 if b.is_ascii_whitespace() {
135 self.advance();
136 } else {
137 break;
138 }
139 }
140 }
141
142 fn err(&self, msg: impl Into<String>) -> ParseError {
143 ParseError::new(msg, self.line, self.col, self.pos)
144 }
145
146 fn parse_nodes<Ext>(&mut self, dom: &mut Dom<Ext>, parent: NodeId) -> Result<Vec<NodeId>>
149 where
150 Ext: Default + 'static,
151 {
152 let mut out = Vec::new();
153 loop {
154 if self.eof() {
155 break;
156 }
157 if self.starts_with("</") {
158 break;
160 }
161 if self.starts_with("<!--") {
162 let id = self.parse_comment(dom, parent)?;
163 out.push(id);
164 continue;
165 }
166 if self.starts_with("<!") {
167 if self.src[self.pos + 2..]
168 .get(..7)
169 .is_some_and(|k| k.eq_ignore_ascii_case("DOCTYPE"))
170 {
171 self.skip_declaration();
174 } else {
175 let id = self.parse_bogus_comment(dom, parent)?;
177 out.push(id);
178 }
179 continue;
180 }
181 if self.starts_with("<?") {
182 let id = self.parse_bogus_comment(dom, parent)?;
184 out.push(id);
185 continue;
186 }
187 if self.peek() == Some(b'<') && self.peek_at(1).is_some_and(|b| b.is_ascii_alphabetic())
188 {
189 let id = self.parse_element(dom, parent)?;
190 out.push(id);
191 continue;
192 }
193 let id = self.parse_text(dom, parent)?;
196 if let Some(id) = id {
197 out.push(id);
198 }
199 }
200 Ok(out)
201 }
202
203 fn parse_comment<Ext>(&mut self, dom: &mut Dom<Ext>, parent: NodeId) -> Result<NodeId>
206 where
207 Ext: Default + 'static,
208 {
209 self.advance_n(4);
211 let start = self.pos;
212 loop {
213 if self.eof() {
214 return Err(self
215 .err("unterminated comment")
216 .with_hint("missing `-->` closing"));
217 }
218 if self.starts_with("-->") {
219 let data = &self.src[start..self.pos];
220 self.advance_n(3);
221 let id = dom.create_comment(data);
222 dom.append_child(parent, id)
223 .map_err(|e| self.err(format!("failed to append comment: {:?}", e)))?;
224 return Ok(id);
225 }
226 self.advance();
227 }
228 }
229
230 fn parse_text<Ext>(&mut self, dom: &mut Dom<Ext>, parent: NodeId) -> Result<Option<NodeId>>
238 where
239 Ext: Default + 'static,
240 {
241 let mut out = String::new();
242 loop {
243 let slice_start = self.pos;
246 while let Some(b) = self.peek() {
247 if b == b'&' || (b == b'<' && self.at_tag_open()) {
248 break;
249 }
250 self.advance();
251 }
252 if slice_start < self.pos {
253 out.push_str(&self.src[slice_start..self.pos]);
254 }
255 match self.peek() {
256 None | Some(b'<') => break,
257 Some(b'&') => {
258 out.push_str(&self.parse_entity(RefContext::Text)?);
259 }
260 _ => unreachable!(),
261 }
262 }
263 if out.is_empty() {
264 return Ok(None);
265 }
266 let id = dom.create_text_node(&out);
267 dom.append_child(parent, id)
268 .map_err(|e| self.err(format!("failed to append text: {:?}", e)))?;
269 Ok(Some(id))
270 }
271
272 fn at_tag_open(&self) -> bool {
275 self.peek() == Some(b'<')
276 && self
277 .peek_at(1)
278 .is_some_and(|b| b.is_ascii_alphabetic() || matches!(b, b'/' | b'!' | b'?'))
279 }
280
281 fn parse_bogus_comment<Ext>(&mut self, dom: &mut Dom<Ext>, parent: NodeId) -> Result<NodeId>
285 where
286 Ext: Default + 'static,
287 {
288 self.advance(); let start = self.pos;
290 while let Some(b) = self.peek() {
291 if b == b'>' {
292 break;
293 }
294 self.advance();
295 }
296 let data = self.src[start..self.pos].to_string();
297 if self.peek() == Some(b'>') {
298 self.advance();
299 }
300 let id = dom.create_comment(&data);
301 dom.append_child(parent, id)
302 .map_err(|e| self.err(format!("failed to append comment: {:?}", e)))?;
303 Ok(id)
304 }
305
306 fn skip_declaration(&mut self) {
309 while let Some(b) = self.advance() {
310 if b == b'>' {
311 return;
312 }
313 }
314 }
315
316 fn find_end_tag(&self, tag_lc: &str) -> Option<usize> {
320 let hay = &self.bytes[self.pos..];
321 let needle_len = 2 + tag_lc.len();
322 let mut i = 0;
323 while i + needle_len <= hay.len() {
324 if hay[i] == b'<' && hay[i + 1] == b'/' {
325 let name = &hay[i + 2..i + needle_len];
326 if name.eq_ignore_ascii_case(tag_lc.as_bytes()) {
327 let after = hay.get(i + needle_len).copied();
328 if after.is_none_or(|b| b.is_ascii_whitespace() || b == b'>' || b == b'/') {
329 return Some(self.pos + i);
330 }
331 }
332 }
333 i += 1;
334 }
335 None
336 }
337
338 fn parse_special_text<Ext>(
343 &mut self,
344 dom: &mut Dom<Ext>,
345 element: NodeId,
346 tag_lc: &str,
347 decode_entities: bool,
348 ) -> Result<()>
349 where
350 Ext: Default + 'static,
351 {
352 let Some(end) = self.find_end_tag(tag_lc) else {
353 return Err(self
354 .err(format!("missing closing tag for <{}>", tag_lc))
355 .with_hint(format!("add </{}> to close", tag_lc)));
356 };
357 let mut raw = &self.src[self.pos..end];
358 if tag_lc == "textarea" {
361 raw = raw
362 .strip_prefix("\r\n")
363 .or_else(|| raw.strip_prefix('\n'))
364 .unwrap_or(raw);
365 }
366 let text = if decode_entities {
367 decode_character_references(raw)
368 } else {
369 raw.to_string()
370 };
371 if !text.is_empty() {
372 let id = dom.create_text_node(&text);
373 dom.append_child(element, id)
374 .map_err(|e| self.err(format!("failed to append text: {:?}", e)))?;
375 }
376 self.advance_n(end - self.pos);
378 self.advance_n(2 + tag_lc.len()); self.skip_ws();
380 if self.peek() != Some(b'>') {
381 return Err(self
382 .err(format!("expected `>` in </{}>", tag_lc))
383 .with_hint("no attributes on closing tags"));
384 }
385 self.advance();
386 Ok(())
387 }
388
389 fn parse_entity(&mut self, context: RefContext) -> Result<String> {
392 debug_assert_eq!(self.peek(), Some(b'&'));
397 match scan_reference(&self.src[self.pos + 1..], context) {
398 Some((decoded, consumed)) => {
399 self.advance_n(1 + consumed);
400 Ok(decoded)
401 }
402 None => {
403 self.advance(); Ok("&".to_string())
405 }
406 }
407 }
408
409 fn parse_element<Ext>(&mut self, dom: &mut Dom<Ext>, parent: NodeId) -> Result<NodeId>
412 where
413 Ext: Default + 'static,
414 {
415 debug_assert_eq!(self.peek(), Some(b'<'));
416 self.advance(); let tag = self.parse_tag_name()?;
419 let tag_lc = tag.to_ascii_lowercase();
420
421 let element = dom.create_element(&tag_lc);
422
423 loop {
425 self.skip_ws();
426 match self.peek() {
427 None => {
428 return Err(self
429 .err(format!("unexpected EOF inside <{}>", tag_lc))
430 .with_hint("missing closing `>`"));
431 }
432 Some(b'>') => {
433 self.advance();
434 break;
435 }
436 Some(b'/') => {
437 self.advance();
439 self.skip_ws();
440 if self.peek() != Some(b'>') {
441 return Err(self
442 .err(format!("expected `>` after `/` in <{}/>", tag_lc))
443 .with_hint("self-closing syntax is `/>`"));
444 }
445 self.advance();
446 dom.append_child(parent, element)
447 .map_err(|e| self.err(format!("failed to append <{}>: {:?}", tag_lc, e)))?;
448 return Ok(element);
449 }
450 Some(_) => {
451 self.parse_attribute(dom, element)?;
452 }
453 }
454 }
455
456 if is_void_element(&tag_lc) {
458 dom.append_child(parent, element)
459 .map_err(|e| self.err(format!("failed to append <{}>: {:?}", tag_lc, e)))?;
460 return Ok(element);
461 }
462
463 match tag_lc.as_str() {
466 "style" | "script" => {
467 self.parse_special_text(dom, element, &tag_lc, false)?;
468 dom.append_child(parent, element)
469 .map_err(|e| self.err(format!("failed to append <{}>: {:?}", tag_lc, e)))?;
470 return Ok(element);
471 }
472 "textarea" | "title" => {
473 self.parse_special_text(dom, element, &tag_lc, true)?;
474 dom.append_child(parent, element)
475 .map_err(|e| self.err(format!("failed to append <{}>: {:?}", tag_lc, e)))?;
476 return Ok(element);
477 }
478 _ => {}
479 }
480
481 self.parse_nodes(dom, element)?;
483
484 if !self.starts_with("</") {
485 return Err(self
486 .err(format!("missing closing tag for <{}>", tag_lc))
487 .with_hint(format!("add </{}> to close", tag_lc)));
488 }
489 self.advance_n(2); let close_tag = self.parse_tag_name()?;
492 if close_tag.to_ascii_lowercase() != tag_lc {
493 return Err(self
494 .err(format!(
495 "mismatched closing tag: found </{}>, expected </{}>",
496 close_tag, tag_lc
497 ))
498 .with_hint("tags must be properly nested"));
499 }
500 self.skip_ws();
501 if self.peek() != Some(b'>') {
502 return Err(self
503 .err(format!("expected `>` in </{}>", tag_lc))
504 .with_hint("no attributes on closing tags"));
505 }
506 self.advance();
507
508 dom.append_child(parent, element)
509 .map_err(|e| self.err(format!("failed to append <{}>: {:?}", tag_lc, e)))?;
510 Ok(element)
511 }
512
513 fn parse_tag_name(&mut self) -> Result<String> {
514 let start = self.pos;
515 while let Some(b) = self.peek() {
516 if b.is_ascii_alphanumeric() || b == b'-' || b == b'_' {
517 self.advance();
518 } else {
519 break;
520 }
521 }
522 if start == self.pos {
523 return Err(self
524 .err("expected tag name")
525 .with_hint("tag names start with a letter"));
526 }
527 Ok(self.src[start..self.pos].to_string())
528 }
529
530 fn parse_attribute<Ext>(&mut self, dom: &mut Dom<Ext>, element: NodeId) -> Result<()>
531 where
532 Ext: Default + 'static,
533 {
534 let name = self.parse_attr_name()?;
535 self.skip_ws();
536
537 let value = if self.peek() == Some(b'=') {
538 self.advance();
539 self.skip_ws();
540 Some(self.parse_attr_value()?)
541 } else {
542 None
543 };
544
545 match value {
546 Some(v) => {
547 if name.eq_ignore_ascii_case("class") {
550 for token in v.split_ascii_whitespace() {
551 dom.add_class(element, token)
552 .map_err(|e| self.err(format!("failed to add class: {:?}", e)))?;
553 }
554 } else {
555 dom.set_attribute(element, &name, &v)
556 .map_err(|e| self.err(format!("failed to set attribute: {:?}", e)))?;
557 }
558 }
559 None => {
560 dom.set_attribute(element, &name, "")
562 .map_err(|e| self.err(format!("failed to set attribute: {:?}", e)))?;
563 }
564 }
565 Ok(())
566 }
567
568 fn parse_attr_name(&mut self) -> Result<String> {
569 let start = self.pos;
570 while let Some(b) = self.peek() {
571 if b.is_ascii_alphanumeric() || b == b'-' || b == b'_' || b == b':' {
572 self.advance();
573 } else {
574 break;
575 }
576 }
577 if start == self.pos {
578 return Err(self.err("expected attribute name"));
579 }
580 Ok(self.src[start..self.pos].to_string())
581 }
582
583 fn parse_attr_value(&mut self) -> Result<String> {
584 let first = self.peek();
585 match first {
586 Some(b'"') => self.parse_quoted(b'"'),
587 Some(b'\'') => self.parse_quoted(b'\''),
588 Some(_) => self.parse_unquoted(),
589 None => Err(self.err("unexpected EOF in attribute value")),
590 }
591 }
592
593 fn parse_quoted(&mut self, quote: u8) -> Result<String> {
594 self.advance(); let mut out = String::new();
596 loop {
597 let slice_start = self.pos;
599 while let Some(b) = self.peek() {
600 if b == quote || b == b'&' {
601 break;
602 }
603 self.advance();
604 }
605 if slice_start < self.pos {
606 out.push_str(&self.src[slice_start..self.pos]);
607 }
608 match self.peek() {
609 None => {
610 return Err(self
611 .err(format!(
612 "unterminated attribute value (expected `{}`)",
613 quote as char
614 ))
615 .with_hint("missing closing quote"));
616 }
617 Some(b) if b == quote => {
618 self.advance();
619 return Ok(out);
620 }
621 Some(b'&') => {
622 out.push_str(&self.parse_entity(RefContext::Attribute)?);
623 }
624 _ => unreachable!(),
625 }
626 }
627 }
628
629 fn parse_unquoted(&mut self) -> Result<String> {
630 let mut out = String::new();
631 loop {
632 let slice_start = self.pos;
633 while let Some(b) = self.peek() {
634 if b.is_ascii_whitespace() || b == b'>' || b == b'/' || b == b'&' {
635 break;
636 }
637 self.advance();
638 }
639 if slice_start < self.pos {
640 out.push_str(&self.src[slice_start..self.pos]);
641 }
642 match self.peek() {
643 Some(b'&') => out.push_str(&self.parse_entity(RefContext::Attribute)?),
644 _ => break,
645 }
646 }
647 if out.is_empty() {
648 return Err(self
649 .err("empty unquoted attribute value")
650 .with_hint("use \"\" or '' for empty value"));
651 }
652 Ok(out)
653 }
654}
655
656const C1_REMAP: [(u32, char); 27] = [
662 (0x80, '\u{20AC}'),
663 (0x82, '\u{201A}'),
664 (0x83, '\u{0192}'),
665 (0x84, '\u{201E}'),
666 (0x85, '\u{2026}'),
667 (0x86, '\u{2020}'),
668 (0x87, '\u{2021}'),
669 (0x88, '\u{02C6}'),
670 (0x89, '\u{2030}'),
671 (0x8A, '\u{0160}'),
672 (0x8B, '\u{2039}'),
673 (0x8C, '\u{0152}'),
674 (0x8E, '\u{017D}'),
675 (0x91, '\u{2018}'),
676 (0x92, '\u{2019}'),
677 (0x93, '\u{201C}'),
678 (0x94, '\u{201D}'),
679 (0x95, '\u{2022}'),
680 (0x96, '\u{2013}'),
681 (0x97, '\u{2014}'),
682 (0x98, '\u{02DC}'),
683 (0x99, '\u{2122}'),
684 (0x9A, '\u{0161}'),
685 (0x9B, '\u{203A}'),
686 (0x9C, '\u{0153}'),
687 (0x9E, '\u{017E}'),
688 (0x9F, '\u{0178}'),
689];
690
691fn decode_numeric(digits: &str, radix: u32) -> char {
695 let n = u32::from_str_radix(digits, radix).unwrap_or(u32::MAX);
696 match n {
697 0 | 0xD800..=0xDFFF => '\u{FFFD}',
698 0x80..=0x9F => C1_REMAP
699 .iter()
700 .find(|(from, _)| *from == n)
701 .map_or_else(|| char::from_u32(n).unwrap_or('\u{FFFD}'), |(_, to)| *to),
702 _ => char::from_u32(n).unwrap_or('\u{FFFD}'),
703 }
704}
705
706fn named_reference(name: &str) -> Option<&'static str> {
708 NAMED_REFERENCES
709 .binary_search_by(|(n, _)| (*n).cmp(name))
710 .ok()
711 .map(|i| NAMED_REFERENCES[i].1)
712}
713
714fn scan_reference(after_amp: &str, context: RefContext) -> Option<(String, usize)> {
726 let bytes = after_amp.as_bytes();
727 if bytes.first() == Some(&b'#') {
728 let (start, radix): (usize, u32) = match bytes.get(1) {
732 Some(b'x' | b'X') => (2, 16),
733 _ => (1, 10),
734 };
735 let mut n = start;
736 while n < bytes.len() && (bytes[n] as char).is_digit(radix) {
737 n += 1;
738 }
739 if n == start {
740 return None;
741 }
742 let c = decode_numeric(&after_amp[start..n], radix);
743 let consumed = if bytes.get(n) == Some(&b';') {
744 n + 1
745 } else {
746 n
747 };
748 return Some((c.to_string(), consumed));
749 }
750 let mut n = 0;
751 while n < bytes.len() && n < LONGEST_NAME && bytes[n].is_ascii_alphanumeric() {
752 n += 1;
753 }
754 if n == 0 {
755 return None;
756 }
757 if bytes.get(n) == Some(&b';')
758 && let Some(v) = named_reference(&after_amp[..=n])
759 {
760 return Some((v.to_string(), n + 1));
761 }
762 for len in (1..=n.min(LONGEST_LEGACY_NAME)).rev() {
764 let Some(v) = named_reference(&after_amp[..len]) else {
765 continue;
766 };
767 if context == RefContext::Attribute
768 && let Some(&next) = bytes.get(len)
769 && (next == b'=' || next.is_ascii_alphanumeric())
770 {
771 return None;
772 }
773 return Some((v.to_string(), len));
774 }
775 None
776}
777
778fn decode_character_references(text: &str) -> String {
781 let mut out = String::with_capacity(text.len());
782 let mut rest = text;
783 while let Some(amp) = rest.find('&') {
784 out.push_str(&rest[..amp]);
785 let after = &rest[amp + 1..];
786 match scan_reference(after, RefContext::Text) {
787 Some((decoded, consumed)) => {
788 out.push_str(&decoded);
789 rest = &after[consumed..];
790 }
791 None => {
792 out.push('&');
793 rest = after;
794 }
795 }
796 }
797 out.push_str(rest);
798 out
799}
800
801#[cfg(test)]
802mod tests {
803 use super::*;
804
805 fn parse_str(s: &str) -> (Dom<()>, Vec<NodeId>) {
806 parse(s).unwrap()
807 }
808
809 #[test]
815 fn numeric_reference_scanner_edges() {
816 let text = |s: &str| scan_reference(s, RefContext::Text);
817 assert_eq!(text("#;x"), None);
818 assert_eq!(text("#x;"), None);
819 assert_eq!(text("#"), None);
820 assert_eq!(text("#65 rest"), Some(("A".to_string(), 3)));
821 assert_eq!(text("#65;"), Some(("A".to_string(), 4)));
822 assert_eq!(text("#65abc;"), Some(("A".to_string(), 3)));
823 assert_eq!(text("#x41g"), Some(("A".to_string(), 4)));
824 assert_eq!(text("#X41;"), Some(("A".to_string(), 5)));
825 assert_eq!(text("#150;"), Some(("\u{2013}".to_string(), 5)));
826 assert_eq!(text("#129;"), Some(("\u{81}".to_string(), 5)));
827 }
828
829 #[test]
832 fn named_reference_scanner_edges() {
833 let text = |s: &str| scan_reference(s, RefContext::Text);
834 let attr = |s: &str| scan_reference(s, RefContext::Attribute);
835 assert_eq!(text("notit;"), Some(("\u{AC}".to_string(), 3)));
836 assert_eq!(text("notin;"), Some(("\u{2209}".to_string(), 6)));
837 assert_eq!(text("Amp;"), None, "names are case-sensitive");
838 assert_eq!(text("AMP;"), Some(("&".to_string(), 4)));
839 assert_eq!(attr("copy=2"), None);
840 assert_eq!(attr("copyx"), None);
841 assert_eq!(attr("copy 2"), Some(("\u{A9}".to_string(), 4)));
842 assert_eq!(attr("copy;=2"), Some(("\u{A9}".to_string(), 5)));
843 assert_eq!(text("ThisIsNotAReferenceAtAllButLong;"), None);
844 }
845
846 #[test]
847 fn named_reference_table_is_the_full_sorted_whatwg_list() {
848 assert_eq!(NAMED_REFERENCES.len(), 2231);
849 let legacy = NAMED_REFERENCES
850 .iter()
851 .filter(|(n, _)| !n.ends_with(';'))
852 .count();
853 assert_eq!(legacy, 106);
854 for w in NAMED_REFERENCES.windows(2) {
855 assert!(
856 w[0].0 < w[1].0,
857 "{:?} must sort before {:?}",
858 w[0].0,
859 w[1].0
860 );
861 }
862 assert!(
863 NAMED_REFERENCES
864 .iter()
865 .all(|(n, _)| n.len() <= LONGEST_NAME)
866 );
867 }
868
869 #[test]
872 fn empty_element() {
873 let (dom, ids) = parse_str("<div></div>");
874 assert_eq!(ids.len(), 1);
875 let n = dom.node(ids[0]);
876 assert_eq!(n.tag_name(), Some("div"));
877 assert_eq!(n.child_nodes().count(), 0);
878 }
879
880 #[test]
881 fn self_closing_element() {
882 let (dom, ids) = parse_str("<br/>");
883 assert_eq!(ids.len(), 1);
884 assert_eq!(dom.node(ids[0]).tag_name(), Some("br"));
885 }
886
887 #[test]
888 fn self_closing_with_space() {
889 let (dom, ids) = parse_str("<br />");
890 assert_eq!(dom.node(ids[0]).tag_name(), Some("br"));
891 }
892
893 #[test]
894 fn void_element_auto_closes() {
895 let (dom, ids) = parse_str("<br>");
897 assert_eq!(ids.len(), 1);
898 assert_eq!(dom.node(ids[0]).tag_name(), Some("br"));
899 }
900
901 #[test]
902 fn multiple_void_elements() {
903 let (dom, ids) = parse_str("<br><hr><img>");
904 assert_eq!(ids.len(), 3);
905 assert_eq!(dom.node(ids[0]).tag_name(), Some("br"));
906 assert_eq!(dom.node(ids[1]).tag_name(), Some("hr"));
907 assert_eq!(dom.node(ids[2]).tag_name(), Some("img"));
908 }
909
910 #[test]
911 fn case_insensitive_tag_names() {
912 let (dom, ids) = parse_str("<DIV></div>");
913 assert_eq!(dom.node(ids[0]).tag_name(), Some("div"));
914 }
915
916 #[test]
919 fn nested_elements() {
920 let (dom, ids) = parse_str("<div><span></span></div>");
921 let outer = ids[0];
922 assert_eq!(dom.node(outer).child_nodes().count(), 1);
923 let inner = dom.node(outer).first_child().unwrap().id();
924 assert_eq!(dom.node(inner).tag_name(), Some("span"));
925 }
926
927 #[test]
928 fn deeply_nested() {
929 let (dom, ids) = parse_str("<a><b><c><d></d></c></b></a>");
930 let mut cur = ids[0];
931 for tag in &["a", "b", "c", "d"] {
932 assert_eq!(dom.node(cur).tag_name(), Some(*tag));
933 cur = dom.node(cur).first_child().map(|n| n.id()).unwrap_or(cur);
934 }
935 }
936
937 #[test]
940 fn text_node() {
941 let (dom, ids) = parse_str("<div>hello</div>");
942 let child = dom.node(ids[0]).first_child().unwrap();
943 assert_eq!(child.node_value(), Some("hello"));
944 }
945
946 #[test]
947 fn mixed_content() {
948 let (dom, ids) = parse_str("<div>before <b>mid</b> after</div>");
949 let div = ids[0];
950 let children: Vec<_> = dom.node(div).child_nodes().collect();
951 assert_eq!(children.len(), 3);
952 assert_eq!(children[0].node_value(), Some("before "));
953 assert_eq!(children[1].tag_name(), Some("b"));
954 assert_eq!(children[2].node_value(), Some(" after"));
955 }
956
957 #[test]
958 fn text_at_top_level() {
959 let (dom, ids) = parse_str("hello <span>world</span>");
960 assert_eq!(ids.len(), 2);
961 let root = dom.root();
962 let first = dom.node(root).first_child().unwrap();
963 assert_eq!(first.node_value(), Some("hello "));
964 }
965
966 #[test]
969 fn double_quoted_attr() {
970 let (dom, ids) = parse_str(r#"<div id="main"></div>"#);
971 assert_eq!(dom.node(ids[0]).get_attribute("id"), Some("main"));
972 }
973
974 #[test]
975 fn single_quoted_attr() {
976 let (dom, ids) = parse_str("<div id='main'></div>");
977 assert_eq!(dom.node(ids[0]).get_attribute("id"), Some("main"));
978 }
979
980 #[test]
981 fn unquoted_attr() {
982 let (dom, ids) = parse_str("<div id=main></div>");
983 assert_eq!(dom.node(ids[0]).get_attribute("id"), Some("main"));
984 }
985
986 #[test]
987 fn boolean_attr() {
988 let (dom, ids) = parse_str("<input disabled>");
989 assert_eq!(dom.node(ids[0]).get_attribute("disabled"), Some(""));
990 assert!(dom.node(ids[0]).has_attribute("disabled"));
991 }
992
993 #[test]
994 fn multiple_attrs() {
995 let (dom, ids) = parse_str(r#"<div id="x" role="banner" data-n="5"></div>"#);
996 let n = dom.node(ids[0]);
997 assert_eq!(n.get_attribute("id"), Some("x"));
998 assert_eq!(n.get_attribute("role"), Some("banner"));
999 assert_eq!(n.get_attribute("data-n"), Some("5"));
1000 }
1001
1002 #[test]
1003 fn class_attr_populates_classlist() {
1004 let (dom, ids) = parse_str(r#"<div class="a b c"></div>"#);
1005 let n = dom.node(ids[0]);
1006 assert!(n.has_class("a"));
1007 assert!(n.has_class("b"));
1008 assert!(n.has_class("c"));
1009 }
1010
1011 #[test]
1012 fn attr_name_case_preserved() {
1013 let (dom, ids) = parse_str(r#"<div dataFoo="bar"></div>"#);
1015 assert_eq!(dom.node(ids[0]).get_attribute("dataFoo"), Some("bar"));
1016 }
1017
1018 #[test]
1019 fn whitespace_around_attrs() {
1020 let (dom, ids) = parse_str("<div id=main role=banner ></div>");
1021 assert_eq!(dom.node(ids[0]).get_attribute("id"), Some("main"));
1022 assert_eq!(dom.node(ids[0]).get_attribute("role"), Some("banner"));
1023 }
1024
1025 #[test]
1026 fn attr_name_with_hyphens_and_colons() {
1027 let (dom, ids) = parse_str(r#"<div data-x="1" aria:label="y"></div>"#);
1028 assert_eq!(dom.node(ids[0]).get_attribute("data-x"), Some("1"));
1029 assert_eq!(dom.node(ids[0]).get_attribute("aria:label"), Some("y"));
1030 }
1031
1032 #[test]
1035 fn entity_amp() {
1036 let (dom, ids) = parse_str("<div>a & b</div>");
1037 let child = dom.node(ids[0]).first_child().unwrap();
1038 assert_eq!(child.node_value(), Some("a & b"));
1039 }
1040
1041 #[test]
1042 fn entity_lt_gt_quot_apos() {
1043 let (dom, ids) = parse_str("<div><tag> "q" 'a'</div>");
1044 let child = dom.node(ids[0]).first_child().unwrap();
1045 assert_eq!(child.node_value(), Some("<tag> \"q\" 'a'"));
1046 }
1047
1048 #[test]
1049 fn entity_decimal_numeric() {
1050 let (dom, ids) = parse_str("<div>AB</div>");
1051 let child = dom.node(ids[0]).first_child().unwrap();
1052 assert_eq!(child.node_value(), Some("AB"));
1053 }
1054
1055 #[test]
1056 fn entity_hex_numeric() {
1057 let (dom, ids) = parse_str("<div>AB</div>");
1058 let child = dom.node(ids[0]).first_child().unwrap();
1059 assert_eq!(child.node_value(), Some("AB"));
1060 }
1061
1062 #[test]
1063 fn entity_in_attr_value() {
1064 let (dom, ids) = parse_str(r#"<div title="a & b"></div>"#);
1065 assert_eq!(dom.node(ids[0]).get_attribute("title"), Some("a & b"));
1066 }
1067
1068 #[test]
1069 fn unknown_entity_preserved_as_literal_amp() {
1070 let (dom, ids) = parse_str("<div>&xyz;</div>");
1072 let child = dom.node(ids[0]).first_child().unwrap();
1073 assert_eq!(child.node_value(), Some("&xyz;"));
1075 }
1076
1077 #[test]
1078 fn entity_nbsp() {
1079 let (dom, ids) = parse_str("<div>a b</div>");
1080 let child = dom.node(ids[0]).first_child().unwrap();
1081 assert_eq!(child.node_value(), Some("a\u{A0}b"));
1082 }
1083
1084 #[test]
1087 fn comment_preserved() {
1088 let (dom, ids) = parse_str("<!-- hello -->");
1089 assert_eq!(ids.len(), 1);
1090 let c = dom.node(ids[0]);
1091 assert_eq!(c.node_type(), rdom_core::NodeType::Comment);
1092 assert_eq!(c.data(), Some(" hello "));
1093 }
1094
1095 #[test]
1096 fn comment_inside_element() {
1097 let (dom, ids) = parse_str("<div><!-- note -->body</div>");
1098 let div = ids[0];
1099 let children: Vec<_> = dom.node(div).child_nodes().collect();
1100 assert_eq!(children.len(), 2);
1101 assert_eq!(children[0].node_type(), rdom_core::NodeType::Comment);
1102 assert_eq!(children[1].node_value(), Some("body"));
1103 }
1104
1105 #[test]
1108 fn error_mismatched_tags() {
1109 let err = parse::<()>("<div></span>").unwrap_err();
1110 assert!(err.msg.contains("mismatched"));
1111 }
1112
1113 #[test]
1114 fn error_missing_close() {
1115 let err = parse::<()>("<div>").unwrap_err();
1116 assert!(err.msg.contains("missing closing"));
1117 }
1118
1119 #[test]
1120 fn error_unterminated_comment() {
1121 let err = parse::<()>("<!-- never ends").unwrap_err();
1122 assert!(err.msg.contains("unterminated"));
1123 }
1124
1125 #[test]
1126 fn error_unterminated_attr_value() {
1127 let err = parse::<()>(r#"<div id="abc>"#).unwrap_err();
1128 assert!(err.msg.contains("unterminated"));
1129 }
1130
1131 #[test]
1132 fn error_position_reported() {
1133 let err = parse::<()>("<div>\n<span></p>\n</div>").unwrap_err();
1134 assert_eq!(err.line, 2);
1136 }
1137
1138 #[test]
1139 fn error_has_hint() {
1140 let err = parse::<()>("<div>").unwrap_err();
1141 assert!(err.hint.is_some());
1142 }
1143
1144 #[test]
1147 fn parse_into_appends_to_mount() {
1148 let mut dom: Dom<()> = Dom::new();
1149 let mount = dom.create_element("body");
1150 let root = dom.root();
1151 dom.append_child(root, mount).unwrap();
1152
1153 let ids = parse_into(&mut dom, "<h1>Title</h1><p>Body</p>", mount).unwrap();
1154 assert_eq!(ids.len(), 2);
1155 assert_eq!(dom.node(mount).child_nodes().count(), 2);
1156 }
1157
1158 #[test]
1161 fn realistic_template() {
1162 let t = r#"
1163 <div class="card" id="hero">
1164 <h1>Welcome</h1>
1165 <p>Hello & welcome to <strong>rdom</strong>.</p>
1166 <br/>
1167 <!-- TODO: add icon -->
1168 <button disabled>OK</button>
1169 </div>
1170 "#;
1171 let (dom, ids) = parse::<()>(t).unwrap();
1172 let div_id = ids
1175 .iter()
1176 .find(|&&id| dom.node(id).tag_name() == Some("div"))
1177 .copied()
1178 .unwrap();
1179 let div = dom.node(div_id);
1180 assert!(div.has_class("card"));
1181 assert_eq!(div.get_attribute("id"), Some("hero"));
1182
1183 let h1 = div
1185 .child_nodes()
1186 .find(|c| c.tag_name() == Some("h1"))
1187 .unwrap();
1188 assert_eq!(
1189 dom.node(h1.id()).first_child().unwrap().node_value(),
1190 Some("Welcome")
1191 );
1192
1193 let btn = div
1195 .child_nodes()
1196 .find(|c| c.tag_name() == Some("button"))
1197 .unwrap();
1198 assert!(dom.node(btn.id()).has_attribute("disabled"));
1199 }
1200
1201 #[test]
1204 fn round_trip_simple() {
1205 let src = "<div><span>hi</span></div>";
1206 let (dom, ids) = parse::<()>(src).unwrap();
1207 let out = dom.outer_markup(ids[0]);
1208 assert_eq!(out, src);
1209 }
1210
1211 #[test]
1212 fn round_trip_with_attrs() {
1213 let src = r#"<div data-x="1" id="main"><p></p></div>"#;
1214 let (dom, ids) = parse::<()>(src).unwrap();
1215 let out = dom.outer_markup(ids[0]);
1216 assert_eq!(out, src);
1218 }
1219
1220 #[test]
1221 fn round_trip_void_element() {
1222 let src = "<hr/>";
1223 let (dom, ids) = parse::<()>(src).unwrap();
1224 let out = dom.outer_markup(ids[0]);
1225 assert_eq!(out, "<hr/>");
1226 }
1227
1228 #[test]
1229 fn round_trip_entities_escaped() {
1230 let src = "<div>a & b <c></div>";
1231 let (dom, ids) = parse::<()>(src).unwrap();
1232 let out = dom.outer_markup(ids[0]);
1233 assert_eq!(out, src);
1234 }
1235
1236 #[test]
1239 fn whitespace_preserved_in_text() {
1240 let (dom, ids) = parse_str("<p> hello world </p>");
1241 let child = dom.node(ids[0]).first_child().unwrap();
1242 assert_eq!(child.node_value(), Some(" hello world "));
1243 }
1244
1245 #[test]
1246 fn newlines_preserved() {
1247 let (dom, ids) = parse_str("<pre>line1\nline2</pre>");
1248 let child = dom.node(ids[0]).first_child().unwrap();
1249 assert_eq!(child.node_value(), Some("line1\nline2"));
1250 }
1251
1252 #[test]
1255 fn many_children() {
1256 let src: String = (0..50).map(|_| "<li>x</li>").collect();
1257 let (dom, ids) = parse::<()>(&format!("<ul>{}</ul>", src)).unwrap();
1258 let ul = ids[0];
1259 assert_eq!(dom.node(ul).child_element_count(), 50);
1260 }
1261
1262 #[test]
1265 fn empty_template() {
1266 let (_, ids) = parse_str("");
1267 assert!(ids.is_empty());
1268 }
1269
1270 #[test]
1271 fn whitespace_only_template() {
1272 let (dom, ids) = parse_str(" \n ");
1273 assert_eq!(ids.len(), 1);
1275 let c = dom.node(ids[0]);
1276 assert_eq!(c.node_type(), rdom_core::NodeType::Text);
1277 }
1278
1279 #[test]
1282 fn hyphenated_tag() {
1283 let (dom, ids) = parse_str("<tree-item></tree-item>");
1284 assert_eq!(dom.node(ids[0]).tag_name(), Some("tree-item"));
1285 }
1286
1287 #[test]
1288 fn underscore_tag() {
1289 let (dom, ids) = parse_str("<my_element></my_element>");
1290 assert_eq!(dom.node(ids[0]).tag_name(), Some("my_element"));
1291 }
1292
1293 #[test]
1296 fn adjacent_elements() {
1297 let (dom, ids) = parse_str("<a></a><b></b>");
1298 assert_eq!(ids.len(), 2);
1299 assert_eq!(dom.node(ids[0]).tag_name(), Some("a"));
1300 assert_eq!(dom.node(ids[1]).tag_name(), Some("b"));
1301 }
1302}