diff --git a/src/browser/Page.zig b/src/browser/Page.zig
index 688c9cb2..1b068374 100644
--- a/src/browser/Page.zig
+++ b/src/browser/Page.zig
@@ -1360,10 +1360,8 @@ pub fn appendNew(self: *Page, parent: *Node, child: Node.NodeOrText) !void {
if (parent.lastChild()) |sibling| {
if (sibling.is(CData.Text)) |tn| {
const cdata = tn._proto;
- const existing = cdata.getData();
- // @metric
- // Inefficient, but we don't expect this to happen often.
- cdata._data = try std.mem.concat(self.arena, u8, &.{ existing, txt });
+ const existing = cdata.getData().str();
+ cdata._data = try String.concat(self.arena, &.{ existing, txt });
return;
}
}
@@ -2193,28 +2191,24 @@ fn populateElementAttributes(self: *Page, element: *Element, list: anytype) !voi
}
pub fn createTextNode(self: *Page, text: []const u8) !*Node {
- // might seem unlikely that we get an intern hit, but we'll get some nodes
- // with just '\n'
- const owned_text = try self.dupeString(text);
const cd = try self._factory.node(CData{
._proto = undefined,
._type = .{ .text = .{
._proto = undefined,
} },
- ._data = owned_text,
+ ._data = try self.dupeSSO(text),
});
cd._type.text._proto = cd;
return cd.asNode();
}
pub fn createComment(self: *Page, text: []const u8) !*Node {
- const owned_text = try self.dupeString(text);
const cd = try self._factory.node(CData{
._proto = undefined,
._type = .{ .comment = .{
._proto = undefined,
} },
- ._data = owned_text,
+ ._data = try self.dupeSSO(text),
});
cd._type.comment._proto = cd;
return cd.asNode();
@@ -2226,8 +2220,6 @@ pub fn createCDATASection(self: *Page, data: []const u8) !*Node {
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,
@@ -2239,7 +2231,7 @@ pub fn createCDATASection(self: *Page, data: []const u8) !*Node {
._type = .{ .cdata_section = .{
._proto = text_node,
} },
- ._data = owned_data,
+ ._data = try self.dupeSSO(data),
});
// Set up the back pointer from Text to CData
@@ -2261,7 +2253,6 @@ pub fn createProcessingInstruction(self: *Page, target: []const u8, data: []cons
try validateXmlName(target);
const owned_target = try self.dupeString(target);
- const owned_data = try self.dupeString(data);
const pi = try self._factory.create(CData.ProcessingInstruction{
._proto = undefined,
@@ -2271,7 +2262,7 @@ pub fn createProcessingInstruction(self: *Page, target: []const u8, data: []cons
const cd = try self._factory.node(CData{
._proto = undefined,
._type = .{ .processing_instruction = pi },
- ._data = owned_data,
+ ._data = try self.dupeSSO(data),
});
// Set up the back pointer from ProcessingInstruction to CData
@@ -2344,6 +2335,10 @@ pub fn dupeString(self: *Page, value: []const u8) ![]const u8 {
return self.arena.dupe(u8, value);
}
+pub fn dupeSSO(self: *Page, value: []const u8) !String {
+ return String.init(self.arena, value, .{ .dupe = true });
+}
+
const RemoveNodeOpts = struct {
will_be_reconnected: bool,
};
@@ -2747,7 +2742,7 @@ pub fn setCustomizedBuiltInDefinition(self: *Page, element: *Element, definition
pub fn characterDataChange(
self: *Page,
target: *Node,
- old_value: []const u8,
+ old_value: String,
) void {
var it: ?*std.DoublyLinkedList.Node = self._mutation_observers.first;
while (it) |node| : (it = node.next) {
diff --git a/src/browser/dump.zig b/src/browser/dump.zig
index ad7402fe..bb666e7f 100644
--- a/src/browser/dump.zig
+++ b/src/browser/dump.zig
@@ -82,19 +82,19 @@ fn _deep(node: *Node, opts: Opts, comptime force_slot: bool, writer: *std.Io.Wri
.cdata => |cd| {
if (node.is(Node.CData.Comment)) |_| {
try writer.writeAll("");
} else if (node.is(Node.CData.ProcessingInstruction)) |pi| {
try writer.writeAll("");
try writer.writeAll(pi._target);
try writer.writeAll(" ");
- try writer.writeAll(cd.getData());
+ try writer.writeAll(cd.getData().str());
try writer.writeAll("?>");
} else {
if (shouldEscapeText(node._parent)) {
- try writeEscapedText(cd.getData(), writer);
+ try writeEscapedText(cd.getData().str(), writer);
} else {
- try writer.writeAll(cd.getData());
+ try writer.writeAll(cd.getData().str());
}
}
},
diff --git a/src/browser/markdown.zig b/src/browser/markdown.zig
index cdb8e87c..26a281bf 100644
--- a/src/browser/markdown.zig
+++ b/src/browser/markdown.zig
@@ -145,7 +145,7 @@ fn render(node: *Node, state: *State, writer: *std.Io.Writer, page: *Page) error
},
.cdata => |cd| {
if (node.is(Node.CData.Text)) |_| {
- var text = cd.getData();
+ var text = cd.getData().str();
if (state.pre_node) |pre| {
if (node.parentNode() == pre and node.nextSibling() == null) {
text = std.mem.trimRight(u8, text, " \t\r\n");
diff --git a/src/browser/webapi/CData.zig b/src/browser/webapi/CData.zig
index 52a555b4..13e075ad 100644
--- a/src/browser/webapi/CData.zig
+++ b/src/browser/webapi/CData.zig
@@ -17,6 +17,7 @@
// along with this program. If not, see .
const std = @import("std");
+const String = @import("../../string.zig").String;
const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
@@ -31,7 +32,7 @@ const CData = @This();
_type: Type,
_proto: *Node,
-_data: []const u8 = "",
+_data: String = .empty,
/// Count UTF-16 code units in a UTF-8 string.
/// 4-byte UTF-8 sequences (codepoints >= U+10000) produce 2 UTF-16 code units (surrogate pair),
@@ -157,7 +158,7 @@ pub fn is(self: *CData, comptime T: type) ?*T {
return null;
}
-pub fn getData(self: *const CData) []const u8 {
+pub fn getData(self: *const CData) String {
return self._data;
}
@@ -172,7 +173,7 @@ pub fn render(self: *const CData, writer: *std.io.Writer, opts: RenderOpts) !boo
var start: usize = 0;
var prev_w: ?bool = null;
var is_w: bool = undefined;
- const s = self._data;
+ const s = self._data.str();
for (s, 0..) |c, i| {
is_w = std.ascii.isWhitespace(c);
@@ -222,9 +223,9 @@ pub fn setData(self: *CData, value: ?[]const u8, page: *Page) !void {
const old_value = self._data;
if (value) |v| {
- self._data = try page.dupeString(v);
+ self._data = try page.dupeSSO(v);
} else {
- self._data = "";
+ self._data = .empty;
}
page.characterDataChange(self.asNode(), old_value);
@@ -243,15 +244,15 @@ pub fn _setData(self: *CData, value: js.Value, page: *Page) !void {
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}),
- .processing_instruction => |pi| writer.print("{s} {s}?>", .{ pi._target, self._data }),
+ .text => writer.print("{f}", .{self._data}),
+ .comment => writer.print("", .{self._data}),
+ .cdata_section => writer.print("", .{self._data}),
+ .processing_instruction => |pi| writer.print("{s} {f}?>", .{ pi._target, self._data }),
};
}
pub fn getLength(self: *const CData) usize {
- return utf16Len(self._data);
+ return utf16Len(self._data.str());
}
pub fn isEqualNode(self: *const CData, other: *const CData) bool {
@@ -267,58 +268,64 @@ pub fn isEqualNode(self: *const CData, other: *const CData) bool {
// if the _targets are equal, we still want to compare the data
}
- return std.mem.eql(u8, self.getData(), other.getData());
+ return self._data.eql(other._data);
}
pub fn appendData(self: *CData, data: []const u8, page: *Page) !void {
- const new_data = try std.mem.concat(page.arena, u8, &.{ self._data, data });
- try self.setData(new_data, page);
+ const old_value = self._data;
+ self._data = try String.concat(page.arena, &.{ self._data.str(), data });
+ page.characterDataChange(self.asNode(), old_value);
}
pub fn deleteData(self: *CData, offset: usize, count: usize, page: *Page) !void {
const end_utf16 = std.math.add(usize, offset, count) catch std.math.maxInt(usize);
- const range = try utf16RangeToUtf8(self._data, offset, end_utf16);
+ const range = try utf16RangeToUtf8(self._data.str(), offset, end_utf16);
- // Just slice - original data stays in arena
- const old_value = self._data;
+ const old_data = self._data;
+ const old_value = old_data.str();
if (range.start == 0) {
- self._data = self._data[range.end..];
- } else if (range.end >= self._data.len) {
- self._data = self._data[0..range.start];
+ self._data = try page.dupeSSO(old_value[range.end..]);
+ } else if (range.end >= old_value.len) {
+ self._data = try page.dupeSSO(old_value[0..range.start]);
} else {
- self._data = try std.mem.concat(page.arena, u8, &.{
- self._data[0..range.start],
- self._data[range.end..],
+ // Deleting from middle - concat prefix and suffix
+ self._data = try String.concat(page.arena, &.{
+ old_value[0..range.start],
+ old_value[range.end..],
});
}
- page.characterDataChange(self.asNode(), old_value);
+ page.characterDataChange(self.asNode(), old_data);
}
pub fn insertData(self: *CData, offset: usize, data: []const u8, page: *Page) !void {
- const byte_offset = try utf16OffsetToUtf8(self._data, offset);
- const new_data = try std.mem.concat(page.arena, u8, &.{
- self._data[0..byte_offset],
+ const byte_offset = try utf16OffsetToUtf8(self._data.str(), offset);
+ const old_value = self._data;
+ const existing = old_value.str();
+ self._data = try String.concat(page.arena, &.{
+ existing[0..byte_offset],
data,
- self._data[byte_offset..],
+ existing[byte_offset..],
});
- try self.setData(new_data, page);
+ page.characterDataChange(self.asNode(), old_value);
}
pub fn replaceData(self: *CData, offset: usize, count: usize, data: []const u8, page: *Page) !void {
const end_utf16 = std.math.add(usize, offset, count) catch std.math.maxInt(usize);
- const range = try utf16RangeToUtf8(self._data, offset, end_utf16);
- const new_data = try std.mem.concat(page.arena, u8, &.{
- self._data[0..range.start],
+ const range = try utf16RangeToUtf8(self._data.str(), offset, end_utf16);
+ const old_value = self._data;
+ const existing = old_value.str();
+ self._data = try String.concat(page.arena, &.{
+ existing[0..range.start],
data,
- self._data[range.end..],
+ existing[range.end..],
});
- try self.setData(new_data, page);
+ page.characterDataChange(self.asNode(), old_value);
}
pub fn substringData(self: *const CData, offset: usize, count: usize) ![]const u8 {
const end_utf16 = std.math.add(usize, offset, count) catch std.math.maxInt(usize);
- const range = try utf16RangeToUtf8(self._data, offset, end_utf16);
- return self._data[range.start..range.end];
+ const range = try utf16RangeToUtf8(self._data.str(), offset, end_utf16);
+ return self._data.str()[range.start..range.end];
}
pub fn remove(self: *CData, page: *Page) !void {
@@ -451,7 +458,7 @@ test "WebApi: CData.render" {
const cdata = CData{
._type = .{ .text = undefined },
._proto = undefined,
- ._data = test_case.value,
+ ._data = .wrap(test_case.value),
};
const result = try cdata.render(&buffer.writer, test_case.opts);
diff --git a/src/browser/webapi/MutationObserver.zig b/src/browser/webapi/MutationObserver.zig
index 68d85fb4..a5dd15dd 100644
--- a/src/browser/webapi/MutationObserver.zig
+++ b/src/browser/webapi/MutationObserver.zig
@@ -243,7 +243,7 @@ pub fn notifyAttributeChange(
pub fn notifyCharacterDataChange(
self: *MutationObserver,
target: *Node,
- old_value: ?[]const u8,
+ old_value: ?String,
page: *Page,
) !void {
for (self._observing.items) |obs| {
@@ -267,7 +267,7 @@ pub fn notifyCharacterDataChange(
._target = target,
._attribute_name = null,
._old_value = if (obs.options.characterDataOldValue and old_value != null)
- try arena.dupe(u8, old_value.?)
+ try arena.dupe(u8, old_value.?.str())
else
null,
._added_nodes = &.{},
diff --git a/src/browser/webapi/Node.zig b/src/browser/webapi/Node.zig
index 7849622a..a62b0b95 100644
--- a/src/browser/webapi/Node.zig
+++ b/src/browser/webapi/Node.zig
@@ -270,7 +270,7 @@ pub fn getTextContent(self: *Node, writer: *std.Io.Writer) error{WriteFailed}!vo
try child.getTextContent(writer);
}
},
- .cdata => |c| try writer.writeAll(c.getData()),
+ .cdata => |c| try writer.writeAll(c.getData().str()),
.document => {},
.document_type => {},
.attribute => |attr| try writer.writeAll(attr._value.str()),
@@ -293,7 +293,7 @@ pub fn setTextContent(self: *Node, data: []const u8, page: *Page) !void {
}
return el.replaceChildren(&.{.{ .text = data }}, page);
},
- .cdata => |c| c._data = try page.arena.dupe(u8, data),
+ .cdata => |c| c._data = try page.dupeSSO(data),
.document => {},
.document_type => {},
.document_fragment => |frag| {
@@ -599,10 +599,10 @@ pub fn replaceChild(self: *Node, new_child: *Node, old_child: *Node, page: *Page
return old_child;
}
-pub fn getNodeValue(self: *const Node) ?[]const u8 {
+pub fn getNodeValue(self: *const Node) ?String {
return switch (self._type) {
.cdata => |c| c.getData(),
- .attribute => |attr| attr._value.str(),
+ .attribute => |attr| attr._value,
.element => null,
.document => null,
.document_type => null,
@@ -694,10 +694,10 @@ pub fn getChildAt(self: *Node, index: u32) ?*Node {
return null;
}
-pub fn getData(self: *const Node) []const u8 {
+pub fn getData(self: *const Node) String {
return switch (self._type) {
.cdata => |c| c.getData(),
- else => "",
+ else => .empty,
};
}
@@ -729,7 +729,7 @@ pub fn cloneNode(self: *Node, deep_: ?bool, page: *Page) CloneError!*Node {
const deep = deep_ orelse false;
switch (self._type) {
.cdata => |cd| {
- const data = cd.getData();
+ const data = cd.getData().str();
return switch (cd._type) {
.text => page.createTextNode(data),
.cdata_section => page.createCDATASection(data),
@@ -884,7 +884,7 @@ fn _normalize(self: *Node, allocator: Allocator, buffer: *std.ArrayList(u8), pag
next_node = node_to_merge.nextSibling();
page.removeNode(self, to_remove, .{ .will_be_reconnected = false });
}
- text_node._proto._data = try page.dupeString(buffer.items);
+ text_node._proto._data = try page.dupeSSO(buffer.items);
buffer.clearRetainingCapacity();
}
}
@@ -1028,7 +1028,7 @@ pub const JsApi = struct {
try self.getTextContent(&buf.writer);
return buf.written();
},
- .cdata => |cdata| return cdata.getData(),
+ .cdata => |cdata| return cdata.getData().str(),
.attribute => |attr| return attr._value.str(),
.document => return null,
.document_type => return null,
diff --git a/src/browser/webapi/Range.zig b/src/browser/webapi/Range.zig
index 758f81e7..d7e7d8c5 100644
--- a/src/browser/webapi/Range.zig
+++ b/src/browser/webapi/Range.zig
@@ -17,9 +17,11 @@
// along with this program. If not, see .
const std = @import("std");
-const js = @import("../js/js.zig");
+const String = @import("../../string.zig").String;
+const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
+
const Node = @import("Node.zig");
const DocumentFragment = @import("DocumentFragment.zig");
const AbstractRange = @import("AbstractRange.zig");
@@ -326,7 +328,7 @@ pub fn insertNode(self: *Range, node: *Node, page: *Page) !void {
if (offset == 0) {
_ = try parent.insertBefore(node, container, page);
} else {
- const text_data = container.getData();
+ const text_data = container.getData().str();
if (offset >= text_data.len) {
_ = try parent.insertBefore(node, container.nextSibling(), page);
} else {
@@ -362,15 +364,15 @@ pub fn deleteContents(self: *Range, page: *Page) !void {
// Simple case: same container
if (self._proto._start_container == self._proto._end_container) {
- if (self._proto._start_container.is(Node.CData)) |_| {
+ if (self._proto._start_container.is(Node.CData)) |cdata| {
// Delete part of text node
- const text_data = self._proto._start_container.getData();
- const new_text = try std.mem.concat(
+ const old_value = cdata.getData();
+ const text_data = old_value.str();
+ cdata._data = try String.concat(
page.arena,
- u8,
&.{ text_data[0..self._proto._start_offset], text_data[self._proto._end_offset..] },
);
- try self._proto._start_container.setData(new_text, page);
+ page.characterDataChange(self._proto._start_container, old_value);
} else {
// Delete child nodes in range
var offset = self._proto._start_offset;
@@ -387,7 +389,7 @@ pub fn deleteContents(self: *Range, page: *Page) !void {
// Complex case: different containers
// Handle start container - if it's a text node, truncate it
if (self._proto._start_container.is(Node.CData)) |_| {
- const text_data = self._proto._start_container.getData();
+ const text_data = self._proto._start_container.getData().str();
if (self._proto._start_offset < text_data.len) {
// Keep only the part before start_offset
const new_text = text_data[0..self._proto._start_offset];
@@ -397,7 +399,7 @@ pub fn deleteContents(self: *Range, page: *Page) !void {
// Handle end container - if it's a text node, truncate it
if (self._proto._end_container.is(Node.CData)) |_| {
- const text_data = self._proto._end_container.getData();
+ const text_data = self._proto._end_container.getData().str();
if (self._proto._end_offset < text_data.len) {
// Keep only the part from end_offset onwards
const new_text = text_data[self._proto._end_offset..];
@@ -433,7 +435,7 @@ pub fn cloneContents(self: *const Range, page: *Page) !*DocumentFragment {
if (self._proto._start_container == self._proto._end_container) {
if (self._proto._start_container.is(Node.CData)) |_| {
// Clone part of text node
- const text_data = self._proto._start_container.getData();
+ const text_data = self._proto._start_container.getData().str();
if (self._proto._start_offset < text_data.len and self._proto._end_offset <= text_data.len) {
const cloned_text = text_data[self._proto._start_offset..self._proto._end_offset];
const text_node = try page.createTextNode(cloned_text);
@@ -453,7 +455,7 @@ pub fn cloneContents(self: *const Range, page: *Page) !*DocumentFragment {
// Complex case: different containers
// Clone partial start container
if (self._proto._start_container.is(Node.CData)) |_| {
- const text_data = self._proto._start_container.getData();
+ const text_data = self._proto._start_container.getData().str();
if (self._proto._start_offset < text_data.len) {
// Clone from start_offset to end of text
const cloned_text = text_data[self._proto._start_offset..];
@@ -474,7 +476,7 @@ pub fn cloneContents(self: *const Range, page: *Page) !*DocumentFragment {
// Clone partial end container
if (self._proto._end_container.is(Node.CData)) |_| {
- const text_data = self._proto._end_container.getData();
+ const text_data = self._proto._end_container.getData().str();
if (self._proto._end_offset > 0 and self._proto._end_offset <= text_data.len) {
// Clone from start to end_offset
const cloned_text = text_data[0..self._proto._end_offset];
@@ -560,7 +562,7 @@ fn writeTextContent(self: *const Range, writer: *std.Io.Writer) !void {
if (start_node == end_node) {
if (start_node.is(Node.CData)) |cdata| {
if (!isCommentOrPI(cdata)) {
- const data = cdata.getData();
+ const data = cdata.getData().str();
const s = @min(start_offset, data.len);
const e = @min(end_offset, data.len);
try writer.writeAll(data[s..e]);
@@ -574,7 +576,7 @@ fn writeTextContent(self: *const Range, writer: *std.Io.Writer) !void {
// Partial start: if start container is a text node, write from offset to end
if (start_node.is(Node.CData)) |cdata| {
if (!isCommentOrPI(cdata)) {
- const data = cdata.getData();
+ const data = cdata.getData().str();
const s = @min(start_offset, data.len);
try writer.writeAll(data[s..]);
}
@@ -601,7 +603,7 @@ fn writeTextContent(self: *const Range, writer: *std.Io.Writer) !void {
}
if (n.is(Node.CData)) |cdata| {
if (!isCommentOrPI(cdata)) {
- try writer.writeAll(cdata.getData());
+ try writer.writeAll(cdata.getData().str());
}
}
current = nextInTreeOrder(n, root);
@@ -612,7 +614,7 @@ fn writeTextContent(self: *const Range, writer: *std.Io.Writer) !void {
if (start_node != end_node) {
if (end_node.is(Node.CData)) |cdata| {
if (!isCommentOrPI(cdata)) {
- const data = cdata.getData();
+ const data = cdata.getData().str();
const e = @min(end_offset, data.len);
try writer.writeAll(data[0..e]);
}
diff --git a/src/browser/webapi/Selection.zig b/src/browser/webapi/Selection.zig
index 9320fcfd..a13390a3 100644
--- a/src/browser/webapi/Selection.zig
+++ b/src/browser/webapi/Selection.zig
@@ -500,20 +500,20 @@ fn modifyByWord(self: *Selection, alter: ModifyAlter, forward: bool, range: *Ran
if (isTextNode(focus_node)) {
if (forward) {
- const i = nextWordEnd(new_node.getData(), new_offset);
+ const i = nextWordEnd(new_node.getData().str(), new_offset);
if (i > new_offset) {
new_offset = i;
} else if (nextTextNode(focus_node)) |next| {
new_node = next;
- new_offset = nextWordEnd(next.getData(), 0);
+ new_offset = nextWordEnd(next.getData().str(), 0);
}
} else {
- const i = prevWordStart(new_node.getData(), new_offset);
+ const i = prevWordStart(new_node.getData().str(), new_offset);
if (i < new_offset) {
new_offset = i;
} else if (prevTextNode(focus_node)) |prev| {
new_node = prev;
- new_offset = prevWordStart(prev.getData(), @intCast(prev.getData().len));
+ new_offset = prevWordStart(prev.getData().str(), @intCast(prev.getData().len));
}
}
} else {
@@ -524,7 +524,7 @@ fn modifyByWord(self: *Selection, alter: ModifyAlter, forward: bool, range: *Ran
const child = focus_node.getChildAt(focus_offset) orelse {
if (nextTextNodeAfter(focus_node)) |next| {
new_node = next;
- new_offset = nextWordEnd(next.getData(), 0);
+ new_offset = nextWordEnd(next.getData().str(), 0);
}
return self.applyModify(alter, new_node, new_offset, page);
};
@@ -534,7 +534,7 @@ fn modifyByWord(self: *Selection, alter: ModifyAlter, forward: bool, range: *Ran
};
new_node = t;
- new_offset = nextWordEnd(t.getData(), 0);
+ new_offset = nextWordEnd(t.getData().str(), 0);
} else {
var idx = focus_offset;
while (idx > 0) {
@@ -544,7 +544,7 @@ fn modifyByWord(self: *Selection, alter: ModifyAlter, forward: bool, range: *Ran
while (bottom.lastChild()) |c| bottom = c;
if (isTextNode(bottom)) {
new_node = bottom;
- new_offset = prevWordStart(bottom.getData(), bottom.getLength());
+ new_offset = prevWordStart(bottom.getData().str(), bottom.getLength());
break;
}
}
diff --git a/src/browser/webapi/cdata/Text.zig b/src/browser/webapi/cdata/Text.zig
index 1e5e644d..5eb096f3 100644
--- a/src/browser/webapi/cdata/Text.zig
+++ b/src/browser/webapi/cdata/Text.zig
@@ -16,6 +16,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
+const String = @import("../../../string.zig").String;
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const CData = @import("../CData.zig");
@@ -30,11 +31,11 @@ pub fn init(str: ?js.NullableString, page: *Page) !*Text {
}
pub fn getWholeText(self: *Text) []const u8 {
- return self._proto._data;
+ return self._proto._data.str();
}
pub fn splitText(self: *Text, offset: usize, page: *Page) !*Text {
- const data = self._proto._data;
+ const data = self._proto._data.str();
const byte_offset = CData.utf16OffsetToUtf8(data, offset) catch return error.IndexSizeError;
diff --git a/src/browser/webapi/element/html/TextArea.zig b/src/browser/webapi/element/html/TextArea.zig
index 916f67d8..e08831a2 100644
--- a/src/browser/webapi/element/html/TextArea.zig
+++ b/src/browser/webapi/element/html/TextArea.zig
@@ -88,18 +88,16 @@ pub fn getDefaultValue(self: *const TextArea) []const u8 {
}
pub fn setDefaultValue(self: *TextArea, value: []const u8, page: *Page) !void {
- const owned = try page.dupeString(value);
-
const node = self.asNode();
if (node.firstChild()) |child| {
if (child.is(Node.CData.Text)) |txt| {
- txt._proto._data = owned;
+ txt._proto._data = try page.dupeSSO(value);
return;
}
}
// No text child exists, create one
- const text_node = try page.createTextNode(owned);
+ const text_node = try page.createTextNode(value);
_ = try node.appendChild(text_node, page);
}
diff --git a/src/cdp/Node.zig b/src/cdp/Node.zig
index 885fa93b..a1f688f1 100644
--- a/src/cdp/Node.zig
+++ b/src/cdp/Node.zig
@@ -307,7 +307,11 @@ pub const Writer = struct {
try w.write(dom_node.getNodeName(&name_buf));
try w.objectField("nodeValue");
- try w.write(dom_node.getNodeValue() orelse "");
+ if (dom_node.getNodeValue()) |nv| {
+ try w.write(nv.str());
+ } else {
+ try w.write("");
+ }
if (include_child_count) {
try w.objectField("childNodeCount");
diff --git a/src/string.zig b/src/string.zig
index bbbc374a..202cfdc9 100644
--- a/src/string.zig
+++ b/src/string.zig
@@ -111,6 +111,38 @@ pub const String = packed struct {
return .init(allocator, self.str(), .{ .dupe = true });
}
+ pub fn concat(allocator: Allocator, parts: []const []const u8) !String {
+ var total_len: usize = 0;
+ for (parts) |part| {
+ total_len += part.len;
+ }
+
+ if (total_len <= 12) {
+ var content: [12]u8 = @splat(0);
+ var pos: usize = 0;
+ for (parts) |part| {
+ @memcpy(content[pos..][0..part.len], part);
+ pos += part.len;
+ }
+ return .{ .len = @intCast(total_len), .payload = .{ .content = content } };
+ }
+
+ const result = try allocator.alloc(u8, total_len);
+ var pos: usize = 0;
+ for (parts) |part| {
+ @memcpy(result[pos..][0..part.len], part);
+ pos += part.len;
+ }
+
+ return .{
+ .len = @intCast(total_len),
+ .payload = .{ .heap = .{
+ .prefix = result[0..4].*,
+ .ptr = (intern(result) orelse result).ptr,
+ } },
+ };
+ }
+
pub fn str(self: *const String) []const u8 {
const l = self.len;
if (l < 0) {
@@ -272,3 +304,118 @@ test "String" {
try testing.expectEqual(false, str.eqlSlice("other_long" ** 100));
}
}
+
+test "String.concat" {
+ {
+ const result = try String.concat(testing.allocator, &.{});
+ defer result.deinit(testing.allocator);
+ try testing.expectEqual(@as(usize, 0), result.str().len);
+ try testing.expectEqual("", result.str());
+ }
+
+ {
+ const result = try String.concat(testing.allocator, &.{"hello"});
+ defer result.deinit(testing.allocator);
+ try testing.expectEqual("hello", result.str());
+ }
+
+ {
+ const result = try String.concat(testing.allocator, &.{ "foo", "bar" });
+ defer result.deinit(testing.allocator);
+ try testing.expectEqual("foobar", result.str());
+ try testing.expectEqual(@as(i32, 6), result.len);
+ }
+
+ {
+ const result = try String.concat(testing.allocator, &.{ "test", "ing", "1234" });
+ defer result.deinit(testing.allocator);
+ try testing.expectEqual("testing1234", result.str());
+ try testing.expectEqual(@as(i32, 11), result.len);
+ }
+
+ {
+ const result = try String.concat(testing.allocator, &.{ "foo", "bar", "baz", "qux" });
+ defer result.deinit(testing.allocator);
+ try testing.expectEqual("foobarbazqux", result.str());
+ try testing.expectEqual(@as(i32, 12), result.len);
+ }
+
+ {
+ const result = try String.concat(testing.allocator, &.{ "hello", " world!" });
+ defer result.deinit(testing.allocator);
+ try testing.expectEqual("hello world!", result.str());
+ try testing.expectEqual(@as(i32, 12), result.len);
+ }
+
+ {
+ const result = try String.concat(testing.allocator, &.{ "a", "b", "c", "d", "e" });
+ defer result.deinit(testing.allocator);
+ try testing.expectEqual("abcde", result.str());
+ try testing.expectEqual(@as(i32, 5), result.len);
+ }
+
+ {
+ const result = try String.concat(testing.allocator, &.{ "one", " ", "two", " ", "three", " ", "four" });
+ defer result.deinit(testing.allocator);
+ try testing.expectEqual("one two three four", result.str());
+ try testing.expectEqual(@as(i32, 18), result.len);
+ }
+
+ {
+ const result = try String.concat(testing.allocator, &.{ "hello", "", "world" });
+ defer result.deinit(testing.allocator);
+ try testing.expectEqual("helloworld", result.str());
+ }
+
+ {
+ const result = try String.concat(testing.allocator, &.{ "", "", "" });
+ defer result.deinit(testing.allocator);
+ try testing.expectEqual("", result.str());
+ try testing.expectEqual(@as(i32, 0), result.len);
+ }
+
+ {
+ const result = try String.concat(testing.allocator, &.{ "café", " ☕" });
+ defer result.deinit(testing.allocator);
+ try testing.expectEqual("café ☕", result.str());
+ }
+
+ {
+ const result = try String.concat(testing.allocator, &.{ "Hello ", "世界", " and ", "مرحبا" });
+ defer result.deinit(testing.allocator);
+ try testing.expectEqual("Hello 世界 and مرحبا", result.str());
+ }
+
+ {
+ const result = try String.concat(testing.allocator, &.{ " ", "test", " " });
+ defer result.deinit(testing.allocator);
+ try testing.expectEqual(" test ", result.str());
+ }
+
+ {
+ const result = try String.concat(testing.allocator, &.{ " ", " " });
+ defer result.deinit(testing.allocator);
+ try testing.expectEqual(" ", result.str());
+ try testing.expectEqual(@as(i32, 4), result.len);
+ }
+
+ {
+ const result = try String.concat(testing.allocator, &.{ "Item ", "1", "2", "3" });
+ defer result.deinit(testing.allocator);
+ try testing.expectEqual("Item 123", result.str());
+ }
+
+ {
+ const original = "Hello, world!";
+ const result = try String.concat(testing.allocator, &.{ original[0..5], original[7..] });
+ defer result.deinit(testing.allocator);
+ try testing.expectEqual("Helloworld!", result.str());
+ }
+
+ {
+ const original = "Hello!";
+ const result = try String.concat(testing.allocator, &.{ original[0..5], " world", original[5..] });
+ defer result.deinit(testing.allocator);
+ try testing.expectEqual("Hello world!", result.str());
+ }
+}