diff --git a/src/browser/Page.zig b/src/browser/Page.zig
index 2c2d7710..843ca037 100644
--- a/src/browser/Page.zig
+++ b/src/browser/Page.zig
@@ -1492,6 +1492,35 @@ pub fn createCDATASection(self: *Page, data: []const u8) !*Node {
return cd.asNode();
}
+pub fn createProcessingInstruction(self: *Page, target: []const u8, data: []const u8) !*Node {
+ // Validate target doesn't contain "?>"
+ if (std.mem.indexOf(u8, target, "?>") != null) {
+ return error.InvalidCharacterError;
+ }
+
+ // Validate target follows XML name rules (similar to attribute name validation)
+ try Element.Attribute.validateAttributeName(target);
+
+ const owned_target = try self.dupeString(target);
+ const owned_data = try self.dupeString(data);
+
+ const pi = try self._factory.create(CData.ProcessingInstruction{
+ ._proto = undefined,
+ ._target = owned_target,
+ });
+
+ const cd = try self._factory.node(CData{
+ ._proto = undefined,
+ ._type = .{ .processing_instruction = pi },
+ ._data = owned_data,
+ });
+
+ // Set up the back pointer from ProcessingInstruction to CData
+ pi._proto = cd;
+
+ return cd.asNode();
+}
+
pub fn dupeString(self: *Page, value: []const u8) ![]const u8 {
if (String.intern(value)) |v| {
return v;
diff --git a/src/browser/dump.zig b/src/browser/dump.zig
index adcef586..b1ca4b29 100644
--- a/src/browser/dump.zig
+++ b/src/browser/dump.zig
@@ -74,6 +74,12 @@ fn _deep(node: *Node, opts: Opts, comptime force_slot: bool, writer: *std.Io.Wri
try writer.writeAll("");
+ } else if (node.is(Node.CData.ProcessingInstruction)) |pi| {
+ try writer.writeAll("");
+ try writer.writeAll(pi._target);
+ try writer.writeAll(" ");
+ try writer.writeAll(cd.getData());
+ try writer.writeAll("?>");
} else {
if (shouldEscapeText(node._parent)) {
try writeEscapedText(cd.getData(), writer);
diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig
index d379ebf2..a37097e1 100644
--- a/src/browser/js/bridge.zig
+++ b/src/browser/js/bridge.zig
@@ -492,6 +492,7 @@ pub const JsApis = flattenTypes(&.{
@import("../webapi/cdata/Comment.zig"),
@import("../webapi/cdata/Text.zig"),
@import("../webapi/cdata/CDATASection.zig"),
+ @import("../webapi/cdata/ProcessingInstruction.zig"),
@import("../webapi/collections.zig"),
@import("../webapi/Console.zig"),
@import("../webapi/Crypto.zig"),
@@ -506,6 +507,7 @@ pub const JsApis = flattenTypes(&.{
@import("../webapi/css/StyleSheetList.zig"),
@import("../webapi/Document.zig"),
@import("../webapi/HTMLDocument.zig"),
+ @import("../webapi/XMLDocument.zig"),
@import("../webapi/History.zig"),
@import("../webapi/KeyValueList.zig"),
@import("../webapi/DocumentFragment.zig"),
diff --git a/src/browser/tests/domexception.html b/src/browser/tests/domexception.html
new file mode 100644
index 00000000..3f2825fd
--- /dev/null
+++ b/src/browser/tests/domexception.html
@@ -0,0 +1,135 @@
+
+
+ DOMException Test
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/browser/tests/domimplementation.html b/src/browser/tests/domimplementation.html
index 7ff1b0e0..d8980a8f 100644
--- a/src/browser/tests/domimplementation.html
+++ b/src/browser/tests/domimplementation.html
@@ -55,7 +55,177 @@
const impl = document.implementation;
const doctype = impl.createDocumentType('html', null, null);
testing.expectEqual('html', doctype.name);
- testing.expectEqual('', doctype.publicId);
- testing.expectEqual('', doctype.systemId);
+ testing.expectEqual('null', doctype.publicId);
+ testing.expectEqual('null', doctype.systemId);
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/browser/tests/legacy/dom/implementation.html b/src/browser/tests/legacy/dom/implementation.html
index 81cce804..6dcc0838 100644
--- a/src/browser/tests/legacy/dom/implementation.html
+++ b/src/browser/tests/legacy/dom/implementation.html
@@ -8,7 +8,7 @@
testing.expectEqual("[object HTMLDocument]", doc.toString());
testing.expectEqual("foo", doc.title);
testing.expectEqual("[object HTMLBodyElement]", doc.body.toString());
- testing.expectEqual("[object Document]", impl.createDocument(null, 'foo').toString());
+ testing.expectEqual("[object XMLDocument]", impl.createDocument(null, 'foo').toString());
testing.expectEqual("[object DocumentType]", impl.createDocumentType('foo', 'bar', 'baz').toString());
testing.expectEqual(true, impl.hasFeature());
diff --git a/src/browser/tests/node/replace_child.html b/src/browser/tests/node/replace_child.html
index deea60a8..18832fd9 100644
--- a/src/browser/tests/node/replace_child.html
+++ b/src/browser/tests/node/replace_child.html
@@ -24,7 +24,7 @@
testing.withError((err) => {
testing.expectEqual(3, err.code);
- testing.expectEqual("HierarchyError", err.name);
+ testing.expectEqual("HierarchyRequestError", err.name);
testing.expectEqual("Hierarchy Error", err.message);
}, () => d1.replaceChild(c4, c3));
diff --git a/src/browser/tests/processing_instruction.html b/src/browser/tests/processing_instruction.html
new file mode 100644
index 00000000..9a6ef119
--- /dev/null
+++ b/src/browser/tests/processing_instruction.html
@@ -0,0 +1,171 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/browser/webapi/CData.zig b/src/browser/webapi/CData.zig
index 82afee54..c7ac88cc 100644
--- a/src/browser/webapi/CData.zig
+++ b/src/browser/webapi/CData.zig
@@ -25,6 +25,7 @@ const Node = @import("Node.zig");
pub const Text = @import("cdata/Text.zig");
pub const Comment = @import("cdata/Comment.zig");
pub const CDATASection = @import("cdata/CDATASection.zig");
+pub const ProcessingInstruction = @import("cdata/ProcessingInstruction.zig");
const CData = @This();
@@ -38,6 +39,7 @@ pub const Type = union(enum) {
// This should be under Text, but that would require storing a _type union
// in text, which would add 8 bytes to every text node.
cdata_section: CDATASection,
+ processing_instruction: *ProcessingInstruction,
};
pub fn asNode(self: *CData) *Node {
@@ -58,6 +60,7 @@ pub fn className(self: *const CData) []const u8 {
.text => "[object Text]",
.comment => "[object Comment]",
.cdata_section => "[object CDATASection]",
+ .processing_instruction => "[object ProcessingInstruction]",
};
}
@@ -139,6 +142,7 @@ pub fn format(self: *const CData, writer: *std.io.Writer) !void {
.text => writer.print("{s}", .{self._data}),
.comment => writer.print("", .{self._data}),
.cdata_section => writer.print("", .{self._data}),
+ .processing_instruction => |pi| writer.print("{s} {s}?>", .{ pi._target, self._data }),
};
}
@@ -147,6 +151,18 @@ pub fn getLength(self: *const CData) usize {
}
pub fn isEqualNode(self: *const CData, other: *const CData) bool {
+ if (std.meta.activeTag(self._type) != std.meta.activeTag(other._type)) {
+ return false;
+ }
+
+ if (self._type == .processing_instruction) {
+ @branchHint(.unlikely);
+ if (std.mem.eql(u8, self._type.processing_instruction._target, other._type.processing_instruction._target) == false) {
+ return false;
+ }
+ // if the _targets are equal, we still want to compare the data
+ }
+
return std.mem.eql(u8, self.getData(), other.getData());
}
diff --git a/src/browser/webapi/DOMException.zig b/src/browser/webapi/DOMException.zig
index 7ae241d2..f2558752 100644
--- a/src/browser/webapi/DOMException.zig
+++ b/src/browser/webapi/DOMException.zig
@@ -16,14 +16,24 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
+const std = @import("std");
const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
const DOMException = @This();
-_code: Code = .none,
-pub fn init() DOMException {
- return .{};
+_code: Code = .none,
+_custom_message: ?[]const u8 = null,
+_custom_name: ?[]const u8 = null,
+
+pub fn init(message: ?[]const u8, name: ?[]const u8) DOMException {
+ // If name is provided, try to map it to a legacy code
+ const code = if (name) |n| Code.fromName(n) else .none;
+ return .{
+ ._code = code,
+ ._custom_message = message,
+ ._custom_name = name,
+ };
}
pub fn fromError(err: anyerror) ?DOMException {
@@ -34,6 +44,21 @@ pub fn fromError(err: anyerror) ?DOMException {
error.NotSupported => .{ ._code = .not_supported },
error.HierarchyError => .{ ._code = .hierarchy_error },
error.IndexSizeError => .{ ._code = .index_size_error },
+ error.InvalidStateError => .{ ._code = .invalid_state_error },
+ error.WrongDocument => .{ ._code = .wrong_document_error },
+ error.NoModificationAllowed => .{ ._code = .no_modification_allowed_error },
+ error.InUseAttribute => .{ ._code = .inuse_attribute_error },
+ error.InvalidModification => .{ ._code = .invalid_modification_error },
+ error.NamespaceError => .{ ._code = .namespace_error },
+ error.InvalidAccess => .{ ._code = .invalid_access_error },
+ error.SecurityError => .{ ._code = .security_error },
+ error.NetworkError => .{ ._code = .network_error },
+ error.AbortError => .{ ._code = .abort_error },
+ error.URLMismatch => .{ ._code = .url_mismatch_error },
+ error.QuotaExceeded => .{ ._code = .quota_exceeded_error },
+ error.TimeoutError => .{ ._code = .timeout_error },
+ error.InvalidNodeType => .{ ._code = .invalid_node_type_error },
+ error.DataClone => .{ ._code = .data_clone_error },
else => null,
};
}
@@ -43,18 +68,40 @@ pub fn getCode(self: *const DOMException) u8 {
}
pub fn getName(self: *const DOMException) []const u8 {
+ if (self._custom_name) |name| {
+ return name;
+ }
+
return switch (self._code) {
.none => "Error",
+ .index_size_error => "IndexSizeError",
+ .hierarchy_error => "HierarchyRequestError",
+ .wrong_document_error => "WrongDocumentError",
.invalid_character_error => "InvalidCharacterError",
- .index_size_error => "IndexSizeErorr",
- .syntax_error => "SyntaxError",
+ .no_modification_allowed_error => "NoModificationAllowedError",
.not_found => "NotFoundError",
.not_supported => "NotSupportedError",
- .hierarchy_error => "HierarchyError",
+ .inuse_attribute_error => "InUseAttributeError",
+ .invalid_state_error => "InvalidStateError",
+ .syntax_error => "SyntaxError",
+ .invalid_modification_error => "InvalidModificationError",
+ .namespace_error => "NamespaceError",
+ .invalid_access_error => "InvalidAccessError",
+ .security_error => "SecurityError",
+ .network_error => "NetworkError",
+ .abort_error => "AbortError",
+ .url_mismatch_error => "URLMismatchError",
+ .quota_exceeded_error => "QuotaExceededError",
+ .timeout_error => "TimeoutError",
+ .invalid_node_type_error => "InvalidNodeTypeError",
+ .data_clone_error => "DataCloneError",
};
}
pub fn getMessage(self: *const DOMException) []const u8 {
+ if (self._custom_message) |msg| {
+ return msg;
+ }
return switch (self._code) {
.none => "",
.invalid_character_error => "Error: Invalid Character",
@@ -63,17 +110,76 @@ pub fn getMessage(self: *const DOMException) []const u8 {
.not_supported => "Not Supported",
.not_found => "Not Found",
.hierarchy_error => "Hierarchy Error",
+ else => @tagName(self._code),
};
}
+pub fn toString(self: *const DOMException) []const u8 {
+ if (self._custom_message) |msg| {
+ return msg;
+ }
+ return switch (self._code) {
+ .none => "Error",
+ else => self.getMessage(),
+ };
+}
+
+pub fn className(_: *const DOMException) []const u8 {
+ return "[object DOMException]";
+}
+
const Code = enum(u8) {
none = 0,
index_size_error = 1,
hierarchy_error = 3,
+ wrong_document_error = 4,
invalid_character_error = 5,
+ no_modification_allowed_error = 7,
not_found = 8,
not_supported = 9,
+ inuse_attribute_error = 10,
+ invalid_state_error = 11,
syntax_error = 12,
+ invalid_modification_error = 13,
+ namespace_error = 14,
+ invalid_access_error = 15,
+ security_error = 18,
+ network_error = 19,
+ abort_error = 20,
+ url_mismatch_error = 21,
+ quota_exceeded_error = 22,
+ timeout_error = 23,
+ invalid_node_type_error = 24,
+ data_clone_error = 25,
+
+ /// Maps a standard error name to its legacy code
+ /// Returns .none (code 0) for non-legacy error names
+ pub fn fromName(name: []const u8) Code {
+ const lookup = std.StaticStringMap(Code).initComptime(.{
+ .{ "IndexSizeError", .index_size_error },
+ .{ "HierarchyRequestError", .hierarchy_error },
+ .{ "WrongDocumentError", .wrong_document_error },
+ .{ "InvalidCharacterError", .invalid_character_error },
+ .{ "NoModificationAllowedError", .no_modification_allowed_error },
+ .{ "NotFoundError", .not_found },
+ .{ "NotSupportedError", .not_supported },
+ .{ "InUseAttributeError", .inuse_attribute_error },
+ .{ "InvalidStateError", .invalid_state_error },
+ .{ "SyntaxError", .syntax_error },
+ .{ "InvalidModificationError", .invalid_modification_error },
+ .{ "NamespaceError", .namespace_error },
+ .{ "InvalidAccessError", .invalid_access_error },
+ .{ "SecurityError", .security_error },
+ .{ "NetworkError", .network_error },
+ .{ "AbortError", .abort_error },
+ .{ "URLMismatchError", .url_mismatch_error },
+ .{ "QuotaExceededError", .quota_exceeded_error },
+ .{ "TimeoutError", .timeout_error },
+ .{ "InvalidNodeTypeError", .invalid_node_type_error },
+ .{ "DataCloneError", .data_clone_error },
+ });
+ return lookup.get(name) orelse .none;
+ }
};
pub const JsApi = struct {
@@ -89,5 +195,10 @@ pub const JsApi = struct {
pub const code = bridge.accessor(DOMException.getCode, null, .{});
pub const name = bridge.accessor(DOMException.getName, null, .{});
pub const message = bridge.accessor(DOMException.getMessage, null, .{});
- pub const toString = bridge.function(DOMException.getMessage, .{});
+ pub const toString = bridge.function(DOMException.toString, .{});
};
+
+const testing = @import("../../testing.zig");
+test "WebApi: DOMException" {
+ try testing.htmlRunner("domexception.html", .{});
+}
diff --git a/src/browser/webapi/DOMImplementation.zig b/src/browser/webapi/DOMImplementation.zig
index e2a86357..467b5ae2 100644
--- a/src/browser/webapi/DOMImplementation.zig
+++ b/src/browser/webapi/DOMImplementation.zig
@@ -21,14 +21,17 @@ const std = @import("std");
const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
const Node = @import("Node.zig");
+const Document = @import("Document.zig");
+const HTMLDocument = @import("HTMLDocument.zig");
const DocumentType = @import("DocumentType.zig");
const DOMImplementation = @This();
pub fn createDocumentType(_: *const DOMImplementation, qualified_name: []const u8, public_id: ?[]const u8, system_id: ?[]const u8, page: *Page) !*DocumentType {
const name = try page.dupeString(qualified_name);
- const pub_id = try page.dupeString(public_id orelse "");
- const sys_id = try page.dupeString(system_id orelse "");
+ // Firefox converts null to the string "null", not empty string
+ const pub_id = if (public_id) |p| try page.dupeString(p) else "null";
+ const sys_id = if (system_id) |s| try page.dupeString(s) else "null";
const doctype = try page._factory.node(DocumentType{
._proto = undefined,
@@ -40,7 +43,60 @@ pub fn createDocumentType(_: *const DOMImplementation, qualified_name: []const u
return doctype;
}
-pub fn hasFeature(_: *const DOMImplementation, _: []const u8, _: ?[]const u8) bool {
+pub fn createHTMLDocument(_: *const DOMImplementation, title: ?[]const u8, page: *Page) !*Document {
+ const document = (try page._factory.document(Node.Document.HTMLDocument{ ._proto = undefined })).asDocument();
+ document._ready_state = .complete;
+
+ {
+ const doctype = try page._factory.node(DocumentType{
+ ._proto = undefined,
+ ._name = "html",
+ ._public_id = "",
+ ._system_id = "",
+ });
+ _ = try document.asNode().appendChild(doctype.asNode(), page);
+ }
+
+ const html_node = try page.createElement(null, "html", null);
+ _ = try document.asNode().appendChild(html_node, page);
+
+ const head_node = try page.createElement(null, "head", null);
+ _ = try html_node.appendChild(head_node, page);
+
+ if (title) |t| {
+ const title_node = try page.createElement(null, "title", null);
+ _ = try head_node.appendChild(title_node, page);
+ const text_node = try page.createTextNode(t);
+ _ = try title_node.appendChild(text_node, page);
+ }
+
+ const body_node = try page.createElement(null, "body", null);
+ _ = try html_node.appendChild(body_node, page);
+
+ return document;
+}
+
+pub fn createDocument(_: *const DOMImplementation, namespace: ?[]const u8, qualified_name: ?[]const u8, doctype: ?*DocumentType, page: *Page) !*Document {
+ // Create XML Document
+ const document = (try page._factory.document(Node.Document.XMLDocument{ ._proto = undefined })).asDocument();
+
+ // Append doctype if provided
+ if (doctype) |dt| {
+ _ = try document.asNode().appendChild(dt.asNode(), page);
+ }
+
+ // Create and append root element if qualified_name provided
+ if (qualified_name) |qname| {
+ if (qname.len > 0) {
+ const root = try page.createElement(namespace, qname, null);
+ _ = try document.asNode().appendChild(root, page);
+ }
+ }
+
+ return document;
+}
+
+pub fn hasFeature(_: *const DOMImplementation, _: ?[]const u8, _: ?[]const u8) bool {
// Modern DOM spec says this should always return true
// This method is deprecated and kept for compatibility only
return true;
@@ -61,6 +117,8 @@ pub const JsApi = struct {
};
pub const createDocumentType = bridge.function(DOMImplementation.createDocumentType, .{ .dom_exception = true });
+ pub const createDocument = bridge.function(DOMImplementation.createDocument, .{});
+ pub const createHTMLDocument = bridge.function(DOMImplementation.createHTMLDocument, .{});
pub const hasFeature = bridge.function(DOMImplementation.hasFeature, .{});
pub const toString = bridge.function(_toString, .{});
diff --git a/src/browser/webapi/Document.zig b/src/browser/webapi/Document.zig
index 87019a54..5f3fab95 100644
--- a/src/browser/webapi/Document.zig
+++ b/src/browser/webapi/Document.zig
@@ -35,6 +35,7 @@ const DOMImplementation = @import("DOMImplementation.zig");
const StyleSheetList = @import("css/StyleSheetList.zig");
pub const HTMLDocument = @import("HTMLDocument.zig");
+pub const XMLDocument = @import("XMLDocument.zig");
const Document = @This();
@@ -50,6 +51,7 @@ _style_sheets: ?*StyleSheetList = null,
pub const Type = union(enum) {
generic,
html: *HTMLDocument,
+ xml: *XMLDocument,
};
pub fn is(self: *Document, comptime T: type) ?*T {
@@ -59,6 +61,11 @@ pub fn is(self: *Document, comptime T: type) ?*T {
return html;
}
},
+ .xml => |xml| {
+ if (T == XMLDocument) {
+ return xml;
+ }
+ },
.generic => {},
}
return null;
@@ -83,6 +90,7 @@ pub fn getURL(_: *const Document, page: *const Page) [:0]const u8 {
pub fn getContentType(self: *const Document) []const u8 {
return switch (self._type) {
.html => "text/html",
+ .xml => "application/xml",
.generic => "application/xml",
};
}
@@ -217,6 +225,7 @@ pub fn className(self: *const Document) []const u8 {
return switch (self._type) {
.generic => "[object Document]",
.html => "[object HTMLDocument]",
+ .xml => "[object XMLDocument]",
};
}
@@ -239,10 +248,15 @@ pub fn createTextNode(_: *const Document, data: []const u8, page: *Page) !*Node
pub fn createCDATASection(self: *const Document, data: []const u8, page: *Page) !*Node {
switch (self._type) {
.html => return error.NotSupported,
+ .xml => return page.createCDATASection(data),
.generic => return page.createCDATASection(data),
}
}
+pub fn createProcessingInstruction(_: *const Document, target: []const u8, data: []const u8, page: *Page) !*Node {
+ return page.createProcessingInstruction(target, data);
+}
+
const Range = @import("Range.zig");
pub fn createRange(_: *const Document, page: *Page) !*Range {
return Range.init(page);
@@ -432,6 +446,7 @@ pub const JsApi = struct {
pub const createTextNode = bridge.function(Document.createTextNode, .{});
pub const createAttribute = bridge.function(Document.createAttribute, .{ .dom_exception = true });
pub const createCDATASection = bridge.function(Document.createCDATASection, .{ .dom_exception = true });
+ pub const createProcessingInstruction = bridge.function(Document.createProcessingInstruction, .{ .dom_exception = true });
pub const createRange = bridge.function(Document.createRange, .{});
pub const createEvent = bridge.function(Document.createEvent, .{ .dom_exception = true });
pub const createTreeWalker = bridge.function(Document.createTreeWalker, .{});
diff --git a/src/browser/webapi/DocumentType.zig b/src/browser/webapi/DocumentType.zig
index aab8052e..0bced680 100644
--- a/src/browser/webapi/DocumentType.zig
+++ b/src/browser/webapi/DocumentType.zig
@@ -71,8 +71,3 @@ pub const JsApi = struct {
return self.className();
}
};
-
-const testing = @import("../../testing.zig");
-test "WebApi: DOMImplementation" {
- try testing.htmlRunner("domimplementation.html", .{});
-}
diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig
index c47896eb..0e68d7fd 100644
--- a/src/browser/webapi/Element.zig
+++ b/src/browser/webapi/Element.zig
@@ -332,6 +332,8 @@ fn _getInnerText(self: *Element, writer: *std.Io.Writer, state: *innerTextState)
// CDATA sections should not be used within HTML. They are
// considered comments and are not displayed.
.cdata_section => {},
+ // Processing instructions are not displayed in innerText
+ .processing_instruction => {},
},
.document => {},
.document_type => {},
diff --git a/src/browser/webapi/Node.zig b/src/browser/webapi/Node.zig
index e27ca505..1495cb09 100644
--- a/src/browser/webapi/Node.zig
+++ b/src/browser/webapi/Node.zig
@@ -229,8 +229,8 @@ pub fn getTextContent(self: *Node, writer: *std.Io.Writer) error{WriteFailed}!vo
.element => {
var it = self.childrenIterator();
while (it.next()) |child| {
- // ignore comments and TODO processing instructions.
- if (child.is(CData.Comment) != null) {
+ // ignore comments and processing instructions.
+ if (child.is(CData.Comment) != null or child.is(CData.ProcessingInstruction) != null) {
continue;
}
try child.getTextContent(writer);
@@ -270,6 +270,7 @@ pub fn getNodeName(self: *const Node, buf: []u8) []const u8 {
.text => "#text",
.cdata_section => "#cdata-section",
.comment => "#comment",
+ .processing_instruction => |pi| pi._target,
},
.document => "#document",
.document_type => |dt| dt.getName(),
@@ -285,6 +286,7 @@ pub fn getNodeType(self: *const Node) u8 {
.cdata => |cd| switch (cd._type) {
.text => 3,
.cdata_section => 4,
+ .processing_instruction => 7,
.comment => 8,
},
.document => 9,
@@ -603,6 +605,7 @@ pub fn cloneNode(self: *Node, deep_: ?bool, page: *Page) error{ OutOfMemory, Str
.text => page.createTextNode(data),
.cdata_section => page.createCDATASection(data),
.comment => page.createComment(data),
+ .processing_instruction => |pi| page.createProcessingInstruction(pi._target, data),
};
},
.element => |el| return el.cloneElement(deep, page),
diff --git a/src/browser/webapi/XMLDocument.zig b/src/browser/webapi/XMLDocument.zig
new file mode 100644
index 00000000..437e4fa4
--- /dev/null
+++ b/src/browser/webapi/XMLDocument.zig
@@ -0,0 +1,52 @@
+// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
+//
+// Francis Bouvier
+// 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 js = @import("../js/js.zig");
+
+const Document = @import("Document.zig");
+const Node = @import("Node.zig");
+
+const XMLDocument = @This();
+
+_proto: *Document,
+
+pub fn asDocument(self: *XMLDocument) *Document {
+ return self._proto;
+}
+
+pub fn asNode(self: *XMLDocument) *Node {
+ return self._proto.asNode();
+}
+
+pub fn asEventTarget(self: *XMLDocument) *@import("EventTarget.zig") {
+ return self._proto.asEventTarget();
+}
+
+pub fn className(_: *const XMLDocument) []const u8 {
+ return "[object XMLDocument]";
+}
+
+pub const JsApi = struct {
+ pub const bridge = js.Bridge(XMLDocument);
+
+ pub const Meta = struct {
+ pub const name = "XMLDocument";
+ pub const prototype_chain = bridge.prototypeChain();
+ pub var class_id: bridge.ClassId = undefined;
+ };
+};
diff --git a/src/browser/webapi/cdata/ProcessingInstruction.zig b/src/browser/webapi/cdata/ProcessingInstruction.zig
new file mode 100644
index 00000000..97024d4e
--- /dev/null
+++ b/src/browser/webapi/cdata/ProcessingInstruction.zig
@@ -0,0 +1,47 @@
+// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
+//
+// Francis Bouvier
+// 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 js = @import("../../js/js.zig");
+
+const CData = @import("../CData.zig");
+
+const ProcessingInstruction = @This();
+
+_proto: *CData,
+_target: []const u8,
+
+pub fn getTarget(self: *const ProcessingInstruction) []const u8 {
+ return self._target;
+}
+
+pub const JsApi = struct {
+ pub const bridge = js.Bridge(ProcessingInstruction);
+
+ pub const Meta = struct {
+ pub const name = "ProcessingInstruction";
+ pub const prototype_chain = bridge.prototypeChain();
+ pub var class_id: bridge.ClassId = undefined;
+ };
+
+ pub const target = bridge.accessor(ProcessingInstruction.getTarget, null, .{});
+};
+
+const testing = @import("../../../testing.zig");
+test "WebApi: ProcessingInstruction" {
+ try testing.htmlRunner("processing_instruction.html", .{});
+}