use std::cell::Ref;
use html5ever::{QualName, local_name};
use crate::TreeNodeOps;
use super::SerializeOp;
use super::{NodeData, NodeRef, child_nodes};
pub(crate) fn format_text(root_node: &NodeRef, include_node: bool) -> String {
let id = root_node.id;
let nodes = root_node.tree.nodes.borrow();
let mut ops = if include_node {
vec![SerializeOp::Open(id)]
} else {
child_nodes(Ref::clone(&nodes), &id, true)
.map(SerializeOp::Open)
.collect()
};
let mut text = String::new();
while let Some(op) = ops.pop() {
match op {
SerializeOp::Open(id) => {
let Some(node) = nodes.get(id.value()) else {
continue;
};
match node.data {
NodeData::Text { ref contents } => {
push_normalized_text(&mut text, contents.as_ref());
}
NodeData::Element(ref e) => {
if !(text.is_empty() || text.ends_with("\n\n"))
&& elem_require_linebreak(&e.name)
{
text.push('\n');
}
ops.push(SerializeOp::Close(&e.name));
if matches!(e.name.local, local_name!("pre")) {
text.push_str(&TreeNodeOps::text_of(Ref::clone(&nodes), id));
continue;
}
ops.extend(
child_nodes(Ref::clone(&nodes), &id, true).map(SerializeOp::Open),
);
}
_ => {}
}
}
SerializeOp::Close(name) => {
adjust_element_offset(&mut text, name);
}
}
}
if !include_node {
text.truncate(text.trim_end().len());
}
text
}
fn push_normalized_text(text: &mut String, new_text: &str) {
let follows_newline = text.ends_with(['\n', ' ']) || text.is_empty();
let push_start_whitespace = !follows_newline && new_text.starts_with(char::is_whitespace);
let push_end_whitespace = new_text.ends_with(char::is_whitespace);
let mut result = String::with_capacity(new_text.len());
let mut iter = new_text.split_whitespace();
if let Some(first) = iter.next() {
if push_start_whitespace {
result.push(' ');
}
result.push_str(first);
for word in iter {
result.push(' ');
result.push_str(word);
}
}
if result.is_empty() && follows_newline {
return;
}
text.push_str(&result);
if push_end_whitespace && !text.ends_with(char::is_whitespace) {
text.push(' ');
}
}
fn trim_trailing_space(s: &mut String) {
s.truncate(s.trim_end_matches(' ').len());
}
fn adjust_element_offset(text: &mut String, name: &QualName) {
if text.is_empty() || text.ends_with("\n\n") {
return;
}
if elem_require_linebreak(name) {
trim_trailing_space(text);
text.push_str("\n\n");
} else if matches!(
name.local,
local_name!("br") | local_name!("hr") | local_name!("li") | local_name!("tr")
) {
trim_trailing_space(text);
text.push('\n');
} else if matches!(name.local, local_name!("td") | local_name!("th"))
&& !text.ends_with(['\n', ' '])
{
text.push(' ');
}
}
const fn elem_require_linebreak(name: &QualName) -> bool {
matches!(
name.local,
local_name!("article")
| local_name!("blockquote")
| local_name!("section")
| local_name!("div")
| local_name!("p")
| local_name!("pre")
| local_name!("h1")
| local_name!("h2")
| local_name!("h3")
| local_name!("h4")
| local_name!("h5")
| local_name!("h6")
| local_name!("ul")
| local_name!("ol")
| local_name!("dl")
| local_name!("table")
)
}
#[cfg(test)]
mod tests {
use crate::Document;
#[test]
fn test_trim_end_multi_byte() {
let doc = Document::fragment("<pre>a \n</pre>");
assert_eq!(&doc.formatted_text(), "a");
let doc = Document::fragment("<pre>a\u{3000}</pre>");
assert_eq!(&doc.formatted_text(), "a");
}
}