Improve DOMImplementation, DocumentType and DOMException

This commit is contained in:
Karl Seguin
2025-12-16 14:58:21 +08:00
parent d26869278f
commit ea399390ef
17 changed files with 833 additions and 21 deletions

View File

@@ -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;

View File

@@ -74,6 +74,12 @@ fn _deep(node: *Node, opts: Opts, comptime force_slot: bool, writer: *std.Io.Wri
try writer.writeAll("<!--");
try writer.writeAll(cd.getData());
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);

View File

@@ -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"),

View File

@@ -0,0 +1,135 @@
<!DOCTYPE html>
<head>
<title>DOMException Test</title>
<script src="testing.js"></script>
</head>
<body>
</body>
<script id=constructor_no_args>
{
const ex = new DOMException();
testing.expectEqual('Error', ex.toString());
testing.expectEqual('Error', ex.name);
testing.expectEqual('', ex.message);
testing.expectEqual(0, ex.code);
}
</script>
<script id=constructor_with_message>
{
const ex = new DOMException('Something went wrong');
testing.expectEqual('Error', ex.name);
testing.expectEqual('Something went wrong', ex.message);
testing.expectEqual(0, ex.code);
}
</script>
<script id=constructor_with_message_and_name>
{
const ex = new DOMException('Custom error message', 'NotFoundError');
testing.expectEqual('NotFoundError', ex.name);
testing.expectEqual('Custom error message', ex.message);
testing.expectEqual(8, ex.code);
}
</script>
<script id=legacy_error_codes>
{
// Test standard errors with legacy codes
const errors = [
{ name: 'IndexSizeError', code: 1 },
{ name: 'HierarchyRequestError', code: 3 },
{ name: 'WrongDocumentError', code: 4 },
{ name: 'InvalidCharacterError', code: 5 },
{ name: 'NoModificationAllowedError', code: 7 },
{ name: 'NotFoundError', code: 8 },
{ name: 'NotSupportedError', code: 9 },
{ name: 'InUseAttributeError', code: 10 },
{ name: 'InvalidStateError', code: 11 },
{ name: 'SyntaxError', code: 12 },
{ name: 'InvalidModificationError', code: 13 },
{ name: 'NamespaceError', code: 14 },
{ name: 'InvalidAccessError', code: 15 },
{ name: 'SecurityError', code: 18 },
{ name: 'NetworkError', code: 19 },
{ name: 'AbortError', code: 20 },
{ name: 'URLMismatchError', code: 21 },
{ name: 'QuotaExceededError', code: 22 },
{ name: 'TimeoutError', code: 23 },
{ name: 'InvalidNodeTypeError', code: 24 },
{ name: 'DataCloneError', code: 25 },
];
for (const { name, code } of errors) {
const ex = new DOMException('test', name);
testing.expectEqual(name, ex.name);
testing.expectEqual(code, ex.code);
testing.expectEqual('test', ex.message);
}
}
</script>
<script id=custom_error_name>
{
// Non-standard error names should have code 0
const ex = new DOMException('Custom message', 'MyCustomError');
testing.expectEqual('MyCustomError', ex.name);
testing.expectEqual('Custom message', ex.message);
testing.expectEqual(0, ex.code);
}
</script>
<script id=modern_errors_no_code>
{
// Modern errors that don't have legacy codes
const modernErrors = [
'EncodingError',
'NotReadableError',
'UnknownError',
'ConstraintError',
'DataError',
'TransactionInactiveError',
'ReadOnlyError',
'VersionError',
'OperationError',
'NotAllowedError'
];
for (const name of modernErrors) {
const ex = new DOMException('test', name);
testing.expectEqual(name, ex.name);
testing.expectEqual(0, ex.code);
}
}
</script>
<script id=thrown_exception>
{
try {
throw new DOMException('Operation failed', 'InvalidStateError');
} catch (e) {
testing.expectEqual('InvalidStateError', e.name);
testing.expectEqual('Operation failed', e.message);
testing.expectEqual(11, e.code);
}
}
</script>
<div id="content">
<a id="link" href="foo" class="ok">OK</a>
</div>
<!-- <script id=hierarchy_error>
let link = $('#link');
let content = $('#content');
testing.withError((err) => {
const msg = "Failed to execute 'appendChild' on 'Node': The new child element contains the parent.";
testing.expectEqual(3, err.code);
testing.expectEqual(msg, err.message);
testing.expectEqual('HierarchyRequestError: ' + msg, err.toString());
testing.expectEqual(true, err instanceof DOMException);
testing.expectEqual(true, err instanceof Error);
}, () => link.appendChild(content));
</script> -->

