diff --git a/src/browser/js/String.zig b/src/browser/js/String.zig index 0018e5b63..ab8c00507 100644 --- a/src/browser/js/String.zig +++ b/src/browser/js/String.zig @@ -153,3 +153,23 @@ pub fn toOneByteSlice(self: String, allocator: Allocator) ![]u8 { } return buf; } + +pub fn writeUtf8( + self: String, + dest: []u8, + flags: enum(c_uint) { + none = v8.WRITE_NONE, + null_terminate = v8.WRITE_NULL_TERMINATE, + replace_invalid_utf8 = v8.WRITE_REPLACE_INVALID_UTF8, + }, + processed_characters_len: ?*usize, +) usize { + return v8.v8__String__WriteUtf8( + self.handle, + self.local.isolate.handle, + dest.ptr, + dest.len, + @intFromEnum(flags), + processed_characters_len, + ); +} diff --git a/src/browser/tests/encoding/text_encoder.html b/src/browser/tests/encoding/text_encoder.html index 99fd19592..ec7ecc26d 100644 --- a/src/browser/tests/encoding/text_encoder.html +++ b/src/browser/tests/encoding/text_encoder.html @@ -11,3 +11,312 @@ testing.expectEqual([226, 130, 172], Array.from(encoder.encode('€'))); testing.expectEqual([111,118,101,114,32,57,48,48,48], encoder.encode("over 9000")); + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/browser/webapi/encoding/TextEncoder.zig b/src/browser/webapi/encoding/TextEncoder.zig index 112d2e321..812cec5e7 100644 --- a/src/browser/webapi/encoding/TextEncoder.zig +++ b/src/browser/webapi/encoding/TextEncoder.zig @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2026 Lightpanda (Selecy SAS) +// Copyright (C) 2023-2026 Lightpanda (Selecy SAS) // // Francis Bouvier // Pierre Tachoire @@ -18,6 +18,7 @@ const std = @import("std"); const js = @import("../../js/js.zig"); +const v8 = js.v8; const TextEncoder = @This(); _pad: bool = false, @@ -26,23 +27,51 @@ pub fn init() TextEncoder { return .{}; } -pub fn encode(_: *const TextEncoder, v_: ?js.Value) !js.TypedArray(u8) { - const v = v_ orelse return .{ .values = "" }; +pub fn encode(_: *const TextEncoder, v_: ?js.Value, exec: *const js.Execution) !js.Value { + const local = exec.js.local.?; - if (v.isUndefined()) { - return .{ .values = "" }; + // The input is an optional USVString defaulting to "": undefined is the + // default, anything else (null included) is stringified. + const source = blk: { + const v = v_ orelse break :blk local.newString(""); + if (v.isUndefined()) { + break :blk local.newString(""); + } + break :blk try v.toString(); + }; + + const array = local.createTypedArray(.uint8, source.len()); + const slice = array.slice(); + _ = source.writeUtf8(slice, .replace_invalid_utf8, null); + + return .{ .local = local, .handle = array.handle }; +} + +// https://encoding.spec.whatwg.org/#dom-textencoder-encodeinto +// `read` counts UTF-16 code units consumed from the source, `written` counts +// bytes written into the destination. +pub const EncodeIntoResult = struct { + read: usize, + written: usize, +}; + +pub fn encodeInto(_: *const TextEncoder, source_: js.Value, destination_: js.Value) !EncodeIntoResult { + // The source is a USVString, so anything is stringified, as encode does. + // Binding it as a []const u8 would instead hand us the raw bytes of a + // typed array, which could even alias the destination. + const source = try source_.toString(); + + if (!destination_.isUint8Array()) { + return error.InvalidArgument; } + const dest = try destination_.toZig([]u8); - if (v.isNull()) { - return .{ .values = "null" }; - } + // V8 encodes straight into the destination, never writing a partial + // sequence, and replaces lone surrogates as the USVString conversion would. + var read: usize = 0; + const written = source.writeUtf8(dest, .replace_invalid_utf8, &read); - const str = try v.toStringSlice(); - if (!std.unicode.utf8ValidateSlice(str)) { - return error.InvalidUtf8; - } - - return .{ .values = str }; + return .{ .read = read, .written = written }; } pub const JsApi = struct { @@ -56,7 +85,8 @@ pub const JsApi = struct { }; pub const constructor = bridge.constructor(TextEncoder.init, .{}); - pub const encode = bridge.function(TextEncoder.encode, .{ .as_typed_array = true }); + pub const encode = bridge.function(TextEncoder.encode, .{}); + pub const encodeInto = bridge.function(TextEncoder.encodeInto, .{}); pub const encoding = bridge.property("utf-8", .{ .template = false }); };