mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-07-31 09:46:05 -04:00
Merge pull request #2951 from lightpanda-io/dom-pr-10-selectors-and-encoding
selector/webapi: :link/:lang/:empty semantics, document encoding, for-in enumerability
This commit is contained in:
@@ -50,6 +50,7 @@ pub fn createHTMLDocument(_: *const DOMImplementation, title: ?js.NullableString
|
||||
const document = (try frame._factory.document(Node.Document.HTMLDocument{ ._proto = undefined })).asDocument();
|
||||
document._ready_state = .complete;
|
||||
document._url = "about:blank";
|
||||
document._charset = "UTF-8";
|
||||
|
||||
{
|
||||
const doctype = try frame._factory.node(DocumentType{
|
||||
@@ -101,6 +102,7 @@ pub fn createDocument(_: *const DOMImplementation, namespace_nullable: js.Nullab
|
||||
// Create XML Document
|
||||
const document = (try frame._factory.document(Node.Document.XMLDocument{ ._proto = undefined })).asDocument();
|
||||
document._url = "about:blank";
|
||||
document._charset = "UTF-8";
|
||||
// Per spec the content type depends on the requested namespace.
|
||||
document._content_type = blk: {
|
||||
const ns = namespace_ orelse break :blk "application/xml";
|
||||
|
||||
@@ -56,6 +56,9 @@ _frame: ?*Frame = null,
|
||||
_url: ?[:0]const u8 = null, // URL for documents created via DOMImplementation (about:blank)
|
||||
// content type override for documents created via DOMImplementation.createDocument
|
||||
_content_type: ?[]const u8 = null,
|
||||
// encoding override: documents synthesized by script (createHTMLDocument,
|
||||
// createDocument) are UTF-8 regardless of the frame's encoding
|
||||
_charset: ?[]const u8 = null,
|
||||
_ready_state: ReadyState = .loading,
|
||||
_current_script: ?*Element.Html.Script = null,
|
||||
_elements_by_id: std.StringHashMapUnmanaged(*Element) = .empty,
|
||||
@@ -159,6 +162,14 @@ pub fn setLocation(self: *Document, url: [:0]const u8) !void {
|
||||
return frame.scheduleNavigation(url, .{ .reason = .script, .kind = .{ .push = null } }, .{ .script = frame });
|
||||
}
|
||||
|
||||
pub fn getCharset(self: *const Document) []const u8 {
|
||||
if (self._charset) |charset| {
|
||||
return charset;
|
||||
}
|
||||
const doc_frame = self._frame orelse return "UTF-8";
|
||||
return doc_frame.charset;
|
||||
}
|
||||
|
||||
pub fn getContentType(self: *const Document) []const u8 {
|
||||
if (self._content_type) |content_type| {
|
||||
return content_type;
|
||||
@@ -1417,6 +1428,7 @@ pub const JsApi = struct {
|
||||
return frame._factory.node(Document{
|
||||
._proto = undefined,
|
||||
._type = .generic,
|
||||
._charset = "UTF-8",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1508,8 +1520,7 @@ pub const JsApi = struct {
|
||||
pub const compatMode = bridge.property("CSS1Compat", .{ .template = false });
|
||||
|
||||
fn getCharacterSet(self: *const Document) []const u8 {
|
||||
const doc_frame = self._frame orelse return "UTF-8";
|
||||
return doc_frame.charset;
|
||||
return self.getCharset();
|
||||
}
|
||||
pub const referrer = bridge.property("", .{ .template = false });
|
||||
|
||||
|
||||
@@ -558,6 +558,13 @@ pub fn ownerDocument(self: *const Node, frame: *const Frame) ?*Document {
|
||||
return frame.document;
|
||||
}
|
||||
|
||||
fn ownerDocumentIncludingSelf(self: *const Node, frame: *const Frame) ?*Document {
|
||||
if (self._type == .document) {
|
||||
return self._type.document;
|
||||
}
|
||||
return self.ownerDocument(frame);
|
||||
}
|
||||
|
||||
// Returns the Frame that owns this node's tree. Used to tie cached state of
|
||||
// "live" collections (NodeList, HTMLCollection, etc.) to the right frame's DOM
|
||||
// version: cross-realm callers must invalidate based on mutations through the
|
||||
@@ -566,10 +573,7 @@ pub fn ownerDocument(self: *const Node, frame: *const Frame) ?*Document {
|
||||
// Falls back to `default` when the node has no associated document yet (e.g.,
|
||||
// freshly created and detached) or its document has no frame.
|
||||
pub fn ownerFrame(self: *const Node, default: *Frame) *Frame {
|
||||
if (self._type == .document) {
|
||||
return self._type.document._frame orelse default;
|
||||
}
|
||||
const doc = self.ownerDocument(default) orelse return default;
|
||||
const doc = self.ownerDocumentIncludingSelf(default) orelse return default;
|
||||
return doc._frame orelse default;
|
||||
}
|
||||
|
||||
@@ -582,7 +586,9 @@ pub const ResolveURLOpts = struct {
|
||||
pub fn resolveURL(self: *const Node, url: anytype, frame: *Frame, opts: ResolveURLOpts) ![:0]const u8 {
|
||||
const owner_frame = self.ownerFrame(frame);
|
||||
const allocator = opts.allocator orelse frame.call_arena;
|
||||
return URL.resolve(allocator, owner_frame.base(), url, .{ .encoding = owner_frame.charset });
|
||||
const doc: ?*const Document = self.ownerDocumentIncludingSelf(frame);
|
||||
const encoding = if (doc) |d| d.getCharset() else owner_frame.charset;
|
||||
return URL.resolve(allocator, owner_frame.base(), url, .{ .encoding = encoding });
|
||||
}
|
||||
|
||||
// Same as `resolveURL` but can't return `TypeError`, this is needed for multiple
|
||||
@@ -596,8 +602,8 @@ pub fn resolveURLReflect(self: *const Node, url: []const u8, frame: *Frame, opts
|
||||
|
||||
pub fn isSameDocumentAs(self: *const Node, other: *const Node, frame: *const Frame) bool {
|
||||
// Get the root document for each node
|
||||
const self_doc = if (self._type == .document) self._type.document else self.ownerDocument(frame);
|
||||
const other_doc = if (other._type == .document) other._type.document else other.ownerDocument(frame);
|
||||
const self_doc = self.ownerDocumentIncludingSelf(frame);
|
||||
const other_doc = other.ownerDocumentIncludingSelf(frame);
|
||||
return self_doc == other_doc;
|
||||
}
|
||||
|
||||
@@ -1373,11 +1379,7 @@ pub const JsApi = struct {
|
||||
|
||||
pub const baseURI = bridge.accessor(_baseURI, null, .{});
|
||||
fn _baseURI(self: *Node, frame: *const Frame) []const u8 {
|
||||
const doc = if (self._type == .document)
|
||||
self._type.document
|
||||
else
|
||||
self.ownerDocument(frame) orelse return frame.base();
|
||||
|
||||
const doc = self.ownerDocumentIncludingSelf(frame) orelse return frame.base();
|
||||
if (doc._frame) |doc_frame| {
|
||||
return doc_frame.base();
|
||||
}
|
||||
|
||||
@@ -180,7 +180,17 @@ pub const JsApi = struct {
|
||||
|
||||
return self.getByName(name, frame) orelse error.NotHandled;
|
||||
}
|
||||
}.wrap, null, deleteByName, getNames, null, defineByName, describeByName, .{ .null_as_undefined = true });
|
||||
}.wrap, null, deleteByName, getNames, queryByName, defineByName, describeByName, .{ .null_as_undefined = true });
|
||||
|
||||
// Named properties are [LegacyUnenumerableNamedProperties]: the query
|
||||
// reports them non-enumerable, so for-in skips them while
|
||||
// Object.getOwnPropertyNames still lists them via the enumerator.
|
||||
fn queryByName(self: *HTMLCollection, name: []const u8, frame: *Frame) !u32 {
|
||||
if (name.len > 0 and self.getByName(name, frame) != null) {
|
||||
return js.v8.DontEnum;
|
||||
}
|
||||
return error.NotHandled;
|
||||
}
|
||||
|
||||
// HTMLCollection has no indexed setter: per Web IDL, assigning to or
|
||||
// defining any array index property fails (TypeError in strict mode).
|
||||
|
||||
@@ -612,13 +612,15 @@ fn matchesPseudoClass(el: *Node.Element, pseudo: Selector.PseudoClass, scope: *N
|
||||
},
|
||||
.focus_visible => return false,
|
||||
|
||||
// Link states
|
||||
.link => return false,
|
||||
.visited => return false,
|
||||
.any_link => {
|
||||
if (el.getTag() != .anchor) return false;
|
||||
// Link states. In a headless browser no link is ever visited, so
|
||||
// :link matches every hyperlink (a or area with an href attribute)
|
||||
// and :visited matches nothing.
|
||||
.link, .any_link => {
|
||||
const tag = el.getTag();
|
||||
if (tag != .anchor and tag != .area) return false;
|
||||
return el.getAttributeSafe(comptime .wrap("href")) != null;
|
||||
},
|
||||
.visited => return false,
|
||||
.target => {
|
||||
const element_id = el.getAttributeSafe(comptime .wrap("id")) orelse return false;
|
||||
const doc = node.ownerDocument(frame) orelse return false;
|
||||
@@ -638,7 +640,19 @@ fn matchesPseudoClass(el: *Node.Element, pseudo: Selector.PseudoClass, scope: *N
|
||||
return node == scope;
|
||||
},
|
||||
.empty => {
|
||||
return node.firstChild() == null;
|
||||
// Only element and content (non-empty text/cdata) children affect
|
||||
// emptiness; comments and processing instructions are ignored.
|
||||
var it = node.childrenIterator();
|
||||
while (it.next()) |child| {
|
||||
switch (child._type) {
|
||||
.cdata => |cdata| switch (cdata._type) {
|
||||
.comment, .processing_instruction => {},
|
||||
else => if (cdata.getLength() > 0) return false,
|
||||
},
|
||||
else => return false,
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
.first_child => return isFirstChild(el),
|
||||
.last_child => return isLastChild(el),
|
||||
@@ -660,7 +674,39 @@ fn matchesPseudoClass(el: *Node.Element, pseudo: Selector.PseudoClass, scope: *N
|
||||
},
|
||||
|
||||
// Functional
|
||||
.lang => return false,
|
||||
.lang => |expected| {
|
||||
if (expected.len == 0) return false;
|
||||
// The element's language is the nearest ancestor-or-self lang
|
||||
// attribute. Elements in a document with no declared language
|
||||
// fall back to the UA default (en); detached subtrees have no
|
||||
// language at all.
|
||||
const lang = blk: {
|
||||
var current: ?*Node = node;
|
||||
while (current) |cur| : (current = cur.parentNode()) {
|
||||
switch (cur._type) {
|
||||
.element => |ancestor| {
|
||||
if (ancestor.getAttributeSafe(comptime .wrap("lang"))) |value| {
|
||||
break :blk value;
|
||||
}
|
||||
},
|
||||
.document => {
|
||||
break :blk "en";
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
if (lang.len < expected.len) {
|
||||
return false;
|
||||
}
|
||||
// Match the exact language or a `-` separated sub-tag prefix
|
||||
// (:lang(en) matches lang="en-AU"), ASCII case-insensitively.
|
||||
if (!std.ascii.eqlIgnoreCase(lang[0..expected.len], expected)) {
|
||||
return false;
|
||||
}
|
||||
return lang.len == expected.len or lang[expected.len] == '-';
|
||||
},
|
||||
.not => |selectors| {
|
||||
for (selectors) |selector| {
|
||||
if (matches(node, selector, scope, frame)) {
|
||||
|
||||
@@ -81,7 +81,11 @@ pub fn parseList(arena: Allocator, input: []const u8) ParseError![]const Selecto
|
||||
var remaining = preprocessed;
|
||||
while (true) {
|
||||
const trimmed = std.mem.trimLeft(u8, remaining, &std.ascii.whitespace);
|
||||
if (trimmed.len == 0) break;
|
||||
if (trimmed.len == 0) {
|
||||
// Either an empty selector or a dangling comma ("div,"): both are
|
||||
// parse errors per the selector-list grammar.
|
||||
return error.InvalidSelector;
|
||||
}
|
||||
|
||||
var comma_pos: usize = trimmed.len;
|
||||
var depth: usize = 0;
|
||||
@@ -137,10 +141,12 @@ pub fn parseList(arena: Allocator, input: []const u8) ParseError![]const Selecto
|
||||
|
||||
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);
|
||||
try selectors.append(arena, selector);
|
||||
if (selector_input.len == 0) {
|
||||
// An empty segment between commas (",div") is a parse error.
|
||||
return error.InvalidSelector;
|
||||
}
|
||||
const selector = try parse(arena, selector_input);
|
||||
try selectors.append(arena, selector);
|
||||
|
||||
if (comma_pos >= trimmed.len) break;
|
||||
remaining = trimmed[comma_pos + 1 ..];
|
||||
@@ -946,6 +952,15 @@ fn attribute(self: *Parser, arena: Allocator) !Selector.Attribute {
|
||||
self.input = self.input[1..];
|
||||
_ = self.skipSpaces();
|
||||
|
||||
// Optional namespace component: `*|name` (any namespace) or `|name`
|
||||
// (no namespace). Attributes are stored by qualified name and almost
|
||||
// never namespaced, so both forms match by name.
|
||||
if (self.peek() == '*' and self.input.len > 1 and self.input[1] == '|') {
|
||||
self.input = self.input[2..];
|
||||
} else if (self.peek() == '|' and self.input.len > 1 and self.input[1] != '=') {
|
||||
self.input = self.input[1..];
|
||||
}
|
||||
|
||||
const attr_name = try self.attributeName(arena);
|
||||
|
||||
// Normalize the name to lowercase for fast matching (consistent with Attribute.normalizeNameForLookup)
|
||||
@@ -953,8 +968,12 @@ fn attribute(self: *Parser, arena: Allocator) !Selector.Attribute {
|
||||
var case_insensitive = false;
|
||||
_ = self.skipSpaces();
|
||||
|
||||
if (self.peek() == ']') {
|
||||
self.input = self.input[1..];
|
||||
// Unexpected EOF closes all open constructs per CSS Syntax, so a
|
||||
// dangling `[foo` is a valid presence selector.
|
||||
if (self.peek() == ']' or self.peek() == 0) {
|
||||
if (self.peek() == ']') {
|
||||
self.input = self.input[1..];
|
||||
}
|
||||
return .{ .name = name, .matcher = .presence, .case_insensitive = case_insensitive };
|
||||
}
|
||||
|
||||
@@ -976,10 +995,12 @@ fn attribute(self: *Parser, arena: Allocator) !Selector.Attribute {
|
||||
_ = self.skipSpaces();
|
||||
}
|
||||
|
||||
if (self.peek() != ']') {
|
||||
// As above, EOF stands in for the closing bracket ([align="center").
|
||||
if (self.peek() == ']') {
|
||||
self.input = self.input[1..];
|
||||
} else if (self.peek() != 0) {
|
||||
return error.InvalidAttributeSelector;
|
||||
}
|
||||
self.input = self.input[1..];
|
||||
|
||||
const matcher: Selector.AttributeMatcher = switch (matcher_type) {
|
||||
.exact => .{ .exact = value },
|
||||
|
||||
Reference in New Issue
Block a user