use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use crate::report::{Rect, ViewportKey};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TextBox {
pub dom_order: u64,
pub bounds: Rect,
pub start: u32,
pub length: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SnapshotNode {
pub dom_order: u64,
pub selector: String,
pub tag: String,
pub attrs: IndexMap<String, String>,
pub computed_styles: IndexMap<String, String>,
pub rect: Option<Rect>,
pub parent: Option<u64>,
pub children: Vec<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PlumbSnapshot {
pub url: String,
pub viewport: ViewportKey,
pub viewport_width: u32,
pub viewport_height: u32,
pub nodes: Vec<SnapshotNode>,
pub text_boxes: Vec<TextBox>,
}
impl PlumbSnapshot {
#[cfg(any(test, feature = "test-fake"))]
#[must_use]
pub fn canned() -> Self {
let mut html_attrs = IndexMap::new();
html_attrs.insert("lang".into(), "en".into());
let mut body_styles = IndexMap::new();
body_styles.insert("margin-top".into(), "0".into());
body_styles.insert("margin-right".into(), "0".into());
body_styles.insert("margin-bottom".into(), "0".into());
body_styles.insert("margin-left".into(), "0".into());
body_styles.insert("padding-top".into(), "13px".into());
body_styles.insert("padding-right".into(), "0".into());
body_styles.insert("padding-bottom".into(), "0".into());
body_styles.insert("padding-left".into(), "0".into());
Self {
url: "plumb-fake://hello".into(),
viewport: ViewportKey::new("desktop"),
viewport_width: 1280,
viewport_height: 800,
text_boxes: Vec::new(),
nodes: vec![
SnapshotNode {
dom_order: 0,
selector: "html".into(),
tag: "html".into(),
attrs: html_attrs,
computed_styles: IndexMap::new(),
rect: Some(Rect {
x: 0,
y: 0,
width: 1280,
height: 800,
}),
parent: None,
children: vec![1, 2],
},
SnapshotNode {
dom_order: 1,
selector: "html > head".into(),
tag: "head".into(),
attrs: IndexMap::new(),
computed_styles: IndexMap::new(),
rect: None,
parent: Some(0),
children: vec![],
},
SnapshotNode {
dom_order: 2,
selector: "html > body".into(),
tag: "body".into(),
attrs: IndexMap::new(),
computed_styles: body_styles,
rect: Some(Rect {
x: 0,
y: 0,
width: 1280,
height: 800,
}),
parent: Some(0),
children: vec![],
},
],
}
}
}
#[derive(Debug)]
pub struct SnapshotCtx<'a> {
snapshot: &'a PlumbSnapshot,
viewports: Vec<ViewportKey>,
rects_by_dom_order: IndexMap<u64, Rect>,
text_box_ranges: IndexMap<u64, (usize, usize)>,
}
impl<'a> SnapshotCtx<'a> {
#[must_use]
pub fn new(snapshot: &'a PlumbSnapshot) -> Self {
Self::with_viewports(snapshot, [snapshot.viewport.clone()])
}
#[must_use]
pub fn with_viewports(
snapshot: &'a PlumbSnapshot,
viewports: impl IntoIterator<Item = ViewportKey>,
) -> Self {
Self {
snapshot,
viewports: viewports.into_iter().collect(),
rects_by_dom_order: rect_index(snapshot),
text_box_ranges: text_box_index(snapshot),
}
}
#[must_use]
pub fn snapshot(&self) -> &'a PlumbSnapshot {
self.snapshot
}
#[must_use]
pub fn viewports(&self) -> &[ViewportKey] {
&self.viewports
}
#[must_use]
pub fn rect_for(&self, dom_order: u64) -> Option<Rect> {
self.rects_by_dom_order.get(&dom_order).copied()
}
#[must_use]
pub fn text_boxes_for(&self, dom_order: u64) -> &[TextBox] {
match self.text_box_ranges.get(&dom_order) {
Some(&(start, count)) => &self.snapshot.text_boxes[start..start + count],
None => &[],
}
}
pub fn nodes(&self) -> impl Iterator<Item = &SnapshotNode> {
self.snapshot.nodes.iter()
}
}
fn rect_index(snapshot: &PlumbSnapshot) -> IndexMap<u64, Rect> {
snapshot
.nodes
.iter()
.filter_map(|node| node.rect.map(|rect| (node.dom_order, rect)))
.collect()
}
fn text_box_index(snapshot: &PlumbSnapshot) -> IndexMap<u64, (usize, usize)> {
debug_assert!(
snapshot
.text_boxes
.windows(2)
.all(|w| { (w[0].dom_order, w[0].start) <= (w[1].dom_order, w[1].start) }),
"text_boxes must be sorted by (dom_order, start)"
);
let mut index: IndexMap<u64, (usize, usize)> = IndexMap::new();
let boxes = &snapshot.text_boxes;
let mut i = 0;
while i < boxes.len() {
let dom_order = boxes[i].dom_order;
let start = i;
while i < boxes.len() && boxes[i].dom_order == dom_order {
i += 1;
}
index.insert(dom_order, (start, i - start));
}
index
}