Skip to content

Commit 77b6377

Browse files
committed
Add TextDecoder (utf8 support only)
1 parent 878dbd8 commit 77b6377

File tree

4 files changed

+150
-24
lines changed

4 files changed

+150
-24
lines changed
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
2+
//
3+
// Francis Bouvier <[email protected]>
4+
// Pierre Tachoire <[email protected]>
5+
//
6+
// This program is free software: you can redistribute it and/or modify
7+
// it under the terms of the GNU Affero General Public License as
8+
// published by the Free Software Foundation, either version 3 of the
9+
// License, or (at your option) any later version.
10+
//
11+
// This program is distributed in the hope that it will be useful,
12+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
// GNU Affero General Public License for more details.
15+
//
16+
// You should have received a copy of the GNU Affero General Public License
17+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
18+
19+
const std = @import("std");
20+
const log = @import("../../log.zig");
21+
22+
const Env = @import("../env.zig").Env;
23+
24+
// https://encoding.spec.whatwg.org/#interface-textdecoder
25+
const TextDecoder = @This();
26+
27+
const SupportedLabels = enum {
28+
utf8,
29+
@"utf-8",
30+
@"unicode-1-1-utf-8",
31+
};
32+
33+
const Options = struct {
34+
fatal: bool = false,
35+
ignoreBOM: bool = false,
36+
};
37+
38+
fatal: bool,
39+
ignore_bom: bool,
40+
41+
pub fn constructor(label_: ?[]const u8, opts_: ?Options) !TextDecoder {
42+
if (label_) |l| {
43+
_ = std.meta.stringToEnum(SupportedLabels, l) orelse {
44+
log.warn(.web_api, "not implemented", .{ .feature = "TextDecoder label", .label = l });
45+
return error.NotImplemented;
46+
};
47+
}
48+
const opts = opts_ orelse Options{};
49+
return .{
50+
.fatal = opts.fatal,
51+
.ignore_bom = opts.ignoreBOM,
52+
};
53+
}
54+
55+
pub fn get_encoding(_: *const TextDecoder) []const u8 {
56+
return "utf-8";
57+
}
58+
59+
pub fn get_ignoreBOM(self: *const TextDecoder) bool {
60+
return self.ignore_bom;
61+
}
62+
63+
pub fn get_fatal(self: *const TextDecoder) bool {
64+
return self.fatal;
65+
}
66+
67+
// TODO: Should accept an ArrayBuffer, TypedArray or DataView
68+
// js.zig will currently only map a TypedArray to our []const u8.
69+
pub fn _decode(self: *const TextDecoder, v: []const u8) ![]const u8 {
70+
if (self.fatal and !std.unicode.utf8ValidateSlice(v)) {
71+
return error.InvalidUtf8;
72+
}
73+
74+
if (self.ignore_bom == false and std.mem.startsWith(u8, v, &.{ 0xEF, 0xBB, 0xBF })) {
75+
return v[3..];
76+
}
77+
78+
return v;
79+
}
80+
81+
const testing = @import("../../testing.zig");
82+
test "Browser.Encoding.TextDecoder" {
83+
var runner = try testing.jsRunner(testing.tracking_allocator, .{
84+
.html = "",
85+
});
86+
defer runner.deinit();
87+
88+
try runner.testCases(&.{
89+
.{ "let d1 = new TextDecoder();", null },
90+
.{ "d1.encoding;", "utf-8" },
91+
.{ "d1.fatal", "false" },
92+
.{ "d1.ignoreBOM", "false" },
93+
.{ "d1.decode(new Uint8Array([240, 160, 174, 183]))", "𠮷" },
94+
.{ "d1.decode(new Uint8Array([0xEF, 0xBB, 0xBF, 240, 160, 174, 183]))", "𠮷" },
95+
96+
.{ "let d2 = new TextDecoder('utf8', {fatal: true})", null },
97+
.{
98+
\\ try {
99+
\\ let data = new Uint8Array([240, 240, 160, 174, 183]);
100+
\\ d2.decode(data);
101+
\\ } catch (e) {e}
102+
,
103+
"Error: InvalidUtf8",
104+
},
105+
}, .{});
106+
}

