use std::{fmt, sync::Arc};
use indexmap::IndexMap;
use slotmap::{Key, SlotMap, new_key_type};
use crate::html::css_parse::{CssAttr, FontContext, LengthContext, Viewport, WhiteSpace};
mod node_type;
pub use node_type::NodeType;
new_key_type! {
pub struct NodeId;
}
pub type NodeArena = SlotMap<NodeId, Node>;
pub type SharedNodeArena = Arc<NodeArena>;
pub fn document_base_href(arena: &NodeArena, from: NodeId) -> Option<String> {
let mut root = from;
while let Some(parent) = arena[root].parent {
root = parent;
}
let mut stack = vec![root];
while let Some(id) = stack.pop() {
let node = &arena[id];
if node.node_type == NodeType::Base
&& let Some(href) = node.attributes.get("href")
&& !href.trim().is_empty()
{
return Some(href.clone());
}
stack.extend(node.children.iter().copied());
}
None
}
pub fn effective_font_size_cells(
arena: &NodeArena,
node_id: NodeId,
font_ctx: FontContext,
viewport: Viewport,
) -> usize {
let node = &arena[node_id];
let declared = node.css_attr.get_font_size();
if let Some(len) = declared {
return len.resolve(LengthContext {
containing: font_ctx.font_size_cells,
font: font_ctx,
viewport,
});
}
if let Some(em) = node.node_type.default_font_size_em() {
let raw = em * font_ctx.font_size_cells as f32;
return raw.round().max(1.0) as usize;
}
font_ctx.font_size_cells.max(1)
}
#[derive(Debug, Clone)]
pub struct Node {
pub node_type: NodeType,
pub data: Option<String>,
pub children: Vec<NodeId>,
pub parent: Option<NodeId>,
pub attributes: IndexMap<String, String>,
pub css_attr: CssAttr,
}
pub(crate) fn is_collapsible_whitespace(c: char) -> bool {
matches!(c, ' ' | '\t' | '\n' | '\r' | '\u{000C}')
}
impl Node {
pub fn from(node_type: NodeType) -> Node {
Node {
node_type,
data: None,
children: vec![],
parent: None,
attributes: IndexMap::new(),
css_attr: CssAttr::new(),
}
}
pub fn text(data: &str) -> Node {
Node {
node_type: NodeType::Text,
data: Some(data.to_string()),
children: vec![],
parent: None,
attributes: IndexMap::new(),
css_attr: CssAttr::new(),
}
}
pub fn comment(data: &str) -> Node {
Node {
node_type: NodeType::Comment,
data: Some(data.to_string()),
children: vec![],
parent: None,
attributes: IndexMap::new(),
css_attr: CssAttr::new(),
}
}
pub fn fragment() -> Node {
Node {
node_type: NodeType::DocumentFragment,
data: None,
children: vec![],
parent: None,
attributes: IndexMap::new(),
css_attr: CssAttr::new(),
}
}
pub fn doctype(name: &str) -> Node {
Node {
node_type: NodeType::DocumentType,
data: Some(name.to_string()),
children: vec![],
parent: None,
attributes: IndexMap::new(),
css_attr: CssAttr::new(),
}
}
pub fn node_type_constant(&self) -> u8 {
match self.node_type {
NodeType::Document => 9,
NodeType::DocumentType => 10,
NodeType::DocumentFragment => 11,
NodeType::Text => 3,
NodeType::Comment => 8,
_ => 1,
}
}
pub fn node_name(&self) -> String {
match self.node_type {
NodeType::Document => "#document".to_string(),
NodeType::DocumentType => self.data.clone().unwrap_or_else(|| "html".to_string()),
NodeType::DocumentFragment => "#document-fragment".to_string(),
NodeType::Text => "#text".to_string(),
NodeType::Comment => "#comment".to_string(),
_ => self.node_type.tag_name().unwrap_or_default().to_uppercase(),
}
}
pub fn text_is_painted(&self, white_space: WhiteSpace) -> bool {
let Some(s) = self.data.as_deref() else {
return true;
};
if white_space.preserves_whitespace() {
return true;
}
if matches!(white_space, WhiteSpace::PreLine) && s.contains('\n') {
return true;
}
!s.chars().all(is_collapsible_whitespace)
}
pub fn white_space_or(&self, parent: WhiteSpace) -> WhiteSpace {
if self.css_attr.is_set("white-space") {
self.css_attr.get_white_space(&self.node_type)
} else {
parent
}
}
pub fn input_button_label(&self) -> Option<String> {
let input_type = self.get_type()?.to_ascii_lowercase();
if !matches!(input_type.as_str(), "button" | "submit" | "reset" | "image") {
return None;
}
let value = self.attributes.get("value").cloned().unwrap_or_default();
match input_type.as_str() {
"submit" if value.is_empty() => Some("Submit".to_string()),
"reset" if value.is_empty() => Some("Reset".to_string()),
"image" if value.is_empty() => {
let alt = self.attributes.get("alt").cloned().unwrap_or_default();
if alt.is_empty() {
Some("Submit Query".to_string())
} else {
Some(alt)
}
}
_ => Some(value),
}
}
pub fn append_child(arena: &mut NodeArena, parent_id: NodeId, child_id: NodeId) {
let child_parent = &mut arena[child_id].parent;
assert!(child_parent.is_none(), "Child already has a parent");
*child_parent = Some(parent_id);
arena[parent_id].children.push(child_id);
}
pub fn child_count(&self) -> usize {
self.children.len()
}
pub fn child_ids(&self) -> impl Iterator<Item = NodeId> + '_ {
self.children.iter().copied()
}
pub fn text_content(&self, arena: &NodeArena) -> String {
let mut out = String::new();
let mut stack: Vec<NodeId> = self.children.iter().rev().copied().collect();
while let Some(id) = stack.pop() {
let node = &arena[id];
if node.node_type == NodeType::Text
&& let Some(s) = &node.data
{
out.push_str(s);
}
stack.extend(node.children.iter().rev().copied());
}
out
}
pub fn descendants<'a>(arena: &'a NodeArena, start_id: NodeId) -> Descendants<'a> {
Descendants {
arena,
stack: vec![start_id],
}
}
}
pub struct Descendants<'a> {
arena: &'a NodeArena,
stack: Vec<NodeId>,
}
impl<'a> Iterator for Descendants<'a> {
type Item = (NodeId, &'a Node);
fn next(&mut self) -> Option<Self::Item> {
let id = self.stack.pop()?;
let node = &self.arena[id];
for &child_id in node.children.iter().rev() {
self.stack.push(child_id);
}
Some((id, node))
}
}
impl fmt::Display for NodeId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "n#{}", self.data().as_ffi() as u32)
}
}