diff --git a/src/browser/ScriptManagerBase.zig b/src/browser/ScriptManagerBase.zig
index 02e6dbdf..f726d1fd 100644
--- a/src/browser/ScriptManagerBase.zig
+++ b/src/browser/ScriptManagerBase.zig
@@ -398,6 +398,16 @@ pub fn staticScriptsDone(self: *ScriptManagerBase) void {
self.evaluate();
}
+// A script-created parser (document.open/write/close) finished. Run any
+// deferred scripts it produced. Unlike staticScriptsDone, this can run after
+// the initial parse already completed (so it must not re-assert the flag): a
+// frame that was loaded (or document.write'd into multiple times) keeps
+// static_scripts_done set, and evaluate() only drains defer_scripts when it is.
+pub fn scriptCreatedParseDone(self: *ScriptManagerBase) void {
+ self.static_scripts_done = true;
+ self.evaluate();
+}
+
pub fn evaluate(self: *ScriptManagerBase) void {
if (self.is_evaluating) {
// It's possible for a script.eval to cause evaluate to be called again.
diff --git a/src/browser/tests/frames/document_write.html b/src/browser/tests/frames/document_write.html
new file mode 100644
index 00000000..7565c28c
--- /dev/null
+++ b/src/browser/tests/frames/document_write.html
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
diff --git a/src/browser/webapi/Document.zig b/src/browser/webapi/Document.zig
index 4a3d718a..89673633 100644
--- a/src/browser/webapi/Document.zig
+++ b/src/browser/webapi/Document.zig
@@ -718,7 +718,13 @@ pub fn writeln(self: *Document, text: []const []const u8, frame: *Frame) !void {
return self.writeInternal(text, true, frame);
}
-fn writeInternal(self: *Document, text: []const []const u8, append_newline: bool, frame: *Frame) !void {
+fn writeInternal(self: *Document, text: []const []const u8, append_newline: bool, call_frame: *Frame) !void {
+ // document.write acts on this document's own frame, which isn't necessarily
+ // the calling frame — e.g. a parent frame writing into an iframe's document.
+ // The markup (and any scripts it contains) must be parsed and run in that
+ // document's context, not the caller's.
+ const frame = self._frame orelse call_frame;
+
if (self._type == .xml) {
return error.InvalidStateError;
}
@@ -730,10 +736,13 @@ fn writeInternal(self: *Document, text: []const []const u8, append_newline: bool
const html = blk: {
var joined: std.ArrayList(u8) = .empty;
for (text) |str| {
- try joined.appendSlice(frame.call_arena, str);
+ // Scratch buffer, consumed synchronously below. Keep it on the
+ // active (calling) frame's call_arena: a script run by the parse
+ // could reset the document frame's call_arena underfoot.
+ try joined.appendSlice(call_frame.call_arena, str);
}
if (append_newline) {
- try joined.append(frame.call_arena, '\n');
+ try joined.append(call_frame.call_arena, '\n');
}
break :blk joined.items;
};
@@ -827,7 +836,9 @@ fn writeInternal(self: *Document, text: []const []const u8, append_newline: bool
self._write_insertion_point = children_to_insert.getLast();
}
-pub fn open(self: *Document, frame: *Frame) !*Document {
+pub fn open(self: *Document, call_frame: *Frame) !*Document {
+ const frame = self._frame orelse call_frame;
+
if (self._type == .xml) {
return error.InvalidStateError;
}
@@ -869,7 +880,9 @@ pub fn open(self: *Document, frame: *Frame) !*Document {
return self;
}
-pub fn close(self: *Document, frame: *Frame) !void {
+pub fn close(self: *Document, call_frame: *Frame) !void {
+ const frame = self._frame orelse call_frame;
+
if (self._type == .xml) {
return error.InvalidStateError;
}
@@ -889,6 +902,12 @@ pub fn close(self: *Document, frame: *Frame) !void {
defer self._script_created_parser = null;
try self._script_created_parser.?.done();
+ // The write'd markup is fully parsed; run any deferred scripts it produced
+ // (e.g. inline modules) before firing the load event. This frame's initial
+ // parse may never have set static_scripts_done (e.g. a freshly-loaded
+ // iframe written into via document.write), so we can't rely on it.
+ frame._script_manager.base.scriptCreatedParseDone();
+
frame.documentIsComplete();
}