cleanup: Mechanical move various frame.parse* method

Follows the observer, user_input, etc.. pattern and relocates various
`Frame.parse*` methods into Frame.parse.* functions.

Goal is just to keep Frame neat.
This commit is contained in:
Karl Seguin
2026-07-23 13:51:34 +08:00
parent dab5ff7779
commit 705bc78b72
15 changed files with 155 additions and 131 deletions

View File

@@ -73,6 +73,7 @@ const milliTimestamp = @import("../datetime.zig").milliTimestamp;
const GlobalEventHandlersLookup = @import("webapi/global_event_handlers.zig").Lookup;
pub const parse = @import("frame/parse.zig");
pub const preload = @import("frame/preload.zig");
pub const observers = @import("frame/observers.zig");
pub const user_input = @import("frame/user_input.zig");
@@ -2867,113 +2868,6 @@ pub fn updateRangesForNodeRemoval(self: *Frame, parent: *Node, child: *Node, chi
}
}
// TODO: optimize and cleanup, this is called a lot (e.g., innerHTML = '')
pub fn parseHtmlAsChildren(self: *Frame, node: *Node, html: []const u8) !void {
return self.parseHtmlAsChildrenInner(node, html, .{});
}
// setHTMLUnsafe variant: parse a fragment that may contain declarative shadow node
pub fn parseHtmlUnsafeAsChildren(self: *Frame, node: *Node, html: []const u8) !void {
return self.parseHtmlAsChildrenInner(node, html, .{ .allow_declarative_shadow = true });
}
// Range.createContextualFragment variant: unlike innerHTML et al., its scripts
// are run when the fragment is inserted into a document.
pub fn parseContextualFragment(self: *Frame, node: *Node, html: []const u8) !void {
return self.parseHtmlAsChildrenInner(node, html, .{ .scripts_runnable = true });
}
const FragmentParseOpts = struct {
scripts_runnable: bool = false,
allow_declarative_shadow: bool = false,
};
fn parseHtmlAsChildrenInner(self: *Frame, node: *Node, html: []const u8, opts: FragmentParseOpts) !void {
const previous_parse_mode = self._parse_mode;
self._parse_mode = .fragment;
defer self._parse_mode = previous_parse_mode;
// The html5ever wrapper-unwrap below rebinds children without going
// through the insertion path, so recompute slot assignments for any
// shadow tree this fragment landed in (idempotent; signals only on diff).
defer if (self._element_shadow_roots.count() != 0) {
const root = node.getRootNode(.{});
if (root.is(ShadowRoot) != null) {
slotting.assignSlottablesForTree(root, self);
}
if (node.is(Element)) |el| {
if (self._element_shadow_roots.get(el)) |shadow_root| {
slotting.assignSlottablesForTree(shadow_root.asNode(), self);
}
}
};
const previous_scripts_runnable = self._fragment_scripts_runnable;
self._fragment_scripts_runnable = opts.scripts_runnable;
defer self._fragment_scripts_runnable = previous_scripts_runnable;
var parser = Parser.init(self.call_arena, node, self, .{ .allow_declarative_shadow = opts.allow_declarative_shadow });
parser.parseFragment(html);
if (parser.terminated) {
return error.ExecutionTerminated;
}
// html5ever wraps fragment output in an <html> element; unwrap so its
// children land directly on `node`. See https://github.com/servo/html5ever/issues/583.
// Because of custom element callbacks, the structure might not be what
// we expect, and nodes might be altogether removed. We deal with this in a
// few different places, but always the same way: leave it as-is.
const children = node._children orelse return;
const first = Node.linkToNode(children.first.?);
if (first.is(Element.Html.Html) == null) {
return;
}
node._children = first._children;
// No mutation records for the unwrapped children either; see the comment
// about fragment parses in _insertNodeRelative.
var it = node.childrenIterator();
while (it.next()) |child| {
child._parent = node;
}
}
// Build a detached XMLDocument from `xml` (DOMParser.parseFromString and
// XMLHttpRequest.responseXML). Returns null when the input isn't well-formed
// XML. The parse borrows the fragment parse-mode so frame-side hooks triggered
// from `Build.created` / `nodeIsReady` (external stylesheet fetches, script
// execution, mutation-observer fan-out, default-script injection) treat the
// parsed nodes as detached and skip side effects on the live document.
pub fn parseXmlDocument(self: *Frame, xml: []const u8) !?*Document.XMLDocument {
const arena = try self.getArena(.medium, "Frame.parseXmlDocument");
defer self.releaseArena(arena);
const previous_parse_mode = self._parse_mode;
self._parse_mode = .fragment;
defer self._parse_mode = previous_parse_mode;
const doc = try self._factory.document(Document.XMLDocument{ ._proto = undefined });
const doc_node = doc.asNode();
var parser = Parser.init(arena, doc_node, self, .{});
parser.parseXML(xml);
if (parser.terminated) {
return error.ExecutionTerminated;
}
if (parser.err != null or doc_node.firstChild() == null) {
return null;
}
// If first node is a `ProcessingInstruction` (e.g. the <?xml?>
// declaration), skip it.
const first_child = doc_node.firstChild().?;
if (first_child.getNodeType() == 7) {
_ = try doc_node.removeChild(first_child, self);
}
return doc;
}
// Runs the "ready" work for an inserted node and, when it's an element with
// children, for its descendants in tree order: appending a subtree
// containing scripts must execute them all, after the whole insertion.

