diff --git a/src/browser/Frame.zig b/src/browser/Frame.zig index 7e13f3db37..ce78f7588b 100644 --- a/src/browser/Frame.zig +++ b/src/browser/Frame.zig @@ -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,77 +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 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; - } -} - // 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. diff --git a/src/browser/Mime.zig b/src/browser/Mime.zig index 8145f49798..72268c7fb6 100644 --- a/src/browser/Mime.zig +++ b/src/browser/Mime.zig @@ -49,6 +49,7 @@ pub const ContentTypeEnum = enum { application_json, unknown, other, + other_xml, }; pub const ContentType = union(ContentTypeEnum) { @@ -68,6 +69,7 @@ pub const ContentType = union(ContentTypeEnum) { // A valid but unrecognized type/subtype. Keeping it would require some // memory management of the input. Nothing needs it right now, so why bother. other: void, + other_xml: void, // i.e. application/xml or */*+xml }; pub fn contentTypeString(mime: *const Mime) []const u8 { @@ -346,6 +348,13 @@ pub fn isHTML(self: *const Mime) bool { return self.content_type == .text_html; } +pub fn isXML(self: *const Mime) bool { + return switch (self.content_type) { + .text_xml, .other_xml => true, + else => false, + }; +} + pub fn isText(mime: *const Mime) bool { return switch (mime.content_type) { .text_xml, .text_html, .text_javascript, .text_plain, .text_css, .text_markdown => true, @@ -378,9 +387,11 @@ fn parseContentType(value: []const u8) !struct { ContentType, usize } { @"image/webp", @"application/json", + @"application/xml", }, type_name)) |known_type| { const ct: ContentType = switch (known_type) { .@"text/xml" => .{ .text_xml = {} }, + .@"application/xml" => .{ .other_xml = {} }, .@"text/html" => .{ .text_html = {} }, .@"text/javascript", .@"application/javascript", .@"application/x-javascript" => .{ .text_javascript = {} }, .@"text/plain" => .{ .text_plain = {} }, @@ -408,6 +419,10 @@ fn parseContentType(value: []const u8) !struct { ContentType, usize } { return error.Invalid; } + if (std.mem.endsWith(u8, type_name, "+xml")) { + return .{ .{ .other_xml = {} }, attribute_start }; + } + return .{ .{ .other = {} }, attribute_start }; } @@ -897,6 +912,28 @@ test "Mime: isHTML" { try assert(false, "over/9000"); } +test "Mime: isXML" { + defer testing.reset(); + + const assert = struct { + fn assert(expected: bool, input: []const u8) !void { + const mutable_input = try testing.arena_allocator.dupe(u8, input); + var mime = try Mime.parse(mutable_input); + try testing.expectEqual(expected, mime.isXML()); + } + }.assert; + try assert(true, "text/xml"); + try assert(true, "TEXT/XML; charset=utf-8"); + try assert(true, "application/xml"); + try assert(true, "image/svg+xml"); + try assert(true, "application/xhtml+xml"); + try assert(true, "application/rss+xml;charset=utf-8"); + try assert(false, "text/html"); + try assert(false, "application/json"); + try assert(false, "application/xmlfoo"); + try assert(false, "text/xm"); +} + test "Mime: sniff" { try testing.expectEqual(null, Mime.sniff("")); try testing.expectEqual(null, Mime.sniff(" +// Pierre Tachoire +// +// 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 . + +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 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 + // declaration), skip it. + const first_child = doc_node.firstChild().?; + if (first_child.getNodeType() == 7) { + _ = try doc_node.removeChild(first_child, frame); + } + + return doc; +} diff --git a/src/browser/interactive.zig b/src/browser/interactive.zig index 8c9c51e313..d6f0928427 100644 --- a/src/browser/interactive.zig +++ b/src/browser/interactive.zig @@ -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); } diff --git a/src/browser/links.zig b/src/browser/links.zig index 45695923a6..95624ce0f5 100644 --- a/src/browser/links.zig +++ b/src/browser/links.zig @@ -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); } diff --git a/src/browser/markdown.zig b/src/browser/markdown.zig index ae702e2d4d..86cdd45f4c 100644 --- a/src/browser/markdown.zig +++ b/src/browser/markdown.zig @@ -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(), \\Link \\Img \\Space @@ -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(), "

Short

"); + try Frame.parse.htmlAsChildren(frame, div.asNode(), "

Short

"); 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(), "

" ++ ("AAAA " ** 100) ++ "

"); + try Frame.parse.htmlAsChildren(frame, div.asNode(), "

" ++ ("AAAA " ** 100) ++ "

"); 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(); diff --git a/src/browser/structured_data.zig b/src/browser/structured_data.zig index 8e5aac6141..0090f81ebd 100644 --- a/src/browser/structured_data.zig +++ b/src/browser/structured_data.zig @@ -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(), "Example"); + try Frame.parse.htmlAsChildren(frame, div.asNode(), "Example"); const data = try collectStructuredData(div.asNode(), frame.call_arena, frame); diff --git a/src/browser/tests/custom_elements/parser_wrapper_removed.html b/src/browser/tests/custom_elements/parser_wrapper_removed.html index 77c637eb56..c2ca9f62bd 100644 --- a/src/browser/tests/custom_elements/parser_wrapper_removed.html +++ b/src/browser/tests/custom_elements/parser_wrapper_removed.html @@ -2,7 +2,7 @@ -