From 9d498fa0698a889dbc885642e7db7dfac1432605 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 8 Jan 2026 20:38:23 +0800 Subject: [PATCH] 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. --- src/browser/Page.zig | 1221 ++++++++--------- src/browser/parser/Parser.zig | 19 +- .../tests/document/create_element_ns.html | 11 +- src/browser/tests/domimplementation.html | 12 +- src/browser/tests/domparser.html | 16 +- src/browser/webapi/DOMImplementation.zig | 13 +- src/browser/webapi/Document.zig | 11 +- src/browser/webapi/Element.zig | 128 +- src/browser/webapi/HTMLDocument.zig | 2 +- src/browser/webapi/Range.zig | 4 +- src/browser/webapi/element/html/Image.zig | 2 +- 11 files changed, 736 insertions(+), 703 deletions(-) diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 77e2d971..9b4fe704 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -1335,625 +1335,624 @@ 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( + Element.Html.Paragraph, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + 'a' => return self.createHtmlElementT( + Element.Html.Anchor, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + 'b' => return self.createHtmlElementT( + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = String.init(undefined, "b", .{}) catch unreachable, ._tag = .b }, + ), + 'i' => return self.createHtmlElementT( + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = String.init(undefined, "i", .{}) catch unreachable, ._tag = .i }, + ), + 's' => return self.createHtmlElementT( + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = String.init(undefined, "s", .{}) catch unreachable, ._tag = .s }, + ), + else => {}, + }, + 2 => switch (@as(u16, @bitCast(name[0..2].*))) { + asUint("br") => return self.createHtmlElementT( + Element.Html.BR, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("ol") => return self.createHtmlElementT( + Element.Html.OL, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("ul") => return self.createHtmlElementT( + Element.Html.UL, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("li") => return self.createHtmlElementT( + Element.Html.LI, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("h1") => return self.createHtmlElementT( + Element.Html.Heading, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = String.init(undefined, "h1", .{}) catch unreachable, ._tag = .h1 }, + ), + asUint("h2") => return self.createHtmlElementT( + Element.Html.Heading, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = String.init(undefined, "h2", .{}) catch unreachable, ._tag = .h2 }, + ), + asUint("h3") => return self.createHtmlElementT( + Element.Html.Heading, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = String.init(undefined, "h3", .{}) catch unreachable, ._tag = .h3 }, + ), + asUint("h4") => return self.createHtmlElementT( + Element.Html.Heading, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = String.init(undefined, "h4", .{}) catch unreachable, ._tag = .h4 }, + ), + asUint("h5") => return self.createHtmlElementT( + Element.Html.Heading, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = String.init(undefined, "h5", .{}) catch unreachable, ._tag = .h5 }, + ), + asUint("h6") => return self.createHtmlElementT( + Element.Html.Heading, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = String.init(undefined, "h6", .{}) catch unreachable, ._tag = .h6 }, + ), + asUint("hr") => return self.createHtmlElementT( + Element.Html.HR, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("em") => return self.createHtmlElementT( + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = String.init(undefined, "em", .{}) catch unreachable, ._tag = .em }, + ), + asUint("dd") => return self.createHtmlElementT( + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = String.init(undefined, "dd", .{}) catch unreachable, ._tag = .dd }, + ), + asUint("dl") => return self.createHtmlElementT( + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = String.init(undefined, "dl", .{}) catch unreachable, ._tag = .dl }, + ), + asUint("dt") => return self.createHtmlElementT( + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = String.init(undefined, "dt", .{}) catch unreachable, ._tag = .dt }, + ), + asUint("td") => return self.createHtmlElementT( + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = String.init(undefined, "td", .{}) catch unreachable, ._tag = .td }, + ), + asUint("th") => return self.createHtmlElementT( + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = String.init(undefined, "th", .{}) catch unreachable, ._tag = .th }, + ), + asUint("tr") => return self.createHtmlElementT( + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = String.init(undefined, "tr", .{}) catch unreachable, ._tag = .tr }, + ), + else => {}, + }, + 3 => switch (@as(u24, @bitCast(name[0..3].*))) { + asUint("div") => return self.createHtmlElementT( + Element.Html.Div, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("img") => return self.createHtmlElementT( + Element.Html.Image, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("nav") => return self.createHtmlElementT( + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = String.init(undefined, "nav", .{}) catch unreachable, ._tag = .nav }, + ), + asUint("del") => return self.createHtmlElementT( + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = String.init(undefined, "del", .{}) catch unreachable, ._tag = .del }, + ), + asUint("ins") => return self.createHtmlElementT( + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = String.init(undefined, "ins", .{}) catch unreachable, ._tag = .ins }, + ), + asUint("sub") => return self.createHtmlElementT( + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = String.init(undefined, "sub", .{}) catch unreachable, ._tag = .sub }, + ), + asUint("sup") => return self.createHtmlElementT( + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = String.init(undefined, "sup", .{}) catch unreachable, ._tag = .sup }, + ), + asUint("dfn") => return self.createHtmlElementT( + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = String.init(undefined, "dfn", .{}) catch unreachable, ._tag = .dfn }, + ), + else => {}, + }, + 4 => switch (@as(u32, @bitCast(name[0..4].*))) { + asUint("span") => return self.createHtmlElementT( + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = String.init(undefined, "span", .{}) catch unreachable, ._tag = .span }, + ), + asUint("meta") => return self.createHtmlElementT( + Element.Html.Meta, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("link") => return self.createHtmlElementT( + Element.Html.Link, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("slot") => return self.createHtmlElementT( + Element.Html.Slot, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("html") => return self.createHtmlElementT( + Element.Html.Html, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("head") => return self.createHtmlElementT( + Element.Html.Head, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("body") => return self.createHtmlElementT( + Element.Html.Body, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("form") => return self.createHtmlElementT( + Element.Html.Form, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("main") => return self.createHtmlElementT( + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = String.init(undefined, "main", .{}) catch unreachable, ._tag = .main }, + ), + asUint("data") => return self.createHtmlElementT( + Element.Html.Data, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("base") => { + const n = try self.createHtmlElementT( + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = String.init(undefined, "base", .{}) catch unreachable, ._tag = .base }, + ); - switch (name.len) { - 1 => switch (name[0]) { - 'p' => return self.createHtmlElementT( - Element.Html.Paragraph, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - 'a' => return self.createHtmlElementT( - Element.Html.Anchor, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - 'b' => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = String.init(undefined, "b", .{}) catch unreachable, ._tag = .b }, - ), - 'i' => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = String.init(undefined, "i", .{}) catch unreachable, ._tag = .i }, - ), - 's' => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = String.init(undefined, "s", .{}) catch unreachable, ._tag = .s }, - ), - else => {}, - }, - 2 => switch (@as(u16, @bitCast(name[0..2].*))) { - asUint("br") => return self.createHtmlElementT( - Element.Html.BR, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("ol") => return self.createHtmlElementT( - Element.Html.OL, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("ul") => return self.createHtmlElementT( - Element.Html.UL, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("li") => return self.createHtmlElementT( - Element.Html.LI, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("h1") => return self.createHtmlElementT( - Element.Html.Heading, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = String.init(undefined, "h1", .{}) catch unreachable, ._tag = .h1 }, - ), - asUint("h2") => return self.createHtmlElementT( - Element.Html.Heading, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = String.init(undefined, "h2", .{}) catch unreachable, ._tag = .h2 }, - ), - asUint("h3") => return self.createHtmlElementT( - Element.Html.Heading, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = String.init(undefined, "h3", .{}) catch unreachable, ._tag = .h3 }, - ), - asUint("h4") => return self.createHtmlElementT( - Element.Html.Heading, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = String.init(undefined, "h4", .{}) catch unreachable, ._tag = .h4 }, - ), - asUint("h5") => return self.createHtmlElementT( - Element.Html.Heading, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = String.init(undefined, "h5", .{}) catch unreachable, ._tag = .h5 }, - ), - asUint("h6") => return self.createHtmlElementT( - Element.Html.Heading, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = String.init(undefined, "h6", .{}) catch unreachable, ._tag = .h6 }, - ), - asUint("hr") => return self.createHtmlElementT( - Element.Html.HR, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("em") => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = String.init(undefined, "em", .{}) catch unreachable, ._tag = .em }, - ), - asUint("dd") => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = String.init(undefined, "dd", .{}) catch unreachable, ._tag = .dd }, - ), - asUint("dl") => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = String.init(undefined, "dl", .{}) catch unreachable, ._tag = .dl }, - ), - asUint("dt") => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = String.init(undefined, "dt", .{}) catch unreachable, ._tag = .dt }, - ), - asUint("td") => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = String.init(undefined, "td", .{}) catch unreachable, ._tag = .td }, - ), - asUint("th") => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = String.init(undefined, "th", .{}) catch unreachable, ._tag = .th }, - ), - asUint("tr") => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = String.init(undefined, "tr", .{}) catch unreachable, ._tag = .tr }, - ), - else => {}, - }, - 3 => switch (@as(u24, @bitCast(name[0..3].*))) { - asUint("div") => return self.createHtmlElementT( - Element.Html.Div, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("img") => return self.createHtmlElementT( - Element.Html.Image, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("nav") => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = String.init(undefined, "nav", .{}) catch unreachable, ._tag = .nav }, - ), - asUint("del") => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = String.init(undefined, "del", .{}) catch unreachable, ._tag = .del }, - ), - asUint("ins") => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = String.init(undefined, "ins", .{}) catch unreachable, ._tag = .ins }, - ), - asUint("sub") => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = String.init(undefined, "sub", .{}) catch unreachable, ._tag = .sub }, - ), - asUint("sup") => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = String.init(undefined, "sup", .{}) catch unreachable, ._tag = .sup }, - ), - asUint("dfn") => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = String.init(undefined, "dfn", .{}) catch unreachable, ._tag = .dfn }, - ), - else => {}, - }, - 4 => switch (@as(u32, @bitCast(name[0..4].*))) { - asUint("span") => return self.createHtmlElementT( - Element.Html.Span, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("meta") => return self.createHtmlElementT( - Element.Html.Meta, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("link") => return self.createHtmlElementT( - Element.Html.Link, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("slot") => return self.createHtmlElementT( - Element.Html.Slot, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("html") => return self.createHtmlElementT( - Element.Html.Html, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("head") => return self.createHtmlElementT( - Element.Html.Head, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("body") => return self.createHtmlElementT( - Element.Html.Body, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("form") => return self.createHtmlElementT( - Element.Html.Form, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("main") => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = String.init(undefined, "main", .{}) catch unreachable, ._tag = .main }, - ), - asUint("data") => return self.createHtmlElementT( - Element.Html.Data, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("base") => { - const n = try self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = String.init(undefined, "base", .{}) catch unreachable, ._tag = .base }, - ); + // If page's base url is not already set, fill it with the base + // tag. + if (self.base_url == null) { + if (n.as(Element).getAttributeSafe("href")) |href| { + self.base_url = try URL.resolve(self.arena, self.url, href, .{}); + } + } - // If page's base url is not already set, fill it with the base - // tag. - if (self.base_url == null) { - if (n.as(Element).getAttributeSafe("href")) |href| { - self.base_url = try URL.resolve(self.arena, self.url, href, .{}); + return n; + }, + asUint("menu") => return self.createHtmlElementT( + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = String.init(undefined, "menu", .{}) catch unreachable, ._tag = .menu }, + ), + asUint("area") => return self.createHtmlElementT( + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = String.init(undefined, "area", .{}) catch unreachable, ._tag = .area }, + ), + asUint("code") => return self.createHtmlElementT( + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = String.init(undefined, "code", .{}) catch unreachable, ._tag = .code }, + ), + asUint("time") => return self.createHtmlElementT( + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = String.init(undefined, "time", .{}) catch unreachable, ._tag = .time }, + ), + else => {}, + }, + 5 => switch (@as(u40, @bitCast(name[0..5].*))) { + asUint("input") => return self.createHtmlElementT( + Element.Html.Input, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("style") => return self.createHtmlElementT( + Element.Html.Style, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("title") => return self.createHtmlElementT( + Element.Html.Title, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("embed") => return self.createHtmlElementT( + Element.Html.Embed, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("audio") => return self.createHtmlMediaElementT( + Element.Html.Media.Audio, + namespace, + attribute_iterator, + ), + asUint("video") => return self.createHtmlMediaElementT( + Element.Html.Media.Video, + namespace, + attribute_iterator, + ), + asUint("aside") => return self.createHtmlElementT( + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = String.init(undefined, "aside", .{}) catch unreachable, ._tag = .aside }, + ), + asUint("meter") => return self.createHtmlElementT( + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = String.init(undefined, "meter", .{}) catch unreachable, ._tag = .meter }, + ), + asUint("table") => return self.createHtmlElementT( + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = String.init(undefined, "table", .{}) catch unreachable, ._tag = .table }, + ), + asUint("thead") => return self.createHtmlElementT( + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = String.init(undefined, "thead", .{}) catch unreachable, ._tag = .thead }, + ), + asUint("tbody") => return self.createHtmlElementT( + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = String.init(undefined, "tbody", .{}) catch unreachable, ._tag = .tbody }, + ), + asUint("tfoot") => return self.createHtmlElementT( + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = String.init(undefined, "tfoot", .{}) catch unreachable, ._tag = .tfoot }, + ), + else => {}, + }, + 6 => switch (@as(u48, @bitCast(name[0..6].*))) { + asUint("script") => return self.createHtmlElementT( + Element.Html.Script, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("button") => return self.createHtmlElementT( + Element.Html.Button, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("canvas") => return self.createHtmlElementT( + Element.Html.Canvas, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("dialog") => return self.createHtmlElementT( + Element.Html.Dialog, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("strong") => return self.createHtmlElementT( + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = String.init(undefined, "strong", .{}) catch unreachable, ._tag = .strong }, + ), + asUint("header") => return self.createHtmlElementT( + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = String.init(undefined, "header", .{}) catch unreachable, ._tag = .header }, + ), + asUint("footer") => return self.createHtmlElementT( + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = String.init(undefined, "footer", .{}) catch unreachable, ._tag = .footer }, + ), + asUint("select") => return self.createHtmlElementT( + Element.Html.Select, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("option") => return self.createHtmlElementT( + Element.Html.Option, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("iframe") => return self.createHtmlElementT( + Element.Html.IFrame, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("figure") => return self.createHtmlElementT( + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = String.init(undefined, "figure", .{}) catch unreachable, ._tag = .figure }, + ), + asUint("hgroup") => return self.createHtmlElementT( + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = String.init(undefined, "hgroup", .{}) catch unreachable, ._tag = .hgroup }, + ), + else => {}, + }, + 7 => switch (@as(u56, @bitCast(name[0..7].*))) { + asUint("section") => return self.createHtmlElementT( + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = String.init(undefined, "section", .{}) catch unreachable, ._tag = .section }, + ), + asUint("article") => return self.createHtmlElementT( + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = String.init(undefined, "article", .{}) catch unreachable, ._tag = .article }, + ), + asUint("details") => return self.createHtmlElementT( + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = String.init(undefined, "details", .{}) catch unreachable, ._tag = .details }, + ), + asUint("summary") => return self.createHtmlElementT( + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = String.init(undefined, "summary", .{}) catch unreachable, ._tag = .summary }, + ), + asUint("caption") => return self.createHtmlElementT( + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = String.init(undefined, "caption", .{}) catch unreachable, ._tag = .caption }, + ), + asUint("marquee") => return self.createHtmlElementT( + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = String.init(undefined, "marquee", .{}) catch unreachable, ._tag = .marquee }, + ), + asUint("address") => return self.createHtmlElementT( + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = String.init(undefined, "address", .{}) catch unreachable, ._tag = .address }, + ), + else => {}, + }, + 8 => switch (@as(u64, @bitCast(name[0..8].*))) { + asUint("textarea") => return self.createHtmlElementT( + Element.Html.TextArea, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("template") => return self.createHtmlElementT( + Element.Html.Template, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._content = undefined }, + ), + asUint("fieldset") => return self.createHtmlElementT( + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = String.init(undefined, "fieldset", .{}) catch unreachable, ._tag = .fieldset }, + ), + asUint("optgroup") => return self.createHtmlElementT( + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = String.init(undefined, "optgroup", .{}) catch unreachable, ._tag = .optgroup }, + ), + asUint("progress") => return self.createHtmlElementT( + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = String.init(undefined, "progress", .{}) catch unreachable, ._tag = .progress }, + ), + asUint("datalist") => return self.createHtmlElementT( + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = String.init(undefined, "datalist", .{}) catch unreachable, ._tag = .datalist }, + ), + else => {}, + }, + 10 => switch (@as(u80, @bitCast(name[0..10].*))) { + asUint("blockquote") => return self.createHtmlElementT( + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = String.init(undefined, "blockquote", .{}) catch unreachable, ._tag = .blockquote }, + ), + else => {}, + }, + else => {}, + } + + const tag_name = try String.init(self.arena, name, .{}); + + // Check if this is a custom element (must have hyphen for HTML namespace) + const has_hyphen = std.mem.indexOfScalar(u8, name, '-') != null; + if (has_hyphen and namespace == .html) { + const definition = self.window._custom_elements._definitions.get(name); + const node = try self.createHtmlElementT(Element.Html.Custom, namespace, attribute_iterator, .{ + ._proto = undefined, + ._tag_name = tag_name, + ._definition = definition, + }); + + const def = definition orelse { + const element = node.as(Element); + const custom = element.is(Element.Html.Custom).?; + try self._undefined_custom_elements.append(self.arena, custom); + return node; + }; + + // Save and restore upgrading element to allow nested createElement calls + const prev_upgrading = self._upgrading_element; + self._upgrading_element = node; + defer self._upgrading_element = prev_upgrading; + + var result: JS.Function.Result = undefined; + _ = def.constructor.newInstance(&result) catch |err| { + log.warn(.js, "custom element constructor", .{ .name = name, .err = err }); + return node; + }; + + // After constructor runs, invoke attributeChangedCallback for initial attributes + const element = node.as(Element); + if (element._attributes) |attributes| { + var it = attributes.iterator(); + while (it.next()) |attr| { + Element.Html.Custom.invokeAttributeChangedCallbackOnElement( + element, + attr._name.str(), + null, // old_value is null for initial attributes + attr._value.str(), + self, + ); } } - return n; - }, - asUint("menu") => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = String.init(undefined, "menu", .{}) catch unreachable, ._tag = .menu }, - ), - asUint("area") => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = String.init(undefined, "area", .{}) catch unreachable, ._tag = .area }, - ), - asUint("code") => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = String.init(undefined, "code", .{}) catch unreachable, ._tag = .code }, - ), - asUint("time") => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = String.init(undefined, "time", .{}) catch unreachable, ._tag = .time }, - ), - else => {}, - }, - 5 => switch (@as(u40, @bitCast(name[0..5].*))) { - asUint("input") => return self.createHtmlElementT( - Element.Html.Input, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("style") => return self.createHtmlElementT( - Element.Html.Style, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("title") => return self.createHtmlElementT( - Element.Html.Title, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("embed") => return self.createHtmlElementT( - Element.Html.Embed, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("audio") => return self.createHtmlMediaElementT( - Element.Html.Media.Audio, - namespace, - attribute_iterator, - ), - asUint("video") => return self.createHtmlMediaElementT( - Element.Html.Media.Video, - namespace, - attribute_iterator, - ), - asUint("aside") => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = String.init(undefined, "aside", .{}) catch unreachable, ._tag = .aside }, - ), - asUint("meter") => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = String.init(undefined, "meter", .{}) catch unreachable, ._tag = .meter }, - ), - asUint("table") => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = String.init(undefined, "table", .{}) catch unreachable, ._tag = .table }, - ), - asUint("thead") => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = String.init(undefined, "thead", .{}) catch unreachable, ._tag = .thead }, - ), - asUint("tbody") => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = String.init(undefined, "tbody", .{}) catch unreachable, ._tag = .tbody }, - ), - asUint("tfoot") => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = String.init(undefined, "tfoot", .{}) catch unreachable, ._tag = .tfoot }, - ), - else => {}, - }, - 6 => switch (@as(u48, @bitCast(name[0..6].*))) { - asUint("script") => return self.createHtmlElementT( - Element.Html.Script, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("button") => return self.createHtmlElementT( - Element.Html.Button, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("canvas") => return self.createHtmlElementT( - Element.Html.Canvas, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("dialog") => return self.createHtmlElementT( - Element.Html.Dialog, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("strong") => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = String.init(undefined, "strong", .{}) catch unreachable, ._tag = .strong }, - ), - asUint("header") => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = String.init(undefined, "header", .{}) catch unreachable, ._tag = .header }, - ), - asUint("footer") => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = String.init(undefined, "footer", .{}) catch unreachable, ._tag = .footer }, - ), - asUint("select") => return self.createHtmlElementT( - Element.Html.Select, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("option") => return self.createHtmlElementT( - Element.Html.Option, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("iframe") => return self.createHtmlElementT( - Element.Html.IFrame, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("figure") => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = String.init(undefined, "figure", .{}) catch unreachable, ._tag = .figure }, - ), - asUint("hgroup") => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = String.init(undefined, "hgroup", .{}) catch unreachable, ._tag = .hgroup }, - ), - else => {}, - }, - 7 => switch (@as(u56, @bitCast(name[0..7].*))) { - asUint("section") => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = String.init(undefined, "section", .{}) catch unreachable, ._tag = .section }, - ), - asUint("article") => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = String.init(undefined, "article", .{}) catch unreachable, ._tag = .article }, - ), - asUint("details") => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = String.init(undefined, "details", .{}) catch unreachable, ._tag = .details }, - ), - asUint("summary") => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = String.init(undefined, "summary", .{}) catch unreachable, ._tag = .summary }, - ), - asUint("caption") => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = String.init(undefined, "caption", .{}) catch unreachable, ._tag = .caption }, - ), - asUint("marquee") => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = String.init(undefined, "marquee", .{}) catch unreachable, ._tag = .marquee }, - ), - asUint("address") => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = String.init(undefined, "address", .{}) catch unreachable, ._tag = .address }, - ), - else => {}, - }, - 8 => switch (@as(u64, @bitCast(name[0..8].*))) { - asUint("textarea") => return self.createHtmlElementT( - Element.Html.TextArea, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("template") => return self.createHtmlElementT( - Element.Html.Template, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._content = undefined }, - ), - asUint("fieldset") => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = String.init(undefined, "fieldset", .{}) catch unreachable, ._tag = .fieldset }, - ), - asUint("optgroup") => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = String.init(undefined, "optgroup", .{}) catch unreachable, ._tag = .optgroup }, - ), - asUint("progress") => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = String.init(undefined, "progress", .{}) catch unreachable, ._tag = .progress }, - ), - asUint("datalist") => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = String.init(undefined, "datalist", .{}) catch unreachable, ._tag = .datalist }, - ), - else => {}, - }, - 10 => switch (@as(u80, @bitCast(name[0..10].*))) { - asUint("blockquote") => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = String.init(undefined, "blockquote", .{}) catch unreachable, ._tag = .blockquote }, - ), - else => {}, - }, - 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) - const has_hyphen = std.mem.indexOfScalar(u8, name, '-') != null; - if (has_hyphen and namespace == .html) { - const definition = self.window._custom_elements._definitions.get(name); - const node = try self.createHtmlElementT(Element.Html.Custom, namespace, attribute_iterator, .{ - ._proto = undefined, - ._tag_name = tag_name, - ._definition = definition, - }); - - const def = definition orelse { - const element = node.as(Element); - const custom = element.is(Element.Html.Custom).?; - try self._undefined_custom_elements.append(self.arena, custom); - return node; - }; - - // Save and restore upgrading element to allow nested createElement calls - const prev_upgrading = self._upgrading_element; - self._upgrading_element = node; - defer self._upgrading_element = prev_upgrading; - - var result: JS.Function.Result = undefined; - _ = def.constructor.newInstance(&result) catch |err| { - log.warn(.js, "custom element constructor", .{ .name = name, .err = err }); - return node; - }; - - // After constructor runs, invoke attributeChangedCallback for initial attributes - const element = node.as(Element); - if (element._attributes) |attributes| { - var it = attributes.iterator(); - while (it.next()) |attr| { - Element.Html.Custom.invokeAttributeChangedCallbackOnElement( - element, - attr._name.str(), - null, // old_value is null for initial attributes - attr._value.str(), - self, - ); + return node; } - } - return node; + 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 }); + }, } - - 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 { diff --git a/src/browser/parser/Parser.zig b/src/browser/parser/Parser.zig index 7e41ecb5..019c3d14 100644 --- a/src/browser/parser/Parser.zig +++ b/src/browser/parser/Parser.zig @@ -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.* = .{ diff --git a/src/browser/tests/document/create_element_ns.html b/src/browser/tests/document/create_element_ns.html index 46773ebc..123ccaac 100644 --- a/src/browser/tests/document/create_element_ns.html +++ b/src/browser/tests/document/create_element_ns.html @@ -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 diff --git a/src/browser/tests/domimplementation.html b/src/browser/tests/domimplementation.html index d8980a8f..1d524e0d 100644 --- a/src/browser/tests/domimplementation.html +++ b/src/browser/tests/domimplementation.html @@ -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); } @@ -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); } @@ -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); } diff --git a/src/browser/tests/domparser.html b/src/browser/tests/domparser.html index 7f08b1ef..70fd6ff9 100644 --- a/src/browser/tests/domparser.html +++ b/src/browser/tests/domparser.html @@ -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); } } } diff --git a/src/browser/webapi/DOMImplementation.zig b/src/browser/webapi/DOMImplementation.zig index 7144b11a..743cdd95 100644 --- a/src/browser/webapi/DOMImplementation.zig +++ b/src/browser/webapi/DOMImplementation.zig @@ -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); } } diff --git a/src/browser/webapi/Document.zig b/src/browser/webapi/Document.zig index 66027c65..3af6a59b 100644 --- a/src/browser/webapi/Document.zig +++ b/src/browser/webapi/Document.zig @@ -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) { diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index b7bf18b0..d7464661 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -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,60 +237,54 @@ 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); + .anchor => "A", + .body => "BODY", + .br => "BR", + .button => "BUTTON", + .canvas => "CANVAS", + .custom => |e| upperTagName(&e._tag_name, buf), + .data => "DATA", + .dialog => "DIALOG", + .div => "DIV", + .embed => "EMBED", + .form => "FORM", + .generic => |e| upperTagName(&e._tag_name, buf), + .heading => |e| upperTagName(&e._tag_name, buf), + .head => "HEAD", + .html => "HTML", + .hr => "HR", + .iframe => "IFRAME", + .img => "IMG", + .input => "INPUT", + .li => "LI", + .link => "LINK", + .meta => "META", + .media => |m| switch (m._type) { + .audio => "AUDIO", + .video => "VIDEO", + .generic => "MEDIA", }, - else => return switch (he._type) { - .anchor => "A", - .body => "BODY", - .br => "BR", - .button => "BUTTON", - .canvas => "CANVAS", - .custom => |e| upperTagName(&e._tag_name, buf), - .data => "DATA", - .dialog => "DIALOG", - .div => "DIV", - .embed => "EMBED", - .form => "FORM", - .generic => |e| upperTagName(&e._tag_name, buf), - .heading => |e| upperTagName(&e._tag_name, buf), - .head => "HEAD", - .html => "HTML", - .hr => "HR", - .iframe => "IFRAME", - .img => "IMG", - .input => "INPUT", - .li => "LI", - .link => "LINK", - .meta => "META", - .media => |m| switch (m._type) { - .audio => "AUDIO", - .video => "VIDEO", - .generic => "MEDIA", - }, - .ol => "OL", - .option => "OPTION", - .p => "P", - .script => "SCRIPT", - .select => "SELECT", - .slot => "SLOT", - .span => "SPAN", - .style => "STYLE", - .template => "TEMPLATE", - .textarea => "TEXTAREA", - .title => "TITLE", - .ul => "UL", - .unknown => |e| switch (self._namespace) { - .html => upperTagName(&e._tag_name, buf), - .svg, .xml, .mathml => return e._tag_name.str(), - }, + .ol => "OL", + .option => "OPTION", + .p => "P", + .script => "SCRIPT", + .select => "SELECT", + .slot => "SLOT", + .span => "SPAN", + .style => "STYLE", + .template => "TEMPLATE", + .textarea => "TEXTAREA", + .title => "TITLE", + .ul => "UL", + .unknown => |e| switch (self._namespace) { + .html => upperTagName(&e._tag_name, buf), + .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| { diff --git a/src/browser/webapi/HTMLDocument.zig b/src/browser/webapi/HTMLDocument.zig index 13278423..18aa9f56 100644 --- a/src/browser/webapi/HTMLDocument.zig +++ b/src/browser/webapi/HTMLDocument.zig @@ -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 diff --git a/src/browser/webapi/Range.zig b/src/browser/webapi/Range.zig index ce9b0d6d..54f4f5a4 100644 --- a/src/browser/webapi/Range.zig +++ b/src/browser/webapi/Range.zig @@ -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); diff --git a/src/browser/webapi/element/html/Image.zig b/src/browser/webapi/element/html/Image.zig index aa6cdd04..8846d0c6 100644 --- a/src/browser/webapi/element/html/Image.zig +++ b/src/browser/webapi/element/html/Image.zig @@ -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: {