///|
/// 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\":["
let pstart = json_index_of(json, pattern, 0)
if pstart < 0 {
return ops
}
let array_start = pstart + pattern.length()
// Simple JSON array of objects parser
let len = json.length()
let mut i = array_start
let mut depth = 0
let mut obj_start = -1
while i < len {
let c = json[i].to_int()
if c == 123 {
if depth == 0 {
obj_start = i
}
depth = depth + 1
} else if c == 125 {
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 == 93 && depth == 0 {
break
}
i = i + 1
}
ops
}
///|
/// Parse a single DOM operation object
fn parse_single_dom_op(json : String) -> DomOp {
// Single pass over one char-array snapshot. The previous form re-scanned the
// object once per field (14 String::find / to_array calls), which dominated
// DOM-ops parse allocation.
let chars = json.to_array()
let len = chars.length()
let mut op = ""
let mut id = 0
let mut parent_id = 0
let mut child_id = 0
let mut ref_id = 0
let mut tag_name = ""
let mut text = ""
let mut name = ""
let mut value = ""
let mut has_name = false
let mut has_value = false
let mut delegates_focus = false
let mut slot_assignment = ""
let mut clonable = false
let mut serializable = false
let mut i = 0
while i < len {
if chars[i] != '"' {
i = i + 1
continue
}
// Read key.
let ks = i + 1
let mut ke = ks
while ke < len && chars[ke] != '"' {
ke = ke + 1
}
let key = String::from_array(chars[ks:ke])
i = ke + 1
// Advance past the ':' separator and whitespace to the value.
while i < len && chars[i] != ':' {
i = i + 1
}
i = i + 1
while i < len && chars[i] == ' ' {
i = i + 1
}
if i >= len {
break
}
let vc = chars[i]
if vc == '"' {
// String value (honoring escaped quotes).
let vs = i + 1
let mut ve = vs
while ve < len && !(chars[ve] == '"' && chars[ve - 1] != '\\') {
ve = ve + 1
}
let val = unescape_json_string(String::from_array(chars[vs:ve]))
i = ve + 1
match key {
"op" => op = val
"tagName" => tag_name = val
"text" => text = val
"name" => name = val
"value" => value = val
"slotAssignment" => slot_assignment = val
_ => ()
}
} else if vc == 't' || vc == 'f' {
let b = vc == 't'
while i < len && chars[i] != ',' && chars[i] != '}' {
i = i + 1
}
match key {
"hasName" => has_name = b
"hasValue" => has_value = b
"delegatesFocus" => delegates_focus = b
"clonable" => clonable = b
"serializable" => serializable = b
_ => ()
}
} else {
// Numeric value.
let vs = i
while i < len && ((chars[i] >= '0' && chars[i] <= '9') || chars[i] == '-') {
i = i + 1
}
let n = @string.parse_int(String::from_array(chars[vs:i])) catch {
_ => 0
}
match key {
"id" => id = n
"parentId" => parent_id = n
"childId" => child_id = n
"refId" => ref_id = n
_ => ()
}
}
}
{
op,
id,
parent_id,
child_id,
ref_id,
tag_name,
text,
name,
value,
has_name,
has_value,
delegates_focus,
slot_assignment,
clonable,
serializable,
}
}
///|
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 + "\":\""
let start = json_index_of(json, pattern, 0)
if start < 0 {
return ""
}
let value_start = start + pattern.length()
let len = json.length()
let mut end = value_start
while end < len {
if json[end].to_int() == 34 &&
(end == value_start || json[end - 1].to_int() != 92) {
break
}
end = end + 1
}
unescape_json_string(json.unsafe_substring(start=value_start, end~))
}
///|
/// Allocation-free substring index search (naive). `String::find` uses
/// Boyer-Moore-Horspool and allocates an ~850-byte skip table per call; the
/// DOM-ops parser calls it ~8 times per op, which dominated parse allocation.
fn json_index_of(s : String, needle : String, from : Int) -> Int {
let slen = s.length()
let nlen = needle.length()
if nlen == 0 || from + nlen > slen {
return -1
}
for i = from; i <= slen - nlen; i = i + 1 {
let mut k = 0
while k < nlen && s[i + k].to_int() == needle[k].to_int() {
k = k + 1
}
if k == nlen {
return i
}
}
-1
}
///|
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 + "\":["
let start = json_index_of(json, pattern, 0)
if start < 0 {
return []
}
let array_start = start + pattern.length()
let len = json.length()
let result : Array[String] = []
let mut i = array_start
while i < len && json[i].to_int() != 93 {
if json[i].to_int() == 34 {
let str_start = i + 1
let mut str_end = str_start
while str_end < len {
if json[str_end].to_int() == 34 && json[str_end - 1].to_int() != 92 {
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
}