///|
/// Page script execution host backed by `dowdiness/js_engine` (pure MoonBit
/// embedded JavaScript engine). App code only sees this wrapper; the engine
/// instance keeps one realm alive across page loads for stateful pages.
///
/// The realm is seeded with a minimal DOM bridge shim (`dom_shim.mbt`):
/// page scripts can use `document`/`window` basics, and after script
/// execution the host serializes the shadow DOM back to HTML for the
/// crater render pipeline.
pub struct JsEngine {
engine : @js_engine.Engine
}
///|
pub struct ScriptRunResult {
outputs : Array[String]
errors : Array[String]
}
///|
pub fn JsEngine::new() -> JsEngine {
let js = { engine: @js_engine.Engine() }
// 安装 DOM shim;失败(引擎语法不兼容)时页面脚本的 document 用法照旧报错
js.engine.eval(dom_shim_js) catch {
_ => ()
}
js
}
///|
/// 新导航前重置影子 DOM 与事件监听;并同步 `location`。
pub fn JsEngine::reset_dom(self : JsEngine, url : String) -> Unit {
self.engine.eval("__moui_reset()") catch {
_ => ()
}
self.engine.eval(
"__moui_set_location(" + @json.to_json(url).stringify() + ")",
) catch {
_ => ()
}
}
///|
/// 脚本执行完后取回影子 DOM 序列化的 body HTML 片段。
pub fn JsEngine::dump_dom_html(self : JsEngine) -> String? {
let r = self.engine.call_json("__moui_dump_html", []) catch {
_ => return None
}
match r {
Json::String(s) => Some(s)
_ => None
}
}
///|
/// 取回页面运行时动态添加的 `<script src>`(appendChild 注册),取走后清空。
pub fn JsEngine::take_dynamic_scripts(self : JsEngine) -> Array[String] {
let r = self.engine.call_json("__moui_take_dynamic_scripts", []) catch {
_ => return []
}
match r {
Json::Array(items) =>
items.filter_map(fn(item) {
match item {
Json::String(s) => Some(s)
_ => None
}
})
_ => []
}
}
///|
/// 脚本执行后的"事件推进":驱动 js_engine 的 timer 回调(页面常用
/// setTimeout/sb_st 调度加载与初始化,如 bing 的 onPP → rms 链),
/// 再派发 DOMContentLoaded/load。
pub fn JsEngine::pump_events(self : JsEngine) -> Unit {
// timer checkpoint 可能触发新脚本注册;有限次迭代覆盖嵌套调度
let mut rounds = 0
while rounds < 8 && self.engine.has_pending_timers() {
self.engine.run_timer_checkpoint() catch {
_ => break
}
rounds = rounds + 1
}
self.dispatch_ready()
}
///|
/// 派发 document 的 DOMContentLoaded 与 window 的 load(脚本全部执行完后)。
pub fn JsEngine::dispatch_ready(self : JsEngine) -> Unit {
self.engine.eval("__moui_dispatch_ready()") catch {
_ => ()
}
}
///|
/// Evaluate each `<script>` body (no `src`) in document order, collecting
/// console output and per-script errors. Errors are non-fatal: later
/// scripts still run.
pub fn JsEngine::run_scripts(
self : JsEngine,
scripts : Array[String],
) -> ScriptRunResult {
let outputs = self.engine.take_output()
let errors : Array[String] = []
for script in scripts {
let ok = try {
self.engine.eval(script)
true
} catch {
err => {
errors.push(err.to_string())
false
}
}
let after = self.engine.take_output()
for line in after {
outputs.push(line)
}
if ok {
// keep going: errors are non-fatal and already collected
}
}
{ outputs, errors }
}
///|
/// Drain any console output produced by previous script activity.
pub fn JsEngine::drain_output(self : JsEngine) -> Array[String] {
self.engine.take_output()
}
///|
/// Depth-first document order `<script>` bodies (scripts with `src` are
/// skipped: external loading is a network concern).
pub fn extract_scripts(html : String) -> Array[String] {
let entries = extract_script_entries(html)
let out : Array[String] = []
for e in entries {
if e.inline {
out.push(e.body)
}
}
out
}
///|
/// 脚本条目:内联(body 非空)或外链(src 非空),按文档顺序。
pub struct ScriptEntry {
inline : Bool
src : String
body : String
}
///|
/// 提取全部 `<script>` 条目(含外链 src),按文档顺序。
pub fn extract_script_entries(html : String) -> Array[ScriptEntry] {
let out : Array[ScriptEntry] = []
let doc = @html.parse_document(html)
fn visit(e : @html.Element) -> Unit {
if e.tag == "script" {
let has_src = e.attributes.contains("src")
let buf = StringBuilder()
for child in e.children {
match child {
@html.Node::Text(text) => buf.write_string(text)
@html.Node::Element(_) => ()
}
}
out.push({
inline: !has_src,
src: if has_src {
match e.attributes.get("src") {
Some(v) => v
None => ""
}
} else {
""
},
body: buf.to_string(),
})
}
for child in e.children {
match child {
@html.Node::Element(sub) => visit(sub)
@html.Node::Text(_) => ()
}
}
}
visit(doc.root)
out
}
///|
/// 提取外链样式表 href(rel="stylesheet"),按文档顺序。
pub fn extract_stylesheets(html : String) -> Array[String] {
let out : Array[String] = []
let doc = @html.parse_document(html)
fn visit(e : @html.Element) -> Unit {
if e.tag == "link" {
let rel = match e.attributes.get("rel") {
Some(v) => v.to_lower()
None => ""
}
if rel.contains("stylesheet") {
match e.attributes.get("href") {
Some(href) => out.push(href)
None => ()
}
}
}
for child in e.children {
match child {
@html.Node::Element(sub) => visit(sub)
@html.Node::Text(_) => ()
}
}
}
visit(doc.root)
out
}