use alloc::string::String;
use alloc::vec::Vec;
use crate::node::NodeKind;
use crate::scan;
use crate::tags;
use crate::tree::{Dom, NodeRef};
impl Dom {
pub fn to_html(&self) -> String {
let mut out = String::with_capacity(self.len() * 16);
let mut stack: Vec<Step<'_>> = Vec::new();
let mut root = self.roots().last();
while let Some(r) = root {
stack.push(Step::Open(r));
root = r.prev_sibling();
}
process(&mut out, &mut stack);
out
}
}
impl<'a> NodeRef<'a> {
pub fn to_html(&self) -> String {
let mut out = String::new();
let mut stack: Vec<Step<'a>> = Vec::new();
stack.push(Step::Open(*self));
process(&mut out, &mut stack);
out
}
pub fn inner_html(&self) -> String {
let mut out = String::new();
let mut stack: Vec<Step<'a>> = Vec::new();
let mut child = self.last_child();
while let Some(c) = child {
stack.push(Step::Open(c));
child = c.prev_sibling();
}
process(&mut out, &mut stack);
if let Some(tag) = self.tag_name().filter(|t| tags::ends_only_at_end_tag(t)) {
neutralize_end_tags(&mut out, 0, tag);
}
out
}
pub fn outer_html(&self) -> String {
self.to_html()
}
}
enum Step<'a> {
Open(NodeRef<'a>),
Close(&'a str),
CloseTextOnly(&'a str),
}
fn process(out: &mut String, stack: &mut Vec<Step<'_>>) {
let mut text_only = None;
while let Some(step) = stack.pop() {
let tag = match step {
Step::Open(node) => {
write_open(out, node, stack, &mut text_only);
continue;
}
Step::Close(tag) => tag,
Step::CloseTextOnly(tag) => {
if let Some(start) = text_only.take() {
neutralize_end_tags(out, start, tag);
}
tag
}
};
out.push_str("</");
out.push_str(tag);
out.push('>');
}
}
fn write_open<'a>(
out: &mut String,
node: NodeRef<'a>,
stack: &mut Vec<Step<'a>>,
text_only: &mut Option<usize>,
) {
match node.kind() {
NodeKind::Element { tag } => {
out.push('<');
out.push_str(tag);
for (k, v) in node.attributes() {
out.push(' ');
out.push_str(k);
if !v.is_empty() {
out.push_str("=\"");
escape_attr(out, v);
out.push('"');
}
}
out.push('>');
if tags::is_void(tag) {
return; }
stack.push(if text_only.is_none() && tags::ends_only_at_end_tag(tag) {
*text_only = Some(out.len());
Step::CloseTextOnly(tag)
} else {
Step::Close(tag)
});
let mut child = node.last_child();
while let Some(c) = child {
stack.push(Step::Open(c));
child = c.prev_sibling();
}
}
NodeKind::Text(t) => {
let raw = node
.parent()
.and_then(|p| p.tag_name())
.is_some_and(tags::is_raw_text);
if raw {
out.push_str(t);
} else {
escape_text(out, t);
}
}
NodeKind::Comment(c) => {
out.push_str("<!--");
out.push_str(c);
out.push_str("-->");
}
NodeKind::Doctype(d) => {
out.push_str("<!doctype ");
out.push_str(d);
out.push('>');
}
}
}
fn neutralize_end_tags(out: &mut String, start: usize, tag: &str) {
let Some(content) = out.get(start..) else {
return;
};
let bytes = content.as_bytes();
let mut fixed = String::new();
let mut copied = 0;
let mut at = 0;
while let Some(i) = bytes.get(at..).and_then(|b| scan::find(b"</", b)) {
at += i + 2;
if let (Some(name), Some(b)) = (bytes.get(at..at + tag.len()), bytes.get(at + tag.len()))
&& name.eq_ignore_ascii_case(tag.as_bytes())
&& matches!(b, b'\t' | b'\n' | b'\x0c' | b'\r' | b' ' | b'/' | b'>')
{
fixed.push_str(content.get(copied..at - 1).unwrap_or_default());
fixed.push('\\');
copied = at - 1;
}
}
if copied > 0 {
fixed.push_str(content.get(copied..).unwrap_or_default());
out.truncate(start);
out.push_str(&fixed);
}
}
fn escape_text(out: &mut String, s: &str) {
for c in s.chars() {
match c {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
c => out.push(c),
}
}
}
fn escape_attr(out: &mut String, s: &str) {
for c in s.chars() {
match c {
'&' => out.push_str("&"),
'"' => out.push_str("""),
c => out.push(c),
}
}
}