add CDATASection

This commit is contained in:
Karl Seguin
2025-12-08 22:09:15 +08:00
parent ef3ba13979
commit 0479813494
8 changed files with 301 additions and 2 deletions

View File

@@ -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;

View File

@@ -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"),

View File

@@ -0,0 +1,217 @@
cdataClassName<!DOCTYPE html>
<script src="../testing.js"></script>
<div id="container"></div>
<script id="createInHTMLDocument">
{
try {
document.createCDATASection('test');
testing.fail('Should have thrown NotSupportedError');
} catch (err) {
testing.expectEqual('NotSupportedError', err.name);
}
}
</script>
<script id="createInXMLDocument">
{
const doc = new Document();
const cdata = doc.createCDATASection('Hello World');
testing.expectEqual(4, cdata.nodeType);
testing.expectEqual('#cdata-section', cdata.nodeName);
testing.expectEqual('Hello World', cdata.data);
testing.expectEqual(11, cdata.length);
}
</script>
<script id="cdataWithSpecialChars">
{
const doc = new Document();
const cdata = doc.createCDATASection('<tag>&amp;"quotes"</tag>');
testing.expectEqual('<tag>&amp;"quotes"</tag>', cdata.data);
}
</script>
<script id="cdataRejectsEndMarker">
{
const doc = new Document();
testing.withError((err) => {
testing.expectEqual('InvalidCharacterError', err.name);
}, () => doc.createCDATASection('foo ]]> bar'));
}
</script>
<script id="cdataRejectsEndMarkerEdgeCase">
{
const doc = new Document();
testing.withError((err) => {
testing.expectEqual('InvalidCharacterError', err.name);
}, () => doc.createCDATASection(']]>'));
testing.withError((err) => {
testing.expectEqual('InvalidCharacterError', err.name);
}, () => doc.createCDATASection('start]]>end'));
}
</script>
<script id="cdataAllowsSimilarPatterns">
{
const doc = new Document();
const cdata1 = doc.createCDATASection(']>');
testing.expectEqual(']>', cdata1.data);
const cdata2 = doc.createCDATASection(']]');
testing.expectEqual(']]', cdata2.data);
const cdata3 = doc.createCDATASection('] ]>');
testing.expectEqual('] ]>', cdata3.data);
}
</script>
<script id="cdataCharacterDataMethods">
{
const doc = new Document();
const cdata = doc.createCDATASection('Hello');
cdata.appendData(' World');
testing.expectEqual('Hello World', cdata.data);
testing.expectEqual(11, cdata.length);
cdata.deleteData(5, 6);
testing.expectEqual('Hello', cdata.data);
cdata.insertData(0, 'Hi ');
testing.expectEqual('Hi Hello', cdata.data);
cdata.replaceData(0, 3, 'Bye');
testing.expectEqual('ByeHello', cdata.data);
const sub = cdata.substringData(0, 3);
testing.expectEqual('Bye', sub);
}
</script>
<script id="cdataInheritance">
{
const doc = new Document();
const cdata = doc.createCDATASection('test');
testing.expectEqual(true, cdata instanceof CDATASection);
testing.expectEqual(true, cdata instanceof Text);
testing.expectEqual(true, cdata instanceof CharacterData);
testing.expectEqual(true, cdata instanceof Node);
}
</script>
<script id="cdataWholeText">
{
const doc = new Document();
const cdata = doc.createCDATASection('test data');
testing.expectEqual('test data', cdata.wholeText);
}
</script>
<script id="cdataClone">
{
const doc = new Document();
const cdata = doc.createCDATASection('original data');
const clone = cdata.cloneNode(false);
testing.expectEqual(4, clone.nodeType);
testing.expectEqual('#cdata-section', clone.nodeName);
testing.expectEqual('original data', clone.data);
testing.expectEqual(true, clone !== cdata);
}
</script>
<script id="cdataRemove">
{
const doc = new Document();
const cdata = doc.createCDATASection('test');
const root = doc.createElement('root');
doc.appendChild(root);
root.appendChild(cdata);
testing.expectEqual(1, root.childNodes.length);
testing.expectEqual(root, cdata.parentNode);
cdata.remove();
testing.expectEqual(0, root.childNodes.length);
testing.expectEqual(null, cdata.parentNode);
}
</script>
<script id="cdataBeforeAfter">
{
const doc = new Document();
const root = doc.createElement('root');
doc.appendChild(root);
const cdata = doc.createCDATASection('middle');
root.appendChild(cdata);
const text1 = doc.createTextNode('before');
const text2 = doc.createTextNode('after');
cdata.before(text1);
cdata.after(text2);
testing.expectEqual(3, root.childNodes.length);
}
</script>
<script id="cdataReplaceWith">
{
const doc = new Document();
const root = doc.createElement('root');
doc.appendChild(root);
const cdata = doc.createCDATASection('old');
root.appendChild(cdata);
const replacement = doc.createTextNode('new');
cdata.replaceWith(replacement);
testing.expectEqual(1, root.childNodes.length);
testing.expectEqual('new', root.childNodes[0].data);
testing.expectEqual(null, cdata.parentNode);
}
</script>
<script id="cdataSiblingNavigation">
{
const doc = new Document();
const root = doc.createElement('root');
doc.appendChild(root);
const elem1 = doc.createElement('first');
const cdata = doc.createCDATASection('middle');
const elem2 = doc.createElement('last');
root.appendChild(elem1);
root.appendChild(cdata);
root.appendChild(elem2);
testing.expectEqual('LAST', cdata.nextElementSibling.tagName);
testing.expectEqual('FIRST', cdata.previousElementSibling.tagName);
}
</script>
<script id="cdataEmptyString">
{
const doc = new Document();
const cdata = doc.createCDATASection('');
testing.expectEqual('', cdata.data);
testing.expectEqual(0, cdata.length);
}
</script>

View File

@@ -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("<text>{s}</text>", .{self._data}),
.comment => writer.print("<!-- {s} -->", .{self._data}),
.cdata_section => writer.print("<![CDATA[{s}]]>", .{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;
};

View File

@@ -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, .{});

View File

@@ -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 => {},

View File

@@ -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),
};
},

View File

@@ -0,0 +1,35 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// 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 <https://www.gnu.org/licenses/>.
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;
};
};