Skip to content

Commit dee49fb

Browse files
committed
add console web api
1 parent 0fb0532 commit dee49fb

File tree

4 files changed

+267
-8
lines changed

4 files changed

+267
-8
lines changed

src/browser/console/console.zig

Lines changed: 215 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,224 @@
1717
// along with this program. If not, see <https://www.gnu.org/licenses/>.
1818

1919
const std = @import("std");
20-
const log = std.log.scoped(.console);
20+
const builtin = @import("builtin");
21+
22+
const JsObject = @import("../env.zig").Env.JsObject;
23+
const SessionState = @import("../env.zig").SessionState;
24+
25+
const log = if (builtin.is_test) &test_capture else std.log.scoped(.console);
2126

2227
pub const Console = struct {
2328
// TODO: configurable writer
29+
timers: std.StringHashMapUnmanaged(u32) = .{},
30+
counts: std.StringHashMapUnmanaged(u32) = .{},
31+
32+
pub fn _log(_: *const Console, values: []JsObject, state: *SessionState) !void {
33+
if (values.len == 0) {
34+
return;
35+
}
36+
log.info("{s}", .{try serializeValues(values, state)});
37+
}
38+
39+
pub fn _info(console: *const Console, values: []JsObject, state: *SessionState) !void {
40+
return console._log(values, state);
41+
}
42+
43+
pub fn _debug(_: *const Console, values: []JsObject, state: *SessionState) !void {
44+
if (values.len == 0) {
45+
return;
46+
}
47+
log.debug("{s}", .{try serializeValues(values, state)});
48+
}
49+
50+
pub fn _warn(_: *const Console, values: []JsObject, state: *SessionState) !void {
51+
if (values.len == 0) {
52+
return;
53+
}
54+
log.warn("{s}", .{try serializeValues(values, state)});
55+
}
56+
57+
pub fn _error(_: *const Console, values: []JsObject, state: *SessionState) !void {
58+
if (values.len == 0) {
59+
return;
60+
}
61+
log.err("{s}", .{try serializeValues(values, state)});
62+
}
63+
64+
pub fn _clear(_: *const Console) void {}
65+
66+
pub fn _count(self: *Console, label_: ?[]const u8, state: *SessionState) !void {
67+
const label = label_ orelse "default";
68+
const gop = try self.counts.getOrPut(state.arena, label);
69+
70+
var current: u32 = 0;
71+
if (gop.found_existing) {
72+
current = gop.value_ptr.*;
73+
} else {
74+
gop.key_ptr.* = try state.arena.dupe(u8, label);
75+
}
76+
77+
const count = current + 1;
78+
gop.value_ptr.* = count;
79+
80+
log.info("{s}: {d}", .{ label, count });
81+
}
82+
83+
pub fn _countReset(self: *Console, label_: ?[]const u8) !void {
84+
const label = label_ orelse "default";
85+
const kv = self.counts.fetchRemove(label) orelse {
86+
log.warn("Counter \"{s}\" doesn't exist.", .{label});
87+
return;
88+
};
89+
90+
log.info("{s}: {d}", .{ label, kv.value });
91+
}
92+
93+
pub fn _time(self: *Console, label_: ?[]const u8, state: *SessionState) !void {
94+
const label = label_ orelse "default";
95+
const gop = try self.timers.getOrPut(state.arena, label);
96+
97+
if (gop.found_existing) {
98+
log.warn("Timer \"{s}\" already exists.", .{label});
99+
return;
100+
}
101+
gop.key_ptr.* = try state.arena.dupe(u8, label);
102+
gop.value_ptr.* = timestamp();
103+
}
104+
105+
pub fn _timeLog(self: *Console, label_: ?[]const u8) void {
106+
const elapsed = timestamp();
107+
const label = label_ orelse "default";
108+
const start = self.timers.get(label) orelse {
109+
log.warn("Timer \"{s}\" doesn't exist.", .{label});
110+
return;
111+
};
112+
113+
log.info("\"{s}\": {d}ms", .{ label, elapsed - start });
114+
}
115+
116+
pub fn _timeStop(self: *Console, label_: ?[]const u8) void {
117+
const elapsed = timestamp();
118+
const label = label_ orelse "default";
119+
const kv = self.timers.fetchRemove(label) orelse {
120+
log.warn("Timer \"{s}\" doesn't exist.", .{label});
121+
return;
122+
};
123+
124+
log.info("\"{s}\": {d}ms - timer ended", .{ label, elapsed - kv.value });
125+
}
126+
127+
pub fn _assert(_: *Console, assertion: JsObject, values: []JsObject, state: *SessionState) !void {
128+
if (assertion.isTruthy()) {
129+
return;
130+
}
131+
var serialized_values: []const u8 = "";
132+
if (values.len > 0) {
133+
serialized_values = try serializeValues(values, state);
134+
}
135+
log.err("Assertion failed: {s}", .{serialized_values});
136+
}
137+
138+
fn serializeValues(values: []JsObject, state: *SessionState) ![]const u8 {
139+
const arena = state.call_arena;
140+
var arr: std.ArrayListUnmanaged(u8) = .{};
141+
try arr.appendSlice(arena, try values[0].toString());
142+
for (values[1..]) |value| {
143+
try arr.append(arena, ' ');
144+
try arr.appendSlice(arena, try value.toString());
145+
}
146+
return arr.items;
147+
}
148+
};
149+
150+
fn timestamp() u32 {
151+
const ts = std.posix.clock_gettime(std.posix.CLOCK.MONOTONIC) catch unreachable;
152+
return @intCast(ts.sec);
153+
}
154+
155+
var test_capture = TestCapture{};
156+
const testing = @import("../../testing.zig");
157+
test "Browser.Console" {
158+
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
159+
defer runner.deinit();
160+
161+
defer testing.reset();
162+
163+
{
164+
try runner.testCases(&.{
165+
.{ "console.log('a')", "undefined" },
166+
.{ "console.warn('hello world', 23, true, new Object())", "undefined" },
167+
}, .{});
168+
169+
const captured = test_capture.captured.items;
170+
try testing.expectEqual("a", captured[0]);
171+
try testing.expectEqual("hello world 23 true [object Object]", captured[1]);
172+
}
173+
174+
{
175+
test_capture.reset();
176+
try runner.testCases(&.{
177+
.{ "console.countReset()", "undefined" },
178+
.{ "console.count()", "undefined" },
179+
.{ "console.count('teg')", "undefined" },
180+
.{ "console.count('teg')", "undefined" },
181+
.{ "console.count('teg')", "undefined" },
182+
.{ "console.count()", "undefined" },
183+
.{ "console.countReset('teg')", "undefined" },
184+
.{ "console.countReset()", "undefined" },
185+
.{ "console.count()", "undefined" },
186+
}, .{});
187+
188+
const captured = test_capture.captured.items;
189+
try testing.expectEqual("Counter \"default\" doesn't exist.", captured[0]);
190+
try testing.expectEqual("default: 1", captured[1]);
191+
try testing.expectEqual("teg: 1", captured[2]);
192+
try testing.expectEqual("teg: 2", captured[3]);
193+
try testing.expectEqual("teg: 3", captured[4]);
194+
try testing.expectEqual("default: 2", captured[5]);
195+
try testing.expectEqual("teg: 3", captured[6]);
196+
try testing.expectEqual("default: 2", captured[7]);
197+
try testing.expectEqual("default: 1", captured[8]);
198+
}
199+
200+
{
201+
test_capture.reset();
202+
try runner.testCases(&.{
203+
.{ "console.assert(true)", "undefined" },
204+
.{ "console.assert('a', 2, 3, 4)", "undefined" },
205+
.{ "console.assert('')", "undefined" },
206+
.{ "console.assert('', 'x', true)", "undefined" },
207+
.{ "console.assert(false, 'x')", "undefined" },
208+
}, .{});
209+
210+
const captured = test_capture.captured.items;
211+
try testing.expectEqual("Assertion failed: ", captured[0]);
212+
try testing.expectEqual("Assertion failed: x true", captured[1]);
213+
try testing.expectEqual("Assertion failed: x", captured[2]);
214+
}
215+
}
216+
217+
const TestCapture = struct {
218+
captured: std.ArrayListUnmanaged([]const u8) = .{},
219+
220+
fn reset(self: *TestCapture) void {
221+
self.captured = .{};
222+
}
223+
224+
fn debug(self: *TestCapture, comptime fmt: []const u8, args: anytype) void {
225+
const str = std.fmt.allocPrint(testing.arena_allocator, fmt, args) catch unreachable;
226+
self.captured.append(testing.arena_allocator, str) catch unreachable;
227+
}
228+
229+
fn info(self: *TestCapture, comptime fmt: []const u8, args: anytype) void {
230+
self.debug(fmt, args);
231+
}
232+
233+
fn warn(self: *TestCapture, comptime fmt: []const u8, args: anytype) void {
234+
self.debug(fmt, args);
235+
}
24236

25-
pub fn _log(_: *const Console, str: []const u8) void {
26-
log.debug("{s}\n", .{str});
237+
fn err(self: *TestCapture, comptime fmt: []const u8, args: anytype) void {
238+
self.debug(fmt, args);
27239
}
28240
};

src/browser/env.zig

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,9 @@ pub const SessionState = struct {
3434
http_client: *HttpClient,
3535
cookie_jar: *storage.CookieJar,
3636
document: ?*parser.DocumentHTML,
37+
38+
// dangerous, but set by the JS framework
39+
// shorter-lived than the arena above, which
40+
// exists for the entire rendering of the page
41+
call_arena: std.mem.Allocator = undefined,
3742
};

src/browser/html/window.zig

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const SessionState = @import("../env.zig").SessionState;
2525
const Navigator = @import("navigator.zig").Navigator;
2626
const History = @import("history.zig").History;
2727
const Location = @import("location.zig").Location;
28+
const Console = @import("../console/console.zig").Console;
2829
const EventTarget = @import("../dom/event_target.zig").EventTarget;
2930

3031
const storage = @import("../storage/storage.zig");
@@ -48,6 +49,7 @@ pub const Window = struct {
4849
timeoutid: u32 = 0,
4950
timeoutids: [512]u64 = undefined,
5051

52+
console: Console = .{},
5153
navigator: Navigator = .{},
5254

5355
pub fn create(target: ?[]const u8, navigator: ?Navigator) Window {
@@ -85,6 +87,10 @@ pub const Window = struct {
8587
return &self.location;
8688
}
8789

90+
pub fn get_console(self: *Window) *Console {
91+
return &self.console;
92+
}
93+
8894
pub fn get_self(self: *Window) *Window {
8995
return self;
9096
}

src/runtime/js.zig

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,18 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
347347
context.setEmbedderData(1, data);
348348
}
349349

350+
{
351+
// If we wanted to overwrite the built-in console, we'd have to
352+
// first delete it:
353+
const js_obj = context.getGlobal();
354+
const console_key = v8.String.initUtf8(isolate, "console");
355+
if (js_obj.deleteValue(context, console_key) == false) {
356+
return error.ConsoleDeleteError;
357+
}
358+
359+
// and then define a `get_console()` our Global object (i.e the window)
360+
}
361+
350362
executor.* = .{
351363
.state = state,
352364
.isolate = isolate,
@@ -370,6 +382,13 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
370382
executor.call_arena = executor._call_arena_instance.allocator();
371383
executor.scope_arena = executor._scope_arena_instance.allocator();
372384

385+
{
386+
const state_type_info = @typeInfo(@TypeOf(state));
387+
if (state_type_info == .pointer and @hasField(state_type_info.pointer.child, "call_arena")) {
388+
executor.state.call_arena = executor.call_arena;
389+
}
390+
}
391+
373392
errdefer self.stopExecutor(executor); // Note: This likely has issues as context.exit() is errdefered as well
374393

375394
// Custom exception
@@ -1309,6 +1328,21 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
13091328
return error.FailedToSet;
13101329
}
13111330
}
1331+
1332+
pub fn isTruthy(self: JsObject) bool {
1333+
const js_value = self.js_obj.toValue();
1334+
return js_value.toBool(self.executor.isolate);
1335+
}
1336+
1337+
pub fn toString(self: JsObject) ![]const u8 {
1338+
const executor = self.executor;
1339+
const js_value = self.js_obj.toValue();
1340+
return valueToString(executor.call_arena, js_value, executor.isolate, executor.context);
1341+
}
1342+
1343+
pub fn format(self: JsObject, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void {
1344+
return writer.writeAll(try self.toString());
1345+
}
13121346
};
13131347

13141348
// This only exists so that we know whether a function wants the opaque
@@ -1993,13 +2027,15 @@ fn Caller(comptime E: type) type {
19932027
is_variadic = true;
19942028
if (js_parameter_count == 0) {
19952029
@field(args, tupleFieldName(params_to_map.len + offset - 1)) = &.{};
1996-
} else {
2030+
} else if (js_parameter_count >= params_to_map.len) {
19972031
const arr = try self.call_arena.alloc(last_parameter_type_info.pointer.child, js_parameter_count - params_to_map.len + 1);
19982032
for (arr, last_js_parameter..) |*a, i| {
19992033
const js_value = info.getArg(@as(u32, @intCast(i)));
20002034
a.* = try self.jsValueToZig(named_function, slice_type, js_value);
20012035
}
20022036
@field(args, tupleFieldName(params_to_map.len + offset - 1)) = arr;
2037+
} else {
2038+
@field(args, tupleFieldName(params_to_map.len + offset - 1)) = &.{};
20032039
}
20042040
}
20052041
}
@@ -2171,10 +2207,6 @@ fn Caller(comptime E: type) type {
21712207
};
21722208
}
21732209

2174-
if (!js_value.isObject()) {
2175-
return error.InvalidArgument;
2176-
}
2177-
21782210
const js_obj = js_value.castTo(v8.Object);
21792211

21802212
if (comptime isJsObject(T)) {
@@ -2186,6 +2218,10 @@ fn Caller(comptime E: type) type {
21862218
};
21872219
}
21882220

2221+
if (!js_value.isObject()) {
2222+
return error.InvalidArgument;
2223+
}
2224+
21892225
const context = self.context;
21902226
const isolate = self.isolate;
21912227

0 commit comments

Comments
 (0)