mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-02-04 06:23:45 +00:00
Improve support for non-HTML namespace
This does a better job of tracking the implicit namespace based on the context. For example, when using DOMParser.parseFromString with an XML namespace, all subsequent elements will be in the XML namespace. Adds support for null namespace. Rather than defaulting to HTML, unknown namespaces now map to a special unknown type. We don't currently preserve the original namespace, but we're at least able to properly handle the casing in this case.
This commit is contained in:
@@ -1335,15 +1335,9 @@ pub fn adoptNodeTree(self: *Page, node: *Node, new_owner: *Document) !void {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn createElement(self: *Page, ns_: ?[]const u8, name: []const u8, attribute_iterator: anytype) !*Node {
|
||||
const namespace: Element.Namespace = blk: {
|
||||
const ns = ns_ orelse break :blk .html;
|
||||
if (std.mem.eql(u8, ns, "http://www.w3.org/2000/svg")) break :blk .svg;
|
||||
if (std.mem.eql(u8, ns, "http://www.w3.org/1998/Math/MathML")) break :blk .mathml;
|
||||
if (std.mem.eql(u8, ns, "http://www.w3.org/XML/1998/namespace")) break :blk .xml;
|
||||
break :blk .html;
|
||||
};
|
||||
|
||||
pub fn createElementNS(self: *Page, namespace: Element.Namespace, name: []const u8, attribute_iterator: anytype) !*Node {
|
||||
switch (namespace) {
|
||||
.html => {
|
||||
switch (name.len) {
|
||||
1 => switch (name[0]) {
|
||||
'p' => return self.createHtmlElementT(
|
||||
@@ -1542,10 +1536,10 @@ pub fn createElement(self: *Page, ns_: ?[]const u8, name: []const u8, attribute_
|
||||
},
|
||||
4 => switch (@as(u32, @bitCast(name[0..4].*))) {
|
||||
asUint("span") => return self.createHtmlElementT(
|
||||
Element.Html.Span,
|
||||
Element.Html.Generic,
|
||||
namespace,
|
||||
attribute_iterator,
|
||||
.{ ._proto = undefined },
|
||||
.{ ._proto = undefined, ._tag_name = String.init(undefined, "span", .{}) catch unreachable, ._tag = .span },
|
||||
),
|
||||
asUint("meta") => return self.createHtmlElementT(
|
||||
Element.Html.Meta,
|
||||
@@ -1889,22 +1883,6 @@ pub fn createElement(self: *Page, ns_: ?[]const u8, name: []const u8, attribute_
|
||||
else => {},
|
||||
}
|
||||
|
||||
if (namespace == .svg) {
|
||||
const tag_name = try String.init(self.arena, name, .{});
|
||||
if (std.ascii.eqlIgnoreCase(name, "svg")) {
|
||||
return self.createSvgElementT(Element.Svg, name, attribute_iterator, .{
|
||||
._proto = undefined,
|
||||
._type = .svg,
|
||||
._tag_name = tag_name,
|
||||
});
|
||||
}
|
||||
|
||||
// Other SVG elements (rect, circle, text, g, etc.)
|
||||
const lower = std.ascii.lowerString(&self.buf, name);
|
||||
const tag = std.meta.stringToEnum(Element.Tag, lower) orelse .unknown;
|
||||
return self.createSvgElementT(Element.Svg.Generic, name, attribute_iterator, .{ ._proto = undefined, ._tag = tag });
|
||||
}
|
||||
|
||||
const tag_name = try String.init(self.arena, name, .{});
|
||||
|
||||
// Check if this is a custom element (must have hyphen for HTML namespace)
|
||||
@@ -1954,6 +1932,27 @@ pub fn createElement(self: *Page, ns_: ?[]const u8, name: []const u8, attribute_
|
||||
}
|
||||
|
||||
return self.createHtmlElementT(Element.Html.Unknown, namespace, attribute_iterator, .{ ._proto = undefined, ._tag_name = tag_name });
|
||||
},
|
||||
.svg => {
|
||||
const tag_name = try String.init(self.arena, name, .{});
|
||||
if (std.ascii.eqlIgnoreCase(name, "svg")) {
|
||||
return self.createSvgElementT(Element.Svg, name, attribute_iterator, .{
|
||||
._proto = undefined,
|
||||
._type = .svg,
|
||||
._tag_name = tag_name,
|
||||
});
|
||||
}
|
||||
|
||||
// Other SVG elements (rect, circle, text, g, etc.)
|
||||
const lower = std.ascii.lowerString(&self.buf, name);
|
||||
const tag = std.meta.stringToEnum(Element.Tag, lower) orelse .unknown;
|
||||
return self.createSvgElementT(Element.Svg.Generic, name, attribute_iterator, .{ ._proto = undefined, ._tag = tag });
|
||||
},
|
||||
else => {
|
||||
const tag_name = try String.init(self.arena, name, .{});
|
||||
return self.createHtmlElementT(Element.Html.Unknown, namespace, attribute_iterator, .{ ._proto = undefined, ._tag_name = tag_name });
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn createHtmlElementT(self: *Page, comptime E: type, namespace: Element.Namespace, attribute_iterator: anytype, html_element: E) !*Node {
|
||||
|
||||
@@ -104,7 +104,7 @@ pub fn parseXML(self: *Parser, xml: []const u8) void {
|
||||
xml.len,
|
||||
&self.container,
|
||||
self,
|
||||
createElementCallback,
|
||||
createXMLElementCallback,
|
||||
getDataCallback,
|
||||
appendCallback,
|
||||
parseErrorCallback,
|
||||
@@ -225,17 +225,26 @@ fn _popCallback(self: *Parser, node: *Node) !void {
|
||||
}
|
||||
|
||||
fn createElementCallback(ctx: *anyopaque, data: *anyopaque, qname: h5e.QualName, attributes: h5e.AttributeIterator) callconv(.c) ?*anyopaque {
|
||||
return _createElementCallbackWithDefaultnamespace(ctx, data, qname, attributes, .unknown);
|
||||
}
|
||||
|
||||
fn createXMLElementCallback(ctx: *anyopaque, data: *anyopaque, qname: h5e.QualName, attributes: h5e.AttributeIterator) callconv(.c) ?*anyopaque {
|
||||
return _createElementCallbackWithDefaultnamespace(ctx, data, qname, attributes, .xml);
|
||||
}
|
||||
|
||||
fn _createElementCallbackWithDefaultnamespace(ctx: *anyopaque, data: *anyopaque, qname: h5e.QualName, attributes: h5e.AttributeIterator, default_namespace: Element.Namespace) ?*anyopaque {
|
||||
const self: *Parser = @ptrCast(@alignCast(ctx));
|
||||
return self._createElementCallback(data, qname, attributes) catch |err| {
|
||||
return self._createElementCallback(data, qname, attributes, default_namespace) catch |err| {
|
||||
self.err = .{ .err = err, .source = .create_element };
|
||||
return null;
|
||||
};
|
||||
}
|
||||
fn _createElementCallback(self: *Parser, data: *anyopaque, qname: h5e.QualName, attributes: h5e.AttributeIterator) !*anyopaque {
|
||||
fn _createElementCallback(self: *Parser, data: *anyopaque, qname: h5e.QualName, attributes: h5e.AttributeIterator, default_namespace: Element.Namespace) !*anyopaque {
|
||||
const page = self.page;
|
||||
const name = qname.local.slice();
|
||||
const namespace = qname.ns.slice();
|
||||
const node = try page.createElement(namespace, name, attributes);
|
||||
const namespace_string = qname.ns.slice();
|
||||
const namespace = if (namespace_string.len == 0) default_namespace else Element.Namespace.parse(namespace_string);
|
||||
const node = try page.createElementNS(namespace, name, attributes);
|
||||
|
||||
const pn = try self.arena.create(ParsedNode);
|
||||
pn.* = .{
|
||||
|
||||
@@ -19,12 +19,13 @@
|
||||
testing.expectEqual('http://www.w3.org/XML/1998/namespace', xmlElement.namespaceURI);
|
||||
|
||||
const nullNsElement = document.createElementNS(null, 'span');
|
||||
testing.expectEqual('SPAN', nullNsElement.tagName);
|
||||
testing.expectEqual('http://www.w3.org/1999/xhtml', nullNsElement.namespaceURI);
|
||||
testing.expectEqual('span', nullNsElement.tagName);
|
||||
testing.expectEqual(null, nullNsElement.namespaceURI);
|
||||
|
||||
const unknownNsElement = document.createElementNS('http://example.com/unknown', 'custom');
|
||||
testing.expectEqual('CUSTOM', unknownNsElement.tagName);
|
||||
testing.expectEqual('http://www.w3.org/1999/xhtml', unknownNsElement.namespaceURI);
|
||||
testing.expectEqual('custom', unknownNsElement.tagName);
|
||||
// Should be http://example.com/unknown
|
||||
testing.expectEqual('http://lightpanda.io/unsupported/namespace', unknownNsElement.namespaceURI);
|
||||
|
||||
const regularDiv = document.createElement('div');
|
||||
testing.expectEqual('DIV', regularDiv.tagName);
|
||||
@@ -36,5 +37,5 @@
|
||||
testing.expectEqual('te:ST', custom.tagName);
|
||||
testing.expectEqual('te', custom.prefix);
|
||||
testing.expectEqual('ST', custom.localName);
|
||||
testing.expectEqual('http://www.w3.org/1999/xhtml', custom.namespaceURI); // Should be test
|
||||
testing.expectEqual('http://lightpanda.io/unsupported/namespace', custom.namespaceURI); // Should be test
|
||||
</script>
|
||||
|
||||
@@ -168,7 +168,7 @@
|
||||
const root = doc.documentElement;
|
||||
testing.expectEqual(true, root !== null);
|
||||
// TODO: XML documents should preserve case, but we currently uppercase
|
||||
testing.expectEqual('ROOT', root.tagName);
|
||||
testing.expectEqual('root', root.tagName);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -206,10 +206,9 @@
|
||||
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);
|
||||
testing.expectEqual('prefix:localName', root.tagName);
|
||||
// TODO: Custom namespaces are being replaced with an empty value
|
||||
testing.expectEqual('http://lightpanda.io/unsupported/namespace', root.namespaceURI);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -224,8 +223,7 @@
|
||||
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('child', doc.documentElement.firstChild.tagName);
|
||||
testing.expectEqual('Test', doc.documentElement.firstChild.textContent);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -364,14 +364,14 @@
|
||||
];
|
||||
|
||||
for (const mime of mimes) {
|
||||
const doc = parser.parseFromString(sampleXML, "text/xml");
|
||||
const doc = parser.parseFromString(sampleXML, mime);
|
||||
const { firstChild: { childNodes, children: collection, tagName }, children } = doc;
|
||||
// doc.
|
||||
testing.expectEqual(true, doc instanceof XMLDocument);
|
||||
testing.expectEqual(1, children.length);
|
||||
// firstChild.
|
||||
// TODO: Modern browsers expect this in lowercase.
|
||||
testing.expectEqual("CATALOG", tagName);
|
||||
testing.expectEqual("catalog", tagName);
|
||||
testing.expectEqual(25, childNodes.length);
|
||||
testing.expectEqual(12, collection.length);
|
||||
// Check children of first child.
|
||||
@@ -379,12 +379,12 @@
|
||||
const {children: elements, id} = collection.item(i);
|
||||
testing.expectEqual("bk" + (100 + i + 1), id);
|
||||
// TODO: Modern browsers expect these in lowercase.
|
||||
testing.expectEqual("AUTHOR", elements.item(0).tagName);
|
||||
testing.expectEqual("TITLE", elements.item(1).tagName);
|
||||
testing.expectEqual("GENRE", elements.item(2).tagName);
|
||||
testing.expectEqual("PRICE", elements.item(3).tagName);
|
||||
testing.expectEqual("PUBLISH_DATE", elements.item(4).tagName);
|
||||
testing.expectEqual("DESCRIPTION", elements.item(5).tagName);
|
||||
testing.expectEqual("author", elements.item(0).tagName);
|
||||
testing.expectEqual("title", elements.item(1).tagName);
|
||||
testing.expectEqual("genre", elements.item(2).tagName);
|
||||
testing.expectEqual("price", elements.item(3).tagName);
|
||||
testing.expectEqual("publish_date", elements.item(4).tagName);
|
||||
testing.expectEqual("description", elements.item(5).tagName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,26 +57,26 @@ pub fn createHTMLDocument(_: *const DOMImplementation, title: ?[]const u8, page:
|
||||
_ = try document.asNode().appendChild(doctype.asNode(), page);
|
||||
}
|
||||
|
||||
const html_node = try page.createElement(null, "html", null);
|
||||
const html_node = try page.createElementNS(.html, "html", null);
|
||||
_ = try document.asNode().appendChild(html_node, page);
|
||||
|
||||
const head_node = try page.createElement(null, "head", null);
|
||||
const head_node = try page.createElementNS(.html, "head", null);
|
||||
_ = try html_node.appendChild(head_node, page);
|
||||
|
||||
if (title) |t| {
|
||||
const title_node = try page.createElement(null, "title", null);
|
||||
const title_node = try page.createElementNS(.html, "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);
|
||||
const body_node = try page.createElementNS(.html, "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 {
|
||||
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();
|
||||
|
||||
@@ -88,7 +88,8 @@ pub fn createDocument(_: *const DOMImplementation, namespace: ?[]const u8, quali
|
||||
// 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);
|
||||
const namespace = if (namespace_) |ns| Node.Element.Namespace.parse(ns) else .xml;
|
||||
const root = try page.createElementNS(namespace, qname, null);
|
||||
_ = try document.asNode().appendChild(root, page);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +124,14 @@ const CreateElementOptions = struct {
|
||||
};
|
||||
|
||||
pub fn createElement(self: *Document, name: []const u8, options_: ?CreateElementOptions, page: *Page) !*Element {
|
||||
const node = try page.createElement(null, name, null);
|
||||
const namespace: Element.Namespace = blk: {
|
||||
if (self._type == .xml) {
|
||||
@branchHint(.unlikely);
|
||||
break :blk .xml;
|
||||
}
|
||||
break :blk .html;
|
||||
};
|
||||
const node = try page.createElementNS(namespace, name, null);
|
||||
const element = node.as(Element);
|
||||
|
||||
// Track owner document if it's not the main document
|
||||
@@ -142,7 +149,7 @@ pub fn createElement(self: *Document, name: []const u8, options_: ?CreateElement
|
||||
}
|
||||
|
||||
pub fn createElementNS(self: *Document, namespace: ?[]const u8, name: []const u8, page: *Page) !*Element {
|
||||
const node = try page.createElement(namespace, name, null);
|
||||
const node = try page.createElementNS(Element.Namespace.parse(namespace), name, null);
|
||||
|
||||
// Track owner document if it's not the main document
|
||||
if (self != page.document) {
|
||||
|
||||
@@ -53,15 +53,41 @@ pub const Namespace = enum(u8) {
|
||||
svg,
|
||||
mathml,
|
||||
xml,
|
||||
// We should keep the original value, but don't. If this becomes important
|
||||
// consider storing it in a page lookup, like `_element_class_lists`, rather
|
||||
// that adding a slice directly here (directly in every element).
|
||||
unknown,
|
||||
null,
|
||||
|
||||
pub fn toUri(self: Namespace) []const u8 {
|
||||
pub fn toUri(self: Namespace) ?[]const u8 {
|
||||
return switch (self) {
|
||||
.html => "http://www.w3.org/1999/xhtml",
|
||||
.svg => "http://www.w3.org/2000/svg",
|
||||
.mathml => "http://www.w3.org/1998/Math/MathML",
|
||||
.xml => "http://www.w3.org/XML/1998/namespace",
|
||||
.unknown => "http://lightpanda.io/unsupported/namespace",
|
||||
.null => null,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn parse(namespace_: ?[]const u8) Namespace {
|
||||
const namespace = namespace_ orelse return .null;
|
||||
if (namespace.len == "http://www.w3.org/1999/xhtml".len) {
|
||||
// Common case, avoid the string comparion. Recklessly
|
||||
@branchHint(.likely);
|
||||
return .html;
|
||||
}
|
||||
if (std.mem.eql(u8, namespace, "http://www.w3.org/XML/1998/namespace")) {
|
||||
return .xml;
|
||||
}
|
||||
if (std.mem.eql(u8, namespace, "http://www.w3.org/2000/svg")) {
|
||||
return .svg;
|
||||
}
|
||||
if (std.mem.eql(u8, namespace, "http://www.w3.org/1998/Math/MathML")) {
|
||||
return .mathml;
|
||||
}
|
||||
return .unknown;
|
||||
}
|
||||
};
|
||||
|
||||
_type: Type,
|
||||
@@ -211,13 +237,8 @@ pub fn getTagNameLower(self: *const Element) []const u8 {
|
||||
}
|
||||
|
||||
pub fn getTagNameSpec(self: *const Element, buf: []u8) []const u8 {
|
||||
switch (self._type) {
|
||||
return switch (self._type) {
|
||||
.html => |he| switch (he._type) {
|
||||
.custom => |e| {
|
||||
@branchHint(.unlikely);
|
||||
return upperTagName(&e._tag_name, buf);
|
||||
},
|
||||
else => return switch (he._type) {
|
||||
.anchor => "A",
|
||||
.body => "BODY",
|
||||
.br => "BR",
|
||||
@@ -259,12 +280,11 @@ pub fn getTagNameSpec(self: *const Element, buf: []u8) []const u8 {
|
||||
.ul => "UL",
|
||||
.unknown => |e| switch (self._namespace) {
|
||||
.html => upperTagName(&e._tag_name, buf),
|
||||
.svg, .xml, .mathml => return e._tag_name.str(),
|
||||
.svg, .xml, .mathml, .unknown, .null => e._tag_name.str(),
|
||||
},
|
||||
},
|
||||
},
|
||||
.svg => |svg| return svg._tag_name.str(),
|
||||
}
|
||||
.svg => |svg| svg._tag_name.str(),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn getTagNameDump(self: *const Element) []const u8 {
|
||||
@@ -274,7 +294,7 @@ pub fn getTagNameDump(self: *const Element) []const u8 {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn getNamespaceURI(self: *const Element) []const u8 {
|
||||
pub fn getNamespaceURI(self: *const Element) ?[]const u8 {
|
||||
return self._namespace.toUri();
|
||||
}
|
||||
|
||||
@@ -996,9 +1016,7 @@ pub fn getElementsByClassName(self: *Element, class_name: []const u8, page: *Pag
|
||||
|
||||
pub fn cloneElement(self: *Element, deep: bool, page: *Page) !*Node {
|
||||
const tag_name = self.getTagNameDump();
|
||||
const namespace_uri = self.getNamespaceURI();
|
||||
|
||||
const node = try page.createElement(namespace_uri, tag_name, self._attributes);
|
||||
const node = try page.createElementNS(self._namespace, tag_name, self._attributes);
|
||||
|
||||
// Allow element-specific types to copy their runtime state
|
||||
_ = Element.Build.call(node.as(Element), "cloned", .{ self, node.as(Element), page }) catch |err| {
|
||||
|
||||
@@ -132,7 +132,7 @@ pub fn setTitle(self: *HTMLDocument, title: []const u8, page: *Page) !void {
|
||||
}
|
||||
|
||||
// No title element found, create one
|
||||
const title_node = try page.createElement(null, "title", null);
|
||||
const title_node = try page.createElementNS(.html, "title", null);
|
||||
const title_element = title_node.as(Element);
|
||||
|
||||
// Only add text if non-empty
|
||||
|
||||
@@ -443,9 +443,9 @@ pub fn createContextualFragment(self: *const Range, html: []const u8, page: *Pag
|
||||
// Create a temporary element of the same type as the context for parsing
|
||||
// This preserves the parsing context without modifying the original node
|
||||
const temp_node = if (context_node.is(Node.Element)) |el|
|
||||
try page.createElement(el._namespace.toUri(), el.getTagNameLower(), null)
|
||||
try page.createElementNS(el._namespace, el.getTagNameLower(), null)
|
||||
else
|
||||
try page.createElement(null, "div", null);
|
||||
try page.createElementNS(.html, "div", null);
|
||||
|
||||
try page.parseHtmlAsChildren(temp_node, html);
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ const Image = @This();
|
||||
_proto: *HtmlElement,
|
||||
|
||||
pub fn constructor(w_: ?u32, h_: ?u32, page: *Page) !*Image {
|
||||
const node = try page.createElement(null, "img", null);
|
||||
const node = try page.createElementNS(.html, "img", null);
|
||||
const el = node.as(Element);
|
||||
|
||||
if (w_) |w| blk: {
|
||||
|
||||
Reference in New Issue
Block a user