View File

@@ -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);
}
</script>
<script id=createHTMLDocument_no_title>
{
const impl = document.implementation;
const doc = impl.createHTMLDocument();
testing.expectEqual(true, doc instanceof HTMLDocument);
testing.expectEqual(true, doc instanceof Document);
testing.expectEqual(9, doc.nodeType);
testing.expectEqual('complete', doc.readyState);
// Should have DOCTYPE
testing.expectEqual(true, doc.firstChild instanceof DocumentType);
testing.expectEqual('html', doc.firstChild.name);
// Should have html element
const html = doc.documentElement;
testing.expectEqual(true, html !== null);
testing.expectEqual('HTML', html.tagName);
// Should have head
const head = doc.head;
testing.expectEqual(true, head !== null);
testing.expectEqual('HEAD', head.tagName);
// Should have body
const body = doc.body;
testing.expectEqual(true, body !== null);
testing.expectEqual('BODY', body.tagName);
// Title should be empty when not provided
testing.expectEqual('', doc.title);
}
</script>
<script id=createHTMLDocument_with_title>
{
const impl = document.implementation;
const doc = impl.createHTMLDocument('Test Document');
testing.expectEqual('Test Document', doc.title);
// Should have title element in head
const titleElement = doc.head.querySelector('title');
testing.expectEqual(true, titleElement !== null);
testing.expectEqual('Test Document', titleElement.textContent);
}
</script>
<script id=createHTMLDocument_structure>
{
const impl = document.implementation;
const doc = impl.createHTMLDocument('My Doc');
// Verify complete structure: DOCTYPE -> html -> (head, body)
testing.expectEqual(2, doc.childNodes.length); // DOCTYPE and html
const html = doc.documentElement;
testing.expectEqual(2, html.childNodes.length); // head and body
testing.expectEqual(doc.head, html.firstChild);
testing.expectEqual(doc.body, html.lastChild);
// Head should contain only title when title is provided
testing.expectEqual(1, doc.head.childNodes.length);
testing.expectEqual('TITLE', doc.head.firstChild.tagName);
}
</script>
<script id=createHTMLDocument_manipulation>
{
const impl = document.implementation;
const doc = impl.createHTMLDocument();
// Should be able to manipulate the created document
const div = doc.createElement('div');
div.textContent = 'Hello World';
doc.body.appendChild(div);
testing.expectEqual(1, doc.body.childNodes.length);
testing.expectEqual('Hello World', doc.body.firstChild.textContent);
}
</script>
<script id=createDocument_minimal>
{
const impl = document.implementation;
const doc = impl.createDocument(null, null, null);
testing.expectEqual(true, doc instanceof Document);
testing.expectEqual(9, doc.nodeType);
testing.expectEqual('[object XMLDocument]', doc.toString());
// Should be empty - no doctype, no root element
testing.expectEqual(0, doc.childNodes.length);
testing.expectEqual(null, doc.documentElement);
}
</script>
<script id=createDocument_with_root>
{
const impl = document.implementation;
const doc = impl.createDocument(null, 'root', null);
testing.expectEqual(1, doc.childNodes.length);
const root = doc.documentElement;
testing.expectEqual(true, root !== null);
// TODO: XML documents should preserve case, but we currently uppercase
testing.expectEqual('ROOT', root.tagName);
}
</script>
<script id=createDocument_with_namespace>
{
const impl = document.implementation;
const doc = impl.createDocument('http://www.w3.org/2000/svg', 'svg', null);
const root = doc.documentElement;
testing.expectEqual('svg', root.nodeName);
testing.expectEqual('http://www.w3.org/2000/svg', root.namespaceURI);
}
</script>
<script id=createDocument_with_doctype>
{
const impl = document.implementation;
const doctype = impl.createDocumentType('svg', '-//W3C//DTD SVG 1.1//EN', 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd');
const doc = impl.createDocument('http://www.w3.org/2000/svg', 'svg', doctype);
testing.expectEqual(2, doc.childNodes.length);
// First child should be doctype
testing.expectEqual(true, doc.firstChild instanceof DocumentType);
testing.expectEqual('svg', doc.firstChild.name);
// Second child should be root element
testing.expectEqual('svg', doc.documentElement.nodeName);
}
</script>
<script id=createDocument_qualified_name>
{
const impl = document.implementation;
const doc = impl.createDocument('http://example.com', 'prefix:localName', null);
const root = doc.documentElement;
// TODO: XML documents should preserve case, but we currently uppercase
testing.expectEqual('prefix:LOCALNAME', root.tagName);
// TODO: Custom namespaces are being overridden to XHTML namespace
testing.expectEqual('http://www.w3.org/1999/xhtml', root.namespaceURI);
}
</script>
<script id=createDocument_manipulation>
{
const impl = document.implementation;
const doc = impl.createDocument(null, 'root', null);
// Should be able to manipulate the created document
const child = doc.createElement('child');
child.textContent = 'Test';
doc.documentElement.appendChild(child);
testing.expectEqual(1, doc.documentElement.childNodes.length);
// TODO: XML documents should preserve case, but we currently uppercase
testing.expectEqual('CHILD', doc.documentElement.firstChild.tagName);
testing.expectEqual('Test', doc.documentElement.firstChild.textContent);
}
</script>

View File

@@ -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());
</script>

