use crossterm::style::Color;
use super::InlineRun;
use super::generation::visible_children;
use crate::{
graphics::{FontStyle, block_style},
html::css_parse::{TextTransform, WhiteSpace},
tdom::node::{Node, NodeArena, NodeId, NodeType},
};
pub fn collect_runs(
arena: &NodeArena,
node_id: NodeId,
node: &Node,
fg: Option<Color>,
bg: Option<Color>,
style: FontStyle,
) -> Vec<InlineRun> {
let transform = node.css_attr.get_text_transform();
let white_space = node.css_attr.get_white_space(&node.node_type);
let mut runs = Vec::new();
walk_runs(
arena,
node_id,
node,
fg,
bg,
style,
transform,
white_space,
None,
&mut runs,
);
runs
}
pub(crate) fn text_node_run(
arena: &NodeArena,
text_node_id: NodeId,
fg: Option<Color>,
bg: Option<Color>,
style: FontStyle,
transform: TextTransform,
white_space: WhiteSpace,
) -> Option<InlineRun> {
let child = &arena[text_node_id];
if child.node_type != NodeType::Text {
return None;
}
if child.text_is_painted(white_space)
&& let Some(s) = child.data.as_deref()
&& !s.is_empty()
{
let text = apply_text_transform(s, transform);
return Some(InlineRun::owned(text, style, fg, bg, None));
}
None
}
fn walk_runs(
arena: &NodeArena,
node_id: NodeId,
node: &Node,
fg: Option<Color>,
bg: Option<Color>,
style: FontStyle,
transform: TextTransform,
white_space: WhiteSpace,
owner: Option<NodeId>,
runs: &mut Vec<InlineRun>,
) {
for child_id in visible_children(arena, node_id) {
let child = &arena[child_id];
if node.css_attr.display().is_some_and(|d| d.is_flex())
&& !matches!(child.node_type, NodeType::Text)
{
continue;
}
match &child.node_type {
NodeType::Text => {
if let Some(s) = child.data.as_deref()
&& !s.is_empty()
{
let text = apply_text_transform(s, transform);
runs.push(InlineRun::owned(text, style, fg, bg, owner));
}
}
NodeType::Span => {
let span_fg = child.css_attr.get_color().or(fg);
let span_bg = child.css_attr.get_bg_color().or(bg);
let span_style = style | block_style(&child.css_attr);
let span_transform = child.css_attr.get_text_transform();
let span_ws = child.white_space_or(white_space);
walk_runs(
arena,
child_id,
child,
span_fg,
span_bg,
span_style,
span_transform,
span_ws,
owner,
runs,
);
}
NodeType::A => {
if child.css_attr.display().is_some_and(|d| d.is_block_level())
|| node.css_attr.display().is_some_and(|d| d.is_flex())
{
continue;
}
let a_fg = if child.css_attr.is_set("color") {
child.css_attr.get_color().or(fg)
} else {
Some(Color::Rgb {
r: 0x00,
g: 0x00,
b: 0xFF,
})
};
let a_bg = child.css_attr.get_bg_color().or(bg);
let mut a_style = style | block_style(&child.css_attr);
if !child.css_attr.is_set("text-decoration") {
a_style |= FontStyle::UNDERLINED;
}
let a_transform = child.css_attr.get_text_transform();
let a_ws = child.white_space_or(white_space);
walk_runs(
arena,
child_id,
child,
a_fg,
a_bg,
a_style,
a_transform,
a_ws,
Some(child_id),
runs,
);
}
NodeType::Em => {
let em_fg = child.css_attr.get_color().or(fg);
let em_bg = child.css_attr.get_bg_color().or(bg);
let mut em_style = style | block_style(&child.css_attr);
if !child.css_attr.is_set("font-style") {
em_style |= FontStyle::ITALIC;
}
let em_transform = child.css_attr.get_text_transform();
let em_ws = child.white_space_or(white_space);
walk_runs(
arena,
child_id,
child,
em_fg,
em_bg,
em_style,
em_transform,
em_ws,
owner,
runs,
);
}
NodeType::Strong | NodeType::B => {
let mut st = style | block_style(&child.css_attr);
if !child.css_attr.is_set("font-weight") {
st |= FontStyle::BOLD;
}
walk_inline_children(arena, child_id, child, fg, bg, st, white_space, owner, runs);
}
NodeType::I | NodeType::Cite | NodeType::Dfn | NodeType::Var => {
let mut st = style | block_style(&child.css_attr);
if !child.css_attr.is_set("font-style") {
st |= FontStyle::ITALIC;
}
walk_inline_children(arena, child_id, child, fg, bg, st, white_space, owner, runs);
}
NodeType::Label => {
let label_fg = child.css_attr.get_color().or(fg);
let label_bg = child.css_attr.get_bg_color().or(bg);
walk_inline_children(
arena,
child_id,
child,
label_fg,
label_bg,
style | block_style(&child.css_attr),
white_space,
Some(child_id),
runs,
);
}
NodeType::U | NodeType::Ins => {
let mut st = style | block_style(&child.css_attr);
if !child.css_attr.is_set("text-decoration") {
st |= FontStyle::UNDERLINED;
}
walk_inline_children(arena, child_id, child, fg, bg, st, white_space, owner, runs);
}
NodeType::S | NodeType::Del => {
let mut st = style | block_style(&child.css_attr);
if !child.css_attr.is_set("text-decoration") {
st |= FontStyle::CROSSED_OUT;
}
walk_inline_children(arena, child_id, child, fg, bg, st, white_space, owner, runs);
}
NodeType::Mark => {
let child_fg = child.css_attr.get_color().or(fg);
let child_bg = child.css_attr.get_bg_color().or(bg);
let mark_fg = if child.css_attr.is_set("color") {
child_fg
} else {
Some(Color::Black)
};
let mark_bg = if child.css_attr.is_set("background-color") {
child_bg
} else {
Some(Color::Yellow)
};
let st = style | block_style(&child.css_attr);
walk_inline_children(
arena,
child_id,
child,
mark_fg,
mark_bg,
st,
white_space,
owner,
runs,
);
}
NodeType::Q => {
let q_fg = child.css_attr.get_color().or(fg);
let q_bg = child.css_attr.get_bg_color().or(bg);
let q_style = style | block_style(&child.css_attr);
let (open, close) = q_quote_pair(arena, child_id);
runs.push(InlineRun::owned(
open.to_string(),
q_style,
q_fg,
q_bg,
owner,
));
let q_transform = child.css_attr.get_text_transform();
let q_ws = child.white_space_or(white_space);
walk_runs(
arena,
child_id,
child,
q_fg,
q_bg,
q_style,
q_transform,
q_ws,
owner,
runs,
);
runs.push(InlineRun::owned(
close.to_string(),
q_style,
q_fg,
q_bg,
owner,
));
}
NodeType::Abbr => {
let fg = child.css_attr.get_color().or(fg);
let bg = child.css_attr.get_bg_color().or(bg);
let mut st = style | block_style(&child.css_attr);
if child.attributes.get("title").is_some()
&& !child.css_attr.is_set("text-decoration")
{
st |= FontStyle::UNDERLINED;
}
walk_inline_children(arena, child_id, child, fg, bg, st, white_space, owner, runs);
}
NodeType::Code | NodeType::Kbd | NodeType::Samp => {
let fg = child.css_attr.get_color().or(fg);
let bg = child.css_attr.get_bg_color().or(bg);
walk_inline_children(
arena,
child_id,
child,
fg,
bg,
style | block_style(&child.css_attr),
white_space,
owner,
runs,
);
}
NodeType::Data | NodeType::Time | NodeType::Output => {
let fg = child.css_attr.get_color().or(fg);
let bg = child.css_attr.get_bg_color().or(bg);
walk_inline_children(
arena,
child_id,
child,
fg,
bg,
style | block_style(&child.css_attr),
white_space,
owner,
runs,
);
}
NodeType::Br => {
runs.push(InlineRun::forced_break(style, fg, bg));
}
_ => {}
}
}
}
fn walk_inline_children(
arena: &NodeArena,
child_id: NodeId,
child: &Node,
fg: Option<Color>,
bg: Option<Color>,
style: FontStyle,
parent_ws: WhiteSpace,
owner: Option<NodeId>,
runs: &mut Vec<InlineRun>,
) {
let transform = child.css_attr.get_text_transform();
let ws = child.white_space_or(parent_ws);
walk_runs(
arena, child_id, child, fg, bg, style, transform, ws, owner, runs,
);
}
pub(crate) fn q_quote_pair(arena: &NodeArena, node_id: NodeId) -> (&'static str, &'static str) {
let mut depth = 0usize;
let mut current = arena[node_id].parent;
while let Some(pid) = current {
if arena[pid].node_type == NodeType::Q {
depth = depth.saturating_add(1);
}
current = arena[pid].parent;
}
if depth.is_multiple_of(2) {
("\u{201C}", "\u{201D}")
} else {
("\u{2018}", "\u{2019}")
}
}
fn apply_text_transform(text: &str, transform: TextTransform) -> String {
match transform {
TextTransform::None => text.to_string(),
TextTransform::Uppercase => text.to_uppercase(),
TextTransform::Lowercase => text.to_lowercase(),
TextTransform::Capitalize => {
let mut out = String::with_capacity(text.len());
let mut new_word = true;
for ch in text.chars() {
if ch.is_alphanumeric() {
if new_word {
out.extend(ch.to_uppercase());
} else {
out.push(ch);
}
new_word = false;
} else {
out.push(ch);
new_word = true;
}
}
out
}
}
}