mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-07-30 09:16:07 -04:00
https://github.com/lightpanda-io/browser/pull/3000 improved the correctness of ResizeObserver. The main changes were (a) making sure an observe results in an initial callback and (b) invoking the callback for cases that we can identity (e.g. visibility change). Like the other observers, we triggered a check on domChanged. But, unlike the other observers, the check is relatively expensive, namely because it involves style lookups. This commit introduces a number of performance improvements to reduce the check and dispatch frequency. 1 - Only trigger on an allow list of attribute changed (id, class, hidden, width ...) 2 - Pre-filter only on observed elements 3 - Leverage the visibility cache for more efficient delivery
672 lines
25 KiB
Zig
672 lines
25 KiB
Zig
// Copyright (C) 2023-2026 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 std = @import("std");
|
||
|
||
const Allocator = std.mem.Allocator;
|
||
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||
|
||
const M = @This();
|
||
|
||
// German-string (small string optimization)
|
||
pub const String = packed struct {
|
||
len: i32,
|
||
payload: packed union {
|
||
// u96 / u32 act as the 12-byte inline buffer and 4-byte prefix.
|
||
content: u96,
|
||
heap: packed struct { prefix: u32, ptr: usize },
|
||
},
|
||
|
||
const tombstone = -1;
|
||
pub const empty = String{ .len = 0, .payload = .{ .content = 0 } };
|
||
pub const deleted = String{ .len = tombstone, .payload = .{ .content = 0 } };
|
||
|
||
// for packages that already have String imported, then can use String.Global
|
||
pub const Global = M.Global;
|
||
|
||
// Wraps an existing string. For strings with len <= 12, this can be done at
|
||
// comptime: comptime String.wrap("id");
|
||
// For strings with len > 12, this must be done at runtime even for a string
|
||
// literal. This is because, at comptime, we do not have a ptr for data and
|
||
// thus can't store it.
|
||
pub fn wrap(input: anytype) String {
|
||
if (@inComptime()) {
|
||
const l = input.len;
|
||
if (l > 12) {
|
||
@compileError("Comptime string must be <= 12 bytes (SSO only): " ++ input);
|
||
}
|
||
|
||
var content: [12]u8 = @splat(0);
|
||
@memcpy(content[0..l], input);
|
||
return .{ .len = @intCast(l), .payload = .{ .content = @bitCast(content) } };
|
||
}
|
||
|
||
// Runtime path - handle both String and []const u8
|
||
if (@TypeOf(input) == String) {
|
||
return input;
|
||
}
|
||
|
||
const l = input.len;
|
||
|
||
if (l <= 12) {
|
||
var content: [12]u8 = @splat(0);
|
||
@memcpy(content[0..l], input);
|
||
return .{ .len = @intCast(l), .payload = .{ .content = @bitCast(content) } };
|
||
}
|
||
|
||
return .{
|
||
.len = @intCast(l),
|
||
.payload = .{ .heap = .{
|
||
.prefix = @bitCast(input[0..4].*),
|
||
.ptr = @intFromPtr(input.ptr),
|
||
} },
|
||
};
|
||
}
|
||
|
||
pub const InitOpts = struct {
|
||
dupe: bool = true,
|
||
};
|
||
pub fn init(allocator: Allocator, input: []const u8, opts: InitOpts) !String {
|
||
if (input.len >= std.math.maxInt(i32)) {
|
||
return error.StringTooLarge;
|
||
}
|
||
const l: u32 = @intCast(input.len);
|
||
if (l <= 12) {
|
||
var content: [12]u8 = @splat(0);
|
||
@memcpy(content[0..l], input);
|
||
return .{ .len = @intCast(l), .payload = .{ .content = @bitCast(content) } };
|
||
}
|
||
|
||
return .{
|
||
.len = @intCast(l),
|
||
.payload = .{ .heap = .{
|
||
.prefix = @bitCast(input[0..4].*),
|
||
.ptr = @intFromPtr((intern(input) orelse (if (opts.dupe) (try allocator.dupe(u8, input)) else input)).ptr),
|
||
} },
|
||
};
|
||
}
|
||
|
||
pub fn deinit(self: *const String, allocator: Allocator) void {
|
||
const len = self.len;
|
||
if (len > 12) {
|
||
const p: [*]const u8 = @ptrFromInt(self.payload.heap.ptr);
|
||
allocator.free(p[0..@intCast(len)]);
|
||
}
|
||
}
|
||
|
||
pub fn dupe(self: *const String, allocator: Allocator) !String {
|
||
return .init(allocator, self.str(), .{ .dupe = true });
|
||
}
|
||
|
||
pub fn trim(self: *const String, values: []const u8) String {
|
||
return wrap(std.mem.trim(u8, self.str(), values));
|
||
}
|
||
|
||
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 = @bitCast(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 = @bitCast(result[0..4].*),
|
||
.ptr = @intFromPtr((intern(result) orelse result).ptr),
|
||
} },
|
||
};
|
||
}
|
||
|
||
pub fn str(self: *const String) []const u8 {
|
||
const l = self.len;
|
||
if (l < 0) {
|
||
return "";
|
||
}
|
||
|
||
const ul: usize = @intCast(l);
|
||
|
||
if (ul <= 12) {
|
||
const slice: []const u8 = @ptrCast(self);
|
||
return slice[4 .. ul + 4];
|
||
}
|
||
|
||
const p: [*]const u8 = @ptrFromInt(self.payload.heap.ptr);
|
||
return p[0..ul];
|
||
}
|
||
|
||
pub fn isDeleted(self: *const String) bool {
|
||
return self.len == tombstone;
|
||
}
|
||
|
||
pub fn format(self: String, writer: *std.Io.Writer) !void {
|
||
return writer.writeAll(self.str());
|
||
}
|
||
|
||
pub fn eql(a: String, b: String) bool {
|
||
if (@as(*const u64, @ptrCast(&a)).* != @as(*const u64, @ptrCast(&b)).*) {
|
||
return false;
|
||
}
|
||
|
||
if (a.len < 0 or b.len < 0) {
|
||
return false;
|
||
}
|
||
return eqlWithSameLen(a, b);
|
||
}
|
||
|
||
// Dangerous. Use this only when you have to (and, obviously, when you know
|
||
// a.len == b.len)
|
||
pub fn eqlWithSameLen(a: String, b: String) bool {
|
||
if (comptime IS_DEBUG) {
|
||
std.debug.assert(a.len == b.len);
|
||
}
|
||
|
||
const len = a.len;
|
||
if (len <= 12) {
|
||
return a.payload.content == b.payload.content;
|
||
}
|
||
|
||
const al: usize = @intCast(len);
|
||
const bl: usize = @intCast(len);
|
||
const ap: [*]const u8 = @ptrFromInt(a.payload.heap.ptr);
|
||
const bp: [*]const u8 = @ptrFromInt(b.payload.heap.ptr);
|
||
return std.mem.eql(u8, ap[0..al], bp[0..bl]);
|
||
}
|
||
|
||
pub fn eqlSlice(a: String, b: []const u8) bool {
|
||
return switch (a.eqlSliceOrDeleted(b)) {
|
||
.equal => |r| r,
|
||
.deleted => false,
|
||
};
|
||
}
|
||
|
||
const EqualOrDeleted = union(enum) {
|
||
deleted,
|
||
equal: bool,
|
||
};
|
||
pub fn eqlSliceOrDeleted(a: String, b: []const u8) EqualOrDeleted {
|
||
if (a.len == tombstone) {
|
||
return .deleted;
|
||
}
|
||
return .{ .equal = std.mem.eql(u8, a.str(), b) };
|
||
}
|
||
|
||
// This can be used outside of the small string optimization
|
||
pub fn intern(input: []const u8) ?[]const u8 {
|
||
switch (input.len) {
|
||
0 => return "",
|
||
1 => switch (input[0]) {
|
||
'\n' => return "\n",
|
||
'\r' => return "\r",
|
||
'\t' => return "\t",
|
||
' ' => return " ",
|
||
'0' => return "0",
|
||
'1' => return "1",
|
||
'2' => return "2",
|
||
'3' => return "3",
|
||
'4' => return "4",
|
||
'5' => return "5",
|
||
'6' => return "6",
|
||
'7' => return "7",
|
||
'8' => return "8",
|
||
'9' => return "9",
|
||
'.' => return ".",
|
||
',' => return ",",
|
||
'-' => return "-",
|
||
'(' => return "(",
|
||
')' => return ")",
|
||
'?' => return "?",
|
||
';' => return ";",
|
||
'=' => return "=",
|
||
else => {},
|
||
},
|
||
2 => switch (@as(u16, @bitCast(input[0..2].*))) {
|
||
asUint("id") => return "id",
|
||
asUint(" ") => return " ",
|
||
asUint("\r\n") => return "\r\n",
|
||
asUint(", ") => return ", ",
|
||
asUint("·") => return "·",
|
||
else => {},
|
||
},
|
||
3 => switch (@as(u24, @bitCast(input[0..3].*))) {
|
||
asUint(" ") => return " ",
|
||
asUint("•") => return "•",
|
||
asUint("src") => return "src",
|
||
asUint("rel") => return "rel",
|
||
asUint("alt") => return "alt",
|
||
asUint("dir") => return "dir",
|
||
asUint("for") => return "for",
|
||
else => {},
|
||
},
|
||
4 => switch (@as(u32, @bitCast(input[0..4].*))) {
|
||
asUint(" ") => return " ",
|
||
asUint(" to ") => return " to ",
|
||
asUint("type") => return "type",
|
||
asUint("name") => return "name",
|
||
asUint("href") => return "href",
|
||
asUint("role") => return "role",
|
||
asUint("lang") => return "lang",
|
||
asUint("true") => return "true",
|
||
asUint("text") => return "text",
|
||
asUint("none") => return "none",
|
||
asUint("lazy") => return "lazy",
|
||
else => {},
|
||
},
|
||
5 => switch (@as(u40, @bitCast(input[0..5].*))) {
|
||
asUint(" ") => return " ",
|
||
asUint(" › ") => return " › ",
|
||
asUint("class") => return "class",
|
||
asUint("style") => return "style",
|
||
asUint("title") => return "title",
|
||
asUint("value") => return "value",
|
||
asUint("width") => return "width",
|
||
asUint("media") => return "media",
|
||
asUint("async") => return "async",
|
||
asUint("defer") => return "defer",
|
||
asUint("false") => return "false",
|
||
else => {},
|
||
},
|
||
6 => switch (@as(u48, @bitCast(input[0..6].*))) {
|
||
asUint(" ") => return " ",
|
||
asUint("height") => return "height",
|
||
asUint("action") => return "action",
|
||
asUint("method") => return "method",
|
||
asUint("target") => return "target",
|
||
asUint("srcset") => return "srcset",
|
||
asUint("hidden") => return "hidden",
|
||
asUint("button") => return "button",
|
||
asUint("submit") => return "submit",
|
||
asUint("_blank") => return "_blank",
|
||
else => {},
|
||
},
|
||
7 => switch (@as(u56, @bitCast(input[0..7].*))) {
|
||
asUint(" ") => return " ",
|
||
asUint("content") => return "content",
|
||
asUint("charset") => return "charset",
|
||
asUint("checked") => return "checked",
|
||
asUint("loading") => return "loading",
|
||
else => {},
|
||
},
|
||
8 => switch (@as(u64, @bitCast(input[0..8].*))) {
|
||
asUint(" ") => return " ",
|
||
asUint("disabled") => return "disabled",
|
||
asUint("multiple") => return "multiple",
|
||
asUint("readonly") => return "readonly",
|
||
asUint("required") => return "required",
|
||
asUint("selected") => return "selected",
|
||
asUint("tabindex") => return "tabindex",
|
||
asUint("checkbox") => return "checkbox",
|
||
asUint("noopener") => return "noopener",
|
||
asUint("download") => return "download",
|
||
else => {},
|
||
},
|
||
9 => switch (@as(u72, @bitCast(input[0..9].*))) {
|
||
asUint(" ") => return " ",
|
||
asUint("autofocus") => return "autofocus",
|
||
asUint("maxlength") => return "maxlength",
|
||
asUint("draggable") => return "draggable",
|
||
asUint("anonymous") => return "anonymous",
|
||
else => {},
|
||
},
|
||
10 => switch (@as(u80, @bitCast(input[0..10].*))) {
|
||
asUint(" ") => return " ",
|
||
asUint("aria-label") => return "aria-label",
|
||
asUint("novalidate") => return "novalidate",
|
||
asUint("spellcheck") => return "spellcheck",
|
||
asUint("stylesheet") => return "stylesheet",
|
||
asUint("noreferrer") => return "noreferrer",
|
||
else => {},
|
||
},
|
||
11 => switch (@as(u88, @bitCast(input[0..11].*))) {
|
||
asUint("aria-hidden") => return "aria-hidden",
|
||
asUint("crossorigin") => return "crossorigin",
|
||
asUint("placeholder") => return "placeholder",
|
||
else => {},
|
||
},
|
||
12 => switch (@as(u96, @bitCast(input[0..12].*))) {
|
||
asUint("autocomplete") => return "autocomplete",
|
||
asUint("aria-current") => return "aria-current",
|
||
asUint("presentation") => return "presentation",
|
||
else => {},
|
||
},
|
||
13 => switch (@as(u104, @bitCast(input[0..13].*))) {
|
||
asUint("border-radius") => return "border-radius",
|
||
asUint("padding-right") => return "padding-right",
|
||
asUint("margin-bottom") => return "margin-bottom",
|
||
asUint("space-between") => return "space-between",
|
||
asUint("aria-expanded") => return "aria-expanded",
|
||
asUint("aria-disabled") => return "aria-disabled",
|
||
asUint("aria-controls") => return "aria-controls",
|
||
else => {},
|
||
},
|
||
14 => switch (@as(u112, @bitCast(input[0..14].*))) {
|
||
asUint("padding-bottom") => return "padding-bottom",
|
||
asUint("text-transform") => return "text-transform",
|
||
asUint("letter-spacing") => return "letter-spacing",
|
||
asUint("vertical-align") => return "vertical-align",
|
||
asUint("referrerpolicy") => return "referrerpolicy",
|
||
else => {},
|
||
},
|
||
15 => switch (@as(u120, @bitCast(input[0..15].*))) {
|
||
asUint("text-decoration") => return "text-decoration",
|
||
asUint("justify-content") => return "justify-content",
|
||
asUint("aria-labelledby") => return "aria-labelledby",
|
||
else => {},
|
||
},
|
||
16 => switch (@as(u128, @bitCast(input[0..16].*))) {
|
||
asUint("background-color") => return "background-color",
|
||
asUint("aria-describedby") => return "aria-describedby",
|
||
else => {},
|
||
},
|
||
else => {},
|
||
}
|
||
return null;
|
||
}
|
||
};
|
||
|
||
pub fn isAllWhitespace(text: []const u8) bool {
|
||
return for (text) |c| {
|
||
if (!std.ascii.isWhitespace(c)) break false;
|
||
} else true;
|
||
}
|
||
|
||
/// True when `needle` is byte-equal to one of the strings in `haystack`.
|
||
pub fn isOneOf(needle: []const u8, haystack: []const []const u8) bool {
|
||
return for (haystack) |s| {
|
||
if (std.mem.eql(u8, s, needle)) break true;
|
||
} else false;
|
||
}
|
||
|
||
/// Largest prefix of `bytes` whose length is at most `max_bytes` and
|
||
/// ends on a UTF-8 codepoint boundary. Invalid sequences count as one
|
||
/// byte each so the function never loops.
|
||
pub fn truncateUtf8(bytes: []const u8, max_bytes: usize) []const u8 {
|
||
if (bytes.len <= max_bytes) return bytes;
|
||
var i: usize = 0;
|
||
while (i < max_bytes) {
|
||
const seq_len = std.unicode.utf8ByteSequenceLength(bytes[i]) catch 1;
|
||
if (i + seq_len > max_bytes) break;
|
||
i += seq_len;
|
||
}
|
||
return bytes[0..i];
|
||
}
|
||
|
||
/// Reinterprets `bytes` as Latin-1 (each byte one codepoint) and encodes it
|
||
/// as UTF-8. For bytes that aren't valid UTF-8 but must become a valid UTF-8
|
||
/// string (JSON, filenames).
|
||
pub fn latin1ToUtf8(allocator: Allocator, bytes: []const u8) ![]u8 {
|
||
var extra: usize = 0;
|
||
for (bytes) |b| {
|
||
if (b >= 0x80) {
|
||
extra += 1;
|
||
}
|
||
}
|
||
if (comptime IS_DEBUG) {
|
||
// The way this is currently used:
|
||
// 1 - the caller always wants the value duped,
|
||
// 2 - the caller only got here because utf8ValidateSlice failed.
|
||
// If both of those ever change, maybe it's worth reconsidering whether
|
||
// this API unconditionally dupes.
|
||
std.debug.assert(extra != 0);
|
||
}
|
||
|
||
const out = try allocator.alloc(u8, bytes.len + extra);
|
||
var i: usize = 0;
|
||
for (bytes) |b| {
|
||
if (b < 0x80) {
|
||
out[i] = b;
|
||
i += 1;
|
||
} else {
|
||
out[i] = 0xC0 | (b >> 6);
|
||
out[i + 1] = 0x80 | (b & 0x3F);
|
||
i += 2;
|
||
}
|
||
}
|
||
return out;
|
||
}
|
||
|
||
// Discriminatory type that signals the bridge to use arena instead of call_arena
|
||
// Use this for strings that need to persist beyond the current call
|
||
// The caller can unwrap and store just the underlying .str field
|
||
pub const Global = struct {
|
||
str: String,
|
||
};
|
||
|
||
fn asUint(comptime string: anytype) std.meta.Int(
|
||
.unsigned,
|
||
@bitSizeOf(@TypeOf(string.*)) - 8, // (- 8) to exclude sentinel 0
|
||
) {
|
||
const byteLength = @sizeOf(@TypeOf(string.*)) - 1;
|
||
const expectedType = *const [byteLength:0]u8;
|
||
if (@TypeOf(string) != expectedType) {
|
||
@compileError("expected : " ++ @typeName(expectedType) ++ ", got: " ++ @typeName(@TypeOf(string)));
|
||
}
|
||
|
||
return @bitCast(@as(*const [byteLength]u8, string).*);
|
||
}
|
||
|
||
const testing = @import("testing.zig");
|
||
|
||
test "truncateUtf8" {
|
||
try testing.expectEqual("", truncateUtf8("", 10));
|
||
try testing.expectEqual("abc", truncateUtf8("abc", 10));
|
||
try testing.expectEqual("abc", truncateUtf8("abcdef", 3));
|
||
|
||
// 'é' = 0xC3 0xA9 — cap inside the codepoint walks back to the leader.
|
||
try testing.expectEqual("", truncateUtf8("é", 1));
|
||
try testing.expectEqual("é", truncateUtf8("é", 2));
|
||
try testing.expectEqual("é", truncateUtf8("éé", 3));
|
||
|
||
// 3-byte codepoint '世' = 0xE4 0xB8 0x96.
|
||
try testing.expectEqual("", truncateUtf8("世", 2));
|
||
try testing.expectEqual("世", truncateUtf8("世界", 3));
|
||
try testing.expectEqual("世", truncateUtf8("世界", 5));
|
||
|
||
// 4-byte codepoint '𝄞' (musical G clef) = 0xF0 0x9D 0x84 0x9E.
|
||
try testing.expectEqual("", truncateUtf8("𝄞", 3));
|
||
try testing.expectEqual("𝄞", truncateUtf8("𝄞x", 4));
|
||
|
||
// Invalid leader byte counts as one byte so the loop terminates.
|
||
try testing.expectEqual("\xFF", truncateUtf8("\xFFx", 1));
|
||
try testing.expectEqual("\xFFx", truncateUtf8("\xFFx", 2));
|
||
}
|
||
|
||
test "latin1ToUtf8" {
|
||
const cases = [_]struct { in: []const u8, out: []const u8 }{
|
||
.{ .in = "caf\xE9.txt", .out = "café.txt" },
|
||
.{ .in = "report\xFF.csv", .out = "report\xC3\xBF.csv" },
|
||
.{ .in = "\x82\xd3\x82\xe9.xlsx", .out = "\xC2\x82\xC3\x93\xC2\x82\xC3\xA9.xlsx" },
|
||
};
|
||
for (cases) |case| {
|
||
const out = try latin1ToUtf8(testing.allocator, case.in);
|
||
defer testing.allocator.free(out);
|
||
try testing.expectEqual(case.out, out);
|
||
try testing.expectEqual(true, std.unicode.utf8ValidateSlice(out));
|
||
}
|
||
}
|
||
|
||
test "String" {
|
||
const other_short = try String.init(undefined, "other_short", .{});
|
||
const other_long = try String.init(testing.allocator, "other_long" ** 100, .{});
|
||
defer other_long.deinit(testing.allocator);
|
||
|
||
inline for (0..100) |i| {
|
||
const input = "a" ** i;
|
||
const str = try String.init(testing.allocator, input, .{});
|
||
defer str.deinit(testing.allocator);
|
||
|
||
try testing.expectEqual(input, str.str());
|
||
|
||
try testing.expectEqual(true, str.eql(str));
|
||
try testing.expectEqual(true, str.eqlSlice(input));
|
||
try testing.expectEqual(false, str.eql(other_short));
|
||
try testing.expectEqual(false, str.eqlSlice("other_short"));
|
||
|
||
try testing.expectEqual(false, str.eql(other_long));
|
||
try testing.expectEqual(false, str.eqlSlice("other_long" ** 100));
|
||
}
|
||
}
|
||
|
||
test "String.trim" {
|
||
const expect = struct {
|
||
fn expect(expected: []const u8, input: []const u8, values: []const u8) !void {
|
||
const s = try String.init(testing.allocator, input, .{});
|
||
defer s.deinit(testing.allocator);
|
||
const trimmed = s.trim(values);
|
||
try testing.expectEqual(expected, trimmed.str());
|
||
}
|
||
}.expect;
|
||
|
||
try expect("hi", " hi ", " "); // SSO, both ends
|
||
try expect("hello", "hello", " "); // nothing to trim (no allocation)
|
||
try expect("", " ", " "); // fully trimmed away
|
||
try expect("x" ** 20, " " ++ ("x" ** 20) ++ "\t", &.{ ' ', '\t' }); // heap stays heap (view)
|
||
try expect("abc", "abc" ++ (" " ** 6), " "); // heap trims down to SSO
|
||
}
|
||
|
||
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());
|
||
}
|
||
}
|