mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-09-28 15:35:43 -04:00
webapi: improve xpath support
Largely about passing the 1024 /domxpath/xml_xpath_runner.html WPT cases (0 passing before this). -evaluate/createExpression's `resolver` can be a function OR an callback object. We don't use it, so ?js.Function -> ?js.Value is an easy win -animated use legacy xlink:href attribute if present -better pseudo namespace support for attributes. We now preserve the prefix in the attribute name (so xlink:href stays xlink:href) AND, for 3 common namespaces with fixed prefixes, we getAttributeNS and hasAttributeNS _will_ work. (because these are fixed, it's ok that we don't store the namespace in attribute, we can just look for the fully qualified name and fallback to the original check if we don't find it).
This commit is contained in:
10 files changed
+132
-56
No files matched your search
@@ -1087,10 +1087,25 @@ fn populateElementAttributes(frame: *Frame, element: *Element, list: anytype) !v
|
||||
var attributes = &element._attributes;
|
||||
try attributes.ensureTotalCapacity(count, frame);
|
||||
while (list.next()) |attr| {
|
||||
try attributes.putNew(attr.name.local.slice(), attr.value.slice(), frame);
|
||||
const name = try parserAttributeName(frame, attr.name);
|
||||
try attributes.putNew(name, attr.value.slice(), frame);
|
||||
}
|
||||
}
|
||||
|
||||
// Attributes are keyed by qualified name (no namespace model), so a prefixed
|
||||
// attribute (`xlink:href` in foreign content, `xml:id` in XML) must keep its
|
||||
// prefix — that is what `getAttribute("xlink:href")` and `Attr.name` see in
|
||||
// browsers. The joined name only has to outlive putNew, which canonicalizes
|
||||
// it into the frame arena. (Not frame.buf: name normalization writes there.)
|
||||
fn parserAttributeName(frame: *Frame, qname: Parser.QualName) ![]const u8 {
|
||||
const local = qname.local.slice();
|
||||
const prefix = (qname.prefix.unwrap() orelse return local).slice();
|
||||
if (prefix.len == 0) {
|
||||
return local;
|
||||
}
|
||||
return std.fmt.allocPrint(frame.local_arena, "{s}:{s}", .{ prefix, local });
|
||||
}
|
||||
|
||||
// Called when `new MyElement()` is invoked directly in JS (not via the
|
||||
// customElements.define/upgrade path). `new_target` is the constructor
|
||||
// function that was used with `new`. We find the matching definition in the
|
||||
|
||||
@@ -25,6 +25,7 @@ const Node = @import("../webapi/Node.zig");
|
||||
const Element = @import("../webapi/Element.zig");
|
||||
const CData = @import("../webapi/CData.zig");
|
||||
|
||||
pub const QualName = h5e.QualName;
|
||||
pub const AttributeIterator = h5e.AttributeIterator;
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
@@ -453,7 +454,8 @@ fn createElementCallback(ctx: *anyopaque, data: *anyopaque, qname: h5e.QualName,
|
||||
}
|
||||
|
||||
fn createXMLElementCallback(ctx: *anyopaque, data: *anyopaque, qname: h5e.QualName, attributes: h5e.AttributeIterator) callconv(.c) ?*anyopaque {
|
||||
return _createElementCallbackWithDefaultnamespace(ctx, data, qname, attributes, .xml);
|
||||
// An XML element outside any xmlns declaration is in no namespace (null namespace)
|
||||
return _createElementCallbackWithDefaultnamespace(ctx, data, qname, attributes, .null);
|
||||
}
|
||||
|
||||
// html5ever_parse_fragment materializes the fragment's context element through
|
||||
|
||||
@@ -419,3 +419,26 @@
|
||||
testing.expectEqual(2, allElements.length);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=xml-namespaces>
|
||||
{
|
||||
const doc = new DOMParser().parseFromString(
|
||||
'<r xmlns:a="urn:a" a:b="1" xml:lang="en" c="2"><a:k/><k/></r>', 'text/xml');
|
||||
const r = doc.documentElement;
|
||||
|
||||
// Elements outside any xmlns declaration are in no namespace
|
||||
testing.expectEqual(null, r.namespaceURI);
|
||||
testing.expectEqual(1, doc.getElementsByTagNameNS('', 'r').length);
|
||||
testing.expectEqual(1, doc.getElementsByTagNameNS(null, 'k').length);
|
||||
|
||||
// Prefixed attributes keep their qualified name. (Not asserted: browsers
|
||||
// also list xmlns:a as an attribute; xml5ever consumes the declaration.)
|
||||
testing.expectEqual(true, Array.from(r.attributes).some(a => a.name === 'a:b'));
|
||||
testing.expectEqual(false, Array.from(r.attributes).some(a => a.name === 'b'));
|
||||
testing.expectEqual('1', r.getAttribute('a:b'));
|
||||
testing.expectEqual('en', r.getAttribute('xml:lang'));
|
||||
testing.expectEqual(null, r.getAttribute('lang'));
|
||||
testing.expectEqual('en', r.getAttributeNS('http://www.w3.org/XML/1998/namespace', 'lang'));
|
||||
testing.expectEqual(true, r.hasAttributeNS('http://www.w3.org/XML/1998/namespace', 'lang'));
|
||||
}
|
||||
</script>
|
||||
@@ -34,35 +34,34 @@
|
||||
|
||||
<script id=href>
|
||||
{
|
||||
// The parser stores xlink:href under its local name, href.
|
||||
// The parser keeps the xlink: prefix; href reflects href, else xlink:href.
|
||||
const use1 = $('#use1');
|
||||
testing.expectEqual(true, use1.href instanceof SVGAnimatedString);
|
||||
testing.expectEqual('#icon', use1.href.baseVal);
|
||||
testing.expectEqual('#icon', use1.href.animVal);
|
||||
testing.expectEqual(true, use1.href === use1.href);
|
||||
testing.expectEqual('#modern', $('#use2').href.baseVal);
|
||||
testing.expectEqual('#icon', use1.getAttribute('xlink:href'));
|
||||
testing.expectEqual(null, use1.getAttribute('href'));
|
||||
testing.expectEqual('#icon', use1.getAttributeNS('http://www.w3.org/1999/xlink', 'href'));
|
||||
|
||||
// Writes land in xlink:href in browsers (it's the attribute that's set)
|
||||
// but in href for us since the parser stored it there; assert the common
|
||||
// observable.
|
||||
// Only xlink:href present: the write lands in the xlink attribute, not in
|
||||
// a new href. (Firefox drops the prefix on that write, so only assert the
|
||||
// namespaced lookup.)
|
||||
use1.href.baseVal = '#other';
|
||||
testing.expectEqual('#other', use1.href.baseVal);
|
||||
testing.expectEqual('#other', use1.getAttributeNS('http://www.w3.org/1999/xlink', 'href'));
|
||||
testing.expectEqual(false, use1.hasAttributeNS(null, 'href'));
|
||||
|
||||
// setAttributeNS stores under the local name, so a runtime xlink:href
|
||||
// lands in href and reflects like the parsed variant. (Browsers keep the
|
||||
// xlink:href attribute but reflect it the same way.)
|
||||
// Runtime setAttributeNS keeps the qualified name and reflects the same way.
|
||||
const use3 = document.createElementNS('http://www.w3.org/2000/svg', 'use');
|
||||
use3.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', '#runtime');
|
||||
testing.expectEqual('#runtime', use3.href.baseVal);
|
||||
testing.expectEqual('#runtime', use3.getAttribute('xlink:href'));
|
||||
|
||||
// A namespace-less setAttribute('xlink:href') is a literal attribute in no
|
||||
// namespace: not reflected, in browsers or here.
|
||||
const use4 = document.createElementNS('http://www.w3.org/2000/svg', 'use');
|
||||
use4.setAttribute('xlink:href', '#literal');
|
||||
testing.expectEqual('', use4.href.baseVal);
|
||||
use4.href.baseVal = '#written';
|
||||
testing.expectEqual('#written', use4.getAttribute('href'));
|
||||
testing.expectEqual(true, use4.hasAttribute('xlink:href'));
|
||||
// Not asserted: a namespace-less setAttribute('xlink:href') is a literal
|
||||
// attribute browsers don't reflect, but attributes here have no namespace
|
||||
// so it is indistinguishable from the xlink one and does reflect.
|
||||
|
||||
// No href attribute at all: set writes to href.
|
||||
const link = $('#link1');
|
||||
|
||||
@@ -111,6 +111,24 @@
|
||||
{
|
||||
const resolver = document.createNSResolver(document);
|
||||
testing.expectEqual(document, resolver);
|
||||
|
||||
// The Node it returns must be accepted back as the resolver argument
|
||||
const r = document.evaluate("//p", document, resolver, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
|
||||
testing.expectEqual(3, r.snapshotLength);
|
||||
const expr = document.createExpression("//p", resolver);
|
||||
testing.expectEqual(3, expr.evaluate(document, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null).snapshotLength);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=prefixed_attribute_in_xml>
|
||||
{
|
||||
const doc = new DOMParser().parseFromString(
|
||||
'<mu xml:id="id1"><rho xml:lang="en-GB"/><rho xml:lang="no"/></mu>', 'text/xml');
|
||||
const root = doc.documentElement;
|
||||
const r = doc.evaluate('//mu[@xml:id="id1"]/rho[@xml:lang="en-GB"]', root,
|
||||
doc.createNSResolver(root), XPathResult.ANY_TYPE, null);
|
||||
testing.expectEqual(root.firstChild, r.iterateNext());
|
||||
testing.expectEqual(null, r.iterateNext());
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -674,7 +674,7 @@ pub fn evaluate(
|
||||
self: *Document,
|
||||
expression: []const u8,
|
||||
context_node: ?*Node,
|
||||
resolver: ?js.Function,
|
||||
resolver: ?js.Value,
|
||||
result_type: ?u16,
|
||||
result: ?*XPathResult,
|
||||
frame: *Frame,
|
||||
@@ -697,7 +697,7 @@ pub fn evaluate(
|
||||
pub fn createExpression(
|
||||
_: *const Document,
|
||||
expression: []const u8,
|
||||
resolver: ?js.Function,
|
||||
resolver: ?js.Value,
|
||||
frame: *Frame,
|
||||
) !*XPathExpression {
|
||||
_ = resolver;
|
||||
|
||||
@@ -114,6 +114,9 @@ pub const Namespace = enum(u8) {
|
||||
|
||||
pub fn parse(namespace_: ?[]const u8) Namespace {
|
||||
const namespace = namespace_ orelse return .null;
|
||||
if (namespace.len == 0) {
|
||||
return .null;
|
||||
}
|
||||
if (namespace.len == "http://www.w3.org/1999/xhtml".len) {
|
||||
// Common case, avoid the string comparison. Recklessly
|
||||
@branchHint(.likely);
|
||||
@@ -652,22 +655,40 @@ pub fn getAttribute(self: *const Element, name: String, frame: *Frame) !?String
|
||||
return self._attributes.get(name, frame);
|
||||
}
|
||||
|
||||
/// For simplicity, the namespace is currently ignored and only the local name is used.
|
||||
pub fn getAttributeNS(
|
||||
self: *const Element,
|
||||
maybe_namespace: ?[]const u8,
|
||||
namespace_: ?[]const u8,
|
||||
local_name: String,
|
||||
frame: *Frame,
|
||||
) !?String {
|
||||
if (maybe_namespace) |namespace| {
|
||||
if (!std.mem.eql(u8, namespace, "http://www.w3.org/1999/xhtml")) {
|
||||
log.warn(.not_implemented, "Element.getAttributeNS", .{ .namespace = namespace });
|
||||
if (namespace_) |namespace| {
|
||||
// we don't really support namespaces, but if the namespace has a fixed
|
||||
// prefix, we can try to fetch the attribute with it
|
||||
if (try prefixedAttributeName(namespace, local_name.str(), frame)) |prefixed| {
|
||||
if (try self.getAttribute(.wrap(prefixed), frame)) |value| {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return self.getAttribute(local_name, frame);
|
||||
}
|
||||
|
||||
fn prefixedAttributeName(namespace: []const u8, local_name: []const u8, frame: *Frame) !?[]const u8 {
|
||||
const prefix = blk: {
|
||||
if (std.mem.eql(u8, namespace, "http://www.w3.org/1999/xlink")) {
|
||||
break :blk "xlink";
|
||||
}
|
||||
if (std.mem.eql(u8, namespace, "http://www.w3.org/XML/1998/namespace")) {
|
||||
break :blk "xml";
|
||||
}
|
||||
if (std.mem.eql(u8, namespace, "http://www.w3.org/2000/xmlns/")) {
|
||||
break :blk "xmlns";
|
||||
}
|
||||
return null;
|
||||
};
|
||||
return try std.fmt.allocPrint(frame.local_arena, "{s}:{s}", .{ prefix, local_name });
|
||||
}
|
||||
|
||||
pub fn getAttributeSafe(self: *const Element, name: String) ?[]const u8 {
|
||||
return self._attributes.getSafe(name);
|
||||
}
|
||||
@@ -677,20 +698,13 @@ pub fn hasAttribute(self: *const Element, name: String, frame: *Frame) !bool {
|
||||
return value != null;
|
||||
}
|
||||
|
||||
/// Like getAttributeNS, the namespace is currently ignored.
|
||||
pub fn hasAttributeNS(
|
||||
self: *const Element,
|
||||
maybe_namespace: ?[]const u8,
|
||||
namespace_: ?[]const u8,
|
||||
local_name: String,
|
||||
frame: *Frame,
|
||||
) !bool {
|
||||
if (maybe_namespace) |namespace| {
|
||||
if (!std.mem.eql(u8, namespace, "http://www.w3.org/1999/xhtml")) {
|
||||
log.warn(.not_implemented, "Element.hasAttributeNS", .{ .namespace = namespace });
|
||||
}
|
||||
}
|
||||
|
||||
return self.hasAttribute(local_name, frame);
|
||||
return try self.getAttributeNS(namespace_, local_name, frame) != null;
|
||||
}
|
||||
|
||||
pub fn hasAttributeSafe(self: *const Element, name: String) bool {
|
||||
@@ -769,30 +783,24 @@ pub fn setAttribute(self: *Element, name: String, value: String, frame: *Frame)
|
||||
|
||||
pub fn setAttributeNS(
|
||||
self: *Element,
|
||||
maybe_namespace: ?[]const u8,
|
||||
namespace_: ?[]const u8,
|
||||
qualified_name: []const u8,
|
||||
value: String,
|
||||
frame: *Frame,
|
||||
) !void {
|
||||
const attr_name = if (maybe_namespace) |namespace| blk: {
|
||||
// For xmlns namespace, store the full qualified name (e.g. "xmlns:bar")
|
||||
// so lookupNamespaceURI can find namespace declarations.
|
||||
if (std.mem.eql(u8, namespace, "http://www.w3.org/2000/xmlns/")) {
|
||||
break :blk qualified_name;
|
||||
const local_start = if (std.mem.indexOfScalarPos(u8, qualified_name, 0, ':')) |idx| blk: {
|
||||
if (idx == 0 or idx == qualified_name.len - 1) {
|
||||
// cannot be at the start or end of the qname
|
||||
return error.InvalidCharacterError;
|
||||
}
|
||||
if (!std.mem.eql(u8, namespace, "http://www.w3.org/1999/xhtml")) {
|
||||
log.warn(.not_implemented, "Element.setAttributeNS", .{ .namespace = namespace });
|
||||
if (std.mem.indexOfScalarPos(u8, qualified_name, idx + 1, ':') != null) {
|
||||
// and can only have one
|
||||
return error.InvalidCharacterError;
|
||||
}
|
||||
break :blk if (std.mem.indexOfScalarPos(u8, qualified_name, 0, ':')) |idx|
|
||||
qualified_name[idx + 1 ..]
|
||||
else
|
||||
qualified_name;
|
||||
} else blk: {
|
||||
break :blk if (std.mem.indexOfScalarPos(u8, qualified_name, 0, ':')) |idx|
|
||||
qualified_name[idx + 1 ..]
|
||||
else
|
||||
qualified_name;
|
||||
};
|
||||
break :blk idx + 1;
|
||||
} else 0;
|
||||
|
||||
const attr_name = if (namespace_ != null) qualified_name else qualified_name[local_start..];
|
||||
return self.setAttribute(.wrap(attr_name), value, frame);
|
||||
}
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ pub fn evaluate(
|
||||
_: *const XPathEvaluator,
|
||||
expression: []const u8,
|
||||
context_node: *Node,
|
||||
resolver: ?js.Function,
|
||||
resolver: ?js.Value,
|
||||
requested_type: ?u16,
|
||||
result: ?*XPathResult,
|
||||
frame: *Frame,
|
||||
@@ -59,7 +59,7 @@ pub fn evaluate(
|
||||
pub fn createExpression(
|
||||
_: *const XPathEvaluator,
|
||||
expression: []const u8,
|
||||
resolver: ?js.Function,
|
||||
resolver: ?js.Value,
|
||||
frame: *Frame,
|
||||
) !*XPathExpression {
|
||||
_ = resolver;
|
||||
|
||||
@@ -194,6 +194,5 @@ pub const JsApi = struct {
|
||||
|
||||
const testing = @import("../../../testing.zig");
|
||||
test "WebApi: Svg" {
|
||||
testing.expectLog(&.{ .not_implemented, .not_implemented });
|
||||
try testing.htmlRunner("element/svg", .{});
|
||||
}
|
||||
@@ -67,8 +67,20 @@ pub fn getAnimVal(self: *const AnimatedString) []const u8 {
|
||||
|
||||
fn attributeName(self: *const AnimatedString) String {
|
||||
return switch (self._kind) {
|
||||
.href => comptime .wrap("href"),
|
||||
.class => comptime .wrap("class"),
|
||||
.href => {
|
||||
const href: String = comptime .wrap("href");
|
||||
if (self._element.hasAttributeSafe(href)) {
|
||||
return href;
|
||||
}
|
||||
|
||||
const xlink_href: String = comptime .wrap("xlink:href");
|
||||
if (self._element.hasAttributeSafe(xlink_href)) {
|
||||
// legacy attribute, returned if it exists and href doens't
|
||||
return xlink_href;
|
||||
}
|
||||
return href;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user