Direct child paragraph
@@ -51,15 +51,15 @@ { const container = $('#desc-container'); - // testing.expectEqual('Nested paragraph', container.querySelector('div p').textContent); - // testing.expectEqual('Nested paragraph', container.querySelector('div.nested p').textContent); + testing.expectEqual('Direct child paragraph', container.querySelector('div p').textContent); + testing.expectEqual('Nested paragraph', container.querySelector('div.nested p').textContent); testing.expectEqual('Deeply nested paragraph', container.querySelector('div span p').textContent); - // testing.expectEqual('Nested paragraph', container.querySelector('.nested .text').textContent); - // testing.expectEqual(null, container.querySelector('article div p')); + testing.expectEqual('Nested paragraph', container.querySelector('.nested .text').textContent); + testing.expectEqual(null, container.querySelector('article div p')); - // const outerDiv = $('#outer-div'); - // testing.expectEqual('deep-span', outerDiv.querySelector('div div span').id); - // testing.expectEqual('deep-span', outerDiv.querySelector('.level1 span').id); - // testing.expectEqual('deep-span', outerDiv.querySelector('.level1 .level2 span').id); + const outerDiv = $('#outer-div'); + testing.expectEqual('deep-span', outerDiv.querySelector('div div span').id); + testing.expectEqual('deep-span', outerDiv.querySelector('.level1 span').id); + testing.expectEqual('deep-span', outerDiv.querySelector('.level1 .level2 span').id); } diff --git a/src/browser/tests/page/module.html b/src/browser/tests/page/module.html index f3dae6d1..1dd79794 100644 --- a/src/browser/tests/page/module.html +++ b/src/browser/tests/page/module.html @@ -1,7 +1,7 @@ - + - - - diff --git a/src/browser/tests/testing.js b/src/browser/tests/testing.js index 89c3d16b..d5ac8b97 100644 --- a/src/browser/tests/testing.js +++ b/src/browser/tests/testing.js @@ -2,6 +2,7 @@ let failed = false; let observed_ids = {}; let eventuallies = []; + let async_capture = null; let current_script_id = null; function expectTrue(actual) { @@ -12,14 +13,17 @@ expectEqual(false, actual); } - function expectEqual(expected, actual) { + function expectEqual(expected, actual, opts) { if (_equal(expected, actual)) { - _registerObservation('ok'); + _registerObservation('ok', opts); return; } failed = true; - _registerObservation('fail'); + _registerObservation('fail', opts); let err = `expected: ${_displayValue(expected)}, got: ${_displayValue(actual)}\n script_id: ${_currentScriptId()}`; + if (async_capture) { + err += `\n stack: ${async_capture.stack}`; + } console.error(err); throw new Error('expectEqual failed'); } @@ -57,7 +61,14 @@ callback: cb, script_id: script_id, }); + } + async function async(cb) { + const script_id = document.currentScript.id; + const stack = new Error().stack; + async_capture = {script_id: script_id, stack: stack}; + await cb(); + async_capture = null; } function assertOk() { @@ -92,6 +103,7 @@ window.testing = { fail: fail, + async: async, assertOk: assertOk, expectTrue: expectTrue, expectFalse: expectFalse, @@ -125,7 +137,6 @@ return false; } - if (expected instanceof Node) { if (!(actual instanceof Node)) { return false; @@ -145,8 +156,8 @@ return true; } - function _registerObservation(status) { - const script_id = _currentScriptId(); + function _registerObservation(status, opts) { + script_id = opts?.script_id || _currentScriptId(); if (!script_id) { return; } @@ -161,7 +172,12 @@ return current_script_id; } + if (async_capture) { + return async_capture.script_id; + } + const current_script = document.currentScript; + if (!current_script) { return null; } diff --git a/src/browser/tests/window/location.html b/src/browser/tests/window/location.html index f5ce7ffb..01a4049d 100644 --- a/src/browser/tests/window/location.html +++ b/src/browser/tests/window/location.html @@ -2,6 +2,6 @@ diff --git a/src/browser/tests/window/report_error.html b/src/browser/tests/window/report_error.html index 6796d46f..c2d66125 100644 --- a/src/browser/tests/window/report_error.html +++ b/src/browser/tests/window/report_error.html @@ -35,9 +35,6 @@ window.reportError(err); testing.expectEqual(true, evt.message.includes('Detailed error')); - testing.expectEqual('script.js', evt.filename); - testing.expectEqual(100, evt.lineno); - testing.expectEqual(25, evt.colno); testing.expectEqual(err, evt.error); } diff --git a/src/browser/webapi/Window.zig b/src/browser/webapi/Window.zig index d5261fed..0ea5fc06 100644 --- a/src/browser/webapi/Window.zig +++ b/src/browser/webapi/Window.zig @@ -147,6 +147,29 @@ pub fn cancelAnimationFrame(self: *Window, id: u32) void { sc.removed = true; } +pub fn reportError(self: *Window, err: js.Object, page: *Page) !void { + const error_event = try ErrorEvent.init("error", .{ + .@"error" = err, + .message = err.toString() catch "Unknown error", + .bubbles = false, + .cancelable = true, + }, page); + + const event = error_event.asEvent(); + try page._event_manager.dispatch(self.asEventTarget(), event); + + if (comptime builtin.is_test == false) { + if (!event._prevent_default) { + log.warn(.js, "window.reportError", .{ + .message = error_event._message, + .filename = error_event._filename, + .line_number = error_event._line_number, + .column_number = error_event._column_number, + }); + } + } +} + pub fn matchMedia(_: *const Window, query: []const u8, page: *Page) !*MediaQueryList { return page._factory.eventTarget(MediaQueryList{ ._proto = undefined, @@ -290,6 +313,7 @@ pub const JsApi = struct { pub const matchMedia = bridge.function(Window.matchMedia, .{}); pub const btoa = bridge.function(Window.btoa, .{}); pub const atob = bridge.function(Window.atob, .{}); + pub const reportError = bridge.function(Window.reportError, .{}); }; const testing = @import("../../testing.zig"); diff --git a/src/browser/webapi/element/html/Body.zig b/src/browser/webapi/element/html/Body.zig index 9822502f..585e74d6 100644 --- a/src/browser/webapi/element/html/Body.zig +++ b/src/browser/webapi/element/html/Body.zig @@ -30,11 +30,11 @@ pub const JsApi = struct { pub const Build = struct { pub fn complete(node: *Node, page: *Page) !void { - _ = node; - _ = page; - // @ZIGDOM - // const el = node.as(Element); - // const on_load = el.getAttributeSafe("onload") orelse return; - // page.window._on_load = page.js.stringToFunction(on_load); + const el = node.as(Element); + const on_load = el.getAttributeSafe("onload") orelse return; + page.window._on_load = page.js.stringToFunction(on_load) catch |err| blk: { + log.err(.js, "body.onload", .{.err = err, .str = on_load}); + break :blk null; + }; } }; diff --git a/src/browser/webapi/element/html/Script.zig b/src/browser/webapi/element/html/Script.zig index e6081106..df224ae2 100644 --- a/src/browser/webapi/element/html/Script.zig +++ b/src/browser/webapi/element/html/Script.zig @@ -1,3 +1,4 @@ +const log = @import("../../../../log.zig"); const js = @import("../../../js/js.zig"); const Page = @import("../../../Page.zig"); @@ -77,15 +78,19 @@ pub const Build = struct { const element = self.asElement(); self._src = element.getAttributeSafe("src") orelse ""; - // @ZIGDOM - _ = page; - // if (element.getAttributeSafe("onload")) |on_load| { - // self._on_load = page.js.stringToFunction(on_load); - // } + if (element.getAttributeSafe("onload")) |on_load| { + self._on_load = page.js.stringToFunction(on_load) catch |err| blk: { + log.err(.js, "script.onload", .{.err = err, .str = on_load}); + break :blk null; + }; + } - // if (element.getAttributeSafe("onerror")) |on_error| { - // self._on_error = page.js.stringToFunction(on_error); - // } + if (element.getAttributeSafe("onerror")) |on_error| { + self._on_error = page.js.stringToFunction(on_error) catch |err| blk: { + log.err(.js, "script.onerror", .{.err = err, .str = on_error}); + break :blk null; + }; + } } }; diff --git a/src/browser/webapi/selector/List.zig b/src/browser/webapi/selector/List.zig index 0283178d..0bedc46c 100644 --- a/src/browser/webapi/selector/List.zig +++ b/src/browser/webapi/selector/List.zig @@ -22,32 +22,26 @@ pub const EntryIterator = GenericIterator(Iterator, null); pub const KeyIterator = GenericIterator(Iterator, "0"); pub const ValueIterator = GenericIterator(Iterator, "1"); -pub fn init(arena: Allocator, root: *Node, selector: Selector.Selector, page: *Page) !*List { - var list = try page._factory.create(List{ - ._arena = arena, - ._nodes = &.{}, - }); - +pub fn collect( + allocator: std.mem.Allocator, + root: *Node, + selector: Selector.Selector, + nodes: *std.AutoArrayHashMapUnmanaged(*Node, void), + page: *Page, +) !void { if (optimizeSelector(root, &selector, page)) |result| { - var nodes: std.ArrayListUnmanaged(*Node) = .empty; - var tw = TreeWalker.init(result.root, .{}); - const optimized_selector = result.selector; if (result.exclude_root) { _ = tw.next(); } - // When exclude_root is true, pass root as boundary so it can match but we won't search beyond it - // When exclude_root is false, pass null so there's no boundary (root already matched, searching descendants) + const boundary = if (result.exclude_root) result.root else null; while (tw.next()) |node| { - if (matches(node, optimized_selector, boundary)) { - try nodes.append(arena, node); + if (matches(node, result.selector, boundary)) { + try nodes.put(allocator, node, {}); } } - list._nodes = nodes.items; } - - return list; } // used internally to find the first match @@ -135,7 +129,7 @@ fn optimizeSelector(root: *Node, selector: *const Selector.Selector, page: *Page .first = selector.first, .segments = selector.segments, }, - .exclude_root = false, + .exclude_root = true, }; } @@ -238,7 +232,7 @@ fn findIdSelector(selector: *const Selector.Selector) ?IdAnchor { return null; } -fn matches(node: *Node, selector: Selector.Selector, root: ?*Node) bool { +pub fn matches(node: *Node, selector: Selector.Selector, root: ?*Node) bool { const el = node.is(Node.Element) orelse return false; if (selector.segments.len == 0) { @@ -333,8 +327,9 @@ fn matchChild(node: *Node, compound: Selector.Compound, root: ?*Node) ?*Node { const parent = node._parent orelse return null; // Don't match beyond the root boundary + // If there's a boundary, check if parent is outside (an ancestor of) the boundary if (root) |boundary| { - if (parent == boundary) { + if (!boundary.contains(parent)) { return null; } } diff --git a/src/browser/webapi/selector/Parser.zig b/src/browser/webapi/selector/Parser.zig index 7fccc812..0e88df0d 100644 --- a/src/browser/webapi/selector/Parser.zig +++ b/src/browser/webapi/selector/Parser.zig @@ -1,5 +1,7 @@ const std = @import("std"); +const Allocator = std.mem.Allocator; + const Page = @import("../../Page.zig"); const Node = @import("../Node.zig"); @@ -8,7 +10,6 @@ const Part = Selector.Part; const Combinator = Selector.Combinator; const Segment = Selector.Segment; const Attribute = @import("../element/Attribute.zig"); -const Allocator = std.mem.Allocator; const Parser = @This(); @@ -26,10 +27,56 @@ const ParseError = error{ InvalidTagSelector, InvalidSelector, }; + +pub fn parseList(arena: Allocator, input: []const u8, page: *Page) ParseError![]const Selector.Selector { + var selectors: std.ArrayList(Selector.Selector) = .empty; + + var remaining = input; + while (true) { + const trimmed = std.mem.trimLeft(u8, remaining, &std.ascii.whitespace); + if (trimmed.len == 0) break; + + var comma_pos: usize = trimmed.len; + var depth: usize = 0; + for (trimmed, 0..) |c, i| { + switch (c) { + '(' => depth += 1, + ')' => { + if (depth > 0) depth -= 1; + }, + ',' => { + if (depth == 0) { + comma_pos = i; + break; + } + }, + else => {}, + } + } + + const selector_input = std.mem.trimRight(u8, trimmed[0..comma_pos], &std.ascii.whitespace); + + if (selector_input.len > 0) { + const selector = try parse(arena, selector_input, page); + try selectors.append(arena, selector); + } + + if (comma_pos >= trimmed.len) break; + remaining = trimmed[comma_pos + 1 ..]; + } + + if (selectors.items.len == 0) { + return error.InvalidSelector; + } + + return selectors.items; +} + pub fn parse(arena: Allocator, input: []const u8, page: *Page) ParseError!Selector.Selector { var parser = Parser{ .input = input }; - var segments: std.ArrayListUnmanaged(Segment) = .empty; - var current_compound: std.ArrayListUnmanaged(Part) = .empty; + var segments: std.ArrayList(Segment) = .empty; + var current_compound: std.ArrayList(Part) = .empty; + // Parse the first compound (no combinator before it) while (parser.skipSpaces()) { @@ -302,7 +349,7 @@ fn pseudoClass(self: *Parser, arena: Allocator, page: *Page) !Selector.PseudoCla if (std.mem.eql(u8, name, "not")) { // CSS Level 4: :not() can contain a full selector list (comma-separated selectors) // e.g., :not(div, .class, #id > span) - var selectors: std.ArrayListUnmanaged(Selector.Selector) = .empty; + var selectors: std.ArrayList(Selector.Selector) = .empty; _ = self.skipSpaces(); diff --git a/src/browser/webapi/selector/Selector.zig b/src/browser/webapi/selector/Selector.zig index 7e4da77c..8839b1b6 100644 --- a/src/browser/webapi/selector/Selector.zig +++ b/src/browser/webapi/selector/Selector.zig @@ -10,24 +10,29 @@ pub fn querySelector(root: *Node, input: []const u8, page: *Page) !?*Node.Elemen return error.SyntaxError; } - const selector = try Parser.parse(page.call_arena, input, page); + const arena = page.call_arena; + const selectors = try Parser.parseList(arena, input, page); - // Fast path: single compound with only an ID selector - if (selector.segments.len == 0 and selector.first.parts.len == 1) { - const first = selector.first.parts[0]; - if (first == .id) { - const el = page.document._elements_by_id.get(first.id) orelse return null; - // Check if the element is within the root subtree - if (root.contains(el.asNode())) { + for (selectors) |selector| { + // Fast path: single compound with only an ID selector + if (selector.segments.len == 0 and selector.first.parts.len == 1) { + const first = selector.first.parts[0]; + if (first == .id) { + const el = page.document._elements_by_id.get(first.id) orelse continue; + // Check if the element is within the root subtree + if (root.contains(el.asNode())) { + return el; + } + continue; + } + } + + if (List.initOne(root, selector, page)) |node| { + if (node.is(Node.Element)) |el| { return el; } - return null; } } - - if (List.initOne(root, selector, page)) |node| { - return node.is(Node.Element); - } return null; } @@ -37,8 +42,33 @@ pub fn querySelectorAll(root: *Node, input: []const u8, page: *Page) !*List { } const arena = page.arena; - const selector = try Parser.parse(arena, input, page); - return List.init(arena, root, selector, page); + var nodes: std.AutoArrayHashMapUnmanaged(*Node, void) = .empty; + + const selectors = try Parser.parseList(arena, input, page); + for (selectors) |selector| { + try List.collect(arena, root, selector, &nodes, page); + } + + return page._factory.create(List{ + ._arena = arena, + ._nodes = nodes.keys(), + }); +} + +pub fn matches(el: *Node.Element, input: []const u8, page: *Page) !bool { + if (input.len == 0) { + return error.SyntaxError; + } + + const arena = page.call_arena; + const selectors = try Parser.parseList(arena, input, page); + + for (selectors) |selector| { + if (List.matches(el.asNode(), selector, null)) { + return true; + } + } + return false; } pub fn classAttributeContains(class_attr: []const u8, class_name: []const u8) bool { diff --git a/src/log.zig b/src/log.zig index d0f02bf9..f791e9f7 100644 --- a/src/log.zig +++ b/src/log.zig @@ -131,7 +131,7 @@ pub fn log(comptime scope: Scope, level: Level, comptime msg: []const u8, data: var writer = stderr.writer(&buf); logTo(scope, level, msg, data, &writer.interface) catch |log_err| { - std.debug.print("$time={d} $level=fatal $scope={s} $msg=\"log err\" err={s} log_msg=\"{s}\"", .{ timestamp(.clock), @errorName(log_err), @tagName(scope), msg }); + std.debug.print("$time={d} $level=fatal $scope={s} $msg=\"log err\" err={s} log_msg=\"{s}\"\n", .{ timestamp(.clock), @errorName(log_err), @tagName(scope), msg }); }; } @@ -147,7 +147,6 @@ fn logTo(comptime scope: Scope, level: Level, comptime msg: []const u8, data: an } } } - switch (opts.format) { .logfmt => try logLogfmt(scope, level, msg, data, out), .pretty => try logPretty(scope, level, msg, data, out),