use alloc::string::String;
use super::{is_space, push_verbatim, skip_newline, walk};
use crate::node::{NodeId, NodeKind};
use crate::tags::{self, Display};
use crate::tree::{Dom, NodeRef};
impl Dom {
pub fn to_text(&self) -> String {
let mut text = Text::default();
for root in self.roots() {
walk(root, |node, enter| text.visit(node, enter));
}
text.finish()
}
}
impl NodeRef<'_> {
pub fn to_text(&self) -> String {
let mut text = Text {
root: Some(self.id()),
..Text::default()
};
walk(*self, |node, enter| text.visit(node, enter));
text.finish()
}
}
#[derive(Default)]
struct Text {
out: String,
breaks: u8,
tabs: u32,
space: bool,
pre: u32,
fresh: bool,
lists: u32,
cell: bool,
root: Option<NodeId>,
}
impl Text {
fn visit(&mut self, node: NodeRef<'_>, enter: bool) -> bool {
let tag = match node.kind() {
NodeKind::Element { tag } => tag,
NodeKind::Text(t) if self.pre > 0 => {
self.verbatim(t);
return false;
}
NodeKind::Text(t) => {
self.collapse(t);
return false;
}
NodeKind::Comment(_) | NodeKind::Doctype(_) => return false,
};
let list = matches!(tag, "ul" | "ol" | "menu" | "dl");
if list && !enter {
self.lists = self.lists.saturating_sub(1);
}
let root = self.root == Some(node.id());
if !root && node.hides_itself() {
return false;
}
match tags::display(tag) {
Display::Hidden if !root => return false,
Display::Hidden => {
self.newlines(1);
if matches!(tag, "script" | "style") {
if enter {
self.pre += 1;
self.fresh = true;
} else {
self.pre = self.pre.saturating_sub(1);
}
}
}
Display::Paragraph if list && self.lists > 0 => self.newlines(1),
Display::Paragraph => self.newlines(2),
Display::Block => self.newlines(1),
Display::Inline => {}
}
if list && enter {
self.lists += 1;
}
match tag {
"br" if enter && self.pre > 0 => {
self.fresh = false;
self.content();
self.out.push('\n');
}
"br" if enter => self.breaks = (self.breaks + 1).min(2),
"pre" | "textarea" if enter => {
self.pre += 1;
self.fresh = true;
}
"pre" | "textarea" => {
self.pre = self.pre.saturating_sub(1);
let end = self.out.trim_end().len();
self.out.truncate(end);
}
"td" | "th" if enter => {
if is_cell(node.prev_element_sibling()) {
self.breaks = 0;
self.tabs += 1;
}
self.cell = true;
}
"td" | "th" => {
if !self.cell {
self.breaks = 0;
}
self.cell = false;
}
_ => {}
}
true
}
fn newlines(&mut self, n: u8) {
if !self.cell {
self.breaks = self.breaks.max(n);
self.tabs = 0;
}
}
fn collapse(&mut self, text: &str) {
for (i, word) in text.split(is_space).enumerate() {
if i > 0 {
self.space = true;
}
if !word.is_empty() {
self.content();
self.out.push_str(word);
}
}
}
fn verbatim(&mut self, text: &str) {
let text = if core::mem::take(&mut self.fresh) {
skip_newline(text)
} else {
text
};
if !text.is_empty() {
self.content();
push_verbatim(&mut self.out, text);
}
}
fn content(&mut self) {
if self.out.is_empty() {
} else if self.breaks > 0 {
let want = usize::from(self.breaks);
let have = self
.out
.bytes()
.rev()
.take(want)
.take_while(|&b| b == b'\n')
.count();
for _ in have..want {
self.out.push('\n');
}
for _ in 0..self.tabs {
self.out.push('\t');
}
} else if self.tabs > 0 {
for _ in 0..self.tabs {
self.out.push('\t');
}
} else if self.space && !self.out.ends_with('\n') {
self.out.push(' ');
}
self.breaks = 0;
self.tabs = 0;
self.space = false;
self.cell = false;
}
fn finish(mut self) -> String {
let end = self.out.trim_end().len();
self.out.truncate(end);
let first = self.out.find(|c: char| !c.is_whitespace()).unwrap_or(0);
let start = self
.out
.get(..first)
.and_then(|lead| lead.rfind('\n'))
.map_or(0, |i| i + 1);
self.out.drain(..start);
self.out
}
}
fn is_cell(node: Option<NodeRef<'_>>) -> bool {
matches!(node.and_then(|n| n.tag_name()), Some("td" | "th"))
}