///|
/// Initialize DOM tree and script executor
fn Browser::init_js_execution(self : Browser) -> Unit {
  // Build from parsed_doc/html_content when available so scripts can mutate
  // existing DOM and those mutations can be reflected back to rendering.
  let shadow_source_html = if html_source_requires_runtime_rebuild(
      self.source_html,
    ) {
    self.source_html
  } else if html_source_requires_runtime_rebuild(self.dom_snapshot_html) {
    self.dom_snapshot_html
  } else if html_source_requires_runtime_rebuild(self.html_content) {
    self.html_content
  } else {
    ""
  }
  let dom = if shadow_source_html.length() > 0 {
    match build_dom_tree_from_source_html(shadow_source_html) {
      Some(dom) => dom
      None =>
        match self.parsed_doc {
          Some(doc) => build_dom_tree_from_document(doc)
          None =>
            if self.html_content.length() > 0 {
              let doc = @html.assign_synthetic_ids(
                @html.parse_document(self.html_content),
              )
              self.parsed_doc = Some(doc)
              build_dom_tree_from_document(doc)
            } else {
              create_empty_html_dom_tree()
            }
        }
    }
  } else {
    match self.parsed_doc {
      Some(doc) => build_dom_tree_from_document(doc)
      None =>
        if self.html_content.length() > 0 {
          let doc = @html.assign_synthetic_ids(
            @html.parse_document(self.html_content),
          )
          self.parsed_doc = Some(doc)
          build_dom_tree_from_document(doc)
        } else {
          create_empty_html_dom_tree()
        }
    }
  }
  self.dom_tree = Some(dom)
  self.script_executor = Some(
    match self.js_runtime_override {
      Some(runtime) => @js.ScriptExecutor::new_with_runtime(dom, runtime)
      None => @js.ScriptExecutor::new_deferred(dom)
    },
  )
  // Fresh realm: force the layout bridge to (re)inject on the next call.
  self.bridge_injected_html = ""
  match self.script_executor {
    Some(executor) =>
      ignore(executor.execute_source(browser_form_submit_bridge_source())) catch {
        err => println("[JS] Error: " + err.to_string())
      }
    None => ()
  }
}

///|
/// Apply JS-mutated DomTree back to renderer inputs and invalidate caches
fn Browser::sync_render_state_from_dom_tree(self : Browser) -> Bool {
  match self.dom_tree {
    Some(dom) => {
      let next_html = @js.serialize_dom_to_html(dom)
      let next_snapshot_html = @js.serialize_dom_to_snapshot_html(dom)
      if next_html.length() == 0 {
        return false
      }
      let render_changed = next_html != self.html_content
      let snapshot_changed = next_snapshot_html != self.dom_snapshot_html
      if !render_changed && !snapshot_changed {
        return false
      }
      self.dom_snapshot_html = next_snapshot_html
      self.source_html = next_snapshot_html
      if !render_changed {
        return false
      }
      self.html_content = next_html
      self.html_has_declarative_shadow_dom = html_source_has_declarative_shadow_dom(
        self.html_content,
      )
      let next_doc = build_render_document_from_dom_tree(dom)
      self.parsed_doc = Some(next_doc)
      self.refresh_links_from_render_source()
      if self.links.length() == 0 {
        self.focused_link = 0
      } else if self.focused_link >= self.links.length() {
        self.focused_link = self.links.length() - 1
      }
      self.clear_render_cache()
      self.content_height = @renderer.get_content_height_with_document(
        next_doc,
        self.viewport_width,
        self.viewport_height,
        self.external_css,
      )
      self.build_accessibility_tree()
      true
    }
    None => false
  }
}