///|
/// CDP Session
///
/// A CDP session combines all domains and provides
/// the unified interface for protocol operations.
///|
/// CDP Session - combines all domains
pub struct CdpSession {
/// Shared DOM tree
tree : @dom.DomTree
/// DOM domain
dom : DomDomain
/// Input domain
input : InputDomain
/// Page domain
page : PageDomain
/// Session ID
session_id : String
/// Target ID (for DevTools protocol)
target_id : String
}
///|
/// Create new CDP session
pub fn CdpSession::new(session_id : String) -> CdpSession {
let tree = @dom.DomTree::new()
{
tree,
dom: DomDomain::new(tree),
input: InputDomain::new(tree),
page: PageDomain::new(tree),
session_id,
target_id: "page-" + session_id,
}
}
///|
/// Get DOM domain
pub fn CdpSession::get_dom(self : CdpSession) -> DomDomain {
self.dom
}
///|
/// Get Input domain
pub fn CdpSession::get_input(self : CdpSession) -> InputDomain {
self.input
}
///|
/// Get Page domain
pub fn CdpSession::get_page(self : CdpSession) -> PageDomain {
self.page
}
///|
/// Get underlying DOM tree for direct manipulation
pub fn CdpSession::get_tree(self : CdpSession) -> @dom.DomTree {
self.tree
}
///|
/// Get session ID
pub fn CdpSession::get_session_id(self : CdpSession) -> String {
self.session_id
}
///|
/// Get target ID
pub fn CdpSession::get_target_id(self : CdpSession) -> String {
self.target_id
}
// =============================================================================
// High-level convenience methods
// =============================================================================
///|
/// Navigate to URL (combines Page.navigate with state update)
pub fn CdpSession::navigate_to(
self : CdpSession,
url : String,
) -> Result[CdpNavigateResult, CdpError] {
self.page.navigate(url)
}
///|
/// Get current URL
pub fn CdpSession::get_url(self : CdpSession) -> String {
self.page.get_url()
}
///|
/// Get page title
pub fn CdpSession::get_title(self : CdpSession) -> String {
self.page.get_title()
}
///|
/// Set page title
pub fn CdpSession::set_title(self : CdpSession, title : String) -> Unit {
self.page.set_title(title)
}
///|
/// Find element by selector
pub fn CdpSession::query_selector(
self : CdpSession,
selector : String,
) -> Result[Int?, CdpError] {
let doc = self.tree.get_document()
self.dom.query_selector(doc.to_int(), selector)
}
///|
/// Find all elements by selector
pub fn CdpSession::query_selector_all(
self : CdpSession,
selector : String,
) -> Result[Array[Int], CdpError] {
let doc = self.tree.get_document()
self.dom.query_selector_all(doc.to_int(), selector)
}
///|
/// Click element by node ID
pub fn CdpSession::click_element(
self : CdpSession,
node_id : Int,
) -> Result[Unit, CdpError] {
// Get element's box model
match self.dom.get_box_model(node_id) {
Ok(box_model) => {
// Calculate center of element
let center_x = (box_model.content[0] + box_model.content[2]) / 2.0
let center_y = (box_model.content[1] + box_model.content[5]) / 2.0
// Dispatch mouse events
let _ = self.input.dispatch_mouse_event(
"mousePressed", center_x, center_y, "left", 1,
)
self.input.dispatch_mouse_event(
"mouseReleased", center_x, center_y, "left", 1,
)
}
Err(e) => Err(e)
}
}
///|
/// Type text into focused element
pub fn CdpSession::type_text(
self : CdpSession,
text : String,
) -> Result[Unit, CdpError] {
self.input.insert_text(text)
}
///|
/// Load HTML content into the DOM tree
pub fn CdpSession::load_html(self : CdpSession, html : String) -> Unit {
let doc = self.tree.get_document()
// Clear existing children
match self.tree.get_children(doc) {
Ok(children) =>
for child in children {
let _ = self.tree.remove_child(doc, child)
}
Err(_) => ()
}
// Create html > body structure
let html_elem = self.tree.create_element("html")
let body = self.tree.create_element("body")
let _ = self.tree.append_child(doc, html_elem)
let _ = self.tree.append_child(html_elem, body)
// Parse content into body
parse_html_into(self.tree, body, html)
}
///|
/// Parse HTML content into a parent node
fn parse_html_into(
tree : @dom.DomTree,
parent : @dom.NodeId,
html : String,
) -> Unit {
let chars = html.to_array()
let len = chars.length()
let i = Ref(0)
while i.val < len {
if chars[i.val] == '<' {
// Check for closing tag
if i.val + 1 < len && chars[i.val + 1] == '/' {
// Skip closing tag
while i.val < len && chars[i.val] != '>' {
i.val += 1
}
i.val += 1
continue
}
// Parse opening tag
i.val += 1
let tag_start = i.val
while i.val < len && chars[i.val] != ' ' && chars[i.val] != '>' {
i.val += 1
}
let tag_name = make_substring(chars, tag_start, i.val)
let elem = tree.create_element(tag_name)
let _ = tree.append_child(parent, elem)
// Parse attributes
while i.val < len && chars[i.val] != '>' {
// Skip whitespace
while i.val < len && chars[i.val] == ' ' {
i.val += 1
}
if i.val < len && chars[i.val] != '>' {
// Parse attribute name
let attr_start = i.val
while i.val < len &&
chars[i.val] != '=' &&
chars[i.val] != ' ' &&
chars[i.val] != '>' {
i.val += 1
}
let attr_name = make_substring(chars, attr_start, i.val)
if i.val < len && chars[i.val] == '=' {
i.val += 1
// Skip opening quote if present.
let quote = if i.val < len &&
(chars[i.val] == '"' || chars[i.val] == '\'') {
let q = chars[i.val]
i.val += 1
Some(q)
} else {
None
}
let val_start = i.val
while i.val < len {
match quote {
Some(q) => if chars[i.val] == q { break }
None => if chars[i.val] == ' ' || chars[i.val] == '>' { break }
}
i.val += 1
}
let attr_val = make_substring(chars, val_start, i.val)
match quote {
Some(q) => if i.val < len && chars[i.val] == q { i.val += 1 }
None => ()
}
let _ = tree.set_attribute(elem, attr_name, attr_val)
}
}
}
if i.val < len && chars[i.val] == '>' {
i.val += 1
}
// Parse children (recursively find content until closing tag)
let content_start = i.val
let depth = Ref(1)
while i.val < len && depth.val > 0 {
if chars[i.val] == '<' {
if i.val + 1 < len && chars[i.val + 1] == '/' {
depth.val -= 1
if depth.val == 0 {
break
}
} else if i.val + 1 < len && chars[i.val + 1] != '!' {
depth.val += 1
}
}
i.val += 1
}
let inner = make_substring(chars, content_start, i.val)
if inner.length() > 0 {
parse_html_into(tree, elem, inner)
}
// Skip closing tag
while i.val < len && chars[i.val] != '>' {
i.val += 1
}
i.val += 1
} else {
// Text content
let text_start = i.val
while i.val < len && chars[i.val] != '<' {
i.val += 1
}
let text = make_substring(chars, text_start, i.val).trim().to_owned()
if text.length() > 0 {
let text_node = tree.create_text(text)
let _ = tree.append_child(parent, text_node)
}
}
}
}
///|
fn make_substring(chars : Array[Char], start : Int, end : Int) -> String {
let buf = StringBuilder::new()
for i = start; i < end; i = i + 1 {
buf.write_char(chars[i])
}
buf.to_string()
}
///|
/// Focus element
pub fn CdpSession::focus_element(
self : CdpSession,
node_id : Int,
) -> Result[Unit, CdpError] {
let node = @dom.NodeId::from_int(node_id)
// Verify node exists
match self.tree.get_node_info(node) {
Ok(_) => {
self.tree.set_focus(Some(node))
Ok(())
}
Err(e) => Err(core_to_cdp_error(e))
}
}