diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 17f25ade..fb70f9d8 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -1348,6 +1348,34 @@ pub fn createComment(self: *Page, text: []const u8) !*Node { return cd.asNode(); } +pub fn createCDATASection(self: *Page, data: []const u8) !*Node { + // Validate that the data doesn't contain "]]>" + if (std.mem.indexOf(u8, data, "]]>") != null) { + return error.InvalidCharacterError; + } + + const owned_data = try self.dupeString(data); + + // First allocate the Text node separately + const text_node = try self._factory.create(CData.Text{ + ._proto = undefined, + }); + + // Then create the CData with cdata_section variant + const cd = try self._factory.node(CData{ + ._proto = undefined, + ._type = .{ .cdata_section = .{ + ._proto = text_node, + } }, + ._data = owned_data, + }); + + // Set up the back pointer from Text to CData + text_node._proto = cd; + + return cd.asNode(); +} + pub fn dupeString(self: *Page, value: []const u8) ![]const u8 { if (String.intern(value)) |v| { return v; diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index 7f67f3a0..7cd2aace 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -491,6 +491,7 @@ pub const JsApis = flattenTypes(&.{ @import("../webapi/CData.zig"), @import("../webapi/cdata/Comment.zig"), @import("../webapi/cdata/Text.zig"), + @import("../webapi/cdata/CDATASection.zig"), @import("../webapi/collections.zig"), @import("../webapi/Console.zig"), @import("../webapi/Crypto.zig"), diff --git a/src/browser/tests/cdata/cdata_section.html b/src/browser/tests/cdata/cdata_section.html new file mode 100644 index 00000000..5c991bb5 --- /dev/null +++ b/src/browser/tests/cdata/cdata_section.html @@ -0,0 +1,217 @@ +cdataClassName + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/browser/webapi/CData.zig b/src/browser/webapi/CData.zig index 0d2bc53e..a22a0d6c 100644 --- a/src/browser/webapi/CData.zig +++ b/src/browser/webapi/CData.zig @@ -24,6 +24,7 @@ const Page = @import("../Page.zig"); const Node = @import("Node.zig"); pub const Text = @import("cdata/Text.zig"); pub const Comment = @import("cdata/Comment.zig"); +pub const CDATASection = @import("cdata/CDATASection.zig"); const CData = @This(); @@ -34,6 +35,9 @@ _data: []const u8 = "", pub const Type = union(enum) { text: Text, comment: Comment, + // This should be under Text, but that would require storing a _type union + // in text, which would add 8 bytes to every text node. + cdata_section: CDATASection, }; pub fn asNode(self: *CData) *Node { @@ -53,6 +57,7 @@ pub fn className(self: *const CData) []const u8 { return switch (self._type) { .text => "[object Text]", .comment => "[object Comment]", + .cdata_section => "[object CDATASection]", }; } @@ -128,6 +133,7 @@ pub fn format(self: *const CData, writer: *std.io.Writer) !void { return switch (self._type) { .text => writer.print("{s}", .{self._data}), .comment => writer.print("", .{self._data}), + .cdata_section => writer.print("", .{self._data}), }; } @@ -248,7 +254,7 @@ pub const JsApi = struct { pub const bridge = js.Bridge(CData); pub const Meta = struct { - pub const name = "CData"; + pub const name = "CharacterData"; pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; }; diff --git a/src/browser/webapi/Document.zig b/src/browser/webapi/Document.zig index 928b97c0..1973b8f0 100644 --- a/src/browser/webapi/Document.zig +++ b/src/browser/webapi/Document.zig @@ -226,6 +226,13 @@ pub fn createTextNode(_: *const Document, data: []const u8, page: *Page) !*Node return page.createTextNode(data); } +pub fn createCDATASection(self: *const Document, data: []const u8, page: *Page) !*Node { + switch (self._type) { + .html => return error.NotSupported, + .generic => return page.createCDATASection(data), + } +} + const Range = @import("Range.zig"); pub fn createRange(_: *const Document, page: *Page) !*Range { return Range.init(page); @@ -353,6 +360,7 @@ pub const JsApi = struct { pub const createDocumentFragment = bridge.function(Document.createDocumentFragment, .{}); pub const createComment = bridge.function(Document.createComment, .{}); pub const createTextNode = bridge.function(Document.createTextNode, .{}); + pub const createCDATASection = bridge.function(Document.createCDATASection, .{ .dom_exception = true }); pub const createRange = bridge.function(Document.createRange, .{}); pub const createEvent = bridge.function(Document.createEvent, .{ .dom_exception = true }); pub const createTreeWalker = bridge.function(Document.createTreeWalker, .{}); diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index 32a2b18a..91560010 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -242,6 +242,7 @@ pub fn getInnerText(self: *Element, writer: *std.Io.Writer) !void { .cdata => |c| switch (c._type) { .comment => continue, .text => try c.render(writer, .{ .trim_right = false, .trim_left = false }), + .cdata_section => try writer.writeAll(c._data), }, .document => {}, .document_type => {}, diff --git a/src/browser/webapi/Node.zig b/src/browser/webapi/Node.zig index ef6de4a7..796aa018 100644 --- a/src/browser/webapi/Node.zig +++ b/src/browser/webapi/Node.zig @@ -213,6 +213,7 @@ pub fn getNodeName(self: *const Node, page: *Page) []const u8 { .element => |el| el.getTagNameSpec(&page.buf), .cdata => |cd| switch (cd._type) { .text => "#text", + .cdata_section => "#cdata-section", .comment => "#comment", }, .document => "#document", @@ -228,6 +229,7 @@ pub fn nodeType(self: *const Node) u8 { .attribute => 2, .cdata => |cd| switch (cd._type) { .text => 3, + .cdata_section => 4, .comment => 8, }, .document => 9, @@ -507,13 +509,14 @@ pub fn normalize(self: *Node, page: *Page) !void { return self._normalize(page.call_arena, &buffer, page); } -pub fn cloneNode(self: *Node, deep_: ?bool, page: *Page) error{ OutOfMemory, StringTooLarge, NotSupported, NotImplemented }!*Node { +pub fn cloneNode(self: *Node, deep_: ?bool, page: *Page) error{ OutOfMemory, StringTooLarge, NotSupported, NotImplemented, InvalidCharacterError }!*Node { const deep = deep_ orelse false; switch (self._type) { .cdata => |cd| { const data = cd.getData(); return switch (cd._type) { .text => page.createTextNode(data), + .cdata_section => page.createCDATASection(data), .comment => page.createComment(data), }; }, diff --git a/src/browser/webapi/cdata/CDATASection.zig b/src/browser/webapi/cdata/CDATASection.zig new file mode 100644 index 00000000..9a9d9e08 --- /dev/null +++ b/src/browser/webapi/cdata/CDATASection.zig @@ -0,0 +1,35 @@ +// Copyright (C) 2023-2025 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 Text = @import("Text.zig"); + +const CDATASection = @This(); + +_proto: *Text, + +pub const JsApi = struct { + pub const bridge = js.Bridge(CDATASection); + + pub const Meta = struct { + pub const name = "CDATASection"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; +};