+
+
+
+
diff --git a/src/browser/tests/element/svg/hierarchy.html b/src/browser/tests/element/svg/hierarchy.html
new file mode 100644
index 000000000..17dd71194
--- /dev/null
+++ b/src/browser/tests/element/svg/hierarchy.html
@@ -0,0 +1,115 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/browser/tests/element/svg/svgsvg.html b/src/browser/tests/element/svg/svgsvg.html
new file mode 100644
index 000000000..e73b55587
--- /dev/null
+++ b/src/browser/tests/element/svg/svgsvg.html
@@ -0,0 +1,85 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig
index 65dc44bcb..bfb758281 100644
--- a/src/browser/webapi/Element.zig
+++ b/src/browser/webapi/Element.zig
@@ -126,7 +126,7 @@ pub fn is(self: *Element, comptime T: type) ?*T {
if (T == Svg) {
return svg;
}
- if (comptime std.mem.startsWith(u8, type_name, "webapi.element.svg.")) {
+ if (comptime std.mem.startsWith(u8, type_name, "browser.webapi.element.svg.")) {
return svg.is(T);
}
},
@@ -583,6 +583,22 @@ pub fn hasAttribute(self: *const Element, name: String, frame: *Frame) !bool {
return value != null;
}
+/// Like getAttributeNS, the namespace is currently ignored.
+pub fn hasAttributeNS(
+ self: *const Element,
+ maybe_namespace: ?[]const u8,
+ local_name: String,
+ frame: *Frame,
+) !bool {
+ if (maybe_namespace) |namespace| {
+ if (!std.mem.eql(u8, namespace, "http://www.w3.org/1999/xhtml")) {
+ log.warn(.not_implemented, "Element.hasAttributeNS", .{ .namespace = namespace });
+ }
+ }
+
+ return self.hasAttribute(local_name, frame);
+}
+
pub fn hasAttributeSafe(self: *const Element, name: String) bool {
return self._attributes.hasSafe(name);
}
@@ -1673,10 +1689,7 @@ pub fn getTag(self: *const Element) Tag {
.head => .head,
.unknown => .unknown,
},
- .svg => |se| switch (se._type) {
- .svg => .svg,
- .generic => |g| g._tag,
- },
+ .svg => |se| se.getTag(),
};
}
@@ -1940,6 +1953,7 @@ pub const JsApi = struct {
pub const style = bridge.accessor(Element.getOrCreateStyle, Element.setStyle, .{});
pub const attributes = bridge.accessor(Element.getAttributeNamedNodeMap, null, .{});
pub const hasAttribute = bridge.function(Element.hasAttribute, .{});
+ pub const hasAttributeNS = bridge.function(Element.hasAttributeNS, .{});
pub const hasAttributes = bridge.function(Element.hasAttributes, .{});
pub const getAttribute = bridge.function(Element.getAttribute, .{});
pub const getAttributeNS = bridge.function(Element.getAttributeNS, .{});
diff --git a/src/browser/webapi/element/Svg.zig b/src/browser/webapi/element/Svg.zig
index 1d30d41c6..8594eaa5b 100644
--- a/src/browser/webapi/element/Svg.zig
+++ b/src/browser/webapi/element/Svg.zig
@@ -16,13 +16,17 @@
// 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 lp = @import("lightpanda");
const js = @import("../../js/js.zig");
+const Frame = @import("../../Frame.zig");
const Node = @import("../Node.zig");
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");
const String = lp.String;
@@ -32,24 +36,49 @@ _proto: *Element,
_tag_name: String, // Svg elements are case-preserving
pub const Type = union(enum) {
- svg,
+ graphics: *Graphics,
generic: *Generic,
};
pub fn is(self: *Svg, comptime T: type) ?*T {
- inline for (@typeInfo(Type).@"union".fields) |f| {
- if (@field(Type, f.name) == self._type) {
- if (f.type == T) {
- return &@field(self._type, f.name);
+ switch (self._type) {
+ .graphics => |g| {
+ if (T == Graphics) {
+ return g;
}
- if (f.type == *T) {
- return @field(self._type, f.name);
+ return g.is(T);
+ },
+ .generic => |g| {
+ if (T == Generic) {
+ return g;
}
- }
+ },
}
return null;
}
+pub fn getTag(self: *const Svg) Element.Tag {
+ return switch (self._type) {
+ .graphics => |g| switch (g._type) {
+ .svg => .svg,
+ .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,
+ .geometry => |geo| switch (geo._type) {
+ .rect => .rect,
+ .circle => .circle,
+ .ellipse => .ellipse,
+ .line => .line,
+ .path => .path,
+ .polygon => .polygon,
+ .polyline => .polyline,
+ },
+ },
+ .generic => |g| g._tag,
+ };
+}
+
pub fn asElement(self: *Svg) *Element {
return self._proto;
}
@@ -57,6 +86,25 @@ pub fn asNode(self: *Svg) *Node {
return self.asElement().asNode();
}
+// The nearest ancestor element, null when this is the outermost svg.
+pub fn getOwnerSvgElement(self: *Svg) ?*Graphics.Svg {
+ var node = self.asNode().parentNode();
+ while (node) |n| : (node = n.parentNode()) {
+ const element = n.is(Element) orelse return null;
+ if (element._namespace != .svg) {
+ return null;
+ }
+ const svg = element.as(Svg);
+ if (svg.is(Graphics.Svg)) |gfx_svg| {
+ return gfx_svg;
+ }
+ if (svg._tag_name.eql(.wrap("foreignObject"))) {
+ return null;
+ }
+ }
+ return null;
+}
+
pub const JsApi = struct {
pub const bridge = js.Bridge(Svg);
@@ -65,6 +113,16 @@ pub const JsApi = struct {
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
+
+ // Overrides el.className to return a readonly AnimatedString
+ pub const className = bridge.accessor(_className, null, .{});
+ fn _className(self: *Svg, frame: *Frame) !*AnimatedString {
+ return AnimatedString.getOrCreate(self.asElement(), .class, frame);
+ }
+
+ pub const ownerSVGElement = bridge.accessor(Svg.getOwnerSvgElement, null, .{});
+ // closest thing we can provide
+ pub const viewportElement = bridge.accessor(Svg.getOwnerSvgElement, null, .{});
};
const testing = @import("../../../testing.zig");
diff --git a/src/browser/webapi/element/svg/A.zig b/src/browser/webapi/element/svg/A.zig
new file mode 100644
index 000000000..da5cad8dd
--- /dev/null
+++ b/src/browser/webapi/element/svg/A.zig
@@ -0,0 +1,51 @@
+// 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 Frame = @import("../../../Frame.zig");
+
+const Node = @import("../../Node.zig");
+const Element = @import("../../Element.zig");
+const AnimatedString = @import("../../svg/AnimatedString.zig");
+
+const Graphics = @import("Graphics.zig");
+
+const A = @This();
+_proto: *Graphics,
+
+pub fn asElement(self: *A) *Element {
+ return self._proto.asElement();
+}
+pub fn asNode(self: *A) *Node {
+ return self.asElement().asNode();
+}
+
+pub const JsApi = struct {
+ pub const bridge = js.Bridge(A);
+
+ pub const Meta = struct {
+ pub const name = "SVGAElement";
+ pub const prototype_chain = bridge.prototypeChain();
+ pub var class_id: bridge.ClassId = undefined;
+ };
+
+ pub const href = bridge.accessor(_href, null, .{});
+ fn _href(self: *A, frame: *Frame) !*AnimatedString {
+ return AnimatedString.getOrCreate(self.asElement(), .href, frame);
+ }
+};
diff --git a/src/browser/webapi/element/svg/Circle.zig b/src/browser/webapi/element/svg/Circle.zig
new file mode 100644
index 000000000..067f23904
--- /dev/null
+++ b/src/browser/webapi/element/svg/Circle.zig
@@ -0,0 +1,44 @@
+// 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 Geometry = @import("Geometry.zig");
+
+const Circle = @This();
+_proto: *Geometry,
+
+pub fn asElement(self: *Circle) *Element {
+ return self._proto.asElement();
+}
+pub fn asNode(self: *Circle) *Node {
+ return self.asElement().asNode();
+}
+
+pub const JsApi = struct {
+ pub const bridge = js.Bridge(Circle);
+
+ pub const Meta = struct {
+ pub const name = "SVGCircleElement";
+ pub const prototype_chain = bridge.prototypeChain();
+ pub var class_id: bridge.ClassId = undefined;
+ };
+};
diff --git a/src/browser/webapi/element/svg/Defs.zig b/src/browser/webapi/element/svg/Defs.zig
new file mode 100644
index 000000000..ba3f85cce
--- /dev/null
+++ b/src/browser/webapi/element/svg/Defs.zig
@@ -0,0 +1,44 @@
+// 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 Graphics = @import("Graphics.zig");
+
+const Defs = @This();
+_proto: *Graphics,
+
+pub fn asElement(self: *Defs) *Element {
+ return self._proto.asElement();
+}
+pub fn asNode(self: *Defs) *Node {
+ return self.asElement().asNode();
+}
+
+pub const JsApi = struct {
+ pub const bridge = js.Bridge(Defs);
+
+ pub const Meta = struct {
+ pub const name = "SVGDefsElement";
+ pub const prototype_chain = bridge.prototypeChain();
+ pub var class_id: bridge.ClassId = undefined;
+ };
+};
diff --git a/src/browser/webapi/element/svg/Ellipse.zig b/src/browser/webapi/element/svg/Ellipse.zig
new file mode 100644
index 000000000..00a1311b5
--- /dev/null
+++ b/src/browser/webapi/element/svg/Ellipse.zig
@@ -0,0 +1,44 @@
+// 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 Geometry = @import("Geometry.zig");
+
+const Ellipse = @This();
+_proto: *Geometry,
+
+pub fn asElement(self: *Ellipse) *Element {
+ return self._proto.asElement();
+}
+pub fn asNode(self: *Ellipse) *Node {
+ return self.asElement().asNode();
+}
+
+pub const JsApi = struct {
+ pub const bridge = js.Bridge(Ellipse);
+
+ pub const Meta = struct {
+ pub const name = "SVGEllipseElement";
+ pub const prototype_chain = bridge.prototypeChain();
+ pub var class_id: bridge.ClassId = undefined;
+ };
+};
diff --git a/src/browser/webapi/element/svg/G.zig b/src/browser/webapi/element/svg/G.zig
new file mode 100644
index 000000000..26a264173
--- /dev/null
+++ b/src/browser/webapi/element/svg/G.zig
@@ -0,0 +1,44 @@
+// 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 Graphics = @import("Graphics.zig");
+
+const G = @This();
+_proto: *Graphics,
+
+pub fn asElement(self: *G) *Element {
+ return self._proto.asElement();
+}
+pub fn asNode(self: *G) *Node {
+ return self.asElement().asNode();
+}
+
+pub const JsApi = struct {
+ pub const bridge = js.Bridge(G);
+
+ pub const Meta = struct {
+ pub const name = "SVGGElement";
+ pub const prototype_chain = bridge.prototypeChain();
+ pub var class_id: bridge.ClassId = undefined;
+ };
+};
diff --git a/src/browser/webapi/element/svg/Generic.zig b/src/browser/webapi/element/svg/Generic.zig
index dfde518d0..86af4455e 100644
--- a/src/browser/webapi/element/svg/Generic.zig
+++ b/src/browser/webapi/element/svg/Generic.zig
@@ -17,8 +17,10 @@
// 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 Generic = @This();
diff --git a/src/browser/webapi/element/svg/Geometry.zig b/src/browser/webapi/element/svg/Geometry.zig
new file mode 100644
index 000000000..280feb818
--- /dev/null
+++ b/src/browser/webapi/element/svg/Geometry.zig
@@ -0,0 +1,73 @@
+// 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 Graphics = @import("Graphics.zig");
+pub const Rect = @import("Rect.zig");
+pub const Circle = @import("Circle.zig");
+pub const Ellipse = @import("Ellipse.zig");
+pub const Line = @import("Line.zig");
+pub const Path = @import("Path.zig");
+pub const Polygon = @import("Polygon.zig");
+pub const Polyline = @import("Polyline.zig");
+
+const Geometry = @This();
+_proto: *Graphics,
+_type: Type,
+
+pub const Type = union(enum) {
+ rect: *Rect,
+ circle: *Circle,
+ ellipse: *Ellipse,
+ line: *Line,
+ path: *Path,
+ polygon: *Polygon,
+ polyline: *Polyline,
+};
+
+pub fn is(self: *Geometry, comptime T: type) ?*T {
+ inline for (@typeInfo(Type).@"union".fields) |f| {
+ if (@field(Type, f.name) == self._type) {
+ if (f.type == *T) {
+ return @field(self._type, f.name);
+ }
+ }
+ }
+ return null;
+}
+
+pub fn asElement(self: *Geometry) *Element {
+ return self._proto.asElement();
+}
+pub fn asNode(self: *Geometry) *Node {
+ return self.asElement().asNode();
+}
+
+pub const JsApi = struct {
+ pub const bridge = js.Bridge(Geometry);
+
+ pub const Meta = struct {
+ pub const name = "SVGGeometryElement";
+ pub const prototype_chain = bridge.prototypeChain();
+ pub var class_id: bridge.ClassId = undefined;
+ };
+};
diff --git a/src/browser/webapi/element/svg/Graphics.zig b/src/browser/webapi/element/svg/Graphics.zig
new file mode 100644
index 000000000..ccb1386b9
--- /dev/null
+++ b/src/browser/webapi/element/svg/Graphics.zig
@@ -0,0 +1,75 @@
+// 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 SvgElement = @import("../Svg.zig");
+
+pub const Svg = @import("Svg.zig");
+pub const G = @import("G.zig");
+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 Geometry = @import("Geometry.zig");
+
+const Graphics = @This();
+_proto: *SvgElement,
+_type: Type,
+
+pub const Type = union(enum) {
+ svg: *Svg,
+ g: *G,
+ a: *A,
+ use: *Use,
+ image: *Image,
+ defs: *Defs,
+ geometry: *Geometry,
+};
+
+pub fn is(self: *Graphics, comptime T: type) ?*T {
+ inline for (@typeInfo(Type).@"union".fields) |f| {
+ if (@field(Type, f.name) == self._type) {
+ if (f.type == *T) {
+ return @field(self._type, f.name);
+ }
+ }
+ }
+ if (self._type == .geometry) {
+ return self._type.geometry.is(T);
+ }
+ return null;
+}
+
+pub fn asElement(self: *Graphics) *Element {
+ return self._proto.asElement();
+}
+pub fn asNode(self: *Graphics) *Node {
+ return self.asElement().asNode();
+}
+
+pub const JsApi = struct {
+ pub const bridge = js.Bridge(Graphics);
+
+ pub const Meta = struct {
+ pub const name = "SVGGraphicsElement";
+ pub const prototype_chain = bridge.prototypeChain();
+ pub var class_id: bridge.ClassId = undefined;
+ };
+};
diff --git a/src/browser/webapi/element/svg/Image.zig b/src/browser/webapi/element/svg/Image.zig
new file mode 100644
index 000000000..73a36abac
--- /dev/null
+++ b/src/browser/webapi/element/svg/Image.zig
@@ -0,0 +1,51 @@
+// 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 Frame = @import("../../../Frame.zig");
+
+const Node = @import("../../Node.zig");
+const Element = @import("../../Element.zig");
+const AnimatedString = @import("../../svg/AnimatedString.zig");
+
+const Graphics = @import("Graphics.zig");
+
+const Image = @This();
+_proto: *Graphics,
+
+pub fn asElement(self: *Image) *Element {
+ return self._proto.asElement();
+}
+pub fn asNode(self: *Image) *Node {
+ return self.asElement().asNode();
+}
+
+pub const JsApi = struct {
+ pub const bridge = js.Bridge(Image);
+
+ pub const Meta = struct {
+ pub const name = "SVGImageElement";
+ pub const prototype_chain = bridge.prototypeChain();
+ pub var class_id: bridge.ClassId = undefined;
+ };
+
+ pub const href = bridge.accessor(_href, null, .{});
+ fn _href(self: *Image, frame: *Frame) !*AnimatedString {
+ return AnimatedString.getOrCreate(self.asElement(), .href, frame);
+ }
+};
diff --git a/src/browser/webapi/element/svg/Line.zig b/src/browser/webapi/element/svg/Line.zig
new file mode 100644
index 000000000..d40e1e305
--- /dev/null
+++ b/src/browser/webapi/element/svg/Line.zig
@@ -0,0 +1,44 @@
+// 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 Geometry = @import("Geometry.zig");
+
+const Line = @This();
+_proto: *Geometry,
+
+pub fn asElement(self: *Line) *Element {
+ return self._proto.asElement();
+}
+pub fn asNode(self: *Line) *Node {
+ return self.asElement().asNode();
+}
+
+pub const JsApi = struct {
+ pub const bridge = js.Bridge(Line);
+
+ pub const Meta = struct {
+ pub const name = "SVGLineElement";
+ pub const prototype_chain = bridge.prototypeChain();
+ pub var class_id: bridge.ClassId = undefined;
+ };
+};
diff --git a/src/browser/webapi/element/svg/Path.zig b/src/browser/webapi/element/svg/Path.zig
new file mode 100644
index 000000000..9308449fd
--- /dev/null
+++ b/src/browser/webapi/element/svg/Path.zig
@@ -0,0 +1,44 @@
+// 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 Geometry = @import("Geometry.zig");
+
+const Path = @This();
+_proto: *Geometry,
+
+pub fn asElement(self: *Path) *Element {
+ return self._proto.asElement();
+}
+pub fn asNode(self: *Path) *Node {
+ return self.asElement().asNode();
+}
+
+pub const JsApi = struct {
+ pub const bridge = js.Bridge(Path);
+
+ pub const Meta = struct {
+ pub const name = "SVGPathElement";
+ pub const prototype_chain = bridge.prototypeChain();
+ pub var class_id: bridge.ClassId = undefined;
+ };
+};
diff --git a/src/browser/webapi/element/svg/Polygon.zig b/src/browser/webapi/element/svg/Polygon.zig
new file mode 100644
index 000000000..752917079
--- /dev/null
+++ b/src/browser/webapi/element/svg/Polygon.zig
@@ -0,0 +1,44 @@
+// 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 Geometry = @import("Geometry.zig");
+
+const Polygon = @This();
+_proto: *Geometry,
+
+pub fn asElement(self: *Polygon) *Element {
+ return self._proto.asElement();
+}
+pub fn asNode(self: *Polygon) *Node {
+ return self.asElement().asNode();
+}
+
+pub const JsApi = struct {
+ pub const bridge = js.Bridge(Polygon);
+
+ pub const Meta = struct {
+ pub const name = "SVGPolygonElement";
+ pub const prototype_chain = bridge.prototypeChain();
+ pub var class_id: bridge.ClassId = undefined;
+ };
+};
diff --git a/src/browser/webapi/element/svg/Polyline.zig b/src/browser/webapi/element/svg/Polyline.zig
new file mode 100644
index 000000000..a232e4950
--- /dev/null
+++ b/src/browser/webapi/element/svg/Polyline.zig
@@ -0,0 +1,44 @@
+// 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 Geometry = @import("Geometry.zig");
+
+const Polyline = @This();
+_proto: *Geometry,
+
+pub fn asElement(self: *Polyline) *Element {
+ return self._proto.asElement();
+}
+pub fn asNode(self: *Polyline) *Node {
+ return self.asElement().asNode();
+}
+
+pub const JsApi = struct {
+ pub const bridge = js.Bridge(Polyline);
+
+ pub const Meta = struct {
+ pub const name = "SVGPolylineElement";
+ pub const prototype_chain = bridge.prototypeChain();
+ pub var class_id: bridge.ClassId = undefined;
+ };
+};
diff --git a/src/browser/webapi/element/svg/Rect.zig b/src/browser/webapi/element/svg/Rect.zig
index 43edf942c..c6dd4f40c 100644
--- a/src/browser/webapi/element/svg/Rect.zig
+++ b/src/browser/webapi/element/svg/Rect.zig
@@ -19,13 +19,13 @@
const js = @import("../../../js/js.zig");
const Node = @import("../../Node.zig");
const Element = @import("../../Element.zig");
-const Svg = @import("../Svg.zig");
+const Geometry = @import("Geometry.zig");
const Rect = @This();
-_proto: *Svg,
+_proto: *Geometry,
pub fn asElement(self: *Rect) *Element {
- return self._proto._proto;
+ return self._proto.asElement();
}
pub fn asNode(self: *Rect) *Node {
return self.asElement().asNode();
diff --git a/src/browser/webapi/element/svg/Svg.zig b/src/browser/webapi/element/svg/Svg.zig
new file mode 100644
index 000000000..920b25c7f
--- /dev/null
+++ b/src/browser/webapi/element/svg/Svg.zig
@@ -0,0 +1,95 @@
+// 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 TreeWalker = @import("../../TreeWalker.zig");
+const DOMRect = @import("../../DOMRect.zig");
+const DOMPoint = @import("../../DOMPoint.zig");
+const DOMMatrix = @import("../../DOMMatrix.zig");
+const SvgNumber = @import("../../svg/Number.zig");
+
+const Graphics = @import("Graphics.zig");
+
+const Svg = @This();
+_proto: *Graphics,
+
+pub fn asElement(self: *Svg) *Element {
+ return self._proto.asElement();
+}
+pub fn asNode(self: *Svg) *Node {
+ return self.asElement().asNode();
+}
+
+pub fn getElementById(self: *Svg, id: []const u8) ?*Element {
+ if (id.len == 0) {
+ return null;
+ }
+
+ var tw = TreeWalker.Full.Elements.init(self.asNode(), .{});
+ while (tw.next()) |el| {
+ const element_id = el.getAttributeSafe(comptime .wrap("id")) orelse continue;
+ if (std.mem.eql(u8, element_id, id)) {
+ return el;
+ }
+ }
+ return null;
+}
+
+pub const JsApi = struct {
+ pub const bridge = js.Bridge(Svg);
+
+ pub const Meta = struct {
+ pub const name = "SVGSVGElement";
+ pub const prototype_chain = bridge.prototypeChain();
+ pub var class_id: bridge.ClassId = undefined;
+ };
+
+ pub const getElementById = bridge.function(Svg.getElementById, .{});
+
+ pub const createSVGPoint = bridge.function(_createSVGPoint, .{});
+ fn _createSVGPoint(_: *Svg, frame: *Frame) !*DOMPoint {
+ return DOMPoint.create(0, 0, 0, 1, frame._page);
+ }
+
+ pub const createSVGMatrix = bridge.function(_createSVGMatrix, .{});
+ fn _createSVGMatrix(_: *Svg, frame: *Frame) !*DOMMatrix {
+ const identity: [16]f64 = .{
+ 1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 1, 0,
+ 0, 0, 0, 1,
+ };
+ return DOMMatrix.create(identity, true, frame._page);
+ }
+
+ pub const createSVGRect = bridge.function(_createSVGRect, .{});
+ fn _createSVGRect(_: *Svg, frame: *Frame) !*DOMRect {
+ return DOMRect.init(0, 0, 0, 0, frame);
+ }
+
+ pub const createSVGNumber = bridge.function(_createSVGNumber, .{});
+ fn _createSVGNumber(_: *Svg, frame: *Frame) !*SvgNumber {
+ return frame._factory.create(SvgNumber{});
+ }
+};
diff --git a/src/browser/webapi/element/svg/Use.zig b/src/browser/webapi/element/svg/Use.zig
new file mode 100644
index 000000000..fc49f7517
--- /dev/null
+++ b/src/browser/webapi/element/svg/Use.zig
@@ -0,0 +1,51 @@
+// 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 Frame = @import("../../../Frame.zig");
+
+const Node = @import("../../Node.zig");
+const Element = @import("../../Element.zig");
+const AnimatedString = @import("../../svg/AnimatedString.zig");
+
+const Graphics = @import("Graphics.zig");
+
+const Use = @This();
+_proto: *Graphics,
+
+pub fn asElement(self: *Use) *Element {
+ return self._proto.asElement();
+}
+pub fn asNode(self: *Use) *Node {
+ return self.asElement().asNode();
+}
+
+pub const JsApi = struct {
+ pub const bridge = js.Bridge(Use);
+
+ pub const Meta = struct {
+ pub const name = "SVGUseElement";
+ pub const prototype_chain = bridge.prototypeChain();
+ pub var class_id: bridge.ClassId = undefined;
+ };
+
+ pub const href = bridge.accessor(_href, null, .{});
+ fn _href(self: *Use, frame: *Frame) !*AnimatedString {
+ return AnimatedString.getOrCreate(self.asElement(), .href, frame);
+ }
+};
diff --git a/src/browser/webapi/svg/AnimatedString.zig b/src/browser/webapi/svg/AnimatedString.zig
new file mode 100644
index 000000000..0f3d6cc5f
--- /dev/null
+++ b/src/browser/webapi/svg/AnimatedString.zig
@@ -0,0 +1,84 @@
+// 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 lp = @import("lightpanda");
+
+const js = @import("../../js/js.zig");
+const Frame = @import("../../Frame.zig");
+const Element = @import("../Element.zig");
+
+pub const Lookup = std.AutoHashMapUnmanaged(Key, *AnimatedString);
+
+const String = lp.String;
+
+const AnimatedString = @This();
+
+_kind: Kind,
+_element: *Element,
+
+pub const Kind = enum { class, href };
+pub const Key = struct {
+ element: *Element,
+ kind: Kind,
+};
+
+// Identity map for AnimatedString, help by the frame
+pub fn getOrCreate(element: *Element, kind: Kind, frame: *Frame) !*AnimatedString {
+ const gop = try frame._svg_animated_strings.getOrPut(frame.arena, .{ .element = element, .kind = kind });
+ if (!gop.found_existing) {
+ gop.value_ptr.* = try frame._factory.create(AnimatedString{
+ ._element = element,
+ ._kind = kind,
+ });
+ }
+ return gop.value_ptr.*;
+}
+
+pub fn getBaseVal(self: *const AnimatedString) []const u8 {
+ return self._element.getAttributeSafe(self.attributeName()) orelse "";
+}
+
+pub fn setBaseVal(self: *AnimatedString, value: String, frame: *Frame) !void {
+ try self._element.setAttribute(self.attributeName(), value, frame);
+}
+
+// No real animation, return the BaseVal
+pub fn getAnimVal(self: *const AnimatedString) []const u8 {
+ return self.getBaseVal();
+}
+
+fn attributeName(self: *const AnimatedString) String {
+ return switch (self._kind) {
+ .href => comptime .wrap("href"),
+ .class => comptime .wrap("class"),
+ };
+}
+
+pub const JsApi = struct {
+ pub const bridge = js.Bridge(AnimatedString);
+
+ pub const Meta = struct {
+ pub const name = "SVGAnimatedString";
+ pub const prototype_chain = bridge.prototypeChain();
+ pub var class_id: bridge.ClassId = undefined;
+ };
+
+ pub const baseVal = bridge.accessor(AnimatedString.getBaseVal, AnimatedString.setBaseVal, .{});
+ pub const animVal = bridge.accessor(AnimatedString.getAnimVal, null, .{});
+};
diff --git a/src/browser/webapi/svg/Number.zig b/src/browser/webapi/svg/Number.zig
new file mode 100644
index 000000000..d1a29feca
--- /dev/null
+++ b/src/browser/webapi/svg/Number.zig
@@ -0,0 +1,42 @@
+// 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 Number = @This();
+_value: f32 = 0,
+
+pub fn getValue(self: *const Number) f32 {
+ return self._value;
+}
+
+pub fn setValue(self: *Number, value: f32) void {
+ self._value = value;
+}
+
+pub const JsApi = struct {
+ pub const bridge = js.Bridge(Number);
+
+ pub const Meta = struct {
+ pub const name = "SVGNumber";
+ pub const prototype_chain = bridge.prototypeChain();
+ pub var class_id: bridge.ClassId = undefined;
+ };
+
+ pub const value = bridge.accessor(Number.getValue, Number.setValue, .{});
+};