use crossterm::style::Color;
use super::InlineRun;
use super::generation::visible_children;
use crate::{
graphics::{FontStyle, block_style},
html::css_parse::{PseudoElement, TextTransform, WhiteSpace, parse_color_str},
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,
spacing: (usize, isize),
) -> 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();
if let Some(run) = pseudo_element_run(arena, node_id, PseudoElement::Before, fg, bg, style) {
runs.push(run);
}
walk_runs(
arena,
node_id,
node,
fg,
bg,
style,
transform,
white_space,
None,
spacing,
&mut runs,
);
if let Some(run) = pseudo_element_run(arena, node_id, PseudoElement::After, fg, bg, style) {
runs.push(run);
}
runs
}
fn pseudo_element_run(
arena: &NodeArena,
node_id: NodeId,
pe: PseudoElement,
fg: Option<Color>,
bg: Option<Color>,
style: FontStyle,
) -> Option<InlineRun> {
let (suffix, content_key) = match pe {
PseudoElement::Before => ("::before", "content::before"),
PseudoElement::After => ("::after", "content::after"),
PseudoElement::SliderThumb | PseudoElement::SliderTrack | PseudoElement::SliderProgress => {
return None;
}
};
let node = &arena[node_id];
let raw = node.css_attr.0.get(content_key)?;
let raw = raw.as_ref();
if raw.eq_ignore_ascii_case("none") || raw.eq_ignore_ascii_case("normal") {
return None;
}
let bytes = raw.trim();
let text = if bytes.len() >= 2
&& ((bytes.starts_with('"') && bytes.ends_with('"'))
|| (bytes.starts_with('\'') && bytes.ends_with('\'')))
{
let inner = &bytes[1..bytes.len() - 1];
Some(inner.replace("\\\"", "\"").replace("\\\\", "\\"))
} else {
None
}?;
if text.is_empty() {
return None;
}
let prefixed = |prop: &str| format!("{prop}{suffix}");
let pfg = node
.css_attr
.0
.get(prefixed("color").as_str())
.and_then(|v| parse_color_str(v))
.or(fg);
let pbg = node
.css_attr
.0
.get(prefixed("background-color").as_str())
.and_then(|v| parse_color_str(v))
.or(bg);
let mut pstyle = style;
if node
.css_attr
.0
.get(prefixed("font-weight").as_str())
.is_some_and(|v| v.trim().eq_ignore_ascii_case("bold"))
{
pstyle |= FontStyle::BOLD;
}
if node
.css_attr
.0
.get(prefixed("font-style").as_str())
.is_some_and(|v| v.trim().eq_ignore_ascii_case("italic"))
{
pstyle |= FontStyle::ITALIC;
}
if node
.css_attr
.0
.get(prefixed("text-decoration").as_str())
.is_some_and(|v| v.contains("underline"))
{
pstyle |= FontStyle::UNDERLINED;
}
let spacing = (0usize, 0isize);
Some(InlineRun::owned_spaced(
text, pstyle, pfg, pbg, None, spacing.0, spacing.1,
))
}
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>,
spacing: (usize, isize),
runs: &mut Vec<InlineRun>,
) {
for &child_id in visible_children(arena, node_id).iter() {
let child = &arena[child_id];
if node.css_attr.display().is_some_and(|d| d.is_flex())
&& !matches!(child.node_type, NodeType::Text)
{
continue;
}
if child.css_attr.display().is_some_and(|d| d.is_grid()) {
continue;
}
if child.css_attr.display().is_some_and(|d| d.is_block_level()) {
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_spaced(
text, style, fg, bg, owner, spacing.0, spacing.1,
));
}
}
NodeType::Br => {
runs.push(InlineRun::forced_break(style, fg, bg));
}
NodeType::Q => {
let (q_fg, q_bg, q_style) = merge_inline_ua(child, fg, bg, style);
let (open, close) = q_quote_pair(arena, child_id);
runs.push(InlineRun::owned_spaced(
open.to_string(),
q_style,
q_fg,
q_bg,
owner,
spacing.0,
spacing.1,
));
walk_inline_children(
arena,
child_id,
child,
q_fg,
q_bg,
q_style,
white_space,
owner,
spacing,
runs,
);
runs.push(InlineRun::owned_spaced(
close.to_string(),
q_style,
q_fg,
q_bg,
owner,
spacing.0,
spacing.1,
));
}
NodeType::Abbr => {
let (a_fg, a_bg, mut a_style) = merge_inline_ua(child, fg, bg, style);
if child.attributes.get("title").is_some()
&& !child.css_attr.is_set("text-decoration")
{
a_style |= FontStyle::UNDERLINED;
}
walk_inline_children(
arena,
child_id,
child,
a_fg,
a_bg,
a_style,
white_space,
owner,
spacing,
runs,
);
}
NodeType::Span
| NodeType::A
| NodeType::Em
| NodeType::Strong
| NodeType::B
| NodeType::I
| NodeType::Cite
| NodeType::Dfn
| NodeType::Var
| NodeType::Label
| NodeType::U
| NodeType::Ins
| NodeType::S
| NodeType::Del
| NodeType::Mark
| NodeType::Code
| NodeType::Kbd
| NodeType::Samp
| NodeType::Data
| NodeType::Time
| NodeType::Output => {
let (child_fg, child_bg, child_style) = merge_inline_ua(child, fg, bg, style);
let owner = if matches!(child.node_type, NodeType::A | NodeType::Label) {
Some(child_id)
} else {
owner
};
walk_inline_children(
arena,
child_id,
child,
child_fg,
child_bg,
child_style,
white_space,
owner,
spacing,
runs,
);
}
_ => {}
}
}
}
fn merge_inline_ua(
child: &Node,
parent_fg: Option<Color>,
parent_bg: Option<Color>,
parent_style: FontStyle,
) -> (Option<Color>, Option<Color>, FontStyle) {
let css = &child.css_attr;
let mut fg = css.get_color();
let mut bg = css.get_bg_color();
let mut style = parent_style | block_style(css);
for &(prop, value) in child.node_type.inline_ua_declarations() {
match prop {
"color" if fg.is_none() => fg = parse_color_str(value),
"background-color" if bg.is_none() => bg = parse_color_str(value),
"font-style" if !css.is_set("font-style") => style |= FontStyle::ITALIC,
"font-weight" if !css.is_set("font-weight") => style |= FontStyle::BOLD,
"text-decoration" if !css.is_set("text-decoration") => {
style |= match value {
"underline" => FontStyle::UNDERLINED,
"line-through" => FontStyle::CROSSED_OUT,
_ => FontStyle::empty(),
};
}
_ => {}
}
}
(fg.or(parent_fg), bg.or(parent_bg), style)
}
pub(crate) fn inline_tree_has_forced_break(arena: &NodeArena, node_id: NodeId) -> bool {
let node = &arena[node_id];
if node.css_attr.display().is_some_and(|d| d.is_flex()) {
return false;
}
for &child_id in visible_children(arena, node_id).iter() {
let child = &arena[child_id];
if child.css_attr.display().is_some_and(|d| d.is_block_level()) {
continue;
}
if child.node_type == NodeType::Br {
return true;
}
if matches!(
child.node_type,
NodeType::Span
| NodeType::A
| NodeType::Em
| NodeType::Strong
| NodeType::B
| NodeType::I
| NodeType::Cite
| NodeType::Dfn
| NodeType::Var
| NodeType::Label
| NodeType::U
| NodeType::Ins
| NodeType::S
| NodeType::Del
| NodeType::Mark
| NodeType::Code
| NodeType::Kbd
| NodeType::Samp
| NodeType::Data
| NodeType::Time
| NodeType::Output
| NodeType::Q
| NodeType::Abbr
) && inline_tree_has_forced_break(arena, child_id)
{
return true;
}
}
false
}
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>,
spacing: (usize, isize),
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, spacing, 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) -> std::borrow::Cow<'_, str> {
match transform {
TextTransform::None => std::borrow::Cow::Borrowed(text),
TextTransform::Uppercase => std::borrow::Cow::Owned(text.to_uppercase()),
TextTransform::Lowercase => std::borrow::Cow::Owned(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;
}
}
std::borrow::Cow::Owned(out)
}
}
}