pochoir_parser/
lib.rs

1//! An HTML parser supporting templating expressions and statements.
2//!
3//! ### Parsing HTML
4//!
5//! To parse an HTML document without doing fancy stuff it is possible to use the helper functions
6//! [`parse`] or [`parse_owned`].
7//!
8//! ```
9//! use pochoir_parser::parse;
10//!
11//! let _tree = parse("index.html", "<h1>Hello!</h1>");
12//! ```
13//!
14//! The HTML parser can be customized by responding to special events defined in the [`ParseEvent`]
15//! enumeration. To do that, you need to make a [`Builder`] and use its
16//! [`on_event`](`Builder::on_event`) method to register a single event handler which will handle
17//! all events (it is **not** possible to define several event handlers). You could then `match`
18//! the event received and manipulate the tree using the two other arguments. Note that removing
19//! the element being parsed when handling the [`ParseEvent::BeforeElement`] event will panic
20//! because it will try to insert children to a parent that does not exist. **It is not possible to
21//! skip parsing children of an element.**
22//!
23//! ```
24//! use pochoir_parser::{Builder, ParseEvent};
25//!
26//! let _tree = Builder::new().on_event(|event, tree, id| {
27//!     if event == ParseEvent::BeforeElement {
28//!         let element_name = tree.get(id).name().unwrap().into_owned();
29//!         println!("Element found: {element_name}");
30//!     }
31//!
32//!     Ok(())
33//! }).parse("index.html", r#"<div>Hello</div><main>a<p>Paragraph</p>b</main>"#);
34//! ```
35//!
36//! ### Manipulating the tree
37//!
38//! The tree is not manipulable by itself, you need to get references to some nodes as [`TreeRef`]s
39//! or [`TreeRefMut`]. You can use [`Tree::get`] and [`Tree::get_mut`] if you know the node's ID
40//! but you can also query the tree using CSS selectors with the [`Tree::select`] and
41//! [`Tree::select_all`] methods. Finally, you can traverse the whole tree using
42//! [`Tree::traverse_breadth`] and [`Tree::traverse_depth`].
43//!
44//! You can then:
45//!
46//! - Insert nodes using [`Tree::insert`] by passing the ID of the parent node you have queried
47//!   before and a hand-built [`Node`]
48//! - Change the node's text using [`TreeRefMut::set_text`]
49//! - Change the node's attributes (if it is an element) using [`TreeRefMut::set_attr`]
50//! - Replace the node with another using [`TreeRefMut::replace_node`]
51//! - Get the queried node parent using [`TreeRef::parent`]
52//! - etc.
53//!
54//! Check out [`Tree`], [`TreeRef`] and [`TreeRefMut`] to learn more about what you can get or
55//! set.
56#![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
111/// The list of HTML empty elements.
112///
113/// See <https://developer.mozilla.org/en-US/docs/Glossary/Empty_element>.
114pub 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
206/// Parse an HTML file with templating expressions and statements and return an owned version of
207/// the HTML tree.
208///
209/// If you don't want an owned version, use [`parse`].
210///
211/// This function is equivalent to `Builder::new().parse_owned(file_path, data)`.
212///
213/// # Errors
214///
215/// This function returns an error if an HTML element is malformed or if an expression or a
216/// statement cannot be parsed.
217///
218/// See the [module documentation](self).
219pub fn parse_owned<P: AsRef<Path>>(file_path: P, data: &str) -> Result<OwnedTree> {
220    Builder::new().parse_owned(file_path, data)
221}
222
223/// Parse an HTML file with templating expressions and statements and return an HTML tree
224/// containing references to the original data string.
225///
226/// If you want an owned version, use [`parse_owned`].
227///
228/// This function is equivalent to `Builder::new().parse(file_path, data)`.
229///
230/// # Errors
231///
232/// This function returns an error if an HTML element is malformed or if an expression or a
233/// statement cannot be parsed.
234///
235/// See the [module documentation](self).
236pub 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    /// Event emitted when an element is discovered and its attributes are parsed, just before parsing its children.
243    BeforeElement,
244
245    /// Event emitted when an element is fully parsed, including its children.
246    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    /// Create a new [`Builder`] with default values.
257    pub fn new() -> Self {
258        Self {
259            event_handler: None,
260        }
261    }
262
263    /// Parse an HTML file with templating expressions and statements and return an owned version of
264    /// the HTML tree.
265    ///
266    /// If you don't want an owned version, use [`parse`].
267    ///
268    /// # Errors
269    ///
270    /// This function returns an error if an HTML element is malformed or if an expression or a
271    /// statement cannot be parsed.
272    ///
273    /// See the [module documentation](self).
274    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    /// Parse an HTML file with templating expressions and statements and return an HTML tree
281    /// containing references to the original data string.
282    ///
283    /// If you want an owned version, use [`parse_owned`].
284    ///
285    /// # Errors
286    ///
287    /// This function returns an error if an HTML element is malformed or if an expression or a
288    /// statement cannot be parsed.
289    ///
290    /// See the [module documentation](self).
291    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    /// Register a handler function called when an event is emitted. Only one event handler can be
309    /// defined, if this function is called several times, only the last event handler will be
310    /// used.
311    ///
312    /// Care needs to be taken when intercepting the [`ParseEvent::BeforeElement`] event because
313    /// the current element being parsed must not be removed because its children are not parsed
314    /// yet, it will panic! If you want to remove the element, intercepting the [`ParseEvent::AfterElement`]
315    /// is required.
316    ///
317    /// [`ParseEvent::BeforeElement`] and [`ParseEvent::AfterElement`] will both be called when an
318    /// empty element is encountered
319    ///
320    /// # Example
321    ///
322    /// ```
323    /// use pochoir_parser::{Builder, ParseEvent};
324    ///
325    /// let file_path = "index.html";
326    /// let source = r#"<div>Hello</div><main>a<p>Paragraph</p>b</main>"#;
327    /// let mut elements = vec![];
328    ///
329    /// let _tree = Builder::new().on_event(|event, tree, id| {
330    ///     if event == ParseEvent::BeforeElement {
331    ///         elements.push(tree.get(id).name().unwrap().into_owned());
332    ///     }
333    ///
334    ///     Ok(())
335    /// }).parse(file_path, source);
336    ///
337    /// assert_eq!(elements, vec!["div".to_string(), "main".to_string(), "p".to_string()]);
338    /// ```
339    #[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            // Emit event `BeforeElement` if needed
453            self.call_event_handler(ParseEvent::BeforeElement, element_id)
454                .map_err(|e| Error::EventHandlerError(e.to_string()))?;
455
456            // Emit event `AfterElement` if needed
457            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            // Append the parent without any children
463            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            // Emit event `BeforeElement` if needed
469            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            // Restore original parent to continue iterating sibling elements
480            self.parent = old_parent;
481
482            let end = self.parser.index();
483
484            // Set span after parsing children
485            self.tree
486                .get_mut(element_id)
487                .spanned_data()
488                .set_span(start..end);
489
490            // Emit event `AfterElement` if needed
491            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        // Some elements require to parse their children as raw text and don't care about HTML
500        // https://html.spec.whatwg.org/multipage/parsing.html#parsing-html-fragments
501        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    // --- Helpers ---
535    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                    // If the next token is a matching end tag then there are no child nodes
542                    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                // If the next token is a closing tag with a different name it's an invalid tree
552                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                // Ignore error if the next characters are not a close tag
562            }
563            Err(e) => return Err(e),
564        }
565        self.parser.set_index(start);
566
567        Ok(!self.parser.is_eoi())
568    }
569
570    /// See <https://html.spec.whatwg.org/#start-tags>
571    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    /// See <https://html.spec.whatwg.org/#end-tags>
615    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 the tag is not finished and there are no attribute separator, raise an error
653            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                // Attribute value with doubles quotes
698                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                // Attribute value with single quotes
712                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                // Attribute value without quotes
726                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            // When the attribute does not have a value, we use the attribute key as span
740            (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        // The comment node is not present in the tree, so the seventh node is the text
1393        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        // From https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Whitespace#example_3
1560        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!&lt;script&gt;alert(&#39;1&#39;)&lt;/script&gt;",
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}