View File

@@ -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));

View File

@@ -0,0 +1,171 @@
<!DOCTYPE html>
<script src="testing.js"></script>
<script id=basic>
{
const pi = document.createProcessingInstruction('xml-stylesheet', 'href="style.css"');
testing.expectEqual('object', typeof pi);
testing.expectEqual(true, pi instanceof ProcessingInstruction);
testing.expectEqual(true, pi instanceof Node);
}
</script>
<script id=properties>
{
const pi = document.createProcessingInstruction('foo', 'bar');
testing.expectEqual('foo', pi.target);
testing.expectEqual('bar', pi.data);
testing.expectEqual(3, pi.length);
testing.expectEqual(7, pi.nodeType);
testing.expectEqual('foo', pi.nodeName);
testing.expectEqual('bar', pi.nodeValue);
testing.expectEqual('bar', pi.textContent);
}
</script>
<script id=empty_data>
{
const pi = document.createProcessingInstruction('target', '');
testing.expectEqual('target', pi.target);
testing.expectEqual('', pi.data);
testing.expectEqual(0, pi.length);
}
</script>
<script id=set_data>
{
const pi = document.createProcessingInstruction('foo', 'bar');
pi.data = 'baz';
testing.expectEqual('baz', pi.data);
testing.expectEqual('foo', pi.target); // target shouldn't change
testing.expectEqual(3, pi.length);
}
</script>
<script id=set_textContent>
{
const pi = document.createProcessingInstruction('target', 'original');
pi.textContent = 'modified';
testing.expectEqual('modified', pi.data);
testing.expectEqual('modified', pi.textContent);
}
</script>
<script id=set_nodeValue>
{
const pi = document.createProcessingInstruction('target', 'original');
pi.nodeValue = 'changed';
testing.expectEqual('changed', pi.data);
testing.expectEqual('changed', pi.nodeValue);
}
</script>
<script id=cloneNode>
{
const pi = document.createProcessingInstruction('xml-stylesheet', 'href="style.css"');
const clone = pi.cloneNode();
testing.expectEqual('xml-stylesheet', clone.target);
testing.expectEqual('href="style.css"', clone.data);
testing.expectEqual(7, clone.nodeType);
testing.expectEqual(true, clone instanceof ProcessingInstruction);
// Clone should be a different object
testing.expectEqual(false, pi === clone);
// Modifying clone shouldn't affect original
clone.data = 'different';
testing.expectEqual('href="style.css"', pi.data);
testing.expectEqual('different', clone.data);
}
</script>
<script id=isEqualNode>
{
const pi1 = document.createProcessingInstruction('target1', 'data1');
const pi2 = document.createProcessingInstruction('target2', 'data2');
const pi3 = document.createProcessingInstruction('target1', 'data1');
const pi4 = document.createProcessingInstruction('target1', 'data2');
testing.expectEqual(true, pi1.isEqualNode(pi1));
testing.expectEqual(true, pi1.isEqualNode(pi3));
testing.expectEqual(false, pi1.isEqualNode(pi2));
testing.expectEqual(false, pi2.isEqualNode(pi3));
testing.expectEqual(false, pi1.isEqualNode(pi4)); // different data
testing.expectEqual(false, pi1.isEqualNode(document));
testing.expectEqual(false, document.isEqualNode(pi1));
}
</script>
<script id=invalid_target_question_mark_gt>
{
try {
document.createProcessingInstruction('tar?>get', 'data');
testing.fail('Should throw InvalidCharacterError for "?>" in target');
} catch (e) {
testing.expectEqual('InvalidCharacterError', e.name);
}
}
</script>
<script id=invalid_target_empty>
{
try {
document.createProcessingInstruction('', 'data');
testing.fail('Should throw InvalidCharacterError for empty target');
} catch (e) {
testing.expectEqual('InvalidCharacterError', e.name);
}
}
</script>
<script id=invalid_target_starts_with_number>
{
try {
document.createProcessingInstruction('0target', 'data');
testing.fail('Should throw InvalidCharacterError for target starting with number');
} catch (e) {
testing.expectEqual('InvalidCharacterError', e.name);
}
}
</script>
<script id=valid_target_with_colon>
{
// xml:foo should be valid
const pi = document.createProcessingInstruction('xml:stylesheet', 'data');
testing.expectEqual('xml:stylesheet', pi.target);
}
</script>
<script id=characterData_methods>
{
const pi = document.createProcessingInstruction('target', 'abcdef');
// substringData
testing.expectEqual('bcd', pi.substringData(1, 3));
testing.expectEqual('def', pi.substringData(3, 10)); // should clamp to end
// appendData
pi.appendData('ghi');
testing.expectEqual('abcdefghi', pi.data);
// insertData
pi.insertData(3, 'XXX');
testing.expectEqual('abcXXXdefghi', pi.data);
// deleteData
pi.deleteData(3, 3);
testing.expectEqual('abcdefghi', pi.data);
// replaceData
pi.replaceData(3, 3, 'YYY');
testing.expectEqual('abcYYYghi', pi.data);
}
</script>
<script id=owner_document>
{
const pi = document.createProcessingInstruction('target', 'data');
testing.expectEqual(document, pi.ownerDocument);
}
</script>

View File

@@ -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("<text>{s}</text>", .{self._data}),
.comment => writer.print("<!-- {s} -->", .{self._data}),
.cdata_section => writer.print("<![CDATA[{s}]]>", .{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());
}

View File

@@ -16,14 +16,24 @@
// 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 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", .{});
}

View File

@@ -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, .{});

View File

@@ -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, .{});

View File

@@ -71,8 +71,3 @@ pub const JsApi = struct {
return self.className();
}
};
const testing = @import("../../testing.zig");
test "WebApi: DOMImplementation" {
try testing.htmlRunner("domimplementation.html", .{});
}

View File

@@ -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 => {},

View File

@@ -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),

View File

@@ -0,0 +1,52 @@
// Copyright (C) 2023-2025 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 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;
};
};

View File

@@ -0,0 +1,47 @@
// Copyright (C) 2023-2025 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 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", .{});
}