View File

@@ -282,7 +282,7 @@ fn testForms(html: []const u8) ![]FormInfo {
const doc = frame.window._document;
const div = try doc.createElement("div", null, frame);
try frame.parseHtmlAsChildren(div.asNode(), html);
try Frame.parse.htmlAsChildren(frame, div.asNode(), html);
return collectForms(frame.call_arena, div.asNode(), frame);
}

129
src/browser/frame/parse.zig Normal file
View File

@@ -0,0 +1,129 @@
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const Frame = @import("../Frame.zig");
const Parser = @import("../parser/Parser.zig");
const Node = @import("../webapi/Node.zig");
const Element = @import("../webapi/Element.zig");
const Document = @import("../webapi/Document.zig");
const ShadowRoot = @import("../webapi/ShadowRoot.zig");
const slotting = @import("../webapi/element/slotting.zig");
pub fn htmlAsChildren(frame: *Frame, node: *Node, html: []const u8) !void {
return htmlAsChildrenInner(frame, node, html, .{});
}
// setHTMLUnsafe variant: parse a fragment that may contain declarative shadow node
pub fn htmlUnsafeAsChildren(frame: *Frame, node: *Node, html: []const u8) !void {
return htmlAsChildrenInner(frame, node, html, .{ .allow_declarative_shadow = true });
}
// Range.createContextualFragment variant: unlike innerHTML et al., its scripts
// are run when the fragment is inserted into a document.
pub fn contextualFragment(frame: *Frame, node: *Node, html: []const u8) !void {
return htmlAsChildrenInner(frame, node, html, .{ .scripts_runnable = true });
}
const FragmentParseOpts = struct {
scripts_runnable: bool = false,
allow_declarative_shadow: bool = false,
};
fn htmlAsChildrenInner(frame: *Frame, node: *Node, html: []const u8, opts: FragmentParseOpts) !void {
const previous_parse_mode = frame._parse_mode;
frame._parse_mode = .fragment;
defer frame._parse_mode = previous_parse_mode;
// The html5ever wrapper-unwrap below rebinds children without going
// through the insertion path, so recompute slot assignments for any
// shadow tree this fragment landed in (idempotent; signals only on diff).
defer if (frame._element_shadow_roots.count() != 0) {
const root = node.getRootNode(.{});
if (root.is(ShadowRoot) != null) {
slotting.assignSlottablesForTree(root, frame);
}
if (node.is(Element)) |el| {
if (frame._element_shadow_roots.get(el)) |shadow_root| {
slotting.assignSlottablesForTree(shadow_root.asNode(), frame);
}
}
};
const previous_scripts_runnable = frame._fragment_scripts_runnable;
frame._fragment_scripts_runnable = opts.scripts_runnable;
defer frame._fragment_scripts_runnable = previous_scripts_runnable;
var parser = Parser.init(frame.call_arena, node, frame, .{ .allow_declarative_shadow = opts.allow_declarative_shadow });
parser.parseFragment(html);
if (parser.terminated) {
return error.ExecutionTerminated;
}
// html5ever wraps fragment output in an <html> element; unwrap so its
// children land directly on `node`. See https://github.com/servo/html5ever/issues/583.
// Because of custom element callbacks, the structure might not be what
// we expect, and nodes might be altogether removed. We deal with this in a
// few different places, but always the same way: leave it as-is.
const children = node._children orelse return;
const first = Node.linkToNode(children.first.?);
if (first.is(Element.Html.Html) == null) {
return;
}
node._children = first._children;
// No mutation records for the unwrapped children either; see the comment
// about fragment parses in _insertNodeRelative.
var it = node.childrenIterator();
while (it.next()) |child| {
child._parent = node;
}
}
// Build a detached XMLDocument from `xml` (DOMParser.parseFromString and
// XMLHttpRequest.responseXML). Returns null when the input isn't well-formed
// XML.
pub fn xmlDocument(frame: *Frame, xml: []const u8) !?*Document.XMLDocument {
const arena = try frame.getArena(.medium, "parse.xmlDocument");
defer frame.releaseArena(arena);
const previous_parse_mode = frame._parse_mode;
frame._parse_mode = .fragment;
defer frame._parse_mode = previous_parse_mode;
const doc = try frame._factory.document(Document.XMLDocument{ ._proto = undefined });
const doc_node = doc.asNode();
var parser = Parser.init(arena, doc_node, frame, .{});
parser.parseXML(xml);
if (parser.terminated) {
return error.ExecutionTerminated;
}
if (parser.err != null or doc_node.firstChild() == null) {
return null;
}
// If first node is a `ProcessingInstruction` (e.g. the <?xml?>
// declaration), skip it.
const first_child = doc_node.firstChild().?;
if (first_child.getNodeType() == 7) {
_ = try doc_node.removeChild(first_child, frame);
}
return doc;
}

