diff --git a/src/browser/frame/node_factory.zig b/src/browser/frame/node_factory.zig
index 8b7f6d74b..024b6614e 100644
--- a/src/browser/frame/node_factory.zig
+++ b/src/browser/frame/node_factory.zig
@@ -901,17 +901,22 @@ pub fn createElementNS(frame: *Frame, namespace: Element.Namespace, name: []cons
},
4 => switch (@as(u32, @bitCast(name[0..4].*))) {
asUint("defs") => return createSvgElementT(frame, Graphics.Defs, name, attribute_iterator, .{ ._proto = undefined }),
+ asUint("desc") => return createSvgElementT(frame, Element.Svg.Desc, name, attribute_iterator, .{ ._proto = undefined }),
asUint("rect") => return createSvgElementT(frame, Geometry.Rect, name, attribute_iterator, .{ ._proto = undefined }),
asUint("line") => return createSvgElementT(frame, Geometry.Line, name, attribute_iterator, .{ ._proto = undefined }),
asUint("path") => return createSvgElementT(frame, Geometry.Path, name, attribute_iterator, .{ ._proto = undefined }),
+ asUint("view") => return createSvgElementT(frame, Element.Svg.View, name, attribute_iterator, .{ ._proto = undefined }),
else => {},
},
5 => switch (@as(u40, @bitCast(name[0..5].*))) {
asUint("image") => return createSvgElementT(frame, Graphics.Image, name, attribute_iterator, .{ ._proto = undefined }),
+ asUint("title") => return createSvgElementT(frame, Element.Svg.Title, name, attribute_iterator, .{ ._proto = undefined }),
else => {},
},
6 => switch (@as(u48, @bitCast(name[0..6].*))) {
asUint("circle") => return createSvgElementT(frame, Geometry.Circle, name, attribute_iterator, .{ ._proto = undefined }),
+ asUint("switch") => return createSvgElementT(frame, Graphics.Switch, name, attribute_iterator, .{ ._proto = undefined }),
+ asUint("symbol") => return createSvgElementT(frame, Graphics.Symbol, name, attribute_iterator, .{ ._proto = undefined }),
else => {},
},
7 => switch (@as(u56, @bitCast(name[0..7].*))) {
@@ -920,9 +925,14 @@ pub fn createElementNS(frame: *Frame, namespace: Element.Namespace, name: []cons
else => {},
},
8 => switch (@as(u64, @bitCast(name[0..8].*))) {
+ asUint("metadata") => return createSvgElementT(frame, Element.Svg.Metadata, name, attribute_iterator, .{ ._proto = undefined }),
asUint("polyline") => return createSvgElementT(frame, Geometry.Polyline, name, attribute_iterator, .{ ._proto = undefined }),
else => {},
},
+ 13 => switch (@as(u104, @bitCast(name[0..13].*))) {
+ asUint("foreignObject") => return createSvgElementT(frame, Graphics.ForeignObject, name, attribute_iterator, .{ ._proto = undefined }),
+ else => {},
+ },
else => {},
}
diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig
index b0c13794e..953d0bf97 100644
--- a/src/browser/js/bridge.zig
+++ b/src/browser/js/bridge.zig
@@ -1061,6 +1061,13 @@ pub const PageJsApis = flattenTypes(&.{
@import("../webapi/element/svg/Use.zig"),
@import("../webapi/element/svg/Image.zig"),
@import("../webapi/element/svg/Defs.zig"),
+ @import("../webapi/element/svg/Symbol.zig"),
+ @import("../webapi/element/svg/Switch.zig"),
+ @import("../webapi/element/svg/ForeignObject.zig"),
+ @import("../webapi/element/svg/View.zig"),
+ @import("../webapi/element/svg/Title.zig"),
+ @import("../webapi/element/svg/Desc.zig"),
+ @import("../webapi/element/svg/Metadata.zig"),
@import("../webapi/element/svg/Rect.zig"),
@import("../webapi/element/svg/Circle.zig"),
@import("../webapi/element/svg/Ellipse.zig"),
diff --git a/src/browser/tests/element/svg/hierarchy.html b/src/browser/tests/element/svg/hierarchy.html
index 441b94b4b..3a1e8a795 100644
--- a/src/browser/tests/element/svg/hierarchy.html
+++ b/src/browser/tests/element/svg/hierarchy.html
@@ -39,6 +39,13 @@
testing.expectEqual('function', typeof SVGAnimatedString);
testing.expectEqual('function', typeof SVGNumber);
testing.expectEqual('function', typeof SVGAnimatedNumber);
+ testing.expectEqual('function', typeof SVGSymbolElement);
+ testing.expectEqual('function', typeof SVGSwitchElement);
+ testing.expectEqual('function', typeof SVGForeignObjectElement);
+ testing.expectEqual('function', typeof SVGViewElement);
+ testing.expectEqual('function', typeof SVGTitleElement);
+ testing.expectEqual('function', typeof SVGDescElement);
+ testing.expectEqual('function', typeof SVGMetadataElement);
}
diff --git a/src/browser/tests/element/svg/structure.html b/src/browser/tests/element/svg/structure.html
new file mode 100644
index 000000000..fc96901b8
--- /dev/null
+++ b/src/browser/tests/element/svg/structure.html
@@ -0,0 +1,108 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/browser/webapi/element/Svg.zig b/src/browser/webapi/element/Svg.zig
index 8594eaa5b..442d60402 100644
--- a/src/browser/webapi/element/Svg.zig
+++ b/src/browser/webapi/element/Svg.zig
@@ -27,6 +27,10 @@ const Element = @import("../Element.zig");
const AnimatedString = @import("../svg/AnimatedString.zig");
pub const Generic = @import("svg/Generic.zig");
pub const Graphics = @import("svg/Graphics.zig");
+pub const View = @import("svg/View.zig");
+pub const Title = @import("svg/Title.zig");
+pub const Desc = @import("svg/Desc.zig");
+pub const Metadata = @import("svg/Metadata.zig");
const String = lp.String;
@@ -37,22 +41,23 @@ _tag_name: String, // Svg elements are case-preserving
pub const Type = union(enum) {
graphics: *Graphics,
+ view: *View,
+ title: *Title,
+ desc: *Desc,
+ metadata: *Metadata,
generic: *Generic,
};
pub fn is(self: *Svg, comptime T: type) ?*T {
- switch (self._type) {
- .graphics => |g| {
- if (T == Graphics) {
- return g;
+ inline for (@typeInfo(Type).@"union".fields) |field| {
+ if (@field(Type, field.name) == self._type) {
+ if (field.type == *T) {
+ return @field(self._type, field.name);
}
- return g.is(T);
- },
- .generic => |g| {
- if (T == Generic) {
- return g;
- }
- },
+ }
+ }
+ if (self._type == .graphics) {
+ return self._type.graphics.is(T);
}
return null;
}
@@ -64,7 +69,7 @@ pub fn getTag(self: *const Svg) Element.Tag {
.g => .g,
// No dedicated Element.Tag values; tag-name matching falls back
// to _tag_name, like it does for generic SVG elements.
- .a, .use, .image, .defs => .unknown,
+ .a, .use, .image, .defs, .symbol, .switch_element, .foreign_object => .unknown,
.geometry => |geo| switch (geo._type) {
.rect => .rect,
.circle => .circle,
@@ -76,6 +81,8 @@ pub fn getTag(self: *const Svg) Element.Tag {
},
},
.generic => |g| g._tag,
+ .title => .title,
+ .view, .desc, .metadata => .unknown,
};
}
diff --git a/src/browser/webapi/element/svg/Desc.zig b/src/browser/webapi/element/svg/Desc.zig
new file mode 100644
index 000000000..0ce39857c
--- /dev/null
+++ b/src/browser/webapi/element/svg/Desc.zig
@@ -0,0 +1,41 @@
+// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
+//
+// Francis Bouvier
+// Pierre Tachoire
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+const js = @import("../../../js/js.zig");
+const Node = @import("../../Node.zig");
+const Element = @import("../../Element.zig");
+const Svg = @import("../Svg.zig");
+
+const Desc = @This();
+_proto: *Svg,
+
+pub fn asElement(self: *Desc) *Element {
+ return self._proto.asElement();
+}
+pub fn asNode(self: *Desc) *Node {
+ return self.asElement().asNode();
+}
+
+pub const JsApi = struct {
+ pub const bridge = js.Bridge(Desc);
+ pub const Meta = struct {
+ pub const name = "SVGDescElement";
+ pub const prototype_chain = bridge.prototypeChain();
+ pub var class_id: bridge.ClassId = undefined;
+ };
+};
diff --git a/src/browser/webapi/element/svg/ForeignObject.zig b/src/browser/webapi/element/svg/ForeignObject.zig
new file mode 100644
index 000000000..aa1d6db2e
--- /dev/null
+++ b/src/browser/webapi/element/svg/ForeignObject.zig
@@ -0,0 +1,81 @@
+// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
+//
+// Francis Bouvier
+// Pierre Tachoire
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+const std = @import("std");
+
+const js = @import("../../../js/js.zig");
+const Frame = @import("../../../Frame.zig");
+const Node = @import("../../Node.zig");
+const Element = @import("../../Element.zig");
+
+const PathData = @import("../../svg/PathData.zig");
+const AnimatedLength = @import("../../svg/AnimatedLength.zig");
+
+const Graphics = @import("Graphics.zig");
+
+const ForeignObject = @This();
+_proto: *Graphics,
+
+pub fn asElement(self: *ForeignObject) *Element {
+ return self._proto.asElement();
+}
+pub fn asNode(self: *ForeignObject) *Node {
+ return self.asElement().asNode();
+}
+
+pub fn getX(self: *ForeignObject, frame: *Frame) !*AnimatedLength {
+ return AnimatedLength.getOrCreate(self.asElement(), .x, frame);
+}
+pub fn getY(self: *ForeignObject, frame: *Frame) !*AnimatedLength {
+ return AnimatedLength.getOrCreate(self.asElement(), .y, frame);
+}
+pub fn getWidth(self: *ForeignObject, frame: *Frame) !*AnimatedLength {
+ return AnimatedLength.getOrCreate(self.asElement(), .width, frame);
+}
+pub fn getHeight(self: *ForeignObject, frame: *Frame) !*AnimatedLength {
+ return AnimatedLength.getOrCreate(self.asElement(), .height, frame);
+}
+
+pub fn getBounds(self: *ForeignObject, frame: *Frame) !PathData.Bounds {
+ const x = (try self.getX(frame)).getBaseVal().getValue(frame);
+ const y = (try self.getY(frame)).getBaseVal().getValue(frame);
+ const width = (try self.getWidth(frame)).getBaseVal().getValue(frame);
+ const height = (try self.getHeight(frame)).getBaseVal().getValue(frame);
+
+ if (!std.math.isFinite(x) or !std.math.isFinite(y) or !std.math.isFinite(width) or !std.math.isFinite(height)) {
+ return .{};
+ }
+ if (width <= 0 or height <= 0) {
+ return .{};
+ }
+ return .{ .min_x = x, .min_y = y, .max_x = x + width, .max_y = y + height };
+}
+
+pub const JsApi = struct {
+ pub const bridge = js.Bridge(ForeignObject);
+ pub const Meta = struct {
+ pub const name = "SVGForeignObjectElement";
+ pub const prototype_chain = bridge.prototypeChain();
+ pub var class_id: bridge.ClassId = undefined;
+ };
+
+ pub const x = bridge.accessor(ForeignObject.getX, null, .{});
+ pub const y = bridge.accessor(ForeignObject.getY, null, .{});
+ pub const width = bridge.accessor(ForeignObject.getWidth, null, .{});
+ pub const height = bridge.accessor(ForeignObject.getHeight, null, .{});
+};
diff --git a/src/browser/webapi/element/svg/Graphics.zig b/src/browser/webapi/element/svg/Graphics.zig
index c4dd4c08f..d9b4dc269 100644
--- a/src/browser/webapi/element/svg/Graphics.zig
+++ b/src/browser/webapi/element/svg/Graphics.zig
@@ -35,6 +35,9 @@ pub const A = @import("A.zig");
pub const Use = @import("Use.zig");
pub const Image = @import("Image.zig");
pub const Defs = @import("Defs.zig");
+pub const Symbol = @import("Symbol.zig");
+pub const Switch = @import("Switch.zig");
+pub const ForeignObject = @import("ForeignObject.zig");
pub const Geometry = @import("Geometry.zig");
const Graphics = @This();
@@ -48,6 +51,9 @@ pub const Type = union(enum) {
use: *Use,
image: *Image,
defs: *Defs,
+ symbol: *Symbol,
+ switch_element: *Switch,
+ foreign_object: *ForeignObject,
geometry: *Geometry,
};
@@ -98,8 +104,9 @@ pub fn getBBox(self: *Graphics, frame: *Frame) !*DOMRect {
defer path.deinit(frame.local_arena);
bounds = path.bounds(.{});
},
- .g, .a, .svg => try accumulateChildren(self, .{}, &bounds, frame, 0),
- .defs, .use, .image => {},
+ .foreign_object => |foreign_object| bounds = try foreign_object.getBounds(frame),
+ .g, .a, .svg => try accumulateChildren(self, .{}, &bounds, frame),
+ .defs, .symbol, .switch_element, .use, .image => {},
}
if (bounds.isEmpty()) {
return DOMRect.create(.{}, frame._factory);
@@ -113,20 +120,31 @@ pub fn getBBox(self: *Graphics, frame: *Frame) !*DOMRect {
}, frame._factory);
}
-const MAX_NESTING_DEPTH = 512;
+fn accumulateChildren(parent: *Graphics, matrix: PathData.Matrix, bounds: *PathData.Bounds, frame: *Frame) !void {
+ const Cursor = struct {
+ next: ?*Node,
+ matrix: PathData.Matrix,
+ };
+ var cursors: std.ArrayList(Cursor) = .empty;
+ try cursors.append(frame.local_arena, .{
+ .next = parent.asNode().firstChild(),
+ .matrix = matrix,
+ });
-fn accumulateChildren(parent: *Graphics, matrix: PathData.Matrix, bounds: *PathData.Bounds, frame: *Frame, depth: usize) !void {
- if (depth == MAX_NESTING_DEPTH) {
- return;
- }
+ while (cursors.items.len != 0) {
+ const top = cursors.items.len - 1;
+ const node = cursors.items[top].next orelse {
+ _ = cursors.pop();
+ continue;
+ };
+ cursors.items[top].next = node.nextSibling();
+ const parent_matrix = cursors.items[top].matrix;
- var child = parent.asNode().firstChild();
- while (child) |node| : (child = node.nextSibling()) {
const element = node.is(Element) orelse continue;
if (element._namespace != .svg) continue;
const svg = element.as(SvgElement);
const graphics = svg.is(Graphics) orelse continue;
- const child_matrix = matrix.multiply(transformMatrix(element));
+ const child_matrix = parent_matrix.multiply(transformMatrix(element));
switch (graphics._type) {
.geometry => |geometry| {
@@ -134,11 +152,30 @@ fn accumulateChildren(parent: *Graphics, matrix: PathData.Matrix, bounds: *PathD
defer path.deinit(frame.local_arena);
bounds.merge(path.bounds(child_matrix));
},
- .g, .a => try accumulateChildren(graphics, child_matrix, bounds, frame, depth + 1),
- // never renders. Nested viewports,