mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-22 04:34:44 +00:00
Merge branch 'main' into semantic-tree
This commit is contained in:
2
.github/workflows/wpt.yml
vendored
2
.github/workflows/wpt.yml
vendored
@@ -108,7 +108,7 @@ jobs:
|
||||
run: |
|
||||
./wpt serve 2> /dev/null & echo $! > WPT.pid
|
||||
sleep 10s
|
||||
./wptrunner -lpd-path ./lightpanda -json -concurrency 6 > wpt.json
|
||||
./wptrunner -lpd-path ./lightpanda -json -concurrency 10 -pool 3 > wpt.json
|
||||
kill `cat WPT.pid`
|
||||
|
||||
- name: write commit
|
||||
|
||||
@@ -652,6 +652,10 @@ fn scheduleNavigationWithArena(originator: *Page, arena: Allocator, request_url:
|
||||
.navigation_type = std.meta.activeTag(nt),
|
||||
};
|
||||
|
||||
if (target._queued_navigation) |existing| {
|
||||
target.arena_pool.release(existing.arena);
|
||||
}
|
||||
|
||||
target._queued_navigation = qn;
|
||||
return session.scheduleNavigation(target);
|
||||
}
|
||||
@@ -2908,9 +2912,12 @@ fn nodeIsReady(self: *Page, comptime from_parser: bool, node: *Node) !void {
|
||||
}
|
||||
if (node.is(Element.Html.Script)) |script| {
|
||||
if ((comptime from_parser == false) and script._src.len == 0) {
|
||||
// script was added via JavaScript, but without a src, don't try
|
||||
// to execute it (we'll execute it if/when the src is set)
|
||||
return;
|
||||
// Script was added via JavaScript without a src attribute.
|
||||
// Only skip if it has no inline content either — scripts with
|
||||
// textContent/text should still execute per spec.
|
||||
if (node.firstChild() == null) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
self.scriptAddedCallback(from_parser, script) catch |err| {
|
||||
|
||||
@@ -535,6 +535,14 @@ fn postCompileModule(self: *Context, mod: js.Module, url: [:0]const u8, local: *
|
||||
nested_gop.key_ptr.* = owned_specifier;
|
||||
nested_gop.value_ptr.* = .{};
|
||||
try script_manager.preloadImport(owned_specifier, url);
|
||||
} else if (nested_gop.value_ptr.module == null) {
|
||||
// Entry exists but module failed to compile previously.
|
||||
// The imported_modules entry may have been consumed, so
|
||||
// re-preload to ensure waitForImport can find it.
|
||||
// Key was stored via dupeZ so it has a sentinel in memory.
|
||||
const key = nested_gop.key_ptr.*;
|
||||
const key_z: [:0]const u8 = key.ptr[0..key.len :0];
|
||||
try script_manager.preloadImport(key_z, url);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -683,7 +691,15 @@ fn _resolveModuleCallback(self: *Context, referrer: js.Module, specifier: [:0]co
|
||||
return local.toLocal(m).handle;
|
||||
}
|
||||
|
||||
var source = try self.script_manager.?.waitForImport(normalized_specifier);
|
||||
var source = self.script_manager.?.waitForImport(normalized_specifier) catch |err| switch (err) {
|
||||
error.UnknownModule => blk: {
|
||||
// Module is in cache but was consumed from imported_modules
|
||||
// (e.g., by a previous failed resolution). Re-preload and retry.
|
||||
try self.script_manager.?.preloadImport(normalized_specifier, referrer_path);
|
||||
break :blk try self.script_manager.?.waitForImport(normalized_specifier);
|
||||
},
|
||||
else => return err,
|
||||
};
|
||||
defer source.deinit();
|
||||
|
||||
var try_catch: js.TryCatch = undefined;
|
||||
|
||||
@@ -730,6 +730,7 @@ pub const JsApis = flattenTypes(&.{
|
||||
@import("../webapi/css/CSSStyleRule.zig"),
|
||||
@import("../webapi/css/CSSStyleSheet.zig"),
|
||||
@import("../webapi/css/CSSStyleProperties.zig"),
|
||||
@import("../webapi/css/FontFace.zig"),
|
||||
@import("../webapi/css/FontFaceSet.zig"),
|
||||
@import("../webapi/css/MediaQueryList.zig"),
|
||||
@import("../webapi/css/StyleSheetList.zig"),
|
||||
|
||||
63
src/browser/tests/css/font_face.html
Normal file
63
src/browser/tests/css/font_face.html
Normal file
@@ -0,0 +1,63 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../testing.js"></script>
|
||||
|
||||
<script id="constructor_basic">
|
||||
{
|
||||
const face = new FontFace("TestFont", "url(test.woff)");
|
||||
testing.expectTrue(face instanceof FontFace);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="constructor_name">
|
||||
{
|
||||
testing.expectEqual('FontFace', FontFace.name);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="family_property">
|
||||
{
|
||||
const face = new FontFace("MyFont", "url(font.woff2)");
|
||||
testing.expectEqual("MyFont", face.family);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="status_is_loaded">
|
||||
{
|
||||
const face = new FontFace("F", "url(f.woff)");
|
||||
testing.expectEqual("loaded", face.status);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="loaded_is_promise">
|
||||
{
|
||||
const face = new FontFace("F", "url(f.woff)");
|
||||
testing.expectTrue(face.loaded instanceof Promise);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="load_returns_promise">
|
||||
{
|
||||
const face = new FontFace("F", "url(f.woff)");
|
||||
testing.expectTrue(face.load() instanceof Promise);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="default_descriptors">
|
||||
{
|
||||
const face = new FontFace("F", "url(f.woff)");
|
||||
testing.expectEqual("normal", face.style);
|
||||
testing.expectEqual("normal", face.weight);
|
||||
testing.expectEqual("normal", face.stretch);
|
||||
testing.expectEqual("normal", face.variant);
|
||||
testing.expectEqual("normal", face.featureSettings);
|
||||
testing.expectEqual("auto", face.display);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="document_fonts_add">
|
||||
{
|
||||
const face = new FontFace("AddedFont", "url(added.woff)");
|
||||
const result = document.fonts.add(face);
|
||||
testing.expectTrue(result === document.fonts);
|
||||
}
|
||||
</script>
|
||||
54
src/browser/tests/element/html/script/dynamic_inline.html
Normal file
54
src/browser/tests/element/html/script/dynamic_inline.html
Normal file
@@ -0,0 +1,54 @@
|
||||
<!DOCTYPE html>
|
||||
<head></head>
|
||||
<script src="../../../testing.js"></script>
|
||||
|
||||
<script id=textContent_inline>
|
||||
window.inline_executed = false;
|
||||
const s1 = document.createElement('script');
|
||||
s1.textContent = 'window.inline_executed = true;';
|
||||
document.head.appendChild(s1);
|
||||
testing.expectTrue(window.inline_executed);
|
||||
</script>
|
||||
|
||||
<script id=text_property_inline>
|
||||
window.text_executed = false;
|
||||
const s2 = document.createElement('script');
|
||||
s2.text = 'window.text_executed = true;';
|
||||
document.head.appendChild(s2);
|
||||
testing.expectTrue(window.text_executed);
|
||||
</script>
|
||||
|
||||
<script id=innerHTML_inline>
|
||||
window.innerHTML_executed = false;
|
||||
const s3 = document.createElement('script');
|
||||
s3.innerHTML = 'window.innerHTML_executed = true;';
|
||||
document.head.appendChild(s3);
|
||||
testing.expectTrue(window.innerHTML_executed);
|
||||
</script>
|
||||
|
||||
<script id=no_double_execute_inline>
|
||||
window.inline_counter = 0;
|
||||
const s4 = document.createElement('script');
|
||||
s4.textContent = 'window.inline_counter++;';
|
||||
document.head.appendChild(s4);
|
||||
document.head.appendChild(s4);
|
||||
testing.expectEqual(1, window.inline_counter);
|
||||
</script>
|
||||
|
||||
<script id=empty_script_no_execute>
|
||||
window.empty_ran = false;
|
||||
const s5 = document.createElement('script');
|
||||
document.head.appendChild(s5);
|
||||
testing.expectFalse(window.empty_ran);
|
||||
</script>
|
||||
|
||||
<script id=module_inline>
|
||||
window.module_executed = false;
|
||||
const s6 = document.createElement('script');
|
||||
s6.type = 'module';
|
||||
s6.textContent = 'window.module_executed = true;';
|
||||
document.head.appendChild(s6);
|
||||
testing.eventually(() => {
|
||||
testing.expectTrue(window.module_executed);
|
||||
});
|
||||
</script>
|
||||
@@ -81,6 +81,17 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="is_empty">
|
||||
{
|
||||
// Empty :is() and :where() are valid per spec and match nothing
|
||||
const isEmptyResult = document.querySelectorAll(':is()');
|
||||
testing.expectEqual(0, isEmptyResult.length);
|
||||
|
||||
const whereEmptyResult = document.querySelectorAll(':where()');
|
||||
testing.expectEqual(0, whereEmptyResult.length);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div id=escaped class=":popover-open"></div>
|
||||
<script id="escaped">
|
||||
{
|
||||
|
||||
@@ -12,8 +12,6 @@
|
||||
// Empty functional pseudo-classes should error
|
||||
testing.expectError("Error: InvalidPseudoClass", () => container.querySelector(':has()'));
|
||||
testing.expectError("Error: InvalidPseudoClass", () => container.querySelector(':not()'));
|
||||
testing.expectError("Error: InvalidPseudoClass", () => container.querySelector(':is()'));
|
||||
testing.expectError("Error: InvalidPseudoClass", () => container.querySelector(':where()'));
|
||||
testing.expectError("Error: InvalidPseudoClass", () => container.querySelector(':lang()'));
|
||||
}
|
||||
</script>
|
||||
|
||||
90
src/browser/webapi/css/FontFace.zig
Normal file
90
src/browser/webapi/css/FontFace.zig
Normal file
@@ -0,0 +1,90 @@
|
||||
// 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 js = @import("../../js/js.zig");
|
||||
const Page = @import("../../Page.zig");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const FontFace = @This();
|
||||
|
||||
_arena: Allocator,
|
||||
_family: []const u8,
|
||||
|
||||
pub fn init(family: []const u8, source: []const u8, page: *Page) !*FontFace {
|
||||
_ = source;
|
||||
|
||||
const arena = try page.getArena(.{.debug = "FontFace"});
|
||||
errdefer page.releaseArena(arena);
|
||||
|
||||
const self = try arena.create(FontFace);
|
||||
self.* = .{
|
||||
._arena = arena,
|
||||
._family = try arena.dupe(u8, family),
|
||||
};
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *FontFace, _: bool, page: *Page) void {
|
||||
page.releaseArena(self._arena);
|
||||
}
|
||||
|
||||
pub fn getFamily(self: *const FontFace) []const u8 {
|
||||
return self._family;
|
||||
}
|
||||
|
||||
// load() - resolves immediately; headless browser has no real font loading.
|
||||
pub fn load(_: *FontFace, page: *Page) !js.Promise {
|
||||
return page.js.local.?.resolvePromise({});
|
||||
}
|
||||
|
||||
// loaded - returns an already-resolved Promise.
|
||||
pub fn getLoaded(_: *FontFace, page: *Page) !js.Promise {
|
||||
return page.js.local.?.resolvePromise({});
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(FontFace);
|
||||
|
||||
pub const Meta = struct {
|
||||
pub const name = "FontFace";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
pub const weak = true;
|
||||
pub const finalizer = bridge.finalizer(FontFace.deinit);
|
||||
};
|
||||
|
||||
pub const constructor = bridge.constructor(FontFace.init, .{});
|
||||
pub const family = bridge.accessor(FontFace.getFamily, null, .{});
|
||||
pub const status = bridge.property("loaded", .{ .template = false, .readonly = true });
|
||||
pub const style = bridge.property("normal", .{ .template = false, .readonly = true });
|
||||
pub const weight = bridge.property("normal", .{ .template = false, .readonly = true });
|
||||
pub const stretch = bridge.property("normal", .{ .template = false, .readonly = true });
|
||||
pub const unicodeRange = bridge.property("U+0-10FFFF", .{ .template = false, .readonly = true });
|
||||
pub const variant = bridge.property("normal", .{ .template = false, .readonly = true });
|
||||
pub const featureSettings = bridge.property("normal", .{ .template = false, .readonly = true });
|
||||
pub const display = bridge.property("auto", .{ .template = false, .readonly = true });
|
||||
pub const loaded = bridge.accessor(FontFace.getLoaded, null, .{});
|
||||
pub const load = bridge.function(FontFace.load, .{});
|
||||
};
|
||||
|
||||
const testing = @import("../../../testing.zig");
|
||||
test "WebApi: FontFace" {
|
||||
try testing.htmlRunner("css/font_face.html", .{});
|
||||
}
|
||||
@@ -1,14 +1,45 @@
|
||||
// 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 js = @import("../../js/js.zig");
|
||||
const Page = @import("../../Page.zig");
|
||||
const FontFace = @import("FontFace.zig");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const FontFaceSet = @This();
|
||||
|
||||
// Padding to avoid zero-size struct, which causes identity_map pointer collisions.
|
||||
_pad: bool = false,
|
||||
_arena: Allocator,
|
||||
|
||||
pub fn init(page: *Page) !*FontFaceSet {
|
||||
return page._factory.create(FontFaceSet{});
|
||||
const arena = try page.getArena(.{.debug = "FontFaceSet"});
|
||||
errdefer page.releaseArena(arena);
|
||||
|
||||
const self = try arena.create(FontFaceSet);
|
||||
self.* = .{
|
||||
._arena = arena,
|
||||
};
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *FontFaceSet, _: bool, page: *Page) void {
|
||||
page.releaseArena(self._arena);
|
||||
}
|
||||
|
||||
// FontFaceSet.ready - returns an already-resolved Promise.
|
||||
@@ -29,6 +60,11 @@ pub fn load(_: *FontFaceSet, font: []const u8, page: *Page) !js.Promise {
|
||||
return page.js.local.?.resolvePromise({});
|
||||
}
|
||||
|
||||
// add(fontFace) - no-op; headless browser does not track loaded fonts.
|
||||
pub fn add(self: *FontFaceSet, _: *FontFace) *FontFaceSet {
|
||||
return self;
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(FontFaceSet);
|
||||
|
||||
@@ -36,6 +72,8 @@ pub const JsApi = struct {
|
||||
pub const name = "FontFaceSet";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
pub const weak = true;
|
||||
pub const finalizer = bridge.finalizer(FontFaceSet.deinit);
|
||||
};
|
||||
|
||||
pub const size = bridge.property(0, .{ .template = false, .readonly = true });
|
||||
@@ -43,6 +81,7 @@ pub const JsApi = struct {
|
||||
pub const ready = bridge.accessor(FontFaceSet.getReady, null, .{});
|
||||
pub const check = bridge.function(FontFaceSet.check, .{});
|
||||
pub const load = bridge.function(FontFaceSet.load, .{});
|
||||
pub const add = bridge.function(FontFaceSet.add, .{});
|
||||
};
|
||||
|
||||
const testing = @import("../../../testing.zig");
|
||||
|
||||
@@ -487,7 +487,7 @@ fn pseudoClass(self: *Parser, arena: Allocator, page: *Page) !Selector.PseudoCla
|
||||
if (self.peek() != ')') return error.InvalidPseudoClass;
|
||||
self.input = self.input[1..];
|
||||
|
||||
if (selectors.items.len == 0) return error.InvalidPseudoClass;
|
||||
// Empty :is() is valid per spec - matches nothing
|
||||
return .{ .is = selectors.items };
|
||||
}
|
||||
|
||||
@@ -514,7 +514,7 @@ fn pseudoClass(self: *Parser, arena: Allocator, page: *Page) !Selector.PseudoCla
|
||||
if (self.peek() != ')') return error.InvalidPseudoClass;
|
||||
self.input = self.input[1..];
|
||||
|
||||
if (selectors.items.len == 0) return error.InvalidPseudoClass;
|
||||
// Empty :where() is valid per spec - matches nothing
|
||||
return .{ .where = selectors.items };
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user