use rdom_core::{Dom, NodeId, NodeType, Range};
use crate::ext::TuiExt;
use crate::layout::{Display, MarginValue, UserSelect, WhiteSpace};
use crate::runtime::selection::user_select;
pub fn serialize_selection(dom: &Dom<TuiExt>, range: &Range) -> String {
let mut walk = Walk {
dom,
range,
state: WalkState::Before,
out: Rendered::default(),
};
let root = dom.root();
let used = user_select::used_value(dom, root);
walk.visit(root, used);
walk.out.finish()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum WalkState {
Before,
Inside,
Done,
}
struct Walk<'a> {
dom: &'a Dom<TuiExt>,
range: &'a Range,
state: WalkState,
out: Rendered,
}
impl Walk<'_> {
fn visit(&mut self, id: NodeId, used: UserSelect) {
if self.out.closed {
return;
}
let dom = self.dom;
let node = dom.node(id);
match node.node_type() {
NodeType::Text => self.text(id, used),
NodeType::Element => {
let computed = node.ext().and_then(|e| e.computed.as_ref());
if computed.is_some_and(|c| c.display == Display::None) {
self.skip_subtree(id);
return;
}
match node.tag_name() {
Some("br") => {
let selected = self.state == WalkState::Inside;
self.out.preserved('\n', selected);
return;
}
Some("td" | "th") => {
self.out.edge();
self.children(id, used);
self.out.edge();
if self.next_cell(id) {
let selected = self.state == WalkState::Inside;
self.out.preserved('\t', selected);
}
return;
}
_ => {}
}
let breaks = match computed {
Some(c) if c.display == Display::Block => {
if node.tag_name() == Some("p") && has_vertical_margin(c) {
2
} else {
1
}
}
_ => 0,
};
if breaks > 0 {
self.out.line_breaks(breaks);
}
self.children(id, used);
if breaks > 0 {
self.out.line_breaks(breaks);
}
}
NodeType::Comment => {}
_ => self.children(id, used),
}
}
fn children(&mut self, id: NodeId, used: UserSelect) {
let dom = self.dom;
let mut child = dom.node(id).first_child().map(|c| c.id());
while let Some(c) = child {
let child_used = if dom.node(c).node_type() == NodeType::Element {
user_select::resolve_child(dom, c, used)
} else {
used
};
self.visit(c, child_used);
if self.out.closed {
return;
}
child = dom.node(c).next_sibling().map(|n| n.id());
}
}
fn text(&mut self, id: NodeId, used: UserSelect) {
let dom = self.dom;
let Some(data) = dom.node(id).node_value() else {
return;
};
let (from, to, ends_here) = self.selected_bytes(id, data.len());
let copyable = used != UserSelect::None;
let collapsible = dom
.node(id)
.parent_element()
.and_then(|p| p.ext().and_then(|e| e.computed.as_ref()))
.is_none_or(|c| matches!(c.white_space, WhiteSpace::Normal | WhiteSpace::NoWrap));
for (i, ch) in data.char_indices() {
if ends_here && i >= to {
self.out.selection_ended();
}
let selected = copyable && i >= from && i < to;
if collapsible {
self.out.collapsible(ch, selected);
} else {
self.out.preserved(ch, selected);
}
if self.out.closed {
return;
}
}
if ends_here {
self.out.selection_ended();
}
}
fn selected_bytes(&mut self, id: NodeId, len: usize) -> (usize, usize, bool) {
let is_start = self.range.start.node == id;
let is_end = self.range.end.node == id;
let start = self.range.start.offset.min(len);
let end = self.range.end.offset.min(len);
match (is_start, is_end, self.state) {
(true, true, _) => {
self.state = WalkState::Done;
(start, end.max(start), true)
}
(true, false, _) => {
self.state = WalkState::Inside;
(start, len, false)
}
(false, true, WalkState::Inside) => {
self.state = WalkState::Done;
(0, end, true)
}
(false, false, WalkState::Inside) => (0, len, false),
_ => (0, 0, false),
}
}
fn skip_subtree(&mut self, id: NodeId) {
let dom = self.dom;
let mut stack = vec![id];
while let Some(n) = stack.pop() {
let node = dom.node(n);
if node.node_type() == NodeType::Text {
let len = node.node_value().map_or(0, str::len);
if self.selected_bytes(n, len).2 {
self.out.selection_ended();
}
}
let kids: Vec<NodeId> = node.child_nodes().map(|c| c.id()).collect();
stack.extend(kids.into_iter().rev());
}
}
fn next_cell(&self, cell: NodeId) -> bool {
let mut sib = self.dom.node(cell).next_sibling().map(|n| n.id());
while let Some(s) = sib {
let node = self.dom.node(s);
if matches!(node.tag_name(), Some("td" | "th"))
&& !node
.ext()
.and_then(|e| e.computed.as_ref())
.is_some_and(|c| c.display == Display::None)
{
return true;
}
sib = node.next_sibling().map(|n| n.id());
}
false
}
}
fn has_vertical_margin(c: &crate::style::ComputedStyle) -> bool {
let nonzero = |m: &MarginValue| !matches!(m, MarginValue::Cells(0) | MarginValue::Auto);
nonzero(&c.margin.top) || nonzero(&c.margin.bottom)
}
#[derive(Default)]
struct Rendered {
out: String,
pending_space: Option<bool>,
mid_line: bool,
pending_breaks: u8,
emitted: bool,
ended: bool,
closed: bool,
}
impl Rendered {
fn collapsible(&mut self, ch: char, selected: bool) {
if matches!(ch, ' ' | '\t' | '\n' | '\r' | '\x0c') {
if !self.mid_line {
return;
}
self.pending_space = Some(self.pending_space.unwrap_or(false) || selected);
} else {
self.render(ch, selected);
}
}
fn preserved(&mut self, ch: char, selected: bool) {
if ch == '\n' {
self.pending_space = None;
self.emit(ch, selected);
self.mid_line = false;
} else {
self.render(ch, selected);
}
}
fn edge(&mut self) {
self.pending_space = None;
self.mid_line = false;
if self.ended {
self.closed = true;
}
}
fn line_breaks(&mut self, n: u8) {
self.edge();
self.pending_breaks = self.pending_breaks.max(n);
}
fn selection_ended(&mut self) {
self.ended = true;
}
fn render(&mut self, ch: char, selected: bool) {
if let Some(space_selected) = self.pending_space.take() {
self.emit(' ', space_selected);
}
self.emit(ch, selected);
self.mid_line = true;
}
fn emit(&mut self, ch: char, selected: bool) {
if self.ended && !selected {
self.closed = true;
return;
}
if selected {
if self.emitted {
for _ in 0..self.pending_breaks {
self.out.push('\n');
}
}
self.out.push(ch);
self.emitted = true;
}
self.pending_breaks = 0;
}
fn finish(self) -> String {
self.out
}
}