use crate::{NodeRef, Tag};
impl NodeRef<'_> {
pub fn html(&self) -> String {
let mut out = String::new();
write_tree(*self, &mut out, true);
out
}
pub fn inner_html(&self) -> String {
let mut out = String::new();
for c in self.children() {
write_tree(c, &mut out, false);
}
out
}
}
fn is_void(tag: Option<Tag>) -> bool {
use Tag::*;
matches!(
tag,
Some(
Area | Base
| Basefont
| Bgsound
| Br
| Col
| Embed
| Frame
| Hr
| Img
| Input
| Keygen
| Link
| Meta
| Param
| Source
| Track
| Wbr
)
)
}
fn is_raw_text(tag: Option<Tag>) -> bool {
use Tag::*;
matches!(
tag,
Some(Style | Script | Xmp | Iframe | Noembed | Noframes | Plaintext)
)
}
fn write_tree(root: NodeRef<'_>, out: &mut String, root_is_top: bool) {
let mut n = root;
loop {
let raw = !(root_is_top && n == root) && is_raw_text(n.parent().and_then(|p| p.tag()));
open(n, out, raw);
if !is_void(n.tag())
&& let Some(c) = n.first_child()
{
n = c;
continue;
}
loop {
close(n, out);
if n == root {
return;
}
match n.next_sibling() {
Some(s) => {
n = s;
break;
}
None => match n.parent() {
Some(p) => n = p,
None => return,
},
}
}
}
}
fn open(n: NodeRef<'_>, out: &mut String, raw: bool) {
if let Some(e) = n.as_element() {
out.push('<');
out.push_str(e.name());
for (k, v) in e.attrs() {
out.push(' ');
out.push_str(k);
out.push_str("=\"");
escape(v, true, out);
out.push('"');
}
out.push('>');
} else if let Some(t) = n.as_text() {
if raw {
out.push_str(t);
} else {
escape(t, false, out);
}
} else if let Some(c) = n.as_comment() {
out.push_str("<!--");
out.push_str(c);
out.push_str("-->");
} else if !n.is_document() {
out.push_str("<!DOCTYPE html>");
}
}
fn close(n: NodeRef<'_>, out: &mut String) {
if let Some(e) = n.as_element()
&& !is_void(Some(e.tag()))
{
out.push_str("</");
out.push_str(e.name());
out.push('>');
}
}
fn escape(s: &str, attr: bool, out: &mut String) {
let b = s.as_bytes();
let mut last = 0;
for (i, &c) in b.iter().enumerate() {
let rep = match c {
b'&' => "&",
b'<' => "<",
b'>' => ">",
b'"' if attr => """,
0xA0 if i > 0 && b[i - 1] == 0xC2 => " ",
_ => continue,
};
let start = if c == 0xA0 { i - 1 } else { i };
out.push_str(&s[last..start]);
out.push_str(rep);
last = i + 1;
}
out.push_str(&s[last..]);
}
#[cfg(test)]
mod tests {
use crate::{Html, Tag};
#[test]
fn outer_and_inner_html() {
let doc = Html::parse(
"<div class=\"a"b\" title='x\u{a0}y'>1 < 2 & <br><img src=x>\u{a0}<!--c-->\
<script>if (a < b) {}</script><style>a>b{}</style><noscript><b>n</b></noscript><svg><style>s<x</style></svg></div>",
);
let div = doc
.root()
.descendants()
.find(|n| n.tag() == Some(Tag::Div))
.unwrap();
assert_eq!(
div.html(),
"<div class=\"a"b\" title=\"x y\">1 < 2 & <br><img src=\"x\"> <!--c-->\
<script>if (a < b) {}</script><style>a>b{}</style><noscript><b>n</b></noscript><svg><style>s<x</style></svg></div>"
);
let br = div.children().find(|n| n.tag() == Some(Tag::Br)).unwrap();
assert_eq!(br.html(), "<br>");
let script = div
.children()
.find(|n| n.tag() == Some(Tag::Script))
.unwrap();
assert_eq!(script.inner_html(), "if (a < b) {}");
assert_eq!(script.first_child().unwrap().html(), "if (a < b) {}");
}
}