use crate::dom::Dom;
use crate::node::NodeData;
use crate::node_id::NodeId;
const VOID_TAGS: &[&str] = &[
"area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source",
"track", "wbr", "vr",
];
fn is_void_tag(tag: &str) -> bool {
VOID_TAGS.contains(&tag)
}
fn escape_for_attr(s: &str, out: &mut String) {
for c in s.chars() {
match c {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'"' => out.push_str("""),
'\'' => out.push_str("'"),
c => out.push(c),
}
}
}
fn serializes_children_raw(tag: &str) -> bool {
matches!(
tag,
"style" | "script" | "xmp" | "iframe" | "noembed" | "noframes" | "plaintext"
)
}
fn escape_for_text(s: &str, out: &mut String) {
for c in s.chars() {
match c {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
c => out.push(c),
}
}
}
impl<Ext> Dom<Ext> {
pub fn outer_markup(&self, id: NodeId) -> String {
let mut out = String::new();
self.write_node(id, &mut out, false);
out
}
pub fn inner_markup(&self, id: NodeId) -> String {
let mut out = String::new();
let Some(node) = self.get_node(id) else {
return out;
};
match &node.data {
NodeData::Element { tag, .. } => {
self.write_children(id, &mut out, serializes_children_raw(tag));
}
NodeData::Fragment => self.write_children(id, &mut out, false),
_ => {}
}
out
}
fn write_children(&self, id: NodeId, out: &mut String, raw_text: bool) {
let mut child = self.get_node(id).and_then(|n| n.first_child);
while let Some(c) = child {
self.write_node(c, out, raw_text);
child = self.get_node(c).and_then(|n| n.next_sibling);
}
}
fn write_node(&self, id: NodeId, out: &mut String, raw_text: bool) {
let Some(node) = self.get_node(id) else {
return;
};
match &node.data {
NodeData::Element {
tag,
attrs,
classes,
..
} => {
out.push('<');
out.push_str(tag);
if !classes.is_empty() {
out.push_str(" class=\"");
let mut first = true;
for c in classes {
if !first {
out.push(' ');
}
first = false;
escape_for_attr(c, out);
}
out.push('"');
}
for (k, v) in attrs.iter().filter(|(k, _)| k.as_str() != "class") {
out.push(' ');
out.push_str(k);
if !v.is_empty() {
out.push_str("=\"");
escape_for_attr(v, out);
out.push('"');
} else {
}
}
if is_void_tag(tag) && node.first_child.is_none() {
out.push_str("/>");
return;
}
out.push('>');
self.write_children(id, out, serializes_children_raw(tag));
out.push_str("</");
out.push_str(tag);
out.push('>');
}
NodeData::Text { data } => {
if raw_text {
out.push_str(data);
} else {
escape_for_text(data, out);
}
}
NodeData::Comment { data } => {
out.push_str("<!--");
out.push_str(data); out.push_str("-->");
}
NodeData::Fragment => self.write_children(id, out, false),
}
}
}
#[cfg(test)]
mod tests {
use crate::Dom;
#[test]
fn element_without_attrs() {
let mut dom: Dom = Dom::new();
let el = dom.create_element("div");
assert_eq!(dom.outer_markup(el), "<div></div>");
}
#[test]
fn element_with_attrs() {
let mut dom: Dom = Dom::new();
let el = dom.create_element("div");
dom.set_attribute(el, "role", "banner").unwrap();
dom.set_attribute(el, "data-x", "1").unwrap();
assert_eq!(
dom.outer_markup(el),
r#"<div data-x="1" role="banner"></div>"#
);
}
#[test]
fn element_with_classes() {
let mut dom: Dom = Dom::new();
let el = dom.create_element("div");
dom.add_class(el, "two").unwrap();
dom.add_class(el, "one").unwrap();
assert_eq!(dom.outer_markup(el), r#"<div class="one two"></div>"#);
}
#[test]
fn void_tag_self_closes() {
let mut dom: Dom = Dom::new();
let el = dom.create_element("hr");
assert_eq!(dom.outer_markup(el), "<hr/>");
}
#[test]
fn nested_elements() {
let mut dom: Dom = Dom::new();
let div = dom.create_element("div");
let span = dom.create_element("span");
let text = dom.create_text_node("hi");
dom.append_child(span, text).unwrap();
dom.append_child(div, span).unwrap();
assert_eq!(dom.outer_markup(div), "<div><span>hi</span></div>");
}
#[test]
fn text_content_is_escaped() {
let mut dom: Dom = Dom::new();
let div = dom.create_element("div");
let t = dom.create_text_node("a & b <c>");
dom.append_child(div, t).unwrap();
assert_eq!(dom.outer_markup(div), "<div>a & b <c></div>");
}
#[test]
fn raw_text_element_children_are_not_escaped() {
let mut dom: Dom = Dom::new();
let style = dom.create_element("style");
let css = dom.create_text_node("a > b { content: \"<&\"; }");
dom.append_child(style, css).unwrap();
assert_eq!(
dom.outer_markup(style),
"<style>a > b { content: \"<&\"; }</style>"
);
assert_eq!(dom.inner_markup(style), "a > b { content: \"<&\"; }");
let ta = dom.create_element("textarea");
let t = dom.create_text_node("<b>&");
dom.append_child(ta, t).unwrap();
assert_eq!(dom.outer_markup(ta), "<textarea><b>&</textarea>");
}
#[test]
fn attribute_values_are_escaped() {
let mut dom: Dom = Dom::new();
let el = dom.create_element("div");
dom.set_attribute(el, "title", r#"she said "hi""#).unwrap();
assert_eq!(
dom.outer_markup(el),
r#"<div title="she said "hi""></div>"#
);
}
#[test]
fn comment_node_serializes() {
let mut dom: Dom = Dom::new();
let c = dom.create_comment(" note ");
assert_eq!(dom.outer_markup(c), "<!-- note -->");
}
#[test]
fn fragment_is_childrens_concat() {
let mut dom: Dom = Dom::new();
let frag = dom.create_document_fragment();
let a = dom.create_element("a");
let b = dom.create_element("b");
dom.append_child(frag, a).unwrap();
dom.append_child(frag, b).unwrap();
assert_eq!(dom.outer_markup(frag), "<a></a><b></b>");
}
#[test]
fn inner_markup_omits_wrapper() {
let mut dom: Dom = Dom::new();
let div = dom.create_element("div");
let span = dom.create_element("span");
let text = dom.create_text_node("inner");
dom.append_child(span, text).unwrap();
dom.append_child(div, span).unwrap();
assert_eq!(dom.inner_markup(div), "<span>inner</span>");
}
#[test]
fn boolean_attribute_emits_bare_name() {
let mut dom: Dom = Dom::new();
let el = dom.create_element("input");
dom.toggle_attribute(el, "disabled").unwrap(); assert_eq!(dom.outer_markup(el), "<input disabled/>");
}
#[test]
fn class_attribute_goes_through_class_list() {
let mut dom: Dom = Dom::new();
let el = dom.create_element("div");
dom.add_class(el, "a").unwrap();
dom.add_class(el, "b").unwrap();
assert_eq!(dom.outer_markup(el), r#"<div class="a b"></div>"#);
}
#[test]
fn deep_tree() {
let mut dom: Dom = Dom::new();
let html = dom.create_element("html");
let body = dom.create_element("body");
let h1 = dom.create_element("h1");
let t = dom.create_text_node("Welcome");
dom.append_child(h1, t).unwrap();
dom.append_child(body, h1).unwrap();
dom.append_child(html, body).unwrap();
assert_eq!(
dom.outer_markup(html),
"<html><body><h1>Welcome</h1></body></html>"
);
}
}