View File

@@ -501,7 +501,7 @@ fn testInteractive(html: []const u8) ![]InteractiveElement {
const doc = frame.window._document;
const div = try doc.createElement("div", null, frame);
try frame.parseHtmlAsChildren(div.asNode(), html);
try Frame.parse.htmlAsChildren(frame, div.asNode(), html);
return collectInteractiveElements(div.asNode(), frame.call_arena, frame);
}

View File

@@ -94,7 +94,7 @@ fn testLinks(html: []const u8) ![]Link {
const doc = frame.window._document;
const div = try doc.createElement("div", null, frame);
try frame.parseHtmlAsChildren(div.asNode(), html);
try Frame.parse.htmlAsChildren(frame, div.asNode(), html);
return collectLinks(frame.call_arena, div.asNode(), frame);
}

View File

@@ -599,7 +599,7 @@ fn testMarkdownHTML(html: []const u8, expected: []const u8) !void {
const doc = frame.window._document;
const div = try doc.createElement("div", null, frame);
try frame.parseHtmlAsChildren(div.asNode(), html);
try Frame.parse.htmlAsChildren(frame, div.asNode(), html);
var aw: std.Io.Writer.Allocating = .init(testing.allocator);
defer aw.deinit();
@@ -800,7 +800,7 @@ test "browser.markdown: resolve links" {
const doc = frame.window._document;
const div = try doc.createElement("div", null, frame);
try frame.parseHtmlAsChildren(div.asNode(),
try Frame.parse.htmlAsChildren(frame, div.asNode(),
\\<a href="b">Link</a>
\\<img src="../c.png" alt="Img">
\\<a href="/my page">Space</a>
@@ -839,7 +839,7 @@ test "browser.markdown: max_bytes leaves output untouched when under cap" {
const doc = frame.window._document;
const div = try doc.createElement("div", null, frame);
try frame.parseHtmlAsChildren(div.asNode(), "<p>Short</p>");
try Frame.parse.htmlAsChildren(frame, div.asNode(), "<p>Short</p>");
var aw: std.Io.Writer.Allocating = .init(testing.allocator);
defer aw.deinit();
@@ -855,7 +855,7 @@ test "browser.markdown: max_bytes truncates with marker" {
const doc = frame.window._document;
const div = try doc.createElement("div", null, frame);
try frame.parseHtmlAsChildren(div.asNode(), "<p>" ++ ("AAAA " ** 100) ++ "</p>");
try Frame.parse.htmlAsChildren(frame, div.asNode(), "<p>" ++ ("AAAA " ** 100) ++ "</p>");
var aw: std.Io.Writer.Allocating = .init(testing.allocator);
defer aw.deinit();
@@ -878,11 +878,11 @@ fn testMarkdownShadow(light: []const u8, shadow: []const u8, expected: []const u
const host = try doc.createElement("div", null, frame);
if (light.len > 0) {
try frame.parseHtmlAsChildren(host.asNode(), light);
try Frame.parse.htmlAsChildren(frame, host.asNode(), light);
}
const sr = try host.attachShadow(.{ .mode = .open }, frame);
try frame.parseHtmlAsChildren(sr.asNode(), shadow);
try Frame.parse.htmlAsChildren(frame, sr.asNode(), shadow);
var aw: std.Io.Writer.Allocating = .init(testing.allocator);
defer aw.deinit();

View File

@@ -494,7 +494,7 @@ fn testStructuredData(html: []const u8) !StructuredData {
const doc = frame.window._document;
const div = try doc.createElement("div", null, frame);
try frame.parseHtmlAsChildren(div.asNode(), html);
try Frame.parse.htmlAsChildren(frame, div.asNode(), html);
return collectStructuredData(div.asNode(), frame.call_arena, frame);
}
@@ -714,7 +714,7 @@ test "structured_data: link headers from response" {
const doc = frame.window._document;
const div = try doc.createElement("div", null, frame);
try frame.parseHtmlAsChildren(div.asNode(), "<title>Example</title>");
try Frame.parse.htmlAsChildren(frame, div.asNode(), "<title>Example</title>");
const data = try collectStructuredData(div.asNode(), frame.call_arena, frame);

View File

@@ -2,7 +2,7 @@
<body>
<script src="../testing.js"></script>
<!-- Frame.parseHtmlAsChildren extracts children from the temporary
<!-- Frame.parse.htmlAsChildren extracts children from the temporary
<html> element the fragment parser leaves at the top of the parse
result. A custom element's connectedCallback fires synchronously
during that parse, so it can reach two levels up (this.parentNode is

View File

@@ -55,7 +55,7 @@ pub fn parseFromString(
defer frame.releaseArena(arena);
// DOMParser builds a detached Document. Borrow the same fragment
// parse-mode that `parseHtmlAsChildren` uses so frame-side hooks
// parse-mode that `Frame.parse` uses so frame-side hooks
// triggered from `Build.created` / `nodeIsReady` (external
// stylesheet fetches, script execution, mutation-observer fan-out,
// default-script injection) treat the parsed nodes as detached and
@@ -89,9 +89,9 @@ pub fn parseFromString(
return doc.asDocument();
},
else => {
const doc = (try frame.parseXmlDocument(html)) orelse blk: {
const doc = (try Frame.parse.xmlDocument(frame, html)) orelse blk: {
// Return a document with a <parsererror> element per spec.
break :blk (try frame.parseXmlDocument("<parsererror xmlns=\"http://www.mozilla.org/newlayout/xml/parsererror.xml\">error</parsererror>")).?;
break :blk (try Frame.parse.xmlDocument(frame, "<parsererror xmlns=\"http://www.mozilla.org/newlayout/xml/parsererror.xml\">error</parsererror>")).?;
};
return doc.asDocument();
},

View File

@@ -484,7 +484,7 @@ pub fn setOuterHTML(self: *Element, html: []const u8, frame: *Frame) !void {
var fragment: ?*Node = null;
if (html.len > 0) {
const frag = (try Node.DocumentFragment.init(frame)).asNode();
try frame.parseHtmlAsChildren(frag, html);
try Frame.parse.htmlAsChildren(frame, frag, html);
fragment = frag;
}

View File

@@ -71,7 +71,7 @@ pub fn setBody(self: *HTMLDocument, html: []const u8, frame: *Frame) !void {
// parsing strips any <html>/<body>/<head> wrappers the author included.
const new_body_node = try Frame.node_factory.createElementNS(frame, .html, "body", null);
if (html.len > 0) {
try frame.parseHtmlAsChildren(new_body_node, html);
try Frame.parse.htmlAsChildren(frame, new_body_node, html);
}
const document_node = document_element.asNode();

View File

@@ -1557,9 +1557,9 @@ pub fn setHTML(self: *Node, html: []const u8, allow_declarative_shadow: bool, fr
if (html.len > 0) {
if (allow_declarative_shadow) {
try frame.parseHtmlUnsafeAsChildren(self, html);
try Frame.parse.htmlUnsafeAsChildren(frame, self, html);
} else {
try frame.parseHtmlAsChildren(self, html);
try Frame.parse.htmlAsChildren(frame, self, html);
}
}

View File

@@ -703,7 +703,7 @@ pub fn createContextualFragment(self: *const Range, html: []const u8, frame: *Fr
else
try Frame.node_factory.createElementNS(frame, .html, "div", null);
try frame.parseContextualFragment(temp_node, html);
try Frame.parse.contextualFragment(frame, temp_node, html);
// Move all parsed children to the fragment
// Keep removing first child until temp element is empty

View File

@@ -290,7 +290,7 @@ pub fn insertAdjacentHTML(
) !void {
const DocumentFragment = @import("../DocumentFragment.zig");
const fragment = (try DocumentFragment.init(frame)).asNode();
try frame.parseHtmlAsChildren(fragment, html);
try Frame.parse.htmlAsChildren(frame, fragment, html);
const target_node, const prev_node = try self.asNode().findAdjacentNodes(position, .html);

View File

@@ -26,6 +26,7 @@ const Transfer = @import("../../../network/HttpClient.zig").Transfer;
const URL = @import("../../URL.zig");
const Mime = @import("../../Mime.zig");
const Page = @import("../../Page.zig");
const Frame = @import("../../Frame.zig");
const Node = @import("../Node.zig");
const Event = @import("../Event.zig");
@@ -438,14 +439,14 @@ pub fn getResponse(self: *XMLHttpRequest, exec: *const Execution) !?Response {
.frame => |frame| {
const final: Mime = self._override_mime orelse self._response_mime orelse .{ .content_type = .text_xml };
if (final.isXML()) {
const document = (try frame.parseXmlDocument(data)) orelse return null;
const document = (try Frame.parse.xmlDocument(frame, data)) orelse return null;
break :blk .{ .document = document.asDocument() };
}
if (!final.isHTML()) {
return null;
}
const document = try exec._factory.node(Node.Document{ ._proto = undefined, ._type = .generic });
try frame.parseHtmlAsChildren(document.asNode(), data);
try Frame.parse.htmlAsChildren(frame, document.asNode(), data);
break :blk .{ .document = document };
},
.worker => return error.NotSupportedInWorker,
@@ -487,7 +488,7 @@ pub fn getResponseXML(self: *XMLHttpRequest, exec: *const Execution) !?*Node.Doc
switch (exec.js.global) {
.frame => |frame| {
const xml_document = (try frame.parseXmlDocument(self._response_data.items)) orelse return null;
const xml_document = (try Frame.parse.xmlDocument(frame, self._response_data.items)) orelse return null;
const document = xml_document.asDocument();
self._response_xml = document;
return document;