diff --git a/src/html/elements.zig b/src/html/elements.zig
index f7548d2f..6b6ad83f 100644
--- a/src/html/elements.zig
+++ b/src/html/elements.zig
@@ -15,11 +15,17 @@
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
+const std = @import("std");
const parser = @import("../netsurf.zig");
const generate = @import("../generate.zig");
+const jsruntime = @import("jsruntime");
+const Case = jsruntime.test_utils.Case;
+const checkCases = jsruntime.test_utils.checkCases;
+
const Element = @import("../dom/element.zig").Element;
+const URL = @import("../url/url.zig").URL;
// HTMLElement interfaces
pub const Interfaces = .{
@@ -126,10 +132,270 @@ pub const HTMLUnknownElement = struct {
pub const mem_guarantied = true;
};
+// https://html.spec.whatwg.org/#the-a-element
pub const HTMLAnchorElement = struct {
pub const Self = parser.Anchor;
pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
+
+ pub fn get_target(self: *parser.Anchor) ![]const u8 {
+ return try parser.anchorGetTarget(self);
+ }
+
+ pub fn set_target(self: *parser.Anchor, href: []const u8) !void {
+ return try parser.anchorSetTarget(self, href);
+ }
+
+ pub fn get_download(_: *parser.Anchor) ![]const u8 {
+ return ""; // TODO
+ }
+
+ pub fn get_href(self: *parser.Anchor) ![]const u8 {
+ return try parser.anchorGetHref(self);
+ }
+
+ pub fn set_href(self: *parser.Anchor, href: []const u8) !void {
+ return try parser.anchorSetHref(self, href);
+ }
+
+ pub fn get_hreflang(self: *parser.Anchor) ![]const u8 {
+ return try parser.anchorGetHrefLang(self);
+ }
+
+ pub fn set_hreflang(self: *parser.Anchor, href: []const u8) !void {
+ return try parser.anchorSetHrefLang(self, href);
+ }
+
+ pub fn get_type(self: *parser.Anchor) ![]const u8 {
+ return try parser.anchorGetType(self);
+ }
+
+ pub fn set_type(self: *parser.Anchor, t: []const u8) !void {
+ return try parser.anchorSetType(self, t);
+ }
+
+ pub fn get_rel(self: *parser.Anchor) ![]const u8 {
+ return try parser.anchorGetRel(self);
+ }
+
+ pub fn set_rel(self: *parser.Anchor, t: []const u8) !void {
+ return try parser.anchorSetRel(self, t);
+ }
+
+ pub fn get_text(self: *parser.Anchor) !?[]const u8 {
+ return try parser.nodeTextContent(parser.anchorToNode(self));
+ }
+
+ pub fn set_text(self: *parser.Anchor, v: []const u8) !void {
+ return try parser.nodeSetTextContent(parser.anchorToNode(self), v);
+ }
+
+ inline fn url(self: *parser.Anchor, alloc: std.mem.Allocator) !URL {
+ const href = try parser.anchorGetHref(self);
+ return URL.constructor(alloc, href, null); // TODO inject base url
+ }
+
+ // TODO return a disposable string
+ pub fn get_origin(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 {
+ var u = try url(self, alloc);
+ defer u.deinit(alloc);
+
+ return try u.get_origin(alloc);
+ }
+
+ // TODO return a disposable string
+ pub fn get_protocol(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 {
+ var u = try url(self, alloc);
+ defer u.deinit(alloc);
+
+ return u.get_protocol(alloc);
+ }
+
+ pub fn set_protocol(self: *parser.Anchor, alloc: std.mem.Allocator, v: []const u8) !void {
+ var u = try url(self, alloc);
+ defer u.deinit(alloc);
+
+ u.uri.scheme = v;
+ const href = try u.format(alloc);
+ defer alloc.free(href);
+
+ try parser.anchorSetHref(self, href);
+ }
+
+ // TODO return a disposable string
+ pub fn get_host(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 {
+ var u = try url(self, alloc);
+ defer u.deinit(alloc);
+
+ return try u.get_host(alloc);
+ }
+
+ pub fn set_host(self: *parser.Anchor, alloc: std.mem.Allocator, v: []const u8) !void {
+ // search : separator
+ var p: ?u16 = null;
+ var h: []const u8 = undefined;
+ for (v, 0..) |c, i| {
+ if (c == ':') {
+ h = v[0..i];
+ p = try std.fmt.parseInt(u16, v[i + 1 ..], 10);
+ break;
+ }
+ }
+
+ var u = try url(self, alloc);
+ defer u.deinit(alloc);
+
+ if (p) |pp| {
+ u.uri.host = h;
+ u.uri.port = pp;
+ } else {
+ u.uri.host = v;
+ u.uri.port = null;
+ }
+
+ const href = try u.format(alloc);
+ defer alloc.free(href);
+
+ try parser.anchorSetHref(self, href);
+ }
+
+ // TODO return a disposable string
+ pub fn get_hostname(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 {
+ var u = try url(self, alloc);
+ defer u.deinit(alloc);
+
+ return try alloc.dupe(u8, u.get_hostname());
+ }
+
+ pub fn set_hostname(self: *parser.Anchor, alloc: std.mem.Allocator, v: []const u8) !void {
+ var u = try url(self, alloc);
+ defer u.deinit(alloc);
+
+ u.uri.host = v;
+ const href = try u.format(alloc);
+ try parser.anchorSetHref(self, href);
+ }
+
+ // TODO return a disposable string
+ pub fn get_port(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 {
+ var u = try url(self, alloc);
+ defer u.deinit(alloc);
+
+ return try u.get_port(alloc);
+ }
+
+ pub fn set_port(self: *parser.Anchor, alloc: std.mem.Allocator, v: ?[]const u8) !void {
+ var u = try url(self, alloc);
+ defer u.deinit(alloc);
+
+ if (v != null and v.?.len > 0) {
+ u.uri.port = try std.fmt.parseInt(u16, v.?, 10);
+ } else {
+ u.uri.port = null;
+ }
+
+ const href = try u.format(alloc);
+ defer alloc.free(href);
+
+ try parser.anchorSetHref(self, href);
+ }
+
+ // TODO return a disposable string
+ pub fn get_username(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 {
+ var u = try url(self, alloc);
+ defer u.deinit(alloc);
+
+ return try alloc.dupe(u8, u.get_username());
+ }
+
+ pub fn set_username(self: *parser.Anchor, alloc: std.mem.Allocator, v: ?[]const u8) !void {
+ var u = try url(self, alloc);
+ defer u.deinit(alloc);
+
+ u.uri.user = v;
+ const href = try u.format(alloc);
+ defer alloc.free(href);
+
+ try parser.anchorSetHref(self, href);
+ }
+
+ // TODO return a disposable string
+ pub fn get_password(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 {
+ var u = try url(self, alloc);
+ defer u.deinit(alloc);
+
+ return try alloc.dupe(u8, u.get_password());
+ }
+
+ pub fn set_password(self: *parser.Anchor, alloc: std.mem.Allocator, v: ?[]const u8) !void {
+ var u = try url(self, alloc);
+ defer u.deinit(alloc);
+
+ u.uri.password = v;
+ const href = try u.format(alloc);
+ defer alloc.free(href);
+
+ try parser.anchorSetHref(self, href);
+ }
+
+ // TODO return a disposable string
+ pub fn get_pathname(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 {
+ var u = try url(self, alloc);
+ defer u.deinit(alloc);
+
+ return try alloc.dupe(u8, u.get_pathname());
+ }
+
+ pub fn set_pathname(self: *parser.Anchor, alloc: std.mem.Allocator, v: []const u8) !void {
+ var u = try url(self, alloc);
+ defer u.deinit(alloc);
+
+ u.uri.path = v;
+ const href = try u.format(alloc);
+ defer alloc.free(href);
+
+ try parser.anchorSetHref(self, href);
+ }
+
+ // TODO return a disposable string
+ pub fn get_search(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 {
+ var u = try url(self, alloc);
+ defer u.deinit(alloc);
+
+ return try u.get_search(alloc);
+ }
+
+ pub fn set_search(self: *parser.Anchor, alloc: std.mem.Allocator, v: ?[]const u8) !void {
+ var u = try url(self, alloc);
+ defer u.deinit(alloc);
+
+ u.uri.query = v;
+ const href = try u.format(alloc);
+ defer alloc.free(href);
+
+ try parser.anchorSetHref(self, href);
+ }
+
+ // TODO return a disposable string
+ pub fn get_hash(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 {
+ var u = try url(self, alloc);
+ defer u.deinit(alloc);
+
+ return try u.get_hash(alloc);
+ }
+
+ pub fn set_hash(self: *parser.Anchor, alloc: std.mem.Allocator, v: ?[]const u8) !void {
+ var u = try url(self, alloc);
+ defer u.deinit(alloc);
+
+ u.uri.fragment = v;
+ const href = try u.format(alloc);
+ defer alloc.free(href);
+
+ try parser.anchorSetHref(self, href);
+ }
+
+ pub fn deinit(_: *parser.Anchor, _: std.mem.Allocator) void {}
};
pub const HTMLAppletElement = struct {
@@ -408,10 +674,120 @@ pub const HTMLQuoteElement = struct {
pub const mem_guarantied = true;
};
+// https://html.spec.whatwg.org/#the-script-element
pub const HTMLScriptElement = struct {
pub const Self = parser.Script;
pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
+
+ pub fn get_src(self: *parser.Script) !?[]const u8 {
+ return try parser.elementGetAttribute(
+ parser.scriptToElt(self),
+ "src",
+ ) orelse "";
+ }
+
+ pub fn set_src(self: *parser.Script, v: []const u8) !void {
+ return try parser.elementSetAttribute(
+ parser.scriptToElt(self),
+ "src",
+ v,
+ );
+ }
+
+ pub fn get_type(self: *parser.Script) !?[]const u8 {
+ return try parser.elementGetAttribute(
+ parser.scriptToElt(self),
+ "type",
+ ) orelse "";
+ }
+
+ pub fn set_type(self: *parser.Script, v: []const u8) !void {
+ return try parser.elementSetAttribute(
+ parser.scriptToElt(self),
+ "type",
+ v,
+ );
+ }
+
+ pub fn get_text(self: *parser.Script) !?[]const u8 {
+ return try parser.elementGetAttribute(
+ parser.scriptToElt(self),
+ "text",
+ ) orelse "";
+ }
+
+ pub fn set_text(self: *parser.Script, v: []const u8) !void {
+ return try parser.elementSetAttribute(
+ parser.scriptToElt(self),
+ "text",
+ v,
+ );
+ }
+
+ pub fn get_integrity(self: *parser.Script) !?[]const u8 {
+ return try parser.elementGetAttribute(
+ parser.scriptToElt(self),
+ "integrity",
+ ) orelse "";
+ }
+
+ pub fn set_integrity(self: *parser.Script, v: []const u8) !void {
+ return try parser.elementSetAttribute(
+ parser.scriptToElt(self),
+ "integrity",
+ v,
+ );
+ }
+
+ pub fn get_async(self: *parser.Script) !bool {
+ _ = try parser.elementGetAttribute(
+ parser.scriptToElt(self),
+ "async",
+ ) orelse return false;
+
+ return true;
+ }
+
+ pub fn set_async(self: *parser.Script, v: bool) !void {
+ if (v) {
+ return try parser.elementSetAttribute(parser.scriptToElt(self), "async", "");
+ }
+
+ return try parser.elementRemoveAttribute(parser.scriptToElt(self), "async");
+ }
+
+ pub fn get_defer(self: *parser.Script) !bool {
+ _ = try parser.elementGetAttribute(
+ parser.scriptToElt(self),
+ "defer",
+ ) orelse false;
+ return true;
+ }
+
+ pub fn set_defer(self: *parser.Script, v: bool) !void {
+ if (v) {
+ return try parser.elementSetAttribute(parser.scriptToElt(self), "defer", "");
+ }
+
+ return try parser.elementRemoveAttribute(parser.scriptToElt(self), "defer");
+ }
+
+ pub fn get_noModule(self: *parser.Script) !bool {
+ _ = try parser.elementGetAttribute(
+ parser.scriptToElt(self),
+ "nomodule",
+ ) orelse false;
+ return true;
+ }
+
+ pub fn set_noModule(self: *parser.Script, v: bool) !void {
+ if (v) {
+ return try parser.elementSetAttribute(parser.scriptToElt(self), "nomodule", "");
+ }
+
+ return try parser.elementRemoveAttribute(parser.scriptToElt(self), "nomodule");
+ }
};
pub const HTMLSelectElement = struct {
@@ -589,3 +965,82 @@ pub fn toInterface(comptime T: type, e: *parser.Element) !T {
.undef => .{ .HTMLUnknownElement = @as(*parser.Unknown, @ptrCast(elem)) },
};
}
+
+// Tests
+// -----
+
+pub fn testExecFn(
+ _: std.mem.Allocator,
+ js_env: *jsruntime.Env,
+) anyerror!void {
+ var anchor = [_]Case{
+ .{ .src = "let a = document.getElementById('link')", .ex = "undefined" },
+ .{ .src = "a.target", .ex = "" },
+ .{ .src = "a.target = '_blank'", .ex = "_blank" },
+ .{ .src = "a.target", .ex = "_blank" },
+ .{ .src = "a.target = ''", .ex = "" },
+
+ .{ .src = "a.href", .ex = "foo" },
+ .{ .src = "a.href = 'https://lightpanda.io/'", .ex = "https://lightpanda.io/" },
+ .{ .src = "a.href", .ex = "https://lightpanda.io/" },
+
+ .{ .src = "a.origin", .ex = "https://lightpanda.io" },
+
+ .{ .src = "a.host = 'lightpanda.io:443'", .ex = "lightpanda.io:443" },
+ .{ .src = "a.host", .ex = "lightpanda.io:443" },
+ .{ .src = "a.port", .ex = "443" },
+ .{ .src = "a.hostname", .ex = "lightpanda.io" },
+
+ .{ .src = "a.host = 'lightpanda.io'", .ex = "lightpanda.io" },
+ .{ .src = "a.host", .ex = "lightpanda.io" },
+ .{ .src = "a.port", .ex = "" },
+ .{ .src = "a.hostname", .ex = "lightpanda.io" },
+
+ .{ .src = "a.host", .ex = "lightpanda.io" },
+ .{ .src = "a.hostname", .ex = "lightpanda.io" },
+ .{ .src = "a.hostname = 'foo.bar'", .ex = "foo.bar" },
+ .{ .src = "a.href", .ex = "https://foo.bar/" },
+
+ .{ .src = "a.search", .ex = "" },
+ .{ .src = "a.search = 'q=bar'", .ex = "q=bar" },
+ .{ .src = "a.search", .ex = "?q=bar" },
+ .{ .src = "a.href", .ex = "https://foo.bar/?q=bar" },
+
+ .{ .src = "a.hash", .ex = "" },
+ .{ .src = "a.hash = 'frag'", .ex = "frag" },
+ .{ .src = "a.hash", .ex = "#frag" },
+ .{ .src = "a.href", .ex = "https://foo.bar/?q=bar#frag" },
+
+ .{ .src = "a.port", .ex = "" },
+ .{ .src = "a.port = '443'", .ex = "443" },
+ .{ .src = "a.host", .ex = "foo.bar:443" },
+ .{ .src = "a.hostname", .ex = "foo.bar" },
+ .{ .src = "a.href", .ex = "https://foo.bar:443/?q=bar#frag" },
+ .{ .src = "a.port = null", .ex = "null" },
+ .{ .src = "a.href", .ex = "https://foo.bar/?q=bar#frag" },
+
+ .{ .src = "a.href = 'foo'", .ex = "foo" },
+
+ .{ .src = "a.type", .ex = "" },
+ .{ .src = "a.type = 'text/html'", .ex = "text/html" },
+ .{ .src = "a.type", .ex = "text/html" },
+ .{ .src = "a.type = ''", .ex = "" },
+
+ .{ .src = "a.text", .ex = "OK" },
+ .{ .src = "a.text = 'foo'", .ex = "foo" },
+ .{ .src = "a.text", .ex = "foo" },
+ .{ .src = "a.text = 'OK'", .ex = "OK" },
+ };
+ try checkCases(js_env, &anchor);
+
+ var script = [_]Case{
+ .{ .src = "let script = document.createElement('script')", .ex = "undefined" },
+ .{ .src = "script.src = 'foo.bar'", .ex = "foo.bar" },
+
+ .{ .src = "script.async = true", .ex = "true" },
+ .{ .src = "script.async", .ex = "true" },
+ .{ .src = "script.async = false", .ex = "false" },
+ .{ .src = "script.async", .ex = "false" },
+ };
+ try checkCases(js_env, &script);
+}
diff --git a/src/netsurf.zig b/src/netsurf.zig
index 7f225fe7..f5b3a141 100644
--- a/src/netsurf.zig
+++ b/src/netsurf.zig
@@ -1513,6 +1513,85 @@ pub fn elementHTMLGetTagType(elem_html: *ElementHTML) !Tag {
return @as(Tag, @enumFromInt(tag_type));
}
+// HTMLScriptElement
+
+// scriptToElt is an helper to convert an script to an element.
+pub inline fn scriptToElt(s: *Script) *Element {
+ return @as(*Element, @ptrCast(s));
+}
+
+// HTMLAnchorElement
+
+// anchorToNode is an helper to convert an anchor to a node.
+pub inline fn anchorToNode(a: *Anchor) *Node {
+ return @as(*Node, @ptrCast(a));
+}
+
+pub fn anchorGetTarget(a: *Anchor) ![]const u8 {
+ var res: ?*String = undefined;
+ const err = c.dom_html_anchor_element_get_target(a, &res);
+ try DOMErr(err);
+ if (res == null) return "";
+ return strToData(res.?);
+}
+
+pub fn anchorSetTarget(a: *Anchor, target: []const u8) !void {
+ const err = c.dom_html_anchor_element_set_target(a, try strFromData(target));
+ try DOMErr(err);
+}
+
+pub fn anchorGetHref(a: *Anchor) ![]const u8 {
+ var res: ?*String = undefined;
+ const err = c.dom_html_anchor_element_get_href(a, &res);
+ try DOMErr(err);
+ if (res == null) return "";
+ return strToData(res.?);
+}
+
+pub fn anchorSetHref(a: *Anchor, href: []const u8) !void {
+ const err = c.dom_html_anchor_element_set_href(a, try strFromData(href));
+ try DOMErr(err);
+}
+
+pub fn anchorGetHrefLang(a: *Anchor) ![]const u8 {
+ var res: ?*String = undefined;
+ const err = c.dom_html_anchor_element_get_hreflang(a, &res);
+ try DOMErr(err);
+ if (res == null) return "";
+ return strToData(res.?);
+}
+
+pub fn anchorSetHrefLang(a: *Anchor, href: []const u8) !void {
+ const err = c.dom_html_anchor_element_set_hreflang(a, try strFromData(href));
+ try DOMErr(err);
+}
+
+pub fn anchorGetType(a: *Anchor) ![]const u8 {
+ var res: ?*String = undefined;
+ const err = c.dom_html_anchor_element_get_type(a, &res);
+ try DOMErr(err);
+ if (res == null) return "";
+ return strToData(res.?);
+}
+
+pub fn anchorSetType(a: *Anchor, t: []const u8) !void {
+ const err = c.dom_html_anchor_element_set_type(a, try strFromData(t));
+ try DOMErr(err);
+}
+
+pub fn anchorGetRel(a: *Anchor) ![]const u8 {
+ var res: ?*String = undefined;
+ const err = c.dom_html_anchor_element_get_rel(a, &res);
+ try DOMErr(err);
+ if (res == null) return "";
+ return strToData(res.?);
+}
+
+pub fn anchorSetRel(a: *Anchor, rel: []const u8) !void {
+ const err = c.dom_html_anchor_element_set_rel(a, try strFromData(rel));
+ try DOMErr(err);
+}
+
// ElementsHTML
pub const MediaElement = struct { base: *c.dom_html_element };
diff --git a/src/run_tests.zig b/src/run_tests.zig
index 1d1583a7..3fe20b3f 100644
--- a/src/run_tests.zig
+++ b/src/run_tests.zig
@@ -51,6 +51,7 @@ const XHRTestExecFn = xhr.testExecFn;
const ProgressEventTestExecFn = @import("xhr/progress_event.zig").testExecFn;
const StorageTestExecFn = storage.testExecFn;
const URLTestExecFn = url.testExecFn;
+const HTMLElementTestExecFn = @import("html/elements.zig").testExecFn;
pub const Types = jsruntime.reflect(apiweb.Interfaces);
@@ -117,6 +118,7 @@ fn testsAllExecFn(
ProcessingInstructionTestExecFn,
StorageTestExecFn,
URLTestExecFn,
+ HTMLElementTestExecFn,
};
inline for (testFns) |testFn| {
diff --git a/src/url/url.zig b/src/url/url.zig
index 6858d55c..978a11b7 100644
--- a/src/url/url.zig
+++ b/src/url/url.zig
@@ -67,17 +67,35 @@ pub const URL = struct {
}
pub fn deinit(self: *URL, alloc: std.mem.Allocator) void {
- self.search_params.deinit();
+ self.search_params.deinit(alloc);
alloc.free(self.rawuri);
}
// the caller must free the returned string.
// TODO return a disposable string
// https://github.com/lightpanda-io/jsruntime-lib/issues/195
- pub fn get_href(self: *URL, alloc: std.mem.Allocator) ![]const u8 {
+ pub fn get_origin(self: *URL, alloc: std.mem.Allocator) ![]const u8 {
var buf = std.ArrayList(u8).init(alloc);
defer buf.deinit();
+ try self.uri.writeToStream(.{
+ .scheme = true,
+ .authentication = false,
+ .authority = true,
+ .path = false,
+ .query = false,
+ .fragment = false,
+ }, buf.writer());
+ return try buf.toOwnedSlice();
+ }
+
+ // get_href returns the URL by writing all its components.
+ // The query is replaced by a dump of search params.
+ //
+ // the caller must free the returned string.
+ // TODO return a disposable string
+ // https://github.com/lightpanda-io/jsruntime-lib/issues/195
+ pub fn get_href(self: *URL, alloc: std.mem.Allocator) ![]const u8 {
// retrieve the query search from search_params.
const cur = self.uri.query;
defer self.uri.query = cur;
@@ -86,13 +104,21 @@ pub const URL = struct {
try self.search_params.values.encode(q.writer());
self.uri.query = q.items;
+ return try self.format(alloc);
+ }
+
+ // format the url with all its components.
+ pub fn format(self: *URL, alloc: std.mem.Allocator) ![]const u8 {
+ var buf = std.ArrayList(u8).init(alloc);
+ defer buf.deinit();
+
try self.uri.writeToStream(.{
.scheme = true,
.authentication = true,
.authority = true,
- .path = true,
- .query = true,
- .fragment = true,
+ .path = self.uri.path.len > 0,
+ .query = self.uri.query != null and self.uri.query.?.len > 0,
+ .fragment = self.uri.fragment != null and self.uri.fragment.?.len > 0,
}, buf.writer());
return try buf.toOwnedSlice();
}
@@ -112,8 +138,22 @@ pub const URL = struct {
return self.uri.password orelse "";
}
- pub fn get_host(self: *URL) []const u8 {
- return self.uri.host orelse "";
+ // the caller must free the returned string.
+ // TODO return a disposable string
+ // https://github.com/lightpanda-io/jsruntime-lib/issues/195
+ pub fn get_host(self: *URL, alloc: std.mem.Allocator) ![]const u8 {
+ var buf = std.ArrayList(u8).init(alloc);
+ defer buf.deinit();
+
+ try self.uri.writeToStream(.{
+ .scheme = false,
+ .authentication = false,
+ .authority = true,
+ .path = false,
+ .query = false,
+ .fragment = false,
+ }, buf.writer());
+ return try buf.toOwnedSlice();
}
pub fn get_hostname(self: *URL) []const u8 {
@@ -223,6 +263,7 @@ pub fn testExecFn(
) anyerror!void {
var url = [_]Case{
.{ .src = "var url = new URL('https://foo.bar/path?query#fragment')", .ex = "undefined" },
+ .{ .src = "url.origin", .ex = "https://foo.bar" },
.{ .src = "url.href", .ex = "https://foo.bar/path?query#fragment" },
.{ .src = "url.protocol", .ex = "https:" },
.{ .src = "url.username", .ex = "" },