#![doc(html_logo_url = "https://gitlab.com/encre-org/pochoir/raw/main/.assets/logo.png")]
#![forbid(unsafe_code)]
#![warn(
missing_debug_implementations,
trivial_casts,
trivial_numeric_casts,
unstable_features,
unused_import_braces,
unused_qualifications,
rustdoc::private_doc_tests,
rustdoc::broken_intra_doc_links,
rustdoc::private_intra_doc_links,
clippy::unnecessary_wraps,
clippy::too_many_lines,
clippy::string_to_string,
clippy::explicit_iter_loop,
clippy::unnecessary_cast,
clippy::missing_errors_doc,
clippy::pedantic,
clippy::clone_on_ref_ptr,
clippy::non_ascii_literal,
clippy::dbg_macro,
clippy::map_err_ignore,
clippy::use_debug,
clippy::map_err_ignore,
clippy::use_self,
clippy::useless_let_if_seq,
clippy::verbose_file_reads,
clippy::panic,
clippy::unimplemented,
clippy::todo
)]
#![allow(
clippy::module_name_repetitions,
clippy::must_use_candidate,
clippy::range_plus_one
)]
use error::AutoError;
use pochoir_common::{Spanned, StreamParser};
use pochoir_template_engine::{BlockContext, TemplateCustomParsing};
use std::{borrow::Cow, fmt, ops::ControlFlow, path::Path};
pub mod error;
pub mod node;
mod render;
pub mod tree;
pub use error::{Error, Result};
pub use node::{Attr, Attrs, Node, ParsedNode};
pub use render::render;
pub use tree::{OwnedTree, Tree, TreeRef, TreeRefId, TreeRefMut};
pub type EventHandlerResult = std::result::Result<(), Box<dyn std::error::Error>>;
pub const EMPTY_HTML_ELEMENTS: &[&str] = &[
"area", "base", "br", "col", "embed", "hr", "img", "keygen", "input", "link", "meta", "param",
"source", "track", "wbr",
];
struct HtmlTemplateCustomParsing {
end_tag_name: Option<String>,
}
impl TemplateCustomParsing for HtmlTemplateCustomParsing {
fn each_char(
&self,
ch: char,
parser: &mut StreamParser,
block_context: BlockContext,
) -> ControlFlow<()> {
match ch {
'<' if self.end_tag_name.is_none()
&& block_context.is_none()
&& parser
.next()
.is_ok_and(|ch| ch.is_alphabetic() || ch == '/' || ch == '!') =>
{
parser.set_index(parser.index() - 1);
ControlFlow::Break(())
}
ch if self.end_tag_name.is_some()
&& ch == '<'
&& parser.peek_exact(self.end_tag_name.as_ref().unwrap()) =>
{
parser.set_index(parser.index() - 1);
ControlFlow::Break(())
}
_ => ControlFlow::Continue(()),
}
}
}
struct AttrValueDoubleQuotedCustomParsing;
impl TemplateCustomParsing for AttrValueDoubleQuotedCustomParsing {
fn each_char(
&self,
ch: char,
parser: &mut StreamParser,
_block_context: BlockContext,
) -> ControlFlow<()> {
if ch == '"' {
parser.set_index(parser.index() - 1);
ControlFlow::Break(())
} else {
ControlFlow::Continue(())
}
}
}
struct AttrValueSingleQuotedCustomParsing;
impl TemplateCustomParsing for AttrValueSingleQuotedCustomParsing {
fn each_char(
&self,
ch: char,
parser: &mut StreamParser,
_block_context: BlockContext,
) -> ControlFlow<()> {
if ch == '\'' {
parser.set_index(parser.index() - 1);
ControlFlow::Break(())
} else {
ControlFlow::Continue(())
}
}
}
struct AttrValueWithoutQuotesCustomParsing;
impl TemplateCustomParsing for AttrValueWithoutQuotesCustomParsing {
fn each_char(
&self,
ch: char,
parser: &mut StreamParser,
_block_context: BlockContext,
) -> ControlFlow<()> {
if ch == ' ' || ch == '>' {
parser.set_index(parser.index() - 1);
ControlFlow::Break(())
} else {
ControlFlow::Continue(())
}
}
}
pub fn parse_owned<P: AsRef<Path>>(file_path: P, data: &str) -> Result<OwnedTree> {
Builder::new().parse_owned(file_path, data)
}
pub fn parse<P: AsRef<Path>>(file_path: P, data: &str) -> Result<Tree<'_>> {
Builder::new().parse(file_path, data)
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum ParseEvent {
BeforeElement,
AfterElement,
}
#[allow(clippy::type_complexity)]
pub struct Builder<'a> {
event_handler:
Option<Box<dyn FnMut(ParseEvent, &mut Tree, TreeRefId) -> EventHandlerResult + 'a>>,
}
impl<'a> Builder<'a> {
pub fn new() -> Self {
Self {
event_handler: None,
}
}
pub fn parse_owned<P: AsRef<Path>>(self, file_path: P, data: &str) -> Result<OwnedTree> {
OwnedTree::try_new(data.to_string(), |data: &String| {
self.parse(file_path, data)
})
}
pub fn parse<P: AsRef<Path>>(self, file_path: P, data: &str) -> Result<Tree<'_>> {
let file_path = file_path.as_ref();
let mut parsing_context = ParserContext {
parser: StreamParser::new(file_path, data),
tree: Tree::new(file_path),
file_path,
parent: TreeRefId::Root,
builder: self,
};
while !parsing_context.parser.is_eoi() {
parsing_context.node()?;
}
Ok(parsing_context.tree)
}
#[must_use]
pub fn on_event<F: FnMut(ParseEvent, &mut Tree, TreeRefId) -> EventHandlerResult + 'a>(
mut self,
on_event: F,
) -> Self {
self.event_handler = Some(Box::new(on_event));
self
}
}
impl Default for Builder<'_> {
fn default() -> Self {
Self::new()
}
}
impl fmt::Debug for Builder<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Builder").finish()
}
}
struct ParserContext<'a, 'b, 'c> {
parser: StreamParser<'a>,
tree: Tree<'a>,
file_path: &'b Path,
parent: TreeRefId,
builder: Builder<'c>,
}
impl<'a> ParserContext<'a, '_, '_> {
fn call_event_handler(&mut self, event: ParseEvent, id: TreeRefId) -> EventHandlerResult {
if let Some(ref mut event_handler) = self.builder.event_handler {
event_handler(event, &mut self.tree, id)?;
}
Ok(())
}
fn node(&mut self) -> Result<Vec<TreeRefId>> {
Ok(if self.parser.peek_exact("<") {
if self.parser.peek_early_exact("!", 1) {
if self.parser.peek_early_exact("--", 2) {
vec![self.comment()?]
} else {
vec![self.doctype()?]
}
} else {
vec![self.element()?]
}
} else {
self.content()?
})
}
fn comment(&mut self) -> Result<TreeRefId> {
let start = self.parser.index();
self.parser.take_exact("<!--").auto_error()?;
let comment = self.parser.take_until("-->").auto_error()?.trim();
self.parser.take_exact("-->").auto_error()?;
let end = self.parser.index();
Ok(self.tree.insert(
self.parent,
Spanned::new(Node::Comment(Cow::Borrowed(comment)))
.with_span(start..end)
.with_file_path(self.file_path),
))
}
fn doctype(&mut self) -> Result<TreeRefId> {
let start = self.parser.index();
self.parser.take_exact("<!").auto_error()?;
let word = self
.parser
.take_while(|(_, ch)| char::is_alphabetic(ch))
.trim();
if word.to_lowercase() != "doctype" {
return Err(Spanned::new(Error::UnexpectedInput {
expected: "`doctype`".to_string(),
found: format!("`{word}`"),
})
.with_span(start..start + word.len())
.with_file_path(self.file_path));
}
let doctype = self.parser.take_until(">").auto_error()?.trim();
self.parser.take_exact(">").auto_error()?;
let end = self.parser.index();
Ok(self.tree.insert(
self.parent,
Spanned::new(Node::Doctype(Cow::Borrowed(doctype)))
.with_span(start..end)
.with_file_path(self.file_path),
))
}
fn element(&mut self) -> Result<TreeRefId> {
let start = self.parser.index();
let (name, attrs, self_closing) = self.tag_open()?;
if self_closing || EMPTY_HTML_ELEMENTS.contains(&&*name) {
let end = self.parser.index();
let element_id = self.tree.insert(
self.parent,
Spanned::new(Node::Element(name, attrs))
.with_span(start..end)
.with_file_path(self.file_path),
);
self.call_event_handler(ParseEvent::BeforeElement, element_id)
.map_err(|e| Error::EventHandlerError(e.to_string()))?;
self.call_event_handler(ParseEvent::AfterElement, element_id)
.map_err(|e| Error::EventHandlerError(e.to_string()))?;
Ok(element_id)
} else {
let element_id = self.tree.insert(
self.parent,
Spanned::new(Node::Element(name.clone(), attrs)).with_file_path(self.file_path),
);
self.call_event_handler(ParseEvent::BeforeElement, element_id)
.map_err(|e| Error::EventHandlerError(e.to_string()))?;
let old_parent = self.parent;
self.parent = element_id;
while self.element_has_children(&name)? {
self.node()?;
}
self.parent = old_parent;
let end = self.parser.index();
self.tree
.get_mut(element_id)
.spanned_data()
.set_span(start..end);
self.call_event_handler(ParseEvent::AfterElement, element_id)
.map_err(|e| Error::EventHandlerError(e.to_string()))?;
Ok(element_id)
}
}
fn content(&mut self) -> Result<Vec<TreeRefId>> {
let mut end_tag_name = None;
if self.parent != TreeRefId::Root {
if let Node::Element(parent_el_name, _) = &self.tree.get(self.parent).data() {
if ["script", "noscript", "style", "textarea", "title"].contains(&&**parent_el_name)
{
end_tag_name = Some(format!("/{parent_el_name}>"));
}
}
}
let blocks = pochoir_template_engine::stream_parse_template(
self.file_path,
&mut self.parser,
HtmlTemplateCustomParsing { end_tag_name },
0,
)
.auto_error()?;
Ok(blocks
.into_iter()
.map(|block| {
let span = block.span().clone();
self.tree.insert(
self.parent,
Spanned::new(Node::TemplateBlock(block.into_inner()))
.with_span(span)
.with_file_path(self.file_path),
)
})
.collect())
}
fn element_has_children(&mut self, tag_open_name: &str) -> Result<bool> {
let start = self.parser.index();
match self.tag_close() {
Ok(tag_close_name) => {
if tag_open_name == tag_close_name {
return Ok(false);
} else if EMPTY_HTML_ELEMENTS.contains(&&*tag_close_name) {
return Err(
Spanned::new(Error::ClosedVoidElement(tag_close_name.to_string()))
.with_span(start..self.parser.index())
.with_file_path(self.file_path),
);
}
return Err(Spanned::new(Error::UnexpectedEndTagName {
start_tag: tag_open_name.to_string(),
end_tag: tag_close_name.to_string(),
})
.with_span(start..self.parser.index())
.with_file_path(self.file_path));
}
Err(e) if matches!(&*e, Error::StreamParserError(pochoir_common::Error::UnexpectedInput { expected, .. }) if expected == "</") =>
{
}
Err(e) => return Err(e),
}
self.parser.set_index(start);
Ok(!self.parser.is_eoi())
}
fn tag_open(&mut self) -> Result<(Cow<'a, str>, Attrs<'a>, bool)> {
self.parser.take_exact("<").auto_error()?;
let start = self.parser.index();
let first_char = self.parser.next();
if first_char.as_ref().is_ok_and(|ch| *ch == '/') {
let name = self
.parser
.take_while(|(_, ch)| !char::is_whitespace(ch) && ch != '>')[1..]
.to_string();
if EMPTY_HTML_ELEMENTS.contains(&name.as_str()) {
return Err(Spanned::new(Error::ClosedVoidElement(name))
.with_span(start + 1..self.parser.index())
.with_file_path(self.file_path));
}
return Err(Spanned::new(Error::UnexpectedEndTag(name))
.with_span(start + 1..self.parser.index())
.with_file_path(self.file_path));
} else if first_char
.as_ref()
.is_ok_and(|ch| ch.is_numeric() || ch.is_whitespace())
{
let name = self.parser.take_while(|(_, ch)| ch != '>' && ch != '/')[1..].to_string();
return Err(Spanned::new(Error::InvalidTagName(name))
.with_span(start..self.parser.index())
.with_file_path(self.file_path));
} else if first_char.as_ref().is_ok_and(|ch| *ch == '>') {
return Err(Spanned::new(Error::ExpectedTagName)
.with_span(start - 1..self.parser.index() + 1)
.with_file_path(self.file_path));
}
let name = self
.parser
.take_while(|(_, ch)| !char::is_whitespace(ch) && ch != '>' && ch != '/');
let (attrs, self_closing) = self.attrs()?;
Ok((Cow::Borrowed(name), attrs, self_closing))
}
fn tag_close(&mut self) -> Result<Cow<'a, str>> {
self.parser.take_exact("</").auto_error()?;
let start_name = self.parser.index();
let name = self
.parser
.take_while(|(_, ch)| !char::is_whitespace(ch) && ch != '>');
if name.trim().is_empty() {
return Err(Spanned::new(Error::ExpectedTagName)
.with_span(
start_name..if self.parser.next().is_ok() {
start_name + 1
} else {
start_name
},
)
.with_file_path(self.file_path));
}
self.parser.trim();
self.parser.take_exact(">").auto_error()?;
Ok(Cow::Borrowed(name))
}
fn attrs(&mut self) -> Result<(Attrs<'a>, bool)> {
let self_closing;
let mut attrs = Attrs::new();
loop {
let last_index = self.parser.index();
self.parser.trim();
if self.parser.peek_exact(">") || self.parser.peek_exact("/>") {
break;
}
if self.parser.index() == last_index {
if self.parser.is_eoi() {
return Err(Spanned::new(Error::MissingEndAngleBracket)
.with_span(self.parser.index() - 1..self.parser.index())
.with_file_path(self.file_path));
}
return Err(Spanned::new(Error::MissingWhitespaceBetweenAttributes)
.with_span(last_index..last_index + 1)
.with_file_path(self.file_path));
}
let (key, val) = self.attr()?;
attrs.insert_spanned(key, val);
}
if self.parser.take_exact(">").is_ok() {
self_closing = false;
} else if self.parser.take_exact("/>").is_ok() {
self_closing = true;
} else {
unreachable!();
}
Ok((attrs, self_closing))
}
fn attr(&mut self) -> Result<Attr<'a>> {
let start_key_index = self.parser.index();
let key = self
.parser
.take_while(|(_, ch)| ch != ' ' && ch != '=' && ch != '>')
.trim();
let end_key_index = self.parser.index();
let key_span = start_key_index..end_key_index;
if ['"', '\'', '<'].iter().any(|ch| key.contains(*ch)) {
return Err(Spanned::new(Error::InvalidAttributeName(key.to_string()))
.with_span(key_span)
.with_file_path(self.file_path));
}
let (val, val_span) = if self.parser.take_exact("=").is_ok() {
if self.parser.take_exact("\"").is_ok() {
let start_val_index = self.parser.index();
let blocks = pochoir_template_engine::stream_parse_template(
self.file_path,
&mut self.parser,
AttrValueDoubleQuotedCustomParsing,
0,
)
.auto_error()?;
let end_val_index = self.parser.index();
self.parser.take_exact("\"").auto_error()?;
(blocks, start_val_index..end_val_index)
} else if self.parser.take_exact("'").is_ok() {
let start_val_index = self.parser.index();
let blocks = pochoir_template_engine::stream_parse_template(
self.file_path,
&mut self.parser,
AttrValueSingleQuotedCustomParsing,
0,
)
.auto_error()?;
let end_val_index = self.parser.index();
self.parser.take_exact("'").auto_error()?;
(blocks, start_val_index..end_val_index)
} else {
let start_val_index = self.parser.index();
let blocks = pochoir_template_engine::stream_parse_template(
self.file_path,
&mut self.parser,
AttrValueWithoutQuotesCustomParsing,
0,
)
.auto_error()?;
let end_val_index = self.parser.index();
(blocks, start_val_index..end_val_index)
}
} else {
(vec![], start_key_index..end_key_index)
};
Ok((
Spanned::new(Cow::Borrowed(key))
.with_span(key_span)
.with_file_path(self.file_path),
Spanned::new(val)
.with_span(val_span)
.with_file_path(self.file_path),
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Attrs, Error};
use pochoir_template_engine::{Escaping, TemplateBlock};
use pretty_assertions::assert_eq;
#[test]
fn minimal_element() {
let source = "<foo></foo>";
let tree = parse("index.html", source).unwrap();
assert_eq!(
*tree.get(TreeRefId::Node(0)).spanned_data(),
Spanned::new(Node::Element(Cow::Borrowed("foo"), Attrs::new()))
.with_span(0..11)
.with_file_path("index.html"),
);
}
#[test]
fn minimal_self_closing_element() {
let source = r#"<img /><img/><link rel="stylesheet"/><meta name=viewport/>"#;
let tree = parse("index.html", source).unwrap();
assert_eq!(
*tree.get(TreeRefId::Node(0)).spanned_data(),
Spanned::new(Node::Element(Cow::Borrowed("img"), Attrs::new()))
.with_span(0..7)
.with_file_path("index.html"),
);
assert_eq!(
*tree.get(TreeRefId::Node(2)).spanned_data(),
Spanned::new(Node::Element(
Cow::Borrowed("link"),
Attrs::from_iter([(
Spanned::new(Cow::Borrowed("rel"))
.with_span(19..22)
.with_file_path("index.html"),
Spanned::new(vec![Spanned::new(TemplateBlock::RawText(Cow::Borrowed(
"stylesheet"
)))
.with_span(24..34)
.with_file_path("index.html")])
.with_span(24..34)
.with_file_path("index.html")
)])
))
.with_span(13..37)
.with_file_path("index.html"),
);
assert_eq!(
*tree.get(TreeRefId::Node(3)).spanned_data(),
Spanned::new(Node::Element(
Cow::Borrowed("meta"),
Attrs::from_iter([(
Spanned::new(Cow::Borrowed("name"))
.with_span(43..47)
.with_file_path("index.html"),
Spanned::new(vec![Spanned::new(TemplateBlock::RawText(Cow::Borrowed(
"viewport/"
)))
.with_span(48..57)
.with_file_path("index.html")])
.with_span(48..57)
.with_file_path("index.html")
)])
))
.with_span(37..58)
.with_file_path("index.html"),
);
}
#[test]
fn doctype() {
let source = "<!DOCTYPE html>
<html>
</html>";
let tree = parse("index.html", source).unwrap();
assert_eq!(
*tree.get(TreeRefId::Node(0)).spanned_data(),
Spanned::new(Node::Doctype(Cow::Borrowed("html")))
.with_span(0..15)
.with_file_path("index.html"),
);
assert_eq!(
*tree.get(TreeRefId::Node(2)).spanned_data(),
Spanned::new(Node::Element(Cow::Borrowed("html"), Attrs::new()))
.with_span(24..46)
.with_file_path("index.html"),
);
}
#[test]
fn comment() {
let source = "<!-- comment1 --><div><!-- comment2 --><div /></div>";
let tree = parse("index.html", source).unwrap();
assert_eq!(
*tree.get(TreeRefId::Node(0)).spanned_data(),
Spanned::new(Node::Comment(Cow::Borrowed("comment1")))
.with_span(0..17)
.with_file_path("index.html"),
);
assert_eq!(
*tree.get(TreeRefId::Node(1)).spanned_data(),
Spanned::new(Node::Element(Cow::Borrowed("div"), Attrs::new()))
.with_span(17..52)
.with_file_path("index.html"),
);
assert_eq!(
*tree.get(TreeRefId::Node(2)).spanned_data(),
Spanned::new(Node::Comment(Cow::Borrowed("comment2")))
.with_span(22..39)
.with_file_path("index.html"),
);
assert_eq!(
*tree.get(TreeRefId::Node(3)).spanned_data(),
Spanned::new(Node::Element(Cow::Borrowed("div"), Attrs::new()))
.with_span(39..46)
.with_file_path("index.html"),
);
}
#[test]
fn text() {
let source = "<foo>bar</foo>";
let tree = parse("index.html", source).unwrap();
assert_eq!(
*tree.get(TreeRefId::Node(0)).spanned_data(),
Spanned::new(Node::Element(Cow::Borrowed("foo"), Attrs::new()))
.with_span(0..14)
.with_file_path("index.html"),
);
assert_eq!(
*tree.get(TreeRefId::Node(1)).spanned_data(),
Spanned::new(Node::TemplateBlock(TemplateBlock::RawText(Cow::Borrowed(
"bar"
))))
.with_span(5..8)
.with_file_path("index.html"),
);
}
#[test]
fn attributes() {
let source = r#"<foo bar="moo" hidden baz="42" id=bar checked></foo>"#;
let tree = parse("index.html", source).unwrap();
assert_eq!(
*tree.get(TreeRefId::Node(0)).spanned_data(),
Spanned::new(Node::Element(
Cow::Borrowed("foo"),
Attrs::from_iter([
(
Spanned::new(Cow::Borrowed("bar"))
.with_span(5..8)
.with_file_path("index.html"),
Spanned::new(vec![Spanned::new(TemplateBlock::RawText(Cow::Borrowed(
"moo"
)))
.with_span(10..13)
.with_file_path("index.html")])
.with_span(10..13)
.with_file_path("index.html")
),
(
Spanned::new(Cow::Borrowed("hidden"))
.with_span(15..21)
.with_file_path("index.html"),
Spanned::new(vec![])
.with_span(15..21)
.with_file_path("index.html"),
),
(
Spanned::new(Cow::Borrowed("baz"))
.with_span(22..25)
.with_file_path("index.html"),
Spanned::new(vec![Spanned::new(TemplateBlock::RawText(Cow::Borrowed(
"42"
)))
.with_span(27..29)
.with_file_path("index.html")])
.with_span(27..29)
.with_file_path("index.html"),
),
(
Spanned::new(Cow::Borrowed("id"))
.with_span(31..33)
.with_file_path("index.html"),
Spanned::new(vec![Spanned::new(TemplateBlock::RawText(Cow::Borrowed(
"bar"
)))
.with_span(34..37)
.with_file_path("index.html")])
.with_span(34..37)
.with_file_path("index.html"),
),
(
Spanned::new(Cow::Borrowed("checked"))
.with_span(38..45)
.with_file_path("index.html"),
Spanned::new(vec![])
.with_span(38..45)
.with_file_path("index.html"),
),
]),
))
.with_span(0..52)
.with_file_path("index.html"),
);
}
#[test]
fn multi_lines_attributes_and_text() {
let source = r#"<foo foo="bar"
baz="qux
lorem"
checked
life="42"
>Hello world
On multiple
Lines with extra spaces</foo>"#;
let tree = parse("index.html", source).unwrap();
assert_eq!(
*tree.get(TreeRefId::Node(0)).spanned_data(),
Spanned::new(Node::Element(
Cow::Borrowed("foo"),
Attrs::from_iter([
(
Spanned::new(Cow::Borrowed("foo"))
.with_span(5..8)
.with_file_path("index.html"),
Spanned::new(vec![Spanned::new(TemplateBlock::RawText(Cow::Borrowed(
"bar"
)))
.with_span(10..13)
.with_file_path("index.html")])
.with_span(10..13)
.with_file_path("index.html"),
),
(
Spanned::new(Cow::Borrowed("baz"))
.with_span(27..30)
.with_file_path("index.html"),
Spanned::new(vec![Spanned::new(TemplateBlock::RawText(Cow::Borrowed(
"qux\n lorem"
)))
.with_span(32..43)
.with_file_path("index.html")])
.with_span(32..43)
.with_file_path("index.html"),
),
(
Spanned::new(Cow::Borrowed("checked"))
.with_span(57..65)
.with_file_path("index.html"),
Spanned::new(vec![])
.with_span(57..65)
.with_file_path("index.html"),
),
(
Spanned::new(Cow::Borrowed("life"))
.with_span(77..81)
.with_file_path("index.html"),
Spanned::new(vec![Spanned::new(TemplateBlock::RawText(Cow::Borrowed(
"42"
)))
.with_span(83..85)
.with_file_path("index.html")])
.with_span(83..85)
.with_file_path("index.html"),
),
]),
))
.with_span(0..151)
.with_file_path("index.html")
);
assert_eq!(
*tree.get(TreeRefId::Node(1)).spanned_data(),
Spanned::new(Node::TemplateBlock(TemplateBlock::RawText(Cow::Borrowed(
"Hello world\nOn multiple\n Lines with extra spaces"
))))
.with_span(96..145)
.with_file_path("index.html"),
);
}
#[test]
fn text_with_elements() {
let source =
r#"<div> <p>Hello {{word}}</p> <span class="bold"> kind person!</span> </div>"#;
let tree = parse("index.html", source).unwrap();
assert_eq!(
*tree.get(TreeRefId::Node(0)).spanned_data(),
Spanned::new(Node::Element(Cow::Borrowed("div"), Attrs::new()))
.with_span(0..78)
.with_file_path("index.html"),
);
assert_eq!(
*tree.get(TreeRefId::Node(1)).spanned_data(),
Spanned::new(Node::TemplateBlock(TemplateBlock::RawText(Cow::Borrowed(
" "
))))
.with_span(5..9)
.with_file_path("index.html"),
);
assert_eq!(
*tree.get(TreeRefId::Node(2)).spanned_data(),
Spanned::new(Node::Element(Cow::Borrowed("p"), Attrs::new()))
.with_span(9..30)
.with_file_path("index.html"),
);
assert_eq!(
*tree.get(TreeRefId::Node(3)).spanned_data(),
Spanned::new(Node::TemplateBlock(TemplateBlock::RawText(Cow::Borrowed(
"Hello "
))))
.with_span(12..18)
.with_file_path("index.html"),
);
assert_eq!(
*tree.get(TreeRefId::Node(4)).spanned_data(),
Spanned::new(Node::TemplateBlock(TemplateBlock::Expr(
Cow::Borrowed("word"),
true
)))
.with_span(20..24)
.with_file_path("index.html"),
);
assert_eq!(
*tree.get(TreeRefId::Node(5)).spanned_data(),
Spanned::new(Node::TemplateBlock(TemplateBlock::RawText(Cow::Borrowed(
" "
))))
.with_span(30..32)
.with_file_path("index.html"),
);
assert_eq!(
*tree.get(TreeRefId::Node(6)).spanned_data(),
Spanned::new(Node::Element(
Cow::Borrowed("span"),
Attrs::from_iter([(
Spanned::new(Cow::Borrowed("class"))
.with_span(38..43)
.with_file_path("index.html"),
Spanned::new(vec![Spanned::new(TemplateBlock::RawText(Cow::Borrowed(
"bold"
)))
.with_span(45..49)
.with_file_path("index.html")])
.with_span(45..49)
.with_file_path("index.html"),
)])
))
.with_span(32..71)
.with_file_path("index.html"),
);
assert_eq!(
*tree.get(TreeRefId::Node(7)).spanned_data(),
Spanned::new(Node::TemplateBlock(TemplateBlock::RawText(Cow::Borrowed(
" kind person!"
))))
.with_span(51..64)
.with_file_path("index.html"),
);
assert_eq!(
*tree.get(TreeRefId::Node(8)).spanned_data(),
Spanned::new(Node::TemplateBlock(TemplateBlock::RawText(Cow::Borrowed(
" "
))))
.with_span(71..72)
.with_file_path("index.html"),
);
}
#[test]
fn test_path_as_tag_name() {
let source = "<some::path />";
let tree = parse("index.html", source).unwrap();
assert_eq!(
*tree.get(TreeRefId::Node(0)).spanned_data(),
Spanned::new(Node::Element(Cow::Borrowed("some::path"), Attrs::new()))
.with_span(0..14)
.with_file_path("index.html"),
);
}
#[test]
fn test_dashed_attribute_name() {
let source = r#"<div data-foo="bar" />"#;
let tree = parse("index.html", source).unwrap();
assert_eq!(
*tree.get(TreeRefId::Node(0)).spanned_data(),
Spanned::new(Node::Element(
Cow::Borrowed("div"),
Attrs::from_iter([(
Spanned::new(Cow::Borrowed("data-foo"))
.with_span(5..13)
.with_file_path("index.html"),
Spanned::new(vec![Spanned::new(TemplateBlock::RawText(Cow::Borrowed(
"bar"
)))
.with_span(15..18)
.with_file_path("index.html")])
.with_span(15..18)
.with_file_path("index.html"),
)]),
))
.with_span(0..22)
.with_file_path("index.html"),
);
}
#[test]
fn test_coloned_attribute_name() {
let source = "<div on:click={{foo}} />";
let tree = parse("index.html", source).unwrap();
assert_eq!(
*tree.get(TreeRefId::Node(0)).spanned_data(),
Spanned::new(Node::Element(
Cow::Borrowed("div"),
Attrs::from_iter([(
Spanned::new(Cow::Borrowed("on:click"))
.with_span(5..13)
.with_file_path("index.html"),
Spanned::new(vec![Spanned::new(TemplateBlock::Expr(
Cow::Borrowed("foo"),
true
))
.with_span(16..19)
.with_file_path("index.html")])
.with_span(14..21)
.with_file_path("index.html"),
)]),
))
.with_span(0..24)
.with_file_path("index.html"),
);
}
#[test]
#[allow(clippy::too_many_lines)]
fn empty_element() {
let source = r#"<img src="/path/to/image.png" alt="My image"><p></p>"#;
assert_eq!(
*parse("index.html", source)
.unwrap()
.get(TreeRefId::Node(0))
.spanned_data(),
Spanned::new(Node::Element(
Cow::Borrowed("img"),
Attrs::from_iter([
(
Spanned::new(Cow::Borrowed("src"))
.with_span(5..8)
.with_file_path("index.html"),
Spanned::new(vec![Spanned::new(TemplateBlock::RawText(Cow::Borrowed(
"/path/to/image.png"
)))
.with_span(10..28)
.with_file_path("index.html")])
.with_span(10..28)
.with_file_path("index.html"),
),
(
Spanned::new(Cow::Borrowed("alt"))
.with_span(30..33)
.with_file_path("index.html"),
Spanned::new(vec![Spanned::new(TemplateBlock::RawText(Cow::Borrowed(
"My image"
)))
.with_span(35..43)
.with_file_path("index.html")])
.with_span(35..43)
.with_file_path("index.html"),
),
]),
))
.with_span(0..45)
.with_file_path("index.html"),
);
let source = r#"<img src="/path/to/image.png" alt="My image" /><p></p>"#;
assert_eq!(
*parse("index.html", source)
.unwrap()
.get(TreeRefId::Node(0))
.spanned_data(),
Spanned::new(Node::Element(
Cow::Borrowed("img"),
Attrs::from_iter([
(
Spanned::new(Cow::Borrowed("src"))
.with_span(5..8)
.with_file_path("index.html"),
Spanned::new(vec![Spanned::new(TemplateBlock::RawText(Cow::Borrowed(
"/path/to/image.png"
)))
.with_span(10..28)
.with_file_path("index.html")])
.with_span(10..28)
.with_file_path("index.html"),
),
(
Spanned::new(Cow::Borrowed("alt"))
.with_span(30..33)
.with_file_path("index.html"),
Spanned::new(vec![Spanned::new(TemplateBlock::RawText(Cow::Borrowed(
"My image"
)))
.with_span(35..43)
.with_file_path("index.html")])
.with_span(35..43)
.with_file_path("index.html"),
),
]),
))
.with_span(0..47)
.with_file_path("index.html"),
);
let source = r#"<img src="/path/to/image.png" alt="My image"/><p></p>"#;
assert_eq!(
*parse("index.html", source)
.unwrap()
.get(TreeRefId::Node(0))
.spanned_data(),
Spanned::new(Node::Element(
Cow::Borrowed("img"),
Attrs::from_iter([
(
Spanned::new(Cow::Borrowed("src"))
.with_span(5..8)
.with_file_path("index.html"),
Spanned::new(vec![Spanned::new(TemplateBlock::RawText(Cow::Borrowed(
"/path/to/image.png"
)))
.with_span(10..28)
.with_file_path("index.html")])
.with_span(10..28)
.with_file_path("index.html"),
),
(
Spanned::new(Cow::Borrowed("alt"))
.with_span(30..33)
.with_file_path("index.html"),
Spanned::new(vec![Spanned::new(TemplateBlock::RawText(Cow::Borrowed(
"My image"
)))
.with_span(35..43)
.with_file_path("index.html")])
.with_span(35..43)
.with_file_path("index.html"),
),
]),
))
.with_span(0..46)
.with_file_path("index.html"),
);
let source = r#"<img src="/path/to/image.png" alt="My image"><p></p></img>"#;
assert_eq!(
parse("index.html", source).unwrap_err(),
Spanned::new(Error::ClosedVoidElement("img".to_string()))
.with_span(54..57)
.with_file_path("index.html"),
);
}
#[test]
fn script_style_element() {
let source = "<script>if (0 < 1) { console.log('Hello world!'); }</script><style>p::before { content: '</h1>'; }</style>";
assert_eq!(
*parse("index.html", source)
.unwrap()
.get(TreeRefId::Node(0))
.spanned_data(),
Spanned::new(Node::Element(Cow::Borrowed("script"), Attrs::new()))
.with_span(0..60)
.with_file_path("index.html"),
);
}
#[test]
fn parse_template_syntax() {
let source = "<div>{{ hello }}</div>{% if hello %}a{% else %}{! not_hello !}{%endif%}{# a comment #}end";
assert_eq!(
*parse("index.html", source)
.unwrap()
.get(TreeRefId::Node(1))
.spanned_data(),
Spanned::new(Node::TemplateBlock(TemplateBlock::Expr(
Cow::Borrowed("hello"),
true
)))
.with_span(8..13)
.with_file_path("index.html"),
);
assert_eq!(
*parse("index.html", source)
.unwrap()
.get(TreeRefId::Node(2))
.spanned_data(),
Spanned::new(Node::TemplateBlock(TemplateBlock::Stmt(Cow::Borrowed(
"if hello"
))))
.with_span(25..33)
.with_file_path("index.html"),
);
assert_eq!(
*parse("index.html", source)
.unwrap()
.get(TreeRefId::Node(5))
.spanned_data(),
Spanned::new(Node::TemplateBlock(TemplateBlock::Expr(
Cow::Borrowed("not_hello"),
false
)))
.with_span(50..59)
.with_file_path("index.html"),
);
assert_eq!(
*parse("index.html", source)
.unwrap()
.get(TreeRefId::Node(7))
.spanned_data(),
Spanned::new(Node::TemplateBlock(TemplateBlock::RawText(Cow::Borrowed(
"end"
))))
.with_span(86..89)
.with_file_path("index.html"),
);
}
#[test]
fn expr_and_text_in_attribute() {
let source = r#"<div class="hello{{ expr }} world"></div>"#;
assert_eq!(
*parse("index.html", source)
.unwrap()
.get(TreeRefId::Node(0))
.spanned_data(),
Spanned::new(Node::Element(
Cow::Borrowed("div"),
Attrs::from_iter([(
Spanned::new(Cow::Borrowed("class"))
.with_span(5..10)
.with_file_path("index.html"),
Spanned::new(vec![
Spanned::new(TemplateBlock::RawText(Cow::Borrowed("hello")))
.with_span(12..17)
.with_file_path("index.html"),
Spanned::new(TemplateBlock::Expr(Cow::Borrowed("expr"), true))
.with_span(20..24)
.with_file_path("index.html"),
Spanned::new(TemplateBlock::RawText(Cow::Borrowed(" world")))
.with_span(27..34)
.with_file_path("index.html")
])
.with_span(12..34)
.with_file_path("index.html"),
)])
))
.with_span(0..42)
.with_file_path("index.html"),
);
}
#[test]
fn html_in_content_expr() {
let source = r#"<div>{! "<div>hello</div>" !}</div>"#;
assert_eq!(
*parse("index.html", source)
.unwrap()
.get(TreeRefId::Node(1))
.spanned_data(),
Spanned::new(Node::TemplateBlock(TemplateBlock::Expr(
Cow::Borrowed("\"<div>hello</div>\""),
false
)))
.with_span(8..26)
.with_file_path("index.html")
);
}
#[test]
fn html_in_attribute() {
let source = r#"<div class="<hello></world>"></div>"#;
assert_eq!(
*parse("index.html", source)
.unwrap()
.get(TreeRefId::Node(0))
.spanned_data(),
Spanned::new(Node::Element(
Cow::Borrowed("div"),
Attrs::from_iter([(
Spanned::new(Cow::Borrowed("class"))
.with_span(5..10)
.with_file_path("index.html"),
Spanned::new(vec![Spanned::new(TemplateBlock::RawText(Cow::Borrowed(
"<hello></world>"
)))
.with_span(12..27)
.with_file_path("index.html")])
.with_span(12..27)
.with_file_path("index.html"),
)])
))
.with_span(0..35)
.with_file_path("index.html"),
);
}
#[test]
fn html_in_attribute_expr() {
let source = r#"<div class="{{ '<a href=\'https://example.com\'></a>' }}"></div>"#;
assert_eq!(
*parse("index.html", source)
.unwrap()
.get(TreeRefId::Node(0))
.spanned_data(),
Spanned::new(Node::Element(
Cow::Borrowed("div"),
Attrs::from_iter([(
Spanned::new(Cow::Borrowed("class"))
.with_span(5..10)
.with_file_path("index.html"),
Spanned::new(vec![Spanned::new(TemplateBlock::Expr(
Cow::Borrowed("'<a href=\\'https://example.com\\'></a>'"),
true
))
.with_span(15..53)
.with_file_path("index.html")])
.with_span(12..56)
.with_file_path("index.html"),
)])
))
.with_span(0..64)
.with_file_path("index.html"),
);
}
#[test]
fn nested_curly() {
let source = r#"<div>{! "{{nope}}" !}</div>"#;
assert_eq!(
*parse("index.html", source)
.unwrap()
.get(TreeRefId::Node(1))
.spanned_data(),
Spanned::new(Node::TemplateBlock(TemplateBlock::Expr(
Cow::Borrowed("\"{{nope}}\""),
false,
)))
.with_span(8..18)
.with_file_path("index.html"),
);
}
#[test]
fn curly_without_expression() {
let source = "<div>{{ hello }} {hello}</div>";
assert_eq!(
parse("index.html", source)
.unwrap()
.get(TreeRefId::Node(0))
.text(),
"{{hello}} {hello}",
);
let source = "{{ hello }} {";
assert_eq!(
*parse("index.html", source)
.unwrap()
.get(TreeRefId::Node(1))
.spanned_data(),
Spanned::new(Node::TemplateBlock(TemplateBlock::RawText(Cow::Borrowed(
" {"
))))
.with_span(11..12)
.with_file_path("index.html"),
);
}
#[test]
#[allow(clippy::too_many_lines)]
fn whitespace_are_preserved() {
let source = r#"<ul class="people-list">
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
</ul>"#;
let tree = parse("index.html", source).unwrap();
assert_eq!(
*tree.get(TreeRefId::Node(0)).spanned_data(),
Spanned::new(Node::Element(
Cow::Borrowed("ul"),
Attrs::from_iter([(
Spanned::new(Cow::Borrowed("class"))
.with_span(4..9)
.with_file_path("index.html"),
Spanned::new(vec![Spanned::new(TemplateBlock::RawText(Cow::Borrowed(
"people-list"
)))
.with_span(11..22)
.with_file_path("index.html")])
.with_span(11..22)
.with_file_path("index.html"),
)])
))
.with_span(0..108)
.with_file_path("index.html"),
);
assert_eq!(
*tree.get(TreeRefId::Node(1)).spanned_data(),
Spanned::new(Node::TemplateBlock(TemplateBlock::RawText(Cow::Borrowed(
"\n\n "
))))
.with_span(24..30)
.with_file_path("index.html"),
);
assert_eq!(
*tree.get(TreeRefId::Node(2)).spanned_data(),
Spanned::new(Node::Element(Cow::Borrowed("li"), Attrs::new()))
.with_span(30..39)
.with_file_path("index.html"),
);
assert_eq!(
*tree.get(TreeRefId::Node(3)).spanned_data(),
Spanned::new(Node::TemplateBlock(TemplateBlock::RawText(Cow::Borrowed(
"\n\n "
))))
.with_span(39..45)
.with_file_path("index.html"),
);
assert_eq!(
*tree.get(TreeRefId::Node(4)).spanned_data(),
Spanned::new(Node::Element(Cow::Borrowed("li"), Attrs::new()))
.with_span(45..54)
.with_file_path("index.html"),
);
assert_eq!(
*tree.get(TreeRefId::Node(5)).spanned_data(),
Spanned::new(Node::TemplateBlock(TemplateBlock::RawText(Cow::Borrowed(
"\n\n "
))))
.with_span(54..60)
.with_file_path("index.html"),
);
assert_eq!(
*tree.get(TreeRefId::Node(6)).spanned_data(),
Spanned::new(Node::Element(Cow::Borrowed("li"), Attrs::new()))
.with_span(60..69)
.with_file_path("index.html"),
);
assert_eq!(
*tree.get(TreeRefId::Node(7)).spanned_data(),
Spanned::new(Node::TemplateBlock(TemplateBlock::RawText(Cow::Borrowed(
"\n\n "
))))
.with_span(69..75)
.with_file_path("index.html"),
);
assert_eq!(
*tree.get(TreeRefId::Node(8)).spanned_data(),
Spanned::new(Node::Element(Cow::Borrowed("li"), Attrs::new()))
.with_span(75..84)
.with_file_path("index.html"),
);
assert_eq!(
*tree.get(TreeRefId::Node(9)).spanned_data(),
Spanned::new(Node::TemplateBlock(TemplateBlock::RawText(Cow::Borrowed(
"\n\n "
))))
.with_span(84..90)
.with_file_path("index.html"),
);
assert_eq!(
*tree.get(TreeRefId::Node(10)).spanned_data(),
Spanned::new(Node::Element(Cow::Borrowed("li"), Attrs::new()))
.with_span(90..99)
.with_file_path("index.html"),
);
assert_eq!(
*tree.get(TreeRefId::Node(11)).spanned_data(),
Spanned::new(Node::TemplateBlock(TemplateBlock::RawText(Cow::Borrowed(
"\n\n "
))))
.with_span(99..103)
.with_file_path("index.html"),
);
}
#[test]
fn missing_whitespace_between_attributes_error() {
let source = r#"<div attr="a""></div>"#;
assert_eq!(
parse("index.html", source).unwrap_err(),
Spanned::new(Error::MissingWhitespaceBetweenAttributes)
.with_span(13..14)
.with_file_path("index.html"),
);
}
#[test]
fn missing_end_angle_bracket() {
let source = "<";
assert_eq!(
parse("index.html", source).unwrap_err(),
Spanned::new(Error::MissingEndAngleBracket)
.with_span(0..1)
.with_file_path("index.html"),
);
}
#[test]
fn unexpected_character_in_attribute_name_error() {
let source = r#"<div attr="a" "></div>"#;
assert_eq!(
parse("index.html", source).unwrap_err(),
Spanned::new(Error::InvalidAttributeName("\"".to_string()))
.with_span(14..15)
.with_file_path("index.html")
);
}
#[test]
fn select_class_id_test() {
let source = r#"<ul><li class="one">One</li><li class="two">Two</li><li class="three" id="three-id">Three</li></ul>"#;
let tree = parse("index.html", source).unwrap();
assert_eq!(
tree.get(tree.select(".three#three-id").unwrap().unwrap())
.text(),
"Three",
);
}
#[test]
fn select_second_element_test() {
let source = r#"<div><input type="text" value="Hello"><input type="checkbox" checked><input type="text" value="Hello 2"></div>"#;
let tree = parse("index.html", source).unwrap();
assert_eq!(
*tree
.get(tree.select("input:nth-child(2)").unwrap().unwrap())
.spanned_data(),
Spanned::new(Node::Element(
Cow::Borrowed("input"),
Attrs::from_iter([
(
Spanned::new(Cow::Borrowed("type"))
.with_span(45..49)
.with_file_path("index.html"),
Spanned::new(vec![Spanned::new(TemplateBlock::RawText(Cow::Borrowed(
"checkbox"
)))
.with_span(51..59)
.with_file_path("index.html")])
.with_span(51..59)
.with_file_path("index.html"),
),
(
Spanned::new(Cow::Borrowed("checked"))
.with_span(61..68)
.with_file_path("index.html"),
Spanned::new(vec![])
.with_span(61..68)
.with_file_path("index.html"),
)
]),
))
.with_span(38..69)
.with_file_path("index.html"),
);
}
#[test]
fn select_attribute_test() {
let source = r#"<input type="text" value="Hello"><input type="checkbox" checked><input type="text" value="Hello 2">"#;
let tree = parse("index.html", source).unwrap();
assert_eq!(
*tree
.get(tree.select(r#"input[type="text"]"#).unwrap().unwrap())
.spanned_data(),
Spanned::new(Node::Element(
Cow::Borrowed("input"),
Attrs::from_iter([
(
Spanned::new(Cow::Borrowed("type"))
.with_span(7..11)
.with_file_path("index.html"),
Spanned::new(vec![Spanned::new(TemplateBlock::RawText(Cow::Borrowed(
"text"
)))
.with_span(13..17)
.with_file_path("index.html")])
.with_span(13..17)
.with_file_path("index.html"),
),
(
Spanned::new(Cow::Borrowed("value"))
.with_span(19..24)
.with_file_path("index.html"),
Spanned::new(vec![Spanned::new(TemplateBlock::RawText(Cow::Borrowed(
"Hello"
)))
.with_span(26..31)
.with_file_path("index.html")])
.with_span(26..31)
.with_file_path("index.html"),
)
]),
))
.with_span(0..33)
.with_file_path("index.html"),
);
}
#[test]
fn select_parent_test() {
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>"#;
let tree = parse("index.html", source).unwrap();
assert_eq!(
tree.get(tree.select(".list .one").unwrap().unwrap()).text(),
"One"
);
assert_eq!(
tree.get(tree.select(".list > .two").unwrap().unwrap())
.text(),
"Two"
);
}
#[test]
fn select_sibling_test() {
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>"#;
let tree = parse("index.html", source).unwrap();
assert_eq!(
tree.get(tree.select(".list .one ~ .two").unwrap().unwrap())
.text(),
"Two"
);
assert_eq!(
tree.get(tree.select(".list .two + .three").unwrap().unwrap())
.text(),
"Three"
);
}
#[test]
fn select_all_classes() {
let source = r#"<ul><li class="list-item">One</li><li class="list-item">Two</li><li class="not-list-item">Three</li></ul>"#;
let tree = parse("index.html", source).unwrap();
let selection_nodes = tree.select_all("li.list-item");
assert_eq!(
*tree.get(selection_nodes[0]).spanned_data(),
Spanned::new(Node::Element(
Cow::Borrowed("li"),
Attrs::from_iter([(
Spanned::new(Cow::Borrowed("class"))
.with_span(8..13)
.with_file_path("index.html"),
Spanned::new(vec![Spanned::new(TemplateBlock::RawText(Cow::Borrowed(
"list-item"
)))
.with_span(15..24)
.with_file_path("index.html")])
.with_span(15..24)
.with_file_path("index.html"),
)]),
))
.with_span(4..34)
.with_file_path("index.html"),
);
assert_eq!(
*tree.get(selection_nodes[1]).spanned_data(),
Spanned::new(Node::Element(
Cow::Borrowed("li"),
Attrs::from_iter([(
Spanned::new(Cow::Borrowed("class"))
.with_span(38..43)
.with_file_path("index.html"),
Spanned::new(vec![Spanned::new(TemplateBlock::RawText(Cow::Borrowed(
"list-item"
)))
.with_span(45..54)
.with_file_path("index.html")])
.with_span(45..54)
.with_file_path("index.html"),
)]),
))
.with_span(34..64)
.with_file_path("index.html"),
);
}
#[test]
fn set_text() {
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>"#;
let mut tree = parse("index.html", source).unwrap();
tree.get_mut(tree.select("li.list-item").unwrap().unwrap())
.set_text(
"Hello world!<script>alert('1')</script>",
Escaping::default(),
);
let selected_node = tree.get(tree.select("li.list-item").unwrap().unwrap());
assert_eq!(
*selected_node.spanned_data(),
Spanned::new(Node::Element(
Cow::Borrowed("li"),
Attrs::from_iter([(
Spanned::new(Cow::Borrowed("class"))
.with_span(8..13)
.with_file_path("index.html"),
Spanned::new(vec![Spanned::new(TemplateBlock::RawText(Cow::Borrowed(
"list-item"
)))
.with_span(15..24)
.with_file_path("index.html")])
.with_span(15..24)
.with_file_path("index.html"),
),]),
))
.with_span(4..79)
.with_file_path("index.html"),
);
let mut children = selected_node.children();
let first_child = children.next().unwrap();
assert_eq!(
tree.get(first_child.id()).text(),
"Hello world!<script>alert('1')</script>",
);
}
#[test]
fn set_attribute() {
let source = r#"<ul><li class="list-item">One</li><li class="list-item">Two</li><li class="not-list-item">Three</li></ul>"#;
let mut tree = parse("index.html", source).unwrap();
tree.get_mut(tree.select("li.list-item").unwrap().unwrap())
.set_attr("id", "first-item", Escaping::default());
let selected_node = tree.get(tree.select("li.list-item").unwrap().unwrap());
assert_eq!(
*selected_node.spanned_data(),
Spanned::new(Node::Element(
Cow::Borrowed("li"),
Attrs::from_iter([
(
Spanned::new(Cow::Borrowed("class"))
.with_span(8..13)
.with_file_path("index.html"),
Spanned::new(vec![Spanned::new(TemplateBlock::RawText(Cow::Borrowed(
"list-item"
)))
.with_span(15..24)
.with_file_path("index.html")])
.with_span(15..24)
.with_file_path("index.html"),
),
(
Spanned::new(Cow::Borrowed("id")),
Spanned::new(vec![Spanned::new(TemplateBlock::RawText(Cow::Borrowed(
"first-item"
)))]),
)
]),
))
.with_span(4..34)
.with_file_path("index.html"),
);
}
#[test]
fn remove_node() {
let source = r#"<ul><li class="list-item">One</li><li class="list-item">Two</li><li class="not-list-item">Three</li></ul>"#;
let mut tree = parse("index.html", source).unwrap();
let selected_node = tree.get(tree.select("li:last-child").unwrap().unwrap());
let old_text = selected_node.text();
let selected_node_id = selected_node.id();
tree.get_mut(selected_node_id).remove();
let selected_node = tree.get(tree.select("li:last-child").unwrap().unwrap());
assert_ne!(selected_node.text(), old_text);
}
#[test]
fn replace_inner_node() {
let source = "<div><h2><span>Heading</span> 2</h2></div>";
let mut tree = parse("index.html", source).unwrap();
let mut new_tree = Tree::new("index.html");
let h2_id = new_tree.insert(
TreeRefId::Root,
Spanned::new(Node::new_simple_element("h2", [("id", "heading-2")])),
);
let a_id = new_tree.insert(
h2_id,
Spanned::new(Node::new_simple_element("a", [("href", "#heading-2")])),
);
new_tree
.get_mut(a_id)
.append_children(&tree.get(tree.select("h2").unwrap().unwrap()).sub_tree());
tree.get_mut(tree.select("h2").unwrap().unwrap())
.replace_node(&new_tree);
assert_eq!(
render(&tree),
r##"<div><h2 id="heading-2"><a href="#heading-2"><span>Heading</span> 2</a></h2></div>"##
);
}
#[test]
fn special_characters_in_text() {
let source = "<h1>{{ word_count(content) < 100 ? 'Short' : 'Long' }} article</h1><p>{{ content }}</p>";
let tree = parse("index.html", source).unwrap();
assert_eq!(
tree.get(tree.select("h1").unwrap().unwrap()).text(),
"{{word_count(content) < 100 ? 'Short' : 'Long'}} article",
);
let source = "<h1>{{ word_count(content) <100 ? 'Short' : 'Long' }} article</h1><p>{{ content }}</p>";
let tree = parse("index.html", source).unwrap();
assert_eq!(
tree.get(tree.select("h1").unwrap().unwrap()).text(),
"{{word_count(content) <100 ? 'Short' : 'Long'}} article",
);
}
#[test]
fn attributes_on_several_lines() {
let source = r#"<div
style="color: red;"
class="hello"
data-test="world"></div>"#;
let tree = parse("index.html", source).unwrap();
assert_eq!(
*tree.get(TreeRefId::Node(0)).spanned_data(),
Spanned::new(Node::Element(
Cow::Borrowed("div"),
Attrs::from_iter([
(
Spanned::new(Cow::Borrowed("style"))
.with_span(5..10)
.with_file_path("index.html"),
Spanned::new(vec![Spanned::new(TemplateBlock::RawText(Cow::Borrowed(
"color: red;"
)))
.with_span(12..23)
.with_file_path("index.html")])
.with_span(12..23)
.with_file_path("index.html")
),
(
Spanned::new(Cow::Borrowed("class"))
.with_span(27..32)
.with_file_path("index.html"),
Spanned::new(vec![Spanned::new(TemplateBlock::RawText(Cow::Borrowed(
"hello"
)))
.with_span(34..39)
.with_file_path("index.html")])
.with_span(34..39)
.with_file_path("index.html"),
),
(
Spanned::new(Cow::Borrowed("data-test"))
.with_span(45..54)
.with_file_path("index.html"),
Spanned::new(vec![Spanned::new(TemplateBlock::RawText(Cow::Borrowed(
"world"
)))
.with_span(56..61)
.with_file_path("index.html")])
.with_span(56..61)
.with_file_path("index.html"),
),
]),
))
.with_span(0..69)
.with_file_path("index.html"),
);
}
#[test]
fn utf8_test() {
let source = "<a>\u{2190}</a>";
let _tree = parse("index.html", source);
}
#[test]
fn owned_tree() {
let source = "<div>Hello world!</div>";
let mut tree = parse_owned("index.html", source).unwrap();
tree.mutate(|tree| {
let div_id = tree.select("div").unwrap().unwrap();
tree.get_mut(div_id)
.set_text("Changed in owned tree", Escaping::Html);
});
let div_id = tree.get_tree().root_nodes()[0];
assert_eq!(tree.get_tree().get(div_id).text(), "Changed in owned tree");
}
}