bt-dom 2.0.0

DOM primitives for browser_tester
Documentation
use std::collections::BTreeMap;

pub const HTML_NAMESPACE_URI: &str = "http://www.w3.org/1999/xhtml";
pub const SVG_NAMESPACE_URI: &str = "http://www.w3.org/2000/svg";
pub const MATHML_NAMESPACE_URI: &str = "http://www.w3.org/1998/Math/MathML";

#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct NodeId {
    index: u32,
    generation: u32,
}

impl NodeId {
    pub const fn new(index: u32, generation: u32) -> Self {
        Self { index, generation }
    }

    pub const fn index(self) -> u32 {
        self.index
    }

    pub const fn generation(self) -> u32 {
        self.generation
    }
}

#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct DocumentState {
    pub title: String,
}

#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct ElementData {
    pub tag_name: String,
    pub local_name: String,
    pub namespace_uri: String,
    pub attributes: BTreeMap<String, String>,
}

#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct TextData {
    pub value: String,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum NodeKind {
    Document,
    Element(ElementData),
    Text(TextData),
    Comment(String),
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct NodeRecord {
    pub id: NodeId,
    pub parent: Option<NodeId>,
    pub children: Vec<NodeId>,
    pub kind: NodeKind,
}

#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct FormControlState {
    pub value: String,
    pub checked: bool,
    pub indeterminate: bool,
}

#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct SelectionState {
    pub start: usize,
    pub end: usize,
}

#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct FileInputState {
    pub files: Vec<String>,
}

#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct DialogState {
    pub open: bool,
}

#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct LayoutStubState {
    pub width: u32,
    pub height: u32,
}

#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct DomIndexes {
    pub id_index: BTreeMap<String, NodeId>,
    pub name_index: BTreeMap<String, Vec<NodeId>>,
    pub tag_index: BTreeMap<String, Vec<NodeId>>,
    pub class_index: BTreeMap<String, Vec<NodeId>>,
}

#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct DomSideTables {
    pub form_controls: BTreeMap<NodeId, FormControlState>,
    pub selection: BTreeMap<NodeId, SelectionState>,
    pub file_inputs: BTreeMap<NodeId, FileInputState>,
    pub dialogs: BTreeMap<NodeId, DialogState>,
    pub layout_stub: BTreeMap<NodeId, LayoutStubState>,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DomStore {
    nodes: Vec<NodeRecord>,
    document: DocumentState,
    document_id: NodeId,
    indexes: DomIndexes,
    side_tables: DomSideTables,
    focused_node: Option<NodeId>,
    target_fragment: Option<String>,
    source_html: Option<String>,
}

impl Default for DomStore {
    fn default() -> Self {
        Self::new_empty()
    }
}

impl DomStore {
    pub fn new_empty() -> Self {
        let document_id = NodeId::new(0, 0);
        let nodes = vec![NodeRecord {
            id: document_id,
            parent: None,
            children: Vec::new(),
            kind: NodeKind::Document,
        }];
        Self {
            nodes,
            document: DocumentState::default(),
            document_id,
            indexes: DomIndexes::default(),
            side_tables: DomSideTables::default(),
            focused_node: None,
            target_fragment: None,
            source_html: None,
        }
    }

    pub fn source_html(&self) -> Option<&str> {
        self.source_html.as_deref()
    }

    pub fn document_id(&self) -> NodeId {
        self.document_id
    }

    pub fn node_count(&self) -> usize {
        self.nodes.len()
    }

    pub fn nodes(&self) -> &[NodeRecord] {
        &self.nodes
    }

    pub fn indexes(&self) -> &DomIndexes {
        &self.indexes
    }

    pub fn side_tables(&self) -> &DomSideTables {
        &self.side_tables
    }

    pub fn focused_node(&self) -> Option<NodeId> {
        self.focused_node
    }

    pub fn set_focused_node(&mut self, focused_node: Option<NodeId>) {
        self.focused_node = focused_node;
    }

    pub fn target_fragment(&self) -> Option<&str> {
        self.target_fragment.as_deref()
    }

    pub fn set_target_fragment(&mut self, target_fragment: Option<String>) {
        self.target_fragment = target_fragment;
    }

    pub fn document_state(&self) -> &DocumentState {
        &self.document
    }
}

mod html_dom;

#[cfg(test)]
mod tests {
    use super::{DomStore, NodeId, NodeKind};

    #[test]
    fn empty_store_has_document_root() {
        let store = DomStore::new_empty();
        assert_eq!(store.node_count(), 1);
        assert_eq!(store.nodes()[0].kind, NodeKind::Document);
        assert_eq!(store.source_html(), None);
    }

    #[test]
    fn bootstrap_html_is_recorded_for_phase_zero() {
        let mut store = DomStore::new_empty();
        store
            .bootstrap_html("<p>Hello</p>")
            .expect("HTML should parse");
        assert_eq!(store.source_html(), Some("<p>Hello</p>"));
        assert_eq!(store.node_count(), 3);
        assert_eq!(store.select("#missing").unwrap(), Vec::<NodeId>::new());
        assert_eq!(store.dump_dom(), "#document\n  <p>\n    \"Hello\"\n  </p>");
    }
}