1#![doc(html_logo_url = "https://gitlab.com/encre-org/pochoir/raw/main/.assets/logo.png")]
57#![forbid(unsafe_code)]
58#![warn(
59 missing_debug_implementations,
60 trivial_casts,
61 trivial_numeric_casts,
62 unstable_features,
63 unused_import_braces,
64 unused_qualifications,
65 rustdoc::private_doc_tests,
66 rustdoc::broken_intra_doc_links,
67 rustdoc::private_intra_doc_links,
68 clippy::unnecessary_wraps,
69 clippy::too_many_lines,
70 clippy::string_to_string,
71 clippy::explicit_iter_loop,
72 clippy::unnecessary_cast,
73 clippy::missing_errors_doc,
74 clippy::pedantic,
75 clippy::clone_on_ref_ptr,
76 clippy::non_ascii_literal,
77 clippy::dbg_macro,
78 clippy::map_err_ignore,
79 clippy::use_debug,
80 clippy::map_err_ignore,
81 clippy::use_self,
82 clippy::useless_let_if_seq,
83 clippy::verbose_file_reads,
84 clippy::panic,
85 clippy::unimplemented,
86 clippy::todo
87)]
88#![allow(
89 clippy::module_name_repetitions,
90 clippy::must_use_candidate,
91 clippy::range_plus_one
92)]
93
94use error::AutoError;
95use pochoir_common::{Spanned, StreamParser};
96use pochoir_template_engine::{BlockContext, TemplateCustomParsing};
97use std::{borrow::Cow, fmt, ops::ControlFlow, path::Path};
98
99pub mod error;
100pub mod node;
101mod render;
102pub mod tree;
103
104pub use error::{Error, Result};
105pub use node::{Attr, Attrs, Node, ParsedNode};
106pub use render::render;
107pub use tree::{OwnedTree, Tree, TreeRef, TreeRefId, TreeRefMut};
108
109pub type EventHandlerResult = std::result::Result<(), Box<dyn std::error::Error>>;
110
111pub const EMPTY_HTML_ELEMENTS: &[&str] = &[
115 "area", "base", "br", "col", "embed", "hr", "img", "keygen", "input", "link", "meta", "param",
116 "source", "track", "wbr",
117];
118
119struct HtmlTemplateCustomParsing {
120 end_tag_name: Option<String>,
121}
122
123impl TemplateCustomParsing for HtmlTemplateCustomParsing {
124 fn each_char(
125 &self,
126 ch: char,
127 parser: &mut StreamParser,
128 block_context: BlockContext,
129 ) -> ControlFlow<()> {
130 match ch {
131 '<' if self.end_tag_name.is_none()
132 && block_context.is_none()
133 && parser
134 .next()
135 .is_ok_and(|ch| ch.is_alphabetic() || ch == '/' || ch == '!') =>
136 {
137 parser.set_index(parser.index() - 1);
138 ControlFlow::Break(())
139 }
140 ch if self.end_tag_name.is_some()
141 && ch == '<'
142 && parser.peek_exact(self.end_tag_name.as_ref().unwrap()) =>
143 {
144 parser.set_index(parser.index() - 1);
145 ControlFlow::Break(())
146 }
147 _ => ControlFlow::Continue(()),
148 }
149 }
150}
151
152struct AttrValueDoubleQuotedCustomParsing;
153
154impl TemplateCustomParsing for AttrValueDoubleQuotedCustomParsing {
155 fn each_char(
156 &self,
157 ch: char,
158 parser: &mut StreamParser,
159 _block_context: BlockContext,
160 ) -> ControlFlow<()> {
161 if ch == '"' {
162 parser.set_index(parser.index() - 1);
163 ControlFlow::Break(())
164 } else {
165 ControlFlow::Continue(())
166 }
167 }
168}
169
170struct AttrValueSingleQuotedCustomParsing;
171
172impl TemplateCustomParsing for AttrValueSingleQuotedCustomParsing {
173 fn each_char(
174 &self,
175 ch: char,
176 parser: &mut StreamParser,
177 _block_context: BlockContext,
178 ) -> ControlFlow<()> {
179 if ch == '\'' {
180 parser.set_index(parser.index() - 1);
181 ControlFlow::Break(())
182 } else {
183 ControlFlow::Continue(())
184 }
185 }
186}
187
188struct AttrValueWithoutQuotesCustomParsing;
189
190impl TemplateCustomParsing for AttrValueWithoutQuotesCustomParsing {
191 fn each_char(
192 &self,
193 ch: char,
194 parser: &mut StreamParser,
195 _block_context: BlockContext,
196 ) -> ControlFlow<()> {
197 if ch == ' ' || ch == '>' {
198 parser.set_index(parser.index() - 1);
199 ControlFlow::Break(())
200 } else {
201 ControlFlow::Continue(())
202 }
203 }
204}
205
206pub fn parse_owned<P: AsRef<Path>>(file_path: P, data: &str) -> Result<OwnedTree> {
220 Builder::new().parse_owned(file_path, data)
221}
222
223pub fn parse<P: AsRef<Path>>(file_path: P, data: &str) -> Result<Tree<'_>> {
237 Builder::new().parse(file_path, data)
238}
239
240#[derive(Debug, PartialEq, Eq, Clone, Copy)]
241pub enum ParseEvent {
242 BeforeElement,
244
245 AfterElement,
247}
248
249#[allow(clippy::type_complexity)]
250pub struct Builder<'a> {
251 event_handler:
252 Option<Box<dyn FnMut(ParseEvent, &mut Tree, TreeRefId) -> EventHandlerResult + 'a>>,
253}
254
255impl<'a> Builder<'a> {
256 pub fn new() -> Self {
258 Self {
259 event_handler: None,
260 }
261 }
262
263 pub fn parse_owned<P: AsRef<Path>>(self, file_path: P, data: &str) -> Result<OwnedTree> {
275 OwnedTree::try_new(data.to_string(), |data: &String| {
276 self.parse(file_path, data)
277 })
278 }
279
280 pub fn parse<P: AsRef<Path>>(self, file_path: P, data: &str) -> Result<Tree<'_>> {
292 let file_path = file_path.as_ref();
293 let mut parsing_context = ParserContext {
294 parser: StreamParser::new(file_path, data),
295 tree: Tree::new(file_path),
296 file_path,
297 parent: TreeRefId::Root,
298 builder: self,
299 };
300
301 while !parsing_context.parser.is_eoi() {
302 parsing_context.node()?;
303 }
304
305 Ok(parsing_context.tree)
306 }
307
308 #[must_use]
340 pub fn on_event<F: FnMut(ParseEvent, &mut Tree, TreeRefId) -> EventHandlerResult + 'a>(
341 mut self,
342 on_event: F,
343 ) -> Self {
344 self.event_handler = Some(Box::new(on_event));
345 self
346 }
347}
348
349impl Default for Builder<'_> {
350 fn default() -> Self {
351 Self::new()
352 }
353}
354
355impl fmt::Debug for Builder<'_> {
356 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
357 f.debug_struct("Builder").finish()
358 }
359}
360
361struct ParserContext<'a, 'b, 'c> {
362 parser: StreamParser<'a>,
363 tree: Tree<'a>,
364 file_path: &'b Path,
365 parent: TreeRefId,
366 builder: Builder<'c>,
367}
368
369impl<'a> ParserContext<'a, '_, '_> {
370 fn call_event_handler(&mut self, event: ParseEvent, id: TreeRefId) -> EventHandlerResult {
371 if let Some(ref mut event_handler) = self.builder.event_handler {
372 event_handler(event, &mut self.tree, id)?;
373 }
374
375 Ok(())
376 }
377
378 fn node(&mut self) -> Result<Vec<TreeRefId>> {
379 Ok(if self.parser.peek_exact("<") {
380 if self.parser.peek_early_exact("!", 1) {
381 if self.parser.peek_early_exact("--", 2) {
382 vec![self.comment()?]
383 } else {
384 vec![self.doctype()?]
385 }
386 } else {
387 vec![self.element()?]
388 }
389 } else {
390 self.content()?
391 })
392 }
393
394 fn comment(&mut self) -> Result<TreeRefId> {
395 let start = self.parser.index();
396 self.parser.take_exact("<!--").auto_error()?;
397 let comment = self.parser.take_until("-->").auto_error()?.trim();
398 self.parser.take_exact("-->").auto_error()?;
399 let end = self.parser.index();
400
401 Ok(self.tree.insert(
402 self.parent,
403 Spanned::new(Node::Comment(Cow::Borrowed(comment)))
404 .with_span(start..end)
405 .with_file_path(self.file_path),
406 ))
407 }
408
409 fn doctype(&mut self) -> Result<TreeRefId> {
410 let start = self.parser.index();
411 self.parser.take_exact("<!").auto_error()?;
412
413 let word = self
414 .parser
415 .take_while(|(_, ch)| char::is_alphabetic(ch))
416 .trim();
417 if word.to_lowercase() != "doctype" {
418 return Err(Spanned::new(Error::UnexpectedInput {
419 expected: "`doctype`".to_string(),
420 found: format!("`{word}`"),
421 })
422 .with_span(start..start + word.len())
423 .with_file_path(self.file_path));
424 }
425
426 let doctype = self.parser.take_until(">").auto_error()?.trim();
427 self.parser.take_exact(">").auto_error()?;
428 let end = self.parser.index();
429
430 Ok(self.tree.insert(
431 self.parent,
432 Spanned::new(Node::Doctype(Cow::Borrowed(doctype)))
433 .with_span(start..end)
434 .with_file_path(self.file_path),
435 ))
436 }
437
438 fn element(&mut self) -> Result<TreeRefId> {
439 let start = self.parser.index();
440
441 let (name, attrs, self_closing) = self.tag_open()?;
442
443 if self_closing || EMPTY_HTML_ELEMENTS.contains(&&*name) {
444 let end = self.parser.index();
445 let element_id = self.tree.insert(
446 self.parent,
447 Spanned::new(Node::Element(name, attrs))
448 .with_span(start..end)
449 .with_file_path(self.file_path),
450 );
451
452 self.call_event_handler(ParseEvent::BeforeElement, element_id)
454 .map_err(|e| Error::EventHandlerError(e.to_string()))?;
455
456 self.call_event_handler(ParseEvent::AfterElement, element_id)
458 .map_err(|e| Error::EventHandlerError(e.to_string()))?;
459
460 Ok(element_id)
461 } else {
462 let element_id = self.tree.insert(
464 self.parent,
465 Spanned::new(Node::Element(name.clone(), attrs)).with_file_path(self.file_path),
466 );
467
468 self.call_event_handler(ParseEvent::BeforeElement, element_id)
470 .map_err(|e| Error::EventHandlerError(e.to_string()))?;
471
472 let old_parent = self.parent;
473 self.parent = element_id;
474
475 while self.element_has_children(&name)? {
476 self.node()?;
477 }
478
479 self.parent = old_parent;
481
482 let end = self.parser.index();
483
484 self.tree
486 .get_mut(element_id)
487 .spanned_data()
488 .set_span(start..end);
489
490 self.call_event_handler(ParseEvent::AfterElement, element_id)
492 .map_err(|e| Error::EventHandlerError(e.to_string()))?;
493
494 Ok(element_id)
495 }
496 }
497
498 fn content(&mut self) -> Result<Vec<TreeRefId>> {
499 let mut end_tag_name = None;
502 if self.parent != TreeRefId::Root {
503 if let Node::Element(parent_el_name, _) = &self.tree.get(self.parent).data() {
504 if ["script", "noscript", "style", "textarea", "title"].contains(&&**parent_el_name)
505 {
506 end_tag_name = Some(format!("/{parent_el_name}>"));
507 }
508 }
509 }
510
511 let blocks = pochoir_template_engine::stream_parse_template(
512 self.file_path,
513 &mut self.parser,
514 HtmlTemplateCustomParsing { end_tag_name },
515 0,
516 )
517 .auto_error()?;
518
519 Ok(blocks
520 .into_iter()
521 .map(|block| {
522 let span = block.span().clone();
523
524 self.tree.insert(
525 self.parent,
526 Spanned::new(Node::TemplateBlock(block.into_inner()))
527 .with_span(span)
528 .with_file_path(self.file_path),
529 )
530 })
531 .collect())
532 }
533
534 fn element_has_children(&mut self, tag_open_name: &str) -> Result<bool> {
536 let start = self.parser.index();
537
538 match self.tag_close() {
539 Ok(tag_close_name) => {
540 if tag_open_name == tag_close_name {
541 return Ok(false);
543 } else if EMPTY_HTML_ELEMENTS.contains(&&*tag_close_name) {
544 return Err(
545 Spanned::new(Error::ClosedVoidElement(tag_close_name.to_string()))
546 .with_span(start..self.parser.index())
547 .with_file_path(self.file_path),
548 );
549 }
550
551 return Err(Spanned::new(Error::UnexpectedEndTagName {
553 start_tag: tag_open_name.to_string(),
554 end_tag: tag_close_name.to_string(),
555 })
556 .with_span(start..self.parser.index())
557 .with_file_path(self.file_path));
558 }
559 Err(e) if matches!(&*e, Error::StreamParserError(pochoir_common::Error::UnexpectedInput { expected, .. }) if expected == "</") =>
560 {
561 }
563 Err(e) => return Err(e),
564 }
565 self.parser.set_index(start);
566
567 Ok(!self.parser.is_eoi())
568 }
569
570 fn tag_open(&mut self) -> Result<(Cow<'a, str>, Attrs<'a>, bool)> {
572 self.parser.take_exact("<").auto_error()?;
573 let start = self.parser.index();
574
575 let first_char = self.parser.next();
576
577 if first_char.as_ref().is_ok_and(|ch| *ch == '/') {
578 let name = self
579 .parser
580 .take_while(|(_, ch)| !char::is_whitespace(ch) && ch != '>')[1..]
581 .to_string();
582 if EMPTY_HTML_ELEMENTS.contains(&name.as_str()) {
583 return Err(Spanned::new(Error::ClosedVoidElement(name))
584 .with_span(start + 1..self.parser.index())
585 .with_file_path(self.file_path));
586 }
587
588 return Err(Spanned::new(Error::UnexpectedEndTag(name))
589 .with_span(start + 1..self.parser.index())
590 .with_file_path(self.file_path));
591 } else if first_char
592 .as_ref()
593 .is_ok_and(|ch| ch.is_numeric() || ch.is_whitespace())
594 {
595 let name = self.parser.take_while(|(_, ch)| ch != '>' && ch != '/')[1..].to_string();
596
597 return Err(Spanned::new(Error::InvalidTagName(name))
598 .with_span(start..self.parser.index())
599 .with_file_path(self.file_path));
600 } else if first_char.as_ref().is_ok_and(|ch| *ch == '>') {
601 return Err(Spanned::new(Error::ExpectedTagName)
602 .with_span(start - 1..self.parser.index() + 1)
603 .with_file_path(self.file_path));
604 }
605
606 let name = self
607 .parser
608 .take_while(|(_, ch)| !char::is_whitespace(ch) && ch != '>' && ch != '/');
609 let (attrs, self_closing) = self.attrs()?;
610
611 Ok((Cow::Borrowed(name), attrs, self_closing))
612 }
613
614 fn tag_close(&mut self) -> Result<Cow<'a, str>> {
616 self.parser.take_exact("</").auto_error()?;
617 let start_name = self.parser.index();
618 let name = self
619 .parser
620 .take_while(|(_, ch)| !char::is_whitespace(ch) && ch != '>');
621
622 if name.trim().is_empty() {
623 return Err(Spanned::new(Error::ExpectedTagName)
624 .with_span(
625 start_name..if self.parser.next().is_ok() {
626 start_name + 1
627 } else {
628 start_name
629 },
630 )
631 .with_file_path(self.file_path));
632 }
633
634 self.parser.trim();
635 self.parser.take_exact(">").auto_error()?;
636
637 Ok(Cow::Borrowed(name))
638 }
639
640 fn attrs(&mut self) -> Result<(Attrs<'a>, bool)> {
641 let self_closing;
642 let mut attrs = Attrs::new();
643
644 loop {
645 let last_index = self.parser.index();
646 self.parser.trim();
647
648 if self.parser.peek_exact(">") || self.parser.peek_exact("/>") {
649 break;
650 }
651
652 if self.parser.index() == last_index {
654 if self.parser.is_eoi() {
655 return Err(Spanned::new(Error::MissingEndAngleBracket)
656 .with_span(self.parser.index() - 1..self.parser.index())
657 .with_file_path(self.file_path));
658 }
659
660 return Err(Spanned::new(Error::MissingWhitespaceBetweenAttributes)
661 .with_span(last_index..last_index + 1)
662 .with_file_path(self.file_path));
663 }
664
665 let (key, val) = self.attr()?;
666 attrs.insert_spanned(key, val);
667 }
668
669 if self.parser.take_exact(">").is_ok() {
670 self_closing = false;
671 } else if self.parser.take_exact("/>").is_ok() {
672 self_closing = true;
673 } else {
674 unreachable!();
675 }
676
677 Ok((attrs, self_closing))
678 }
679
680 fn attr(&mut self) -> Result<Attr<'a>> {
681 let start_key_index = self.parser.index();
682 let key = self
683 .parser
684 .take_while(|(_, ch)| ch != ' ' && ch != '=' && ch != '>')
685 .trim();
686 let end_key_index = self.parser.index();
687 let key_span = start_key_index..end_key_index;
688
689 if ['"', '\'', '<'].iter().any(|ch| key.contains(*ch)) {
690 return Err(Spanned::new(Error::InvalidAttributeName(key.to_string()))
691 .with_span(key_span)
692 .with_file_path(self.file_path));
693 }
694
695 let (val, val_span) = if self.parser.take_exact("=").is_ok() {
696 if self.parser.take_exact("\"").is_ok() {
697 let start_val_index = self.parser.index();
699 let blocks = pochoir_template_engine::stream_parse_template(
700 self.file_path,
701 &mut self.parser,
702 AttrValueDoubleQuotedCustomParsing,
703 0,
704 )
705 .auto_error()?;
706 let end_val_index = self.parser.index();
707 self.parser.take_exact("\"").auto_error()?;
708
709 (blocks, start_val_index..end_val_index)
710 } else if self.parser.take_exact("'").is_ok() {
711 let start_val_index = self.parser.index();
713 let blocks = pochoir_template_engine::stream_parse_template(
714 self.file_path,
715 &mut self.parser,
716 AttrValueSingleQuotedCustomParsing,
717 0,
718 )
719 .auto_error()?;
720 let end_val_index = self.parser.index();
721 self.parser.take_exact("'").auto_error()?;
722
723 (blocks, start_val_index..end_val_index)
724 } else {
725 let start_val_index = self.parser.index();
727 let blocks = pochoir_template_engine::stream_parse_template(
728 self.file_path,
729 &mut self.parser,
730 AttrValueWithoutQuotesCustomParsing,
731 0,
732 )
733 .auto_error()?;
734 let end_val_index = self.parser.index();
735
736 (blocks, start_val_index..end_val_index)
737 }
738 } else {
739 (vec![], start_key_index..end_key_index)
741 };
742
743 Ok((
744 Spanned::new(Cow::Borrowed(key))
745 .with_span(key_span)
746 .with_file_path(self.file_path),
747 Spanned::new(val)
748 .with_span(val_span)
749 .with_file_path(self.file_path),
750 ))
751 }
752}
753
754#[cfg(test)]
755mod tests {
756 use super::*;
757 use crate::{Attrs, Error};
758 use pochoir_template_engine::{Escaping, TemplateBlock};
759 use pretty_assertions::assert_eq;
760
761 #[test]
762 fn minimal_element() {
763 let source = "<foo></foo>";
764 let tree = parse("index.html", source).unwrap();
765 assert_eq!(
766 *tree.get(TreeRefId::Node(0)).spanned_data(),
767 Spanned::new(Node::Element(Cow::Borrowed("foo"), Attrs::new()))
768 .with_span(0..11)
769 .with_file_path("index.html"),
770 );
771 }
772
773 #[test]
774 fn minimal_self_closing_element() {
775 let source = r#"<img /><img/><link rel="stylesheet"/><meta name=viewport/>"#;
776 let tree = parse("index.html", source).unwrap();
777 assert_eq!(
778 *tree.get(TreeRefId::Node(0)).spanned_data(),
779 Spanned::new(Node::Element(Cow::Borrowed("img"), Attrs::new()))
780 .with_span(0..7)
781 .with_file_path("index.html"),
782 );
783 assert_eq!(
784 *tree.get(TreeRefId::Node(2)).spanned_data(),
785 Spanned::new(Node::Element(
786 Cow::Borrowed("link"),
787 Attrs::from_iter([(
788 Spanned::new(Cow::Borrowed("rel"))
789 .with_span(19..22)
790 .with_file_path("index.html"),
791 Spanned::new(vec![Spanned::new(TemplateBlock::RawText(Cow::Borrowed(
792 "stylesheet"
793 )))
794 .with_span(24..34)
795 .with_file_path("index.html")])
796 .with_span(24..34)
797 .with_file_path("index.html")
798 )])
799 ))
800 .with_span(13..37)
801 .with_file_path("index.html"),
802 );
803 assert_eq!(
804 *tree.get(TreeRefId::Node(3)).spanned_data(),
805 Spanned::new(Node::Element(
806 Cow::Borrowed("meta"),
807 Attrs::from_iter([(
808 Spanned::new(Cow::Borrowed("name"))
809 .with_span(43..47)
810 .with_file_path("index.html"),
811 Spanned::new(vec![Spanned::new(TemplateBlock::RawText(Cow::Borrowed(
812 "viewport/"
813 )))
814 .with_span(48..57)
815 .with_file_path("index.html")])
816 .with_span(48..57)
817 .with_file_path("index.html")
818 )])
819 ))
820 .with_span(37..58)
821 .with_file_path("index.html"),
822 );
823 }
824
825 #[test]
826 fn doctype() {
827 let source = "<!DOCTYPE html>
828 <html>
829 </html>";
830 let tree = parse("index.html", source).unwrap();
831
832 assert_eq!(
833 *tree.get(TreeRefId::Node(0)).spanned_data(),
834 Spanned::new(Node::Doctype(Cow::Borrowed("html")))
835 .with_span(0..15)
836 .with_file_path("index.html"),
837 );
838
839 assert_eq!(
840 *tree.get(TreeRefId::Node(2)).spanned_data(),
841 Spanned::new(Node::Element(Cow::Borrowed("html"), Attrs::new()))
842 .with_span(24..46)
843 .with_file_path("index.html"),
844 );
845 }
846
847 #[test]
848 fn comment() {
849 let source = "<!-- comment1 --><div><!-- comment2 --><div /></div>";
850 let tree = parse("index.html", source).unwrap();
851
852 assert_eq!(
853 *tree.get(TreeRefId::Node(0)).spanned_data(),
854 Spanned::new(Node::Comment(Cow::Borrowed("comment1")))
855 .with_span(0..17)
856 .with_file_path("index.html"),
857 );
858 assert_eq!(
859 *tree.get(TreeRefId::Node(1)).spanned_data(),
860 Spanned::new(Node::Element(Cow::Borrowed("div"), Attrs::new()))
861 .with_span(17..52)
862 .with_file_path("index.html"),
863 );
864 assert_eq!(
865 *tree.get(TreeRefId::Node(2)).spanned_data(),
866 Spanned::new(Node::Comment(Cow::Borrowed("comment2")))
867 .with_span(22..39)
868 .with_file_path("index.html"),
869 );
870 assert_eq!(
871 *tree.get(TreeRefId::Node(3)).spanned_data(),
872 Spanned::new(Node::Element(Cow::Borrowed("div"), Attrs::new()))
873 .with_span(39..46)
874 .with_file_path("index.html"),
875 );
876 }
877
878 #[test]
879 fn text() {
880 let source = "<foo>bar</foo>";
881 let tree = parse("index.html", source).unwrap();
882 assert_eq!(
883 *tree.get(TreeRefId::Node(0)).spanned_data(),
884 Spanned::new(Node::Element(Cow::Borrowed("foo"), Attrs::new()))
885 .with_span(0..14)
886 .with_file_path("index.html"),
887 );
888 assert_eq!(
889 *tree.get(TreeRefId::Node(1)).spanned_data(),
890 Spanned::new(Node::TemplateBlock(TemplateBlock::RawText(Cow::Borrowed(
891 "bar"
892 ))))
893 .with_span(5..8)
894 .with_file_path("index.html"),
895 );
896 }
897
898 #[test]
899 fn attributes() {
900 let source = r#"<foo bar="moo" hidden baz="42" id=bar checked></foo>"#;
901 let tree = parse("index.html", source).unwrap();
902
903 assert_eq!(
904 *tree.get(TreeRefId::Node(0)).spanned_data(),
905 Spanned::new(Node::Element(
906 Cow::Borrowed("foo"),
907 Attrs::from_iter([
908 (
909 Spanned::new(Cow::Borrowed("bar"))
910 .with_span(5..8)
911 .with_file_path("index.html"),
912 Spanned::new(vec![Spanned::new(TemplateBlock::RawText(Cow::Borrowed(
913 "moo"
914 )))
915 .with_span(10..13)
916 .with_file_path("index.html")])
917 .with_span(10..13)
918 .with_file_path("index.html")
919 ),
920 (
921 Spanned::new(Cow::Borrowed("hidden"))
922 .with_span(15..21)
923 .with_file_path("index.html"),
924 Spanned::new(vec![])
925 .with_span(15..21)
926 .with_file_path("index.html"),
927 ),
928 (
929 Spanned::new(Cow::Borrowed("baz"))
930 .with_span(22..25)
931 .with_file_path("index.html"),
932 Spanned::new(vec![Spanned::new(TemplateBlock::RawText(Cow::Borrowed(
933 "42"
934 )))
935 .with_span(27..29)
936 .with_file_path("index.html")])
937 .with_span(27..29)
938 .with_file_path("index.html"),
939 ),
940 (
941 Spanned::new(Cow::Borrowed("id"))
942 .with_span(31..33)
943 .with_file_path("index.html"),
944 Spanned::new(vec![Spanned::new(TemplateBlock::RawText(Cow::Borrowed(
945 "bar"
946 )))
947 .with_span(34..37)
948 .with_file_path("index.html")])
949 .with_span(34..37)
950 .with_file_path("index.html"),
951 ),
952 (
953 Spanned::new(Cow::Borrowed("checked"))
954 .with_span(38..45)
955 .with_file_path("index.html"),
956 Spanned::new(vec![])
957 .with_span(38..45)
958 .with_file_path("index.html"),
959 ),
960 ]),
961 ))
962 .with_span(0..52)
963 .with_file_path("index.html"),
964 );
965 }
966
967 #[test]
968 fn multi_lines_attributes_and_text() {
969 let source = r#"<foo foo="bar"
970 baz="qux
971 lorem"
972 checked
973 life="42"
974 >Hello world
975On multiple
976 Lines with extra spaces</foo>"#;
977 let tree = parse("index.html", source).unwrap();
978
979 assert_eq!(
980 *tree.get(TreeRefId::Node(0)).spanned_data(),
981 Spanned::new(Node::Element(
982 Cow::Borrowed("foo"),
983 Attrs::from_iter([
984 (
985 Spanned::new(Cow::Borrowed("foo"))
986 .with_span(5..8)
987 .with_file_path("index.html"),
988 Spanned::new(vec![Spanned::new(TemplateBlock::RawText(Cow::Borrowed(
989 "bar"
990 )))
991 .with_span(10..13)
992 .with_file_path("index.html")])
993 .with_span(10..13)
994 .with_file_path("index.html"),
995 ),
996 (
997 Spanned::new(Cow::Borrowed("baz"))
998 .with_span(27..30)
999 .with_file_path("index.html"),
1000 Spanned::new(vec![Spanned::new(TemplateBlock::RawText(Cow::Borrowed(
1001 "qux\n lorem"
1002 )))
1003 .with_span(32..43)
1004 .with_file_path("index.html")])
1005 .with_span(32..43)
1006 .with_file_path("index.html"),
1007 ),
1008 (
1009 Spanned::new(Cow::Borrowed("checked"))
1010 .with_span(57..65)
1011 .with_file_path("index.html"),
1012 Spanned::new(vec![])
1013 .with_span(57..65)
1014 .with_file_path("index.html"),
1015 ),
1016 (
1017 Spanned::new(Cow::Borrowed("life"))
1018 .with_span(77..81)
1019 .with_file_path("index.html"),
1020 Spanned::new(vec![Spanned::new(TemplateBlock::RawText(Cow::Borrowed(
1021 "42"
1022 )))
1023 .with_span(83..85)
1024 .with_file_path("index.html")])
1025 .with_span(83..85)
1026 .with_file_path("index.html"),
1027 ),
1028 ]),
1029 ))
1030 .with_span(0..151)
1031 .with_file_path("index.html")
1032 );
1033
1034 assert_eq!(
1035 *tree.get(TreeRefId::Node(1)).spanned_data(),
1036 Spanned::new(Node::TemplateBlock(TemplateBlock::RawText(Cow::Borrowed(
1037 "Hello world\nOn multiple\n Lines with extra spaces"
1038 ))))
1039 .with_span(96..145)
1040 .with_file_path("index.html"),
1041 );
1042 }
1043
1044 #[test]
1045 fn text_with_elements() {
1046 let source =
1047 r#"<div> <p>Hello {{word}}</p> <span class="bold"> kind person!</span> </div>"#;
1048 let tree = parse("index.html", source).unwrap();
1049
1050 assert_eq!(
1051 *tree.get(TreeRefId::Node(0)).spanned_data(),
1052 Spanned::new(Node::Element(Cow::Borrowed("div"), Attrs::new()))
1053 .with_span(0..78)
1054 .with_file_path("index.html"),
1055 );
1056
1057 assert_eq!(
1058 *tree.get(TreeRefId::Node(1)).spanned_data(),
1059 Spanned::new(Node::TemplateBlock(TemplateBlock::RawText(Cow::Borrowed(
1060 " "
1061 ))))
1062 .with_span(5..9)
1063 .with_file_path("index.html"),
1064 );
1065
1066 assert_eq!(
1067 *tree.get(TreeRefId::Node(2)).spanned_data(),
1068 Spanned::new(Node::Element(Cow::Borrowed("p"), Attrs::new()))
1069 .with_span(9..30)
1070 .with_file_path("index.html"),
1071 );
1072
1073 assert_eq!(
1074 *tree.get(TreeRefId::Node(3)).spanned_data(),
1075 Spanned::new(Node::TemplateBlock(TemplateBlock::RawText(Cow::Borrowed(
1076 "Hello "
1077 ))))
1078 .with_span(12..18)
1079 .with_file_path("index.html"),
1080 );
1081
1082 assert_eq!(
1083 *tree.get(TreeRefId::Node(4)).spanned_data(),
1084 Spanned::new(Node::TemplateBlock(TemplateBlock::Expr(
1085 Cow::Borrowed("word"),
1086 true
1087 )))
1088 .with_span(20..24)
1089 .with_file_path("index.html"),
1090 );
1091
1092 assert_eq!(
1093 *tree.get(TreeRefId::Node(5)).spanned_data(),
1094 Spanned::new(Node::TemplateBlock(TemplateBlock::RawText(Cow::Borrowed(
1095 " "
1096 ))))
1097 .with_span(30..32)
1098 .with_file_path("index.html"),
1099 );
1100
1101 assert_eq!(
1102 *tree.get(TreeRefId::Node(6)).spanned_data(),
1103 Spanned::new(Node::Element(
1104 Cow::Borrowed("span"),
1105 Attrs::from_iter([(
1106 Spanned::new(Cow::Borrowed("class"))
1107 .with_span(38..43)
1108 .with_file_path("index.html"),
1109 Spanned::new(vec![Spanned::new(TemplateBlock::RawText(Cow::Borrowed(
1110 "bold"
1111 )))
1112 .with_span(45..49)
1113 .with_file_path("index.html")])
1114 .with_span(45..49)
1115 .with_file_path("index.html"),
1116 )])
1117 ))
1118 .with_span(32..71)
1119 .with_file_path("index.html"),
1120 );
1121
1122 assert_eq!(
1123 *tree.get(TreeRefId::Node(7)).spanned_data(),
1124 Spanned::new(Node::TemplateBlock(TemplateBlock::RawText(Cow::Borrowed(
1125 " kind person!"
1126 ))))
1127 .with_span(51..64)
1128 .with_file_path("index.html"),
1129 );
1130
1131 assert_eq!(
1132 *tree.get(TreeRefId::Node(8)).spanned_data(),
1133 Spanned::new(Node::TemplateBlock(TemplateBlock::RawText(Cow::Borrowed(
1134 " "
1135 ))))
1136 .with_span(71..72)
1137 .with_file_path("index.html"),
1138 );
1139 }
1140
1141 #[test]
1142 fn test_path_as_tag_name() {
1143 let source = "<some::path />";
1144 let tree = parse("index.html", source).unwrap();
1145
1146 assert_eq!(
1147 *tree.get(TreeRefId::Node(0)).spanned_data(),
1148 Spanned::new(Node::Element(Cow::Borrowed("some::path"), Attrs::new()))
1149 .with_span(0..14)
1150 .with_file_path("index.html"),
1151 );
1152 }
1153
1154 #[test]
1155 fn test_dashed_attribute_name() {
1156 let source = r#"<div data-foo="bar" />"#;
1157 let tree = parse("index.html", source).unwrap();
1158
1159 assert_eq!(
1160 *tree.get(TreeRefId::Node(0)).spanned_data(),
1161 Spanned::new(Node::Element(
1162 Cow::Borrowed("div"),
1163 Attrs::from_iter([(
1164 Spanned::new(Cow::Borrowed("data-foo"))
1165 .with_span(5..13)
1166 .with_file_path("index.html"),
1167 Spanned::new(vec![Spanned::new(TemplateBlock::RawText(Cow::Borrowed(
1168 "bar"
1169 )))
1170 .with_span(15..18)
1171 .with_file_path("index.html")])
1172 .with_span(15..18)
1173 .with_file_path("index.html"),
1174 )]),
1175 ))
1176 .with_span(0..22)
1177 .with_file_path("index.html"),
1178 );
1179 }
1180
1181 #[test]
1182 fn test_coloned_attribute_name() {
1183 let source = "<div on:click={{foo}} />";
1184 let tree = parse("index.html", source).unwrap();
1185
1186 assert_eq!(
1187 *tree.get(TreeRefId::Node(0)).spanned_data(),
1188 Spanned::new(Node::Element(
1189 Cow::Borrowed("div"),
1190 Attrs::from_iter([(
1191 Spanned::new(Cow::Borrowed("on:click"))
1192 .with_span(5..13)
1193 .with_file_path("index.html"),
1194 Spanned::new(vec![Spanned::new(TemplateBlock::Expr(
1195 Cow::Borrowed("foo"),
1196 true
1197 ))
1198 .with_span(16..19)
1199 .with_file_path("index.html")])
1200 .with_span(14..21)
1201 .with_file_path("index.html"),
1202 )]),
1203 ))
1204 .with_span(0..24)
1205 .with_file_path("index.html"),
1206 );
1207 }
1208
1209 #[test]
1210 #[allow(clippy::too_many_lines)]
1211 fn empty_element() {
1212 let source = r#"<img src="/path/to/image.png" alt="My image"><p></p>"#;
1213 assert_eq!(
1214 *parse("index.html", source)
1215 .unwrap()
1216 .get(TreeRefId::Node(0))
1217 .spanned_data(),
1218 Spanned::new(Node::Element(
1219 Cow::Borrowed("img"),
1220 Attrs::from_iter([
1221 (
1222 Spanned::new(Cow::Borrowed("src"))
1223 .with_span(5..8)
1224 .with_file_path("index.html"),
1225 Spanned::new(vec![Spanned::new(TemplateBlock::RawText(Cow::Borrowed(
1226 "/path/to/image.png"
1227 )))
1228 .with_span(10..28)
1229 .with_file_path("index.html")])
1230 .with_span(10..28)
1231 .with_file_path("index.html"),
1232 ),
1233 (
1234 Spanned::new(Cow::Borrowed("alt"))
1235 .with_span(30..33)
1236 .with_file_path("index.html"),
1237 Spanned::new(vec![Spanned::new(TemplateBlock::RawText(Cow::Borrowed(
1238 "My image"
1239 )))
1240 .with_span(35..43)
1241 .with_file_path("index.html")])
1242 .with_span(35..43)
1243 .with_file_path("index.html"),
1244 ),
1245 ]),
1246 ))
1247 .with_span(0..45)
1248 .with_file_path("index.html"),
1249 );
1250
1251 let source = r#"<img src="/path/to/image.png" alt="My image" /><p></p>"#;
1252 assert_eq!(
1253 *parse("index.html", source)
1254 .unwrap()
1255 .get(TreeRefId::Node(0))
1256 .spanned_data(),
1257 Spanned::new(Node::Element(
1258 Cow::Borrowed("img"),
1259 Attrs::from_iter([
1260 (
1261 Spanned::new(Cow::Borrowed("src"))
1262 .with_span(5..8)
1263 .with_file_path("index.html"),
1264 Spanned::new(vec![Spanned::new(TemplateBlock::RawText(Cow::Borrowed(
1265 "/path/to/image.png"
1266 )))
1267 .with_span(10..28)
1268 .with_file_path("index.html")])
1269 .with_span(10..28)
1270 .with_file_path("index.html"),
1271 ),
1272 (
1273 Spanned::new(Cow::Borrowed("alt"))
1274 .with_span(30..33)
1275 .with_file_path("index.html"),
1276 Spanned::new(vec![Spanned::new(TemplateBlock::RawText(Cow::Borrowed(
1277 "My image"
1278 )))
1279 .with_span(35..43)
1280 .with_file_path("index.html")])
1281 .with_span(35..43)
1282 .with_file_path("index.html"),
1283 ),
1284 ]),
1285 ))
1286 .with_span(0..47)
1287 .with_file_path("index.html"),
1288 );
1289
1290 let source = r#"<img src="/path/to/image.png" alt="My image"/><p></p>"#;
1291 assert_eq!(
1292 *parse("index.html", source)
1293 .unwrap()
1294 .get(TreeRefId::Node(0))
1295 .spanned_data(),
1296 Spanned::new(Node::Element(
1297 Cow::Borrowed("img"),
1298 Attrs::from_iter([
1299 (
1300 Spanned::new(Cow::Borrowed("src"))
1301 .with_span(5..8)
1302 .with_file_path("index.html"),
1303 Spanned::new(vec![Spanned::new(TemplateBlock::RawText(Cow::Borrowed(
1304 "/path/to/image.png"
1305 )))
1306 .with_span(10..28)
1307 .with_file_path("index.html")])
1308 .with_span(10..28)
1309 .with_file_path("index.html"),
1310 ),
1311 (
1312 Spanned::new(Cow::Borrowed("alt"))
1313 .with_span(30..33)
1314 .with_file_path("index.html"),
1315 Spanned::new(vec![Spanned::new(TemplateBlock::RawText(Cow::Borrowed(
1316 "My image"
1317 )))
1318 .with_span(35..43)
1319 .with_file_path("index.html")])
1320 .with_span(35..43)
1321 .with_file_path("index.html"),
1322 ),
1323 ]),
1324 ))
1325 .with_span(0..46)
1326 .with_file_path("index.html"),
1327 );
1328
1329 let source = r#"<img src="/path/to/image.png" alt="My image"><p></p></img>"#;
1330
1331 assert_eq!(
1332 parse("index.html", source).unwrap_err(),
1333 Spanned::new(Error::ClosedVoidElement("img".to_string()))
1334 .with_span(54..57)
1335 .with_file_path("index.html"),
1336 );
1337 }
1338
1339 #[test]
1340 fn script_style_element() {
1341 let source = "<script>if (0 < 1) { console.log('Hello world!'); }</script><style>p::before { content: '</h1>'; }</style>";
1342 assert_eq!(
1343 *parse("index.html", source)
1344 .unwrap()
1345 .get(TreeRefId::Node(0))
1346 .spanned_data(),
1347 Spanned::new(Node::Element(Cow::Borrowed("script"), Attrs::new()))
1348 .with_span(0..60)
1349 .with_file_path("index.html"),
1350 );
1351 }
1352
1353 #[test]
1354 fn parse_template_syntax() {
1355 let source = "<div>{{ hello }}</div>{% if hello %}a{% else %}{! not_hello !}{%endif%}{# a comment #}end";
1356 assert_eq!(
1357 *parse("index.html", source)
1358 .unwrap()
1359 .get(TreeRefId::Node(1))
1360 .spanned_data(),
1361 Spanned::new(Node::TemplateBlock(TemplateBlock::Expr(
1362 Cow::Borrowed("hello"),
1363 true
1364 )))
1365 .with_span(8..13)
1366 .with_file_path("index.html"),
1367 );
1368 assert_eq!(
1369 *parse("index.html", source)
1370 .unwrap()
1371 .get(TreeRefId::Node(2))
1372 .spanned_data(),
1373 Spanned::new(Node::TemplateBlock(TemplateBlock::Stmt(Cow::Borrowed(
1374 "if hello"
1375 ))))
1376 .with_span(25..33)
1377 .with_file_path("index.html"),
1378 );
1379 assert_eq!(
1380 *parse("index.html", source)
1381 .unwrap()
1382 .get(TreeRefId::Node(5))
1383 .spanned_data(),
1384 Spanned::new(Node::TemplateBlock(TemplateBlock::Expr(
1385 Cow::Borrowed("not_hello"),
1386 false
1387 )))
1388 .with_span(50..59)
1389 .with_file_path("index.html"),
1390 );
1391
1392 assert_eq!(
1394 *parse("index.html", source)
1395 .unwrap()
1396 .get(TreeRefId::Node(7))
1397 .spanned_data(),
1398 Spanned::new(Node::TemplateBlock(TemplateBlock::RawText(Cow::Borrowed(
1399 "end"
1400 ))))
1401 .with_span(86..89)
1402 .with_file_path("index.html"),
1403 );
1404 }
1405
1406 #[test]
1407 fn expr_and_text_in_attribute() {
1408 let source = r#"<div class="hello{{ expr }} world"></div>"#;
1409 assert_eq!(
1410 *parse("index.html", source)
1411 .unwrap()
1412 .get(TreeRefId::Node(0))
1413 .spanned_data(),
1414 Spanned::new(Node::Element(
1415 Cow::Borrowed("div"),
1416 Attrs::from_iter([(
1417 Spanned::new(Cow::Borrowed("class"))
1418 .with_span(5..10)
1419 .with_file_path("index.html"),
1420 Spanned::new(vec![
1421 Spanned::new(TemplateBlock::RawText(Cow::Borrowed("hello")))
1422 .with_span(12..17)
1423 .with_file_path("index.html"),
1424 Spanned::new(TemplateBlock::Expr(Cow::Borrowed("expr"), true))
1425 .with_span(20..24)
1426 .with_file_path("index.html"),
1427 Spanned::new(TemplateBlock::RawText(Cow::Borrowed(" world")))
1428 .with_span(27..34)
1429 .with_file_path("index.html")
1430 ])
1431 .with_span(12..34)
1432 .with_file_path("index.html"),
1433 )])
1434 ))
1435 .with_span(0..42)
1436 .with_file_path("index.html"),
1437 );
1438 }
1439
1440 #[test]
1441 fn html_in_content_expr() {
1442 let source = r#"<div>{! "<div>hello</div>" !}</div>"#;
1443 assert_eq!(
1444 *parse("index.html", source)
1445 .unwrap()
1446 .get(TreeRefId::Node(1))
1447 .spanned_data(),
1448 Spanned::new(Node::TemplateBlock(TemplateBlock::Expr(
1449 Cow::Borrowed("\"<div>hello</div>\""),
1450 false
1451 )))
1452 .with_span(8..26)
1453 .with_file_path("index.html")
1454 );
1455 }
1456
1457 #[test]
1458 fn html_in_attribute() {
1459 let source = r#"<div class="<hello></world>"></div>"#;
1460 assert_eq!(
1461 *parse("index.html", source)
1462 .unwrap()
1463 .get(TreeRefId::Node(0))
1464 .spanned_data(),
1465 Spanned::new(Node::Element(
1466 Cow::Borrowed("div"),
1467 Attrs::from_iter([(
1468 Spanned::new(Cow::Borrowed("class"))
1469 .with_span(5..10)
1470 .with_file_path("index.html"),
1471 Spanned::new(vec![Spanned::new(TemplateBlock::RawText(Cow::Borrowed(
1472 "<hello></world>"
1473 )))
1474 .with_span(12..27)
1475 .with_file_path("index.html")])
1476 .with_span(12..27)
1477 .with_file_path("index.html"),
1478 )])
1479 ))
1480 .with_span(0..35)
1481 .with_file_path("index.html"),
1482 );
1483 }
1484
1485 #[test]
1486 fn html_in_attribute_expr() {
1487 let source = r#"<div class="{{ '<a href=\'https://example.com\'></a>' }}"></div>"#;
1488 assert_eq!(
1489 *parse("index.html", source)
1490 .unwrap()
1491 .get(TreeRefId::Node(0))
1492 .spanned_data(),
1493 Spanned::new(Node::Element(
1494 Cow::Borrowed("div"),
1495 Attrs::from_iter([(
1496 Spanned::new(Cow::Borrowed("class"))
1497 .with_span(5..10)
1498 .with_file_path("index.html"),
1499 Spanned::new(vec![Spanned::new(TemplateBlock::Expr(
1500 Cow::Borrowed("'<a href=\\'https://example.com\\'></a>'"),
1501 true
1502 ))
1503 .with_span(15..53)
1504 .with_file_path("index.html")])
1505 .with_span(12..56)
1506 .with_file_path("index.html"),
1507 )])
1508 ))
1509 .with_span(0..64)
1510 .with_file_path("index.html"),
1511 );
1512 }
1513
1514 #[test]
1515 fn nested_curly() {
1516 let source = r#"<div>{! "{{nope}}" !}</div>"#;
1517 assert_eq!(
1518 *parse("index.html", source)
1519 .unwrap()
1520 .get(TreeRefId::Node(1))
1521 .spanned_data(),
1522 Spanned::new(Node::TemplateBlock(TemplateBlock::Expr(
1523 Cow::Borrowed("\"{{nope}}\""),
1524 false,
1525 )))
1526 .with_span(8..18)
1527 .with_file_path("index.html"),
1528 );
1529 }
1530
1531 #[test]
1532 fn curly_without_expression() {
1533 let source = "<div>{{ hello }} {hello}</div>";
1534 assert_eq!(
1535 parse("index.html", source)
1536 .unwrap()
1537 .get(TreeRefId::Node(0))
1538 .text(),
1539 "{{hello}} {hello}",
1540 );
1541
1542 let source = "{{ hello }} {";
1543 assert_eq!(
1544 *parse("index.html", source)
1545 .unwrap()
1546 .get(TreeRefId::Node(1))
1547 .spanned_data(),
1548 Spanned::new(Node::TemplateBlock(TemplateBlock::RawText(Cow::Borrowed(
1549 " {"
1550 ))))
1551 .with_span(11..12)
1552 .with_file_path("index.html"),
1553 );
1554 }
1555
1556 #[test]
1557 #[allow(clippy::too_many_lines)]
1558 fn whitespace_are_preserved() {
1559 let source = r#"<ul class="people-list">
1561
1562 <li></li>
1563
1564 <li></li>
1565
1566 <li></li>
1567
1568 <li></li>
1569
1570 <li></li>
1571
1572 </ul>"#;
1573 let tree = parse("index.html", source).unwrap();
1574
1575 assert_eq!(
1576 *tree.get(TreeRefId::Node(0)).spanned_data(),
1577 Spanned::new(Node::Element(
1578 Cow::Borrowed("ul"),
1579 Attrs::from_iter([(
1580 Spanned::new(Cow::Borrowed("class"))
1581 .with_span(4..9)
1582 .with_file_path("index.html"),
1583 Spanned::new(vec![Spanned::new(TemplateBlock::RawText(Cow::Borrowed(
1584 "people-list"
1585 )))
1586 .with_span(11..22)
1587 .with_file_path("index.html")])
1588 .with_span(11..22)
1589 .with_file_path("index.html"),
1590 )])
1591 ))
1592 .with_span(0..108)
1593 .with_file_path("index.html"),
1594 );
1595
1596 assert_eq!(
1597 *tree.get(TreeRefId::Node(1)).spanned_data(),
1598 Spanned::new(Node::TemplateBlock(TemplateBlock::RawText(Cow::Borrowed(
1599 "\n\n "
1600 ))))
1601 .with_span(24..30)
1602 .with_file_path("index.html"),
1603 );
1604
1605 assert_eq!(
1606 *tree.get(TreeRefId::Node(2)).spanned_data(),
1607 Spanned::new(Node::Element(Cow::Borrowed("li"), Attrs::new()))
1608 .with_span(30..39)
1609 .with_file_path("index.html"),
1610 );
1611
1612 assert_eq!(
1613 *tree.get(TreeRefId::Node(3)).spanned_data(),
1614 Spanned::new(Node::TemplateBlock(TemplateBlock::RawText(Cow::Borrowed(
1615 "\n\n "
1616 ))))
1617 .with_span(39..45)
1618 .with_file_path("index.html"),
1619 );
1620
1621 assert_eq!(
1622 *tree.get(TreeRefId::Node(4)).spanned_data(),
1623 Spanned::new(Node::Element(Cow::Borrowed("li"), Attrs::new()))
1624 .with_span(45..54)
1625 .with_file_path("index.html"),
1626 );
1627
1628 assert_eq!(
1629 *tree.get(TreeRefId::Node(5)).spanned_data(),
1630 Spanned::new(Node::TemplateBlock(TemplateBlock::RawText(Cow::Borrowed(
1631 "\n\n "
1632 ))))
1633 .with_span(54..60)
1634 .with_file_path("index.html"),
1635 );
1636
1637 assert_eq!(
1638 *tree.get(TreeRefId::Node(6)).spanned_data(),
1639 Spanned::new(Node::Element(Cow::Borrowed("li"), Attrs::new()))
1640 .with_span(60..69)
1641 .with_file_path("index.html"),
1642 );
1643
1644 assert_eq!(
1645 *tree.get(TreeRefId::Node(7)).spanned_data(),
1646 Spanned::new(Node::TemplateBlock(TemplateBlock::RawText(Cow::Borrowed(
1647 "\n\n "
1648 ))))
1649 .with_span(69..75)
1650 .with_file_path("index.html"),
1651 );
1652
1653 assert_eq!(
1654 *tree.get(TreeRefId::Node(8)).spanned_data(),
1655 Spanned::new(Node::Element(Cow::Borrowed("li"), Attrs::new()))
1656 .with_span(75..84)
1657 .with_file_path("index.html"),
1658 );
1659
1660 assert_eq!(
1661 *tree.get(TreeRefId::Node(9)).spanned_data(),
1662 Spanned::new(Node::TemplateBlock(TemplateBlock::RawText(Cow::Borrowed(
1663 "\n\n "
1664 ))))
1665 .with_span(84..90)
1666 .with_file_path("index.html"),
1667 );
1668
1669 assert_eq!(
1670 *tree.get(TreeRefId::Node(10)).spanned_data(),
1671 Spanned::new(Node::Element(Cow::Borrowed("li"), Attrs::new()))
1672 .with_span(90..99)
1673 .with_file_path("index.html"),
1674 );
1675
1676 assert_eq!(
1677 *tree.get(TreeRefId::Node(11)).spanned_data(),
1678 Spanned::new(Node::TemplateBlock(TemplateBlock::RawText(Cow::Borrowed(
1679 "\n\n "
1680 ))))
1681 .with_span(99..103)
1682 .with_file_path("index.html"),
1683 );
1684 }
1685
1686 #[test]
1687 fn missing_whitespace_between_attributes_error() {
1688 let source = r#"<div attr="a""></div>"#;
1689 assert_eq!(
1690 parse("index.html", source).unwrap_err(),
1691 Spanned::new(Error::MissingWhitespaceBetweenAttributes)
1692 .with_span(13..14)
1693 .with_file_path("index.html"),
1694 );
1695 }
1696
1697 #[test]
1698 fn missing_end_angle_bracket() {
1699 let source = "<";
1700 assert_eq!(
1701 parse("index.html", source).unwrap_err(),
1702 Spanned::new(Error::MissingEndAngleBracket)
1703 .with_span(0..1)
1704 .with_file_path("index.html"),
1705 );
1706 }
1707
1708 #[test]
1709 fn unexpected_character_in_attribute_name_error() {
1710 let source = r#"<div attr="a" "></div>"#;
1711 assert_eq!(
1712 parse("index.html", source).unwrap_err(),
1713 Spanned::new(Error::InvalidAttributeName("\"".to_string()))
1714 .with_span(14..15)
1715 .with_file_path("index.html")
1716 );
1717 }
1718
1719 #[test]
1720 fn select_class_id_test() {
1721 let source = r#"<ul><li class="one">One</li><li class="two">Two</li><li class="three" id="three-id">Three</li></ul>"#;
1722 let tree = parse("index.html", source).unwrap();
1723 assert_eq!(
1724 tree.get(tree.select(".three#three-id").unwrap().unwrap())
1725 .text(),
1726 "Three",
1727 );
1728 }
1729
1730 #[test]
1731 fn select_second_element_test() {
1732 let source = r#"<div><input type="text" value="Hello"><input type="checkbox" checked><input type="text" value="Hello 2"></div>"#;
1733 let tree = parse("index.html", source).unwrap();
1734
1735 assert_eq!(
1736 *tree
1737 .get(tree.select("input:nth-child(2)").unwrap().unwrap())
1738 .spanned_data(),
1739 Spanned::new(Node::Element(
1740 Cow::Borrowed("input"),
1741 Attrs::from_iter([
1742 (
1743 Spanned::new(Cow::Borrowed("type"))
1744 .with_span(45..49)
1745 .with_file_path("index.html"),
1746 Spanned::new(vec![Spanned::new(TemplateBlock::RawText(Cow::Borrowed(
1747 "checkbox"
1748 )))
1749 .with_span(51..59)
1750 .with_file_path("index.html")])
1751 .with_span(51..59)
1752 .with_file_path("index.html"),
1753 ),
1754 (
1755 Spanned::new(Cow::Borrowed("checked"))
1756 .with_span(61..68)
1757 .with_file_path("index.html"),
1758 Spanned::new(vec![])
1759 .with_span(61..68)
1760 .with_file_path("index.html"),
1761 )
1762 ]),
1763 ))
1764 .with_span(38..69)
1765 .with_file_path("index.html"),
1766 );
1767 }
1768
1769 #[test]
1770 fn select_attribute_test() {
1771 let source = r#"<input type="text" value="Hello"><input type="checkbox" checked><input type="text" value="Hello 2">"#;
1772 let tree = parse("index.html", source).unwrap();
1773
1774 assert_eq!(
1775 *tree
1776 .get(tree.select(r#"input[type="text"]"#).unwrap().unwrap())
1777 .spanned_data(),
1778 Spanned::new(Node::Element(
1779 Cow::Borrowed("input"),
1780 Attrs::from_iter([
1781 (
1782 Spanned::new(Cow::Borrowed("type"))
1783 .with_span(7..11)
1784 .with_file_path("index.html"),
1785 Spanned::new(vec![Spanned::new(TemplateBlock::RawText(Cow::Borrowed(
1786 "text"
1787 )))
1788 .with_span(13..17)
1789 .with_file_path("index.html")])
1790 .with_span(13..17)
1791 .with_file_path("index.html"),
1792 ),
1793 (
1794 Spanned::new(Cow::Borrowed("value"))
1795 .with_span(19..24)
1796 .with_file_path("index.html"),
1797 Spanned::new(vec![Spanned::new(TemplateBlock::RawText(Cow::Borrowed(
1798 "Hello"
1799 )))
1800 .with_span(26..31)
1801 .with_file_path("index.html")])
1802 .with_span(26..31)
1803 .with_file_path("index.html"),
1804 )
1805 ]),
1806 ))
1807 .with_span(0..33)
1808 .with_file_path("index.html"),
1809 );
1810 }
1811
1812 #[test]
1813 fn select_parent_test() {
1814 let source = r#"<ul class="list"><li class="one">One</li><li class="two">Two</li><li class="three" id="three-id">Three</li></ul>"#;
1815 let tree = parse("index.html", source).unwrap();
1816
1817 assert_eq!(
1818 tree.get(tree.select(".list .one").unwrap().unwrap()).text(),
1819 "One"
1820 );
1821 assert_eq!(
1822 tree.get(tree.select(".list > .two").unwrap().unwrap())
1823 .text(),
1824 "Two"
1825 );
1826 }
1827
1828 #[test]
1829 fn select_sibling_test() {
1830 let source = r#"<ul class="list"><li class="one">One</li><li class="two">Two</li><li class="three" id="three-id">Three</li></ul>"#;
1831 let tree = parse("index.html", source).unwrap();
1832
1833 assert_eq!(
1834 tree.get(tree.select(".list .one ~ .two").unwrap().unwrap())
1835 .text(),
1836 "Two"
1837 );
1838 assert_eq!(
1839 tree.get(tree.select(".list .two + .three").unwrap().unwrap())
1840 .text(),
1841 "Three"
1842 );
1843 }
1844
1845 #[test]
1846 fn select_all_classes() {
1847 let source = r#"<ul><li class="list-item">One</li><li class="list-item">Two</li><li class="not-list-item">Three</li></ul>"#;
1848 let tree = parse("index.html", source).unwrap();
1849 let selection_nodes = tree.select_all("li.list-item");
1850
1851 assert_eq!(
1852 *tree.get(selection_nodes[0]).spanned_data(),
1853 Spanned::new(Node::Element(
1854 Cow::Borrowed("li"),
1855 Attrs::from_iter([(
1856 Spanned::new(Cow::Borrowed("class"))
1857 .with_span(8..13)
1858 .with_file_path("index.html"),
1859 Spanned::new(vec![Spanned::new(TemplateBlock::RawText(Cow::Borrowed(
1860 "list-item"
1861 )))
1862 .with_span(15..24)
1863 .with_file_path("index.html")])
1864 .with_span(15..24)
1865 .with_file_path("index.html"),
1866 )]),
1867 ))
1868 .with_span(4..34)
1869 .with_file_path("index.html"),
1870 );
1871
1872 assert_eq!(
1873 *tree.get(selection_nodes[1]).spanned_data(),
1874 Spanned::new(Node::Element(
1875 Cow::Borrowed("li"),
1876 Attrs::from_iter([(
1877 Spanned::new(Cow::Borrowed("class"))
1878 .with_span(38..43)
1879 .with_file_path("index.html"),
1880 Spanned::new(vec![Spanned::new(TemplateBlock::RawText(Cow::Borrowed(
1881 "list-item"
1882 )))
1883 .with_span(45..54)
1884 .with_file_path("index.html")])
1885 .with_span(45..54)
1886 .with_file_path("index.html"),
1887 )]),
1888 ))
1889 .with_span(34..64)
1890 .with_file_path("index.html"),
1891 );
1892 }
1893
1894 #[test]
1895 fn set_text() {
1896 let source = r#"<ul><li class="list-item"><div>Some content which will be removed</div>One</li><li class="list-item">Two</li><li class="not-list-item">Three</li></ul>"#;
1897 let mut tree = parse("index.html", source).unwrap();
1898
1899 tree.get_mut(tree.select("li.list-item").unwrap().unwrap())
1900 .set_text(
1901 "Hello world!<script>alert('1')</script>",
1902 Escaping::default(),
1903 );
1904
1905 let selected_node = tree.get(tree.select("li.list-item").unwrap().unwrap());
1906 assert_eq!(
1907 *selected_node.spanned_data(),
1908 Spanned::new(Node::Element(
1909 Cow::Borrowed("li"),
1910 Attrs::from_iter([(
1911 Spanned::new(Cow::Borrowed("class"))
1912 .with_span(8..13)
1913 .with_file_path("index.html"),
1914 Spanned::new(vec![Spanned::new(TemplateBlock::RawText(Cow::Borrowed(
1915 "list-item"
1916 )))
1917 .with_span(15..24)
1918 .with_file_path("index.html")])
1919 .with_span(15..24)
1920 .with_file_path("index.html"),
1921 ),]),
1922 ))
1923 .with_span(4..79)
1924 .with_file_path("index.html"),
1925 );
1926
1927 let mut children = selected_node.children();
1928 let first_child = children.next().unwrap();
1929 assert_eq!(
1930 tree.get(first_child.id()).text(),
1931 "Hello world!<script>alert('1')</script>",
1932 );
1933 }
1934
1935 #[test]
1936 fn set_attribute() {
1937 let source = r#"<ul><li class="list-item">One</li><li class="list-item">Two</li><li class="not-list-item">Three</li></ul>"#;
1938 let mut tree = parse("index.html", source).unwrap();
1939 tree.get_mut(tree.select("li.list-item").unwrap().unwrap())
1940 .set_attr("id", "first-item", Escaping::default());
1941
1942 let selected_node = tree.get(tree.select("li.list-item").unwrap().unwrap());
1943 assert_eq!(
1944 *selected_node.spanned_data(),
1945 Spanned::new(Node::Element(
1946 Cow::Borrowed("li"),
1947 Attrs::from_iter([
1948 (
1949 Spanned::new(Cow::Borrowed("class"))
1950 .with_span(8..13)
1951 .with_file_path("index.html"),
1952 Spanned::new(vec![Spanned::new(TemplateBlock::RawText(Cow::Borrowed(
1953 "list-item"
1954 )))
1955 .with_span(15..24)
1956 .with_file_path("index.html")])
1957 .with_span(15..24)
1958 .with_file_path("index.html"),
1959 ),
1960 (
1961 Spanned::new(Cow::Borrowed("id")),
1962 Spanned::new(vec![Spanned::new(TemplateBlock::RawText(Cow::Borrowed(
1963 "first-item"
1964 )))]),
1965 )
1966 ]),
1967 ))
1968 .with_span(4..34)
1969 .with_file_path("index.html"),
1970 );
1971 }
1972
1973 #[test]
1974 fn remove_node() {
1975 let source = r#"<ul><li class="list-item">One</li><li class="list-item">Two</li><li class="not-list-item">Three</li></ul>"#;
1976 let mut tree = parse("index.html", source).unwrap();
1977 let selected_node = tree.get(tree.select("li:last-child").unwrap().unwrap());
1978 let old_text = selected_node.text();
1979 let selected_node_id = selected_node.id();
1980 tree.get_mut(selected_node_id).remove();
1981
1982 let selected_node = tree.get(tree.select("li:last-child").unwrap().unwrap());
1983 assert_ne!(selected_node.text(), old_text);
1984 }
1985
1986 #[test]
1987 fn replace_inner_node() {
1988 let source = "<div><h2><span>Heading</span> 2</h2></div>";
1989 let mut tree = parse("index.html", source).unwrap();
1990
1991 let mut new_tree = Tree::new("index.html");
1992
1993 let h2_id = new_tree.insert(
1994 TreeRefId::Root,
1995 Spanned::new(Node::new_simple_element("h2", [("id", "heading-2")])),
1996 );
1997 let a_id = new_tree.insert(
1998 h2_id,
1999 Spanned::new(Node::new_simple_element("a", [("href", "#heading-2")])),
2000 );
2001
2002 new_tree
2003 .get_mut(a_id)
2004 .append_children(&tree.get(tree.select("h2").unwrap().unwrap()).sub_tree());
2005 tree.get_mut(tree.select("h2").unwrap().unwrap())
2006 .replace_node(&new_tree);
2007
2008 assert_eq!(
2009 render(&tree),
2010 r##"<div><h2 id="heading-2"><a href="#heading-2"><span>Heading</span> 2</a></h2></div>"##
2011 );
2012 }
2013
2014 #[test]
2015 fn special_characters_in_text() {
2016 let source = "<h1>{{ word_count(content) < 100 ? 'Short' : 'Long' }} article</h1><p>{{ content }}</p>";
2017 let tree = parse("index.html", source).unwrap();
2018 assert_eq!(
2019 tree.get(tree.select("h1").unwrap().unwrap()).text(),
2020 "{{word_count(content) < 100 ? 'Short' : 'Long'}} article",
2021 );
2022
2023 let source = "<h1>{{ word_count(content) <100 ? 'Short' : 'Long' }} article</h1><p>{{ content }}</p>";
2024 let tree = parse("index.html", source).unwrap();
2025 assert_eq!(
2026 tree.get(tree.select("h1").unwrap().unwrap()).text(),
2027 "{{word_count(content) <100 ? 'Short' : 'Long'}} article",
2028 );
2029 }
2030
2031 #[test]
2032 fn attributes_on_several_lines() {
2033 let source = r#"<div
2034style="color: red;"
2035 class="hello"
2036 data-test="world"></div>"#;
2037 let tree = parse("index.html", source).unwrap();
2038 assert_eq!(
2039 *tree.get(TreeRefId::Node(0)).spanned_data(),
2040 Spanned::new(Node::Element(
2041 Cow::Borrowed("div"),
2042 Attrs::from_iter([
2043 (
2044 Spanned::new(Cow::Borrowed("style"))
2045 .with_span(5..10)
2046 .with_file_path("index.html"),
2047 Spanned::new(vec![Spanned::new(TemplateBlock::RawText(Cow::Borrowed(
2048 "color: red;"
2049 )))
2050 .with_span(12..23)
2051 .with_file_path("index.html")])
2052 .with_span(12..23)
2053 .with_file_path("index.html")
2054 ),
2055 (
2056 Spanned::new(Cow::Borrowed("class"))
2057 .with_span(27..32)
2058 .with_file_path("index.html"),
2059 Spanned::new(vec![Spanned::new(TemplateBlock::RawText(Cow::Borrowed(
2060 "hello"
2061 )))
2062 .with_span(34..39)
2063 .with_file_path("index.html")])
2064 .with_span(34..39)
2065 .with_file_path("index.html"),
2066 ),
2067 (
2068 Spanned::new(Cow::Borrowed("data-test"))
2069 .with_span(45..54)
2070 .with_file_path("index.html"),
2071 Spanned::new(vec![Spanned::new(TemplateBlock::RawText(Cow::Borrowed(
2072 "world"
2073 )))
2074 .with_span(56..61)
2075 .with_file_path("index.html")])
2076 .with_span(56..61)
2077 .with_file_path("index.html"),
2078 ),
2079 ]),
2080 ))
2081 .with_span(0..69)
2082 .with_file_path("index.html"),
2083 );
2084 }
2085
2086 #[test]
2087 fn utf8_test() {
2088 let source = "<a>\u{2190}</a>";
2089 let _tree = parse("index.html", source);
2090 }
2091
2092 #[test]
2093 fn owned_tree() {
2094 let source = "<div>Hello world!</div>";
2095 let mut tree = parse_owned("index.html", source).unwrap();
2096 tree.mutate(|tree| {
2097 let div_id = tree.select("div").unwrap().unwrap();
2098 tree.get_mut(div_id)
2099 .set_text("Changed in owned tree", Escaping::Html);
2100 });
2101
2102 let div_id = tree.get_tree().root_nodes()[0];
2103 assert_eq!(tree.get_tree().get(div_id).text(), "Changed in owned tree");
2104 }
2105}