///|
/// QuickJS runtime DOM operation parsing and application
///
/// Converts the JSON domOps emitted by the JavaScript mock DOM into DomTree
/// mutations.
///|
/// DOM operation type
priv struct DomOp {
op : String
id : Int
parent_id : Int
child_id : Int
ref_id : Int // Reference node ID for insertBefore
tag_name : String
text : String
name : String
value : String
has_name : Bool
has_value : Bool
delegates_focus : Bool
slot_assignment : String
clonable : Bool
serializable : Bool
}
///|
/// Parse JS result JSON with DOM operations
fn parse_js_result_with_ops(json : String) -> (JsResult, Array[DomOp]) {
let success = json.contains("\"success\":true")
let value = if success {
extract_json_field(json, "value")
} else {
extract_json_field(json, "error")
}
let logs = extract_json_array(json, "logs")
let dom_ops = parse_dom_ops(json)
({ value, logs, success }, dom_ops)
}
///|
/// Parse DOM operations from JSON
fn parse_dom_ops(json : String) -> Array[DomOp] {
let ops : Array[DomOp] = []
// Find domOps array
let pattern = "\"domOps\":["
match json.find(pattern) {
Some(start) => {
let array_start = start + pattern.length()
// Simple JSON array of objects parser
let chars = json.to_array()
let mut i = array_start
let mut depth = 0
let mut obj_start = -1
while i < chars.length() {
let c = chars[i]
if c == '{' {
if depth == 0 {
obj_start = i
}
depth = depth + 1
} else if c == '}' {
depth = depth - 1
if depth == 0 && obj_start >= 0 {
let obj_json = json.unsafe_substring(start=obj_start, end=i + 1)
let op = parse_single_dom_op(obj_json)
ops.push(op)
obj_start = -1
}
} else if c == ']' && depth == 0 {
break
}
i = i + 1
}
}
None => ()
}
ops
}
///|
/// Parse a single DOM operation object
fn parse_single_dom_op(json : String) -> DomOp {
{
op: extract_json_field(json, "op"),
id: parse_int_field(json, "id"),
parent_id: parse_int_field(json, "parentId"),
child_id: parse_int_field(json, "childId"),
ref_id: parse_int_field(json, "refId"),
tag_name: extract_json_field(json, "tagName"),
text: extract_json_field(json, "text"),
name: extract_json_field(json, "name"),
value: extract_json_field(json, "value"),
has_name: parse_bool_field(json, "hasName"),
has_value: parse_bool_field(json, "hasValue"),
delegates_focus: parse_bool_field(json, "delegatesFocus"),
slot_assignment: extract_json_field(json, "slotAssignment"),
clonable: parse_bool_field(json, "clonable"),
serializable: parse_bool_field(json, "serializable"),
}
}
///|
fn parse_bool_field(json : String, field : String) -> Bool {
let pattern = "\"" + field + "\":"
match json.find(pattern) {
Some(start) => {
let value_start = start + pattern.length()
let remaining = json.unsafe_substring(
start=value_start,
end=json.length(),
)
remaining.has_prefix("true")
}
None => false
}
}
///|
/// Parse integer field from JSON
fn parse_int_field(json : String, field : String) -> Int {
let pattern = "\"" + field + "\":"
match json.find(pattern) {
Some(start) => {
let value_start = start + pattern.length()
let chars = json.to_array()
let mut end = value_start
while end < chars.length() &&
((chars[end] >= '0' && chars[end] <= '9') || chars[end] == '-') {
end = end + 1
}
if end > value_start {
let num_str = json.unsafe_substring(start=value_start, end~)
@string.parse_int(num_str) catch {
_ => 0
}
} else {
0
}
}
None => 0
}
}
///|
fn parse_custom_states_value(value : String) -> Array[String] {
let states : Array[String] = []
if value.is_empty() {
return states
}
for part in value.split(" ") {
let state = part.trim().to_owned()
if !state.is_empty() {
states.push(state)
}
}
states
}
///|
/// Resolve ID to NodeId for apply_dom_ops
/// If ID is in map, use mapped value; otherwise treat as direct DomTree NodeId
fn resolve_dom_id(id_map : Map[Int, @dom.NodeId], id : Int) -> @dom.NodeId? {
match id_map.get(id) {
Some(node_id) => Some(node_id)
None =>
// ID might be a direct DomTree NodeId (from DOM initialization)
if id > 0 {
Some(@dom.NodeId::from_int(id))
} else {
None
}
}
}
///|
/// Apply DOM operations to DomTree
fn apply_dom_ops(dom : @dom.DomTree, ops : Array[DomOp]) -> Unit {
// Map mock IDs to real NodeIds
let id_map : Map[Int, @dom.NodeId] = {}
// Pre-populate with body (id=2 in mock)
match dom.query_selector(dom.get_document(), "body") {
Ok(Some(body_id)) => id_map.set(2, body_id)
_ => ()
}
// Pre-populate with html (id=1 in mock)
match dom.query_selector(dom.get_document(), "html") {
Ok(Some(html_id)) => id_map.set(1, html_id)
_ => ()
}
for op in ops {
match op.op {
"createElement" =>
match
dom.create_runtime_node(op.id, @dom.Element, tag_name=op.tag_name) {
Ok(node_id) => id_map.set(op.id, node_id)
Err(_) => ()
}
"createTextNode" =>
match dom.create_runtime_node(op.id, @dom.Text, text_content=op.text) {
Ok(node_id) => id_map.set(op.id, node_id)
Err(_) => ()
}
"createComment" =>
match
dom.create_runtime_node(op.id, @dom.Comment, text_content=op.text) {
Ok(node_id) => id_map.set(op.id, node_id)
Err(_) => ()
}
"createDocumentFragment" =>
match dom.create_runtime_node(op.id, @dom.DocumentFragment) {
Ok(node_id) => id_map.set(op.id, node_id)
Err(_) => ()
}
"attachShadow" =>
match
(
resolve_dom_id(id_map, op.parent_id),
resolve_dom_id(id_map, op.child_id),
) {
(Some(host), Some(shadow_root)) => {
let init = @dom.ShadowRootInit::new(
mode=op.value,
delegates_focus=op.delegates_focus,
slot_assignment=if op.slot_assignment.length() > 0 {
op.slot_assignment
} else {
"named"
},
clonable=op.clonable,
serializable=op.serializable,
)
let _ = dom.attach_existing_shadow_root_with_init(
host, shadow_root, init,
)
}
_ => ()
}
"appendChild" =>
match
(
resolve_dom_id(id_map, op.parent_id),
resolve_dom_id(id_map, op.child_id),
) {
(Some(parent), Some(child)) => {
let _ = dom.append_child(parent, child)
}
_ => ()
}
"insertBefore" =>
match
(
resolve_dom_id(id_map, op.parent_id),
resolve_dom_id(id_map, op.child_id),
) {
(Some(parent), Some(child)) => {
let ref_node = if op.ref_id > 0 {
resolve_dom_id(id_map, op.ref_id)
} else {
None
}
let _ = dom.insert_before(parent, child, ref_node)
}
_ => ()
}
"replaceChild" => {
let parent_id = resolve_dom_id(id_map, op.parent_id)
let new_child_id = resolve_dom_id(id_map, op.child_id)
let old_child_id = resolve_dom_id(id_map, op.ref_id)
match (parent_id, new_child_id, old_child_id) {
(Some(parent), Some(new_child), Some(old_child)) => {
// replaceChild: insert new before old, then remove old
let _ = dom.insert_before(parent, new_child, Some(old_child))
let _ = dom.remove_child(parent, old_child)
}
_ => ()
}
}
"removeChild" =>
match
(
resolve_dom_id(id_map, op.parent_id),
resolve_dom_id(id_map, op.child_id),
) {
(Some(parent), Some(child)) => {
let _ = dom.remove_child(parent, child)
}
_ => ()
}
"setAttribute" =>
match resolve_dom_id(id_map, op.id) {
Some(node_id) => {
let _ = dom.set_attribute(node_id, op.name, op.value)
}
None => ()
}
"removeAttribute" =>
match resolve_dom_id(id_map, op.id) {
Some(node_id) => {
let _ = dom.remove_attribute(node_id, op.name)
}
None => ()
}
"setCustomStates" =>
match resolve_dom_id(id_map, op.id) {
Some(node_id) => {
let _ = dom.set_custom_states(
node_id,
parse_custom_states_value(op.value),
)
}
None => ()
}
"setFormValue" =>
match resolve_dom_id(id_map, op.id) {
Some(node_id) => {
let form_value = if op.has_name { Some(op.name) } else { None }
let form_state_value = if op.has_value {
Some(op.value)
} else {
None
}
let _ = dom.set_form_associated_state(
node_id, form_value, form_state_value,
)
}
None => ()
}
"setTextContent" =>
match resolve_dom_id(id_map, op.id) {
Some(node_id) => {
let _ = dom.set_text_content(node_id, op.value)
}
None => ()
}
_ => ()
}
}
}
///|
/// Extract string field from JSON
fn extract_json_field(json : String, field : String) -> String {
let pattern = "\"" + field + "\":\""
match json.find(pattern) {
Some(start) => {
let value_start = start + pattern.length()
let chars = json.to_array()
let mut end = value_start
while end < chars.length() {
if chars[end] == '"' && (end == value_start || chars[end - 1] != '\\') {
break
}
end = end + 1
}
unescape_json_string(json.unsafe_substring(start=value_start, end~))
}
None => ""
}
}
///|
fn unescape_json_string(value : String) -> String {
let out = StringBuilder::new()
let chars = value.to_array()
let mut i = 0
while i < chars.length() {
let c = chars[i]
if c != '\\' || i + 1 >= chars.length() {
out.write_char(c)
i = i + 1
continue
}
let next = chars[i + 1]
match next {
'"' => out.write_char('"')
'\\' => out.write_char('\\')
'/' => out.write_char('/')
'b' => out.write_char('\b')
'f' => out.write_char('\u000C')
'n' => out.write_char('\n')
'r' => out.write_char('\r')
't' => out.write_char('\t')
'u' =>
if i + 5 < chars.length() {
let code = parse_json_hex4(
chars[i + 2],
chars[i + 3],
chars[i + 4],
chars[i + 5],
)
match code {
Some(code) => out.write_char(code.unsafe_to_char())
None => {
out.write_char('\\')
out.write_char('u')
out.write_char(chars[i + 2])
out.write_char(chars[i + 3])
out.write_char(chars[i + 4])
out.write_char(chars[i + 5])
}
}
i = i + 6
continue
} else {
out.write_char('\\')
out.write_char('u')
}
_ => out.write_char(next)
}
i = i + 2
}
out.to_string()
}
///|
fn parse_json_hex_digit(c : Char) -> Int? {
if c >= '0' && c <= '9' {
Some(c.to_int() - '0'.to_int())
} else if c >= 'a' && c <= 'f' {
Some(10 + c.to_int() - 'a'.to_int())
} else if c >= 'A' && c <= 'F' {
Some(10 + c.to_int() - 'A'.to_int())
} else {
None
}
}
///|
fn parse_json_hex4(c0 : Char, c1 : Char, c2 : Char, c3 : Char) -> Int? {
match
(
parse_json_hex_digit(c0),
parse_json_hex_digit(c1),
parse_json_hex_digit(c2),
parse_json_hex_digit(c3),
) {
(Some(d0), Some(d1), Some(d2), Some(d3)) =>
Some(d0 * 4096 + d1 * 256 + d2 * 16 + d3)
_ => None
}
}
///|
/// Extract string array from JSON
fn extract_json_array(json : String, field : String) -> Array[String] {
let pattern = "\"" + field + "\":["
match json.find(pattern) {
Some(start) => {
let array_start = start + pattern.length()
let chars = json.to_array()
let result : Array[String] = []
let mut i = array_start
while i < chars.length() && chars[i] != ']' {
if chars[i] == '"' {
let str_start = i + 1
let mut str_end = str_start
while str_end < chars.length() {
if chars[str_end] == '"' && chars[str_end - 1] != '\\' {
break
}
str_end = str_end + 1
}
result.push(
unescape_json_string(
json.unsafe_substring(start=str_start, end=str_end),
),
)
i = str_end + 1
} else {
i = i + 1
}
}
result
}
None => []
}
}