src/browser/encoding/text_encoder.zig renamed to src/browser/encoding/TextEncoder.zig

Lines changed: 21 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
1+
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
22
//
33
// Francis Bouvier <[email protected]>
44
// Pierre Tachoire <[email protected]>
@@ -20,39 +20,37 @@ const std = @import("std");
2020

2121
const Env = @import("../env.zig").Env;
2222

23-
pub const Interfaces = .{
24-
TextEncoder,
25-
};
26-
2723
// https://encoding.spec.whatwg.org/#interface-textencoder
28-
pub const TextEncoder = struct {
29-
pub fn constructor() !TextEncoder {
30-
return .{};
31-
}
24+
const TextEncoder = @This();
3225

33-
pub fn get_encoding(_: *const TextEncoder) []const u8 {
34-
return "utf-8";
35-
}
26+
pub fn constructor() !TextEncoder {
27+
return .{};
28+
}
3629

37-
pub fn _encode(_: *const TextEncoder, v: []const u8) !Env.TypedArray(u8) {
38-
// Ensure the input is a valid utf-8
39-
// It seems chrome accepts invalid utf-8 sequence.
40-
//
41-
if (!std.unicode.utf8ValidateSlice(v)) {
42-
return error.InvalidUtf8;
43-
}
30+
pub fn get_encoding(_: *const TextEncoder) []const u8 {
31+
return "utf-8";
32+
}
4433

45-
return .{ .values = v };
34+
pub fn _encode(_: *const TextEncoder, v: []const u8) !Env.TypedArray(u8) {
35+
// Ensure the input is a valid utf-8
36+
// It seems chrome accepts invalid utf-8 sequence.
37+
//
38+
if (!std.unicode.utf8ValidateSlice(v)) {
39+
return error.InvalidUtf8;
4640
}
47-
};
41+
42+
return .{ .values = v };
43+
}
4844

4945
const testing = @import("../../testing.zig");
5046
test "Browser.Encoding.TextEncoder" {
51-
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
47+
var runner = try testing.jsRunner(testing.tracking_allocator, .{
48+
.html = "",
49+
});
5250
defer runner.deinit();
5351

5452
try runner.testCases(&.{
55-
.{ "var encoder = new TextEncoder();", "undefined" },
53+
.{ "var encoder = new TextEncoder();", null },
5654
.{ "encoder.encoding;", "utf-8" },
5755
.{ "encoder.encode('€');", "226,130,172" },
5856

src/browser/encoding/encoding.zig

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
2+
//
3+
// Francis Bouvier <[email protected]>
4+
// Pierre Tachoire <[email protected]>
5+
//
6+
// This program is free software: you can redistribute it and/or modify
7+
// it under the terms of the GNU Affero General Public License as
8+
// published by the Free Software Foundation, either version 3 of the
9+
// License, or (at your option) any later version.
10+
//
11+
// This program is distributed in the hope that it will be useful,
12+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
// GNU Affero General Public License for more details.
15+
//
16+
// You should have received a copy of the GNU Affero General Public License
17+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
18+
19+
pub const Interfaces = .{
20+
@import("TextDecoder.zig"),
21+
@import("TextEncoder.zig"),
22+
};

src/browser/env.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ const WebApis = struct {
2626
@import("cssom/cssom.zig").Interfaces,
2727
@import("dom/dom.zig").Interfaces,
2828
@import("dom/shadow_root.zig").ShadowRoot,
29-
@import("encoding/text_encoder.zig").Interfaces,
29+
@import("encoding/encoding.zig").Interfaces,
3030
@import("events/event.zig").Interfaces,
3131
@import("html/html.zig").Interfaces,
3232
@import("iterator/iterator.zig").Interfaces,

0 commit comments

Comments
 (0)