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 = "" },