diff --git a/src/apiweb.zig b/src/apiweb.zig index decac389..cbf2ffa0 100644 --- a/src/apiweb.zig +++ b/src/apiweb.zig @@ -39,3 +39,5 @@ pub const Interfaces = generate.Tuple(.{ Storage.Interfaces, URL.Interfaces, }); + +pub const UserContext = @import("user_context.zig").UserContext; diff --git a/src/browser/browser.zig b/src/browser/browser.zig index 2fb759cc..19982663 100644 --- a/src/browser/browser.zig +++ b/src/browser/browser.zig @@ -39,6 +39,9 @@ const storage = @import("../storage/storage.zig"); const FetchResult = std.http.Client.FetchResult; +const UserContext = @import("../user_context.zig").UserContext; +const HttpClient = @import("../async/Client.zig"); + const log = std.log.scoped(.browser); // Browser is an instance of the browser. @@ -92,6 +95,7 @@ pub const Session = struct { // TODO move the shed to the browser? storageShed: storage.Shed, page: ?*Page = null, + httpClient: HttpClient, jstypes: [Types.len]usize = undefined, @@ -105,9 +109,11 @@ pub const Session = struct { .loader = Loader.init(alloc), .loop = try Loop.init(alloc), .storageShed = storage.Shed.init(alloc), + .httpClient = undefined, }; - self.env = try Env.init(self.arena.allocator(), &self.loop); + self.env = try Env.init(self.arena.allocator(), &self.loop, null); + self.httpClient = .{ .allocator = alloc, .loop = &self.loop }; try self.env.load(&self.jstypes); return self; @@ -122,6 +128,7 @@ pub const Session = struct { self.loader.deinit(); self.loop.deinit(); self.storageShed.deinit(); + self.httpClient.deinit(); self.alloc.destroy(self); } @@ -289,6 +296,12 @@ pub const Page = struct { log.debug("start js env", .{}); try self.session.env.start(alloc); + // replace the user context document with the new one. + try self.session.env.setUserContext(.{ + .document = html_doc, + .httpClient = &self.session.httpClient, + }); + // add global objects log.debug("setup global env", .{}); try self.session.env.bindGlobal(&self.session.window); diff --git a/src/dom/comment.zig b/src/dom/comment.zig index c9baad26..e82c51bc 100644 --- a/src/dom/comment.zig +++ b/src/dom/comment.zig @@ -15,13 +15,45 @@ // // 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 jsruntime = @import("jsruntime"); +const Case = jsruntime.test_utils.Case; +const checkCases = jsruntime.test_utils.checkCases; + const CharacterData = @import("character_data.zig").CharacterData; +const UserContext = @import("../user_context.zig").UserContext; + +// https://dom.spec.whatwg.org/#interface-comment pub const Comment = struct { pub const Self = parser.Comment; pub const prototype = *CharacterData; pub const mem_guarantied = true; + + pub fn constructor(userctx: UserContext, data: ?[]const u8) !*parser.Comment { + return parser.documentCreateComment( + parser.documentHTMLToDocument(userctx.document), + data orelse "", + ); + } }; + +// Tests +// ----- + +pub fn testExecFn( + _: std.mem.Allocator, + js_env: *jsruntime.Env, +) anyerror!void { + var constructor = [_]Case{ + .{ .src = "let comment = new Comment('foo')", .ex = "undefined" }, + .{ .src = "comment.data", .ex = "foo" }, + + .{ .src = "let emptycomment = new Comment()", .ex = "undefined" }, + .{ .src = "emptycomment.data", .ex = "" }, + }; + try checkCases(js_env, &constructor); +} diff --git a/src/dom/document.zig b/src/dom/document.zig index e9f22e76..0741b1d9 100644 --- a/src/dom/document.zig +++ b/src/dom/document.zig @@ -40,14 +40,26 @@ const DocumentType = @import("document_type.zig").DocumentType; const DocumentFragment = @import("document_fragment.zig").DocumentFragment; const DOMImplementation = @import("implementation.zig").DOMImplementation; +const UserContext = @import("../user_context.zig").UserContext; + // WEB IDL https://dom.spec.whatwg.org/#document pub const Document = struct { pub const Self = parser.Document; pub const prototype = *Node; pub const mem_guarantied = true; - pub fn constructor() !*parser.Document { - return try parser.domImplementationCreateHTMLDocument(null); + pub fn constructor(userctx: UserContext) !*parser.DocumentHTML { + const doc = try parser.documentCreateDocument( + try parser.documentHTMLGetTitle(userctx.document), + ); + + // we have to work w/ document instead of html document. + const ddoc = parser.documentHTMLToDocument(doc); + const ccur = parser.documentHTMLToDocument(userctx.document); + try parser.documentSetDocumentURI(ddoc, try parser.documentGetDocumentURI(ccur)); + try parser.documentSetInputEncoding(ddoc, try parser.documentGetInputEncoding(ccur)); + + return doc; } // JS funcs @@ -262,6 +274,13 @@ pub fn testExecFn( .{ .src = "newdoc.children.length", .ex = "0" }, .{ .src = "newdoc.getElementsByTagName('*').length", .ex = "0" }, .{ .src = "newdoc.getElementsByTagName('*').item(0)", .ex = "null" }, + .{ .src = "newdoc.inputEncoding === document.inputEncoding", .ex = "true" }, + .{ .src = "newdoc.documentURI === document.documentURI", .ex = "true" }, + .{ .src = "newdoc.URL === document.URL", .ex = "true" }, + .{ .src = "newdoc.compatMode === document.compatMode", .ex = "true" }, + .{ .src = "newdoc.characterSet === document.characterSet", .ex = "true" }, + .{ .src = "newdoc.charset === document.charset", .ex = "true" }, + .{ .src = "newdoc.contentType === document.contentType", .ex = "true" }, }; try checkCases(js_env, &constructor); diff --git a/src/dom/document_fragment.zig b/src/dom/document_fragment.zig index 7cd1d27b..08d99165 100644 --- a/src/dom/document_fragment.zig +++ b/src/dom/document_fragment.zig @@ -20,20 +20,37 @@ const std = @import("std"); const parser = @import("../netsurf.zig"); +const jsruntime = @import("jsruntime"); +const Case = jsruntime.test_utils.Case; +const checkCases = jsruntime.test_utils.checkCases; + const Node = @import("node.zig").Node; +const UserContext = @import("../user_context.zig").UserContext; + // WEB IDL https://dom.spec.whatwg.org/#documentfragment pub const DocumentFragment = struct { pub const Self = parser.DocumentFragment; pub const prototype = *Node; pub const mem_guarantied = true; - // TODO add constructor, but I need to associate the new DocumentFragment - // with the current document global object... - // > The new DocumentFragment() constructor steps are to set this’s node - // > document to current global object’s associated Document. - // https://dom.spec.whatwg.org/#dom-documentfragment-documentfragment - pub fn constructor() !*parser.DocumentFragment { - return error.NotImplemented; + pub fn constructor(userctx: UserContext) !*parser.DocumentFragment { + return parser.documentCreateDocumentFragment( + parser.documentHTMLToDocument(userctx.document), + ); } }; + +// Tests +// ----- + +pub fn testExecFn( + _: std.mem.Allocator, + js_env: *jsruntime.Env, +) anyerror!void { + var constructor = [_]Case{ + .{ .src = "const dc = new DocumentFragment()", .ex = "undefined" }, + .{ .src = "dc.constructor.name", .ex = "DocumentFragment" }, + }; + try checkCases(js_env, &constructor); +} diff --git a/src/dom/implementation.zig b/src/dom/implementation.zig index f7a613a0..e4fff404 100644 --- a/src/dom/implementation.zig +++ b/src/dom/implementation.zig @@ -75,7 +75,7 @@ pub const DOMImplementation = struct { return try parser.domImplementationCreateDocument(cnamespace, cqname, doctype); } - pub fn _createHTMLDocument(_: *DOMImplementation, title: ?[]const u8) !*parser.Document { + pub fn _createHTMLDocument(_: *DOMImplementation, title: ?[]const u8) !*parser.DocumentHTML { return try parser.domImplementationCreateHTMLDocument(title); } @@ -95,7 +95,8 @@ pub fn testExecFn( ) anyerror!void { var getImplementation = [_]Case{ .{ .src = "let impl = document.implementation", .ex = "undefined" }, - .{ .src = "impl.createHTMLDocument();", .ex = "[object Document]" }, + .{ .src = "impl.createHTMLDocument();", .ex = "[object HTMLDocument]" }, + .{ .src = "impl.createHTMLDocument('foo');", .ex = "[object HTMLDocument]" }, .{ .src = "impl.createDocument(null, 'foo');", .ex = "[object Document]" }, .{ .src = "impl.createDocumentType('foo', 'bar', 'baz')", .ex = "[object DocumentType]" }, .{ .src = "impl.hasFeature()", .ex = "true" }, diff --git a/src/dom/node.zig b/src/dom/node.zig index 58dce2f7..5dd0c811 100644 --- a/src/dom/node.zig +++ b/src/dom/node.zig @@ -277,14 +277,30 @@ pub const Node = struct { return try Node.toInterface(res); } + // Check if the hierarchy node tree constraints are respected. + // For now, it checks only if new nodes are not self. + // TODO implements the others contraints. + // see https://dom.spec.whatwg.org/#concept-node-tree + pub fn hierarchy(self: *parser.Node, nodes: ?Variadic(*parser.Node)) !bool { + if (nodes == null) return true; + if (nodes.?.slice.len == 0) return true; + + for (nodes.?.slice) |node| if (self == node) return false; + + return true; + } + // TODO according with https://dom.spec.whatwg.org/#parentnode, the // function must accept either node or string. // blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/114 pub fn prepend(self: *parser.Node, nodes: ?Variadic(*parser.Node)) !void { if (nodes == null) return; if (nodes.?.slice.len == 0) return; - const first = try parser.nodeFirstChild(self); + // check hierarchy + if (!try hierarchy(self, nodes)) return parser.DOMError.HierarchyRequest; + + const first = try parser.nodeFirstChild(self); if (first == null) { for (nodes.?.slice) |node| { _ = try parser.nodeAppendChild(self, node); @@ -303,6 +319,10 @@ pub const Node = struct { pub fn append(self: *parser.Node, nodes: ?Variadic(*parser.Node)) !void { if (nodes == null) return; if (nodes.?.slice.len == 0) return; + + // check hierarchy + if (!try hierarchy(self, nodes)) return parser.DOMError.HierarchyRequest; + for (nodes.?.slice) |node| { _ = try parser.nodeAppendChild(self, node); } @@ -312,12 +332,15 @@ pub const Node = struct { // function must accept either node or string. // blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/114 pub fn replaceChildren(self: *parser.Node, nodes: ?Variadic(*parser.Node)) !void { - // remove existing children - try removeChildren(self); - if (nodes == null) return; if (nodes.?.slice.len == 0) return; + // check hierarchy + if (!try hierarchy(self, nodes)) return parser.DOMError.HierarchyRequest; + + // remove existing children + try removeChildren(self); + // add new children for (nodes.?.slice) |node| { _ = try parser.nodeAppendChild(self, node); diff --git a/src/dom/text.zig b/src/dom/text.zig index a64c4d88..4ac35ed5 100644 --- a/src/dom/text.zig +++ b/src/dom/text.zig @@ -28,6 +28,8 @@ const parser = @import("../netsurf.zig"); const CharacterData = @import("character_data.zig").CharacterData; const CDATASection = @import("cdata_section.zig").CDATASection; +const UserContext = @import("../user_context.zig").UserContext; + // Text interfaces pub const Interfaces = generate.Tuple(.{ CDATASection, @@ -38,6 +40,13 @@ pub const Text = struct { pub const prototype = *CharacterData; pub const mem_guarantied = true; + pub fn constructor(userctx: UserContext, data: ?[]const u8) !*parser.Text { + return parser.documentCreateTextNode( + parser.documentHTMLToDocument(userctx.document), + data orelse "", + ); + } + // JS funcs // -------- @@ -62,6 +71,15 @@ pub fn testExecFn( _: std.mem.Allocator, js_env: *jsruntime.Env, ) anyerror!void { + var constructor = [_]Case{ + .{ .src = "let t = new Text('foo')", .ex = "undefined" }, + .{ .src = "t.data", .ex = "foo" }, + + .{ .src = "let emptyt = new Text()", .ex = "undefined" }, + .{ .src = "emptyt.data", .ex = "" }, + }; + try checkCases(js_env, &constructor); + var get_whole_text = [_]Case{ .{ .src = "let text = document.getElementById('link').firstChild", .ex = "undefined" }, .{ .src = "text.wholeText === 'OK'", .ex = "true" }, diff --git a/src/main.zig b/src/main.zig index 1c032b68..ab4fb585 100644 --- a/src/main.zig +++ b/src/main.zig @@ -25,6 +25,7 @@ const apiweb = @import("apiweb.zig"); const Window = @import("html/window.zig").Window; pub const Types = jsruntime.reflect(apiweb.Interfaces); +pub const UserContext = apiweb.UserContext; const socket_path = "/tmp/browsercore-server.sock"; @@ -103,5 +104,5 @@ pub fn main() !void { try server.listen(addr); std.debug.print("Listening on: {s}...\n", .{socket_path}); - try jsruntime.loadEnv(&arena, execJS); + try jsruntime.loadEnv(&arena, null, execJS); } diff --git a/src/main_get.zig b/src/main_get.zig index 579edca2..387106a6 100644 --- a/src/main_get.zig +++ b/src/main_get.zig @@ -23,6 +23,7 @@ const jsruntime = @import("jsruntime"); const apiweb = @import("apiweb.zig"); pub const Types = jsruntime.reflect(apiweb.Interfaces); +pub const UserContext = apiweb.UserContext; pub const std_options = struct { pub const log_level = .debug; diff --git a/src/main_shell.zig b/src/main_shell.zig index 4b4967d1..6eb6c11a 100644 --- a/src/main_shell.zig +++ b/src/main_shell.zig @@ -28,6 +28,8 @@ const storage = @import("storage/storage.zig"); const html_test = @import("html_test.zig").html; pub const Types = jsruntime.reflect(apiweb.Interfaces); +pub const UserContext = apiweb.UserContext; +const Client = @import("async/Client.zig"); var doc: *parser.DocumentHTML = undefined; @@ -39,6 +41,14 @@ fn execJS( try js_env.start(alloc); defer js_env.stop(); + var cli = Client{ .allocator = alloc, .loop = js_env.nat_ctx.loop }; + defer cli.deinit(); + + try js_env.setUserContext(UserContext{ + .document = doc, + .httpClient = &cli, + }); + var storageShelf = storage.Shelf.init(alloc); defer storageShelf.deinit(); diff --git a/src/main_wpt.zig b/src/main_wpt.zig index 06c6a249..bc9927fd 100644 --- a/src/main_wpt.zig +++ b/src/main_wpt.zig @@ -49,6 +49,7 @@ const Out = enum { pub const Types = jsruntime.reflect(apiweb.Interfaces); pub const GlobalType = apiweb.GlobalType; +pub const UserContext = apiweb.UserContext; // TODO For now the WPT tests run is specific to WPT. // It manually load js framwork libs, and run the first script w/ js content in diff --git a/src/netsurf.zig b/src/netsurf.zig index f5b3a141..0c4f3b8a 100644 --- a/src/netsurf.zig +++ b/src/netsurf.zig @@ -1763,21 +1763,29 @@ pub inline fn domImplementationCreateDocumentType( return dt.?; } -pub inline fn domImplementationCreateHTMLDocument(title: ?[]const u8) !*Document { - var doc: ?*Document = undefined; - const err = c.dom_implementation_create_document( - c.DOM_IMPLEMENTATION_HTML, - null, - null, - null, - null, - null, - &doc, - ); - try DOMErr(err); - // TODO set title - _ = title; - return doc.?; +pub inline fn domImplementationCreateHTMLDocument(title: ?[]const u8) !*DocumentHTML { + const doc_html = try documentCreateDocument(title); + const doc = documentHTMLToDocument(doc_html); + + // add hierarchy: html, head, body. + const html = try documentCreateElement(doc, "html"); + _ = try nodeAppendChild(documentToNode(doc), elementToNode(html)); + + const head = try documentCreateElement(doc, "head"); + _ = try nodeAppendChild(elementToNode(html), elementToNode(head)); + + if (title) |t| { + try documentHTMLSetTitle(doc_html, t); + const htitle = try documentCreateElement(doc, "title"); + const txt = try documentCreateTextNode(doc, t); + _ = try nodeAppendChild(elementToNode(htitle), @as(*Node, @ptrCast(txt))); + _ = try nodeAppendChild(elementToNode(head), elementToNode(htitle)); + } + + const body = try documentCreateElement(doc, "body"); + _ = try nodeAppendChild(elementToNode(html), elementToNode(body)); + + return doc_html; } // Document @@ -1833,6 +1841,28 @@ pub inline fn documentGetInputEncoding(doc: *Document) ![]const u8 { return strToData(s.?); } +pub inline fn documentSetInputEncoding(doc: *Document, enc: []const u8) !void { + const err = documentVtable(doc).dom_document_set_input_encoding.?(doc, try strFromData(enc)); + try DOMErr(err); +} + +pub inline fn documentCreateDocument(title: ?[]const u8) !*DocumentHTML { + var doc: ?*Document = undefined; + const err = c.dom_implementation_create_document( + c.DOM_IMPLEMENTATION_HTML, + null, + null, + null, + null, + null, + &doc, + ); + try DOMErr(err); + const doc_html = @as(*DocumentHTML, @ptrCast(doc.?)); + if (title) |t| try documentHTMLSetTitle(doc_html, t); + return doc_html; +} + pub inline fn documentCreateElement(doc: *Document, tag_name: []const u8) !*Element { var elem: ?*Element = undefined; const err = documentVtable(doc).dom_document_create_element.?(doc, try strFromData(tag_name), &elem); diff --git a/src/run_tests.zig b/src/run_tests.zig index 3fe20b3f..700d15c0 100644 --- a/src/run_tests.zig +++ b/src/run_tests.zig @@ -30,6 +30,7 @@ const xhr = @import("xhr/xhr.zig"); const storage = @import("storage/storage.zig"); const url = @import("url/url.zig"); const urlquery = @import("url/query.zig"); +const Client = @import("async/Client.zig"); const documentTestExecFn = @import("dom/document.zig").testExecFn; const HTMLDocumentTestExecFn = @import("html/document.zig").testExecFn; @@ -46,6 +47,8 @@ const NodeListTestExecFn = @import("dom/nodelist.zig").testExecFn; const AttrTestExecFn = @import("dom/attribute.zig").testExecFn; const EventTargetTestExecFn = @import("dom/event_target.zig").testExecFn; const ProcessingInstructionTestExecFn = @import("dom/processing_instruction.zig").testExecFn; +const CommentTestExecFn = @import("dom/comment.zig").testExecFn; +const DocumentFragmentTestExecFn = @import("dom/document_fragment.zig").testExecFn; const EventTestExecFn = @import("events/event.zig").testExecFn; const XHRTestExecFn = xhr.testExecFn; const ProgressEventTestExecFn = @import("xhr/progress_event.zig").testExecFn; @@ -54,6 +57,7 @@ const URLTestExecFn = url.testExecFn; const HTMLElementTestExecFn = @import("html/elements.zig").testExecFn; pub const Types = jsruntime.reflect(apiweb.Interfaces); +pub const UserContext = @import("user_context.zig").UserContext; var doc: *parser.DocumentHTML = undefined; @@ -81,6 +85,14 @@ fn testExecFn( std.debug.print("documentHTMLClose error: {s}\n", .{@errorName(err)}); }; + var cli = Client{ .allocator = alloc, .loop = js_env.nat_ctx.loop }; + defer cli.deinit(); + + try js_env.setUserContext(.{ + .document = doc, + .httpClient = &cli, + }); + // alias global as self and window var window = Window.create(null); @@ -111,6 +123,8 @@ fn testsAllExecFn( DOMTokenListExecFn, NodeListTestExecFn, AttrTestExecFn, + CommentTestExecFn, + DocumentFragmentTestExecFn, EventTargetTestExecFn, EventTestExecFn, XHRTestExecFn, @@ -315,7 +329,7 @@ fn testJSRuntime(alloc: std.mem.Allocator) !void { var arena_alloc = std.heap.ArenaAllocator.init(alloc); defer arena_alloc.deinit(); - try jsruntime.loadEnv(&arena_alloc, testsAllExecFn); + try jsruntime.loadEnv(&arena_alloc, null, testsAllExecFn); } test "DocumentHTMLParseFromStr" { diff --git a/src/test_runner.zig b/src/test_runner.zig index d385c813..8b138d0b 100644 --- a/src/test_runner.zig +++ b/src/test_runner.zig @@ -21,6 +21,7 @@ const std = @import("std"); const tests = @import("run_tests.zig"); pub const Types = tests.Types; +pub const UserContext = tests.UserContext; pub fn main() !void { try tests.main(); diff --git a/src/user_context.zig b/src/user_context.zig new file mode 100644 index 00000000..4860100d --- /dev/null +++ b/src/user_context.zig @@ -0,0 +1,8 @@ +const std = @import("std"); +const parser = @import("netsurf.zig"); +const Client = @import("async/Client.zig"); + +pub const UserContext = struct { + document: *parser.DocumentHTML, + httpClient: *Client, +}; diff --git a/src/wpt/run.zig b/src/wpt/run.zig index c7c263a5..4322ee40 100644 --- a/src/wpt/run.zig +++ b/src/wpt/run.zig @@ -30,6 +30,8 @@ const Window = @import("../html/window.zig").Window; const storage = @import("../storage/storage.zig"); const Types = @import("../main_wpt.zig").Types; +const UserContext = @import("../main_wpt.zig").UserContext; +const Client = @import("../async/Client.zig"); // runWPT parses the given HTML file, starts a js env and run the first script // tags containing javascript sources. @@ -50,7 +52,14 @@ pub fn run(arena: *std.heap.ArenaAllocator, comptime dir: []const u8, f: []const // create JS env var loop = try Loop.init(alloc); defer loop.deinit(); - var js_env = try Env.init(alloc, &loop); + + var cli = Client{ .allocator = alloc, .loop = &loop }; + defer cli.deinit(); + + var js_env = try Env.init(alloc, &loop, UserContext{ + .document = html_doc, + .httpClient = &cli, + }); defer js_env.deinit(); var storageShelf = storage.Shelf.init(alloc); diff --git a/src/xhr/xhr.zig b/src/xhr/xhr.zig index 8719e2fc..e395144a 100644 --- a/src/xhr/xhr.zig +++ b/src/xhr/xhr.zig @@ -37,6 +37,8 @@ const Client = @import("../async/Client.zig"); const parser = @import("../netsurf.zig"); +const UserContext = @import("../user_context.zig").UserContext; + const log = std.log.scoped(.xhr); // XHR interfaces @@ -149,7 +151,7 @@ pub const XMLHttpRequest = struct { proto: XMLHttpRequestEventTarget = XMLHttpRequestEventTarget{}, alloc: std.mem.Allocator, - cli: Client, + cli: *Client, impl: YieldImpl, priv_state: PrivState = .new, @@ -185,7 +187,7 @@ pub const XMLHttpRequest = struct { const min_delay: u64 = 50000000; // 50ms - pub fn constructor(alloc: std.mem.Allocator, loop: *Loop) !XMLHttpRequest { + pub fn constructor(alloc: std.mem.Allocator, loop: *Loop, userctx: UserContext) !XMLHttpRequest { return .{ .alloc = alloc, .headers = .{ .allocator = alloc, .owned = true }, @@ -195,8 +197,7 @@ pub const XMLHttpRequest = struct { .url = null, .uri = undefined, .state = UNSENT, - // TODO retrieve the HTTP client globally to reuse existing connections. - .cli = .{ .allocator = alloc, .loop = loop }, + .cli = userctx.httpClient, }; } @@ -235,9 +236,6 @@ pub const XMLHttpRequest = struct { self.response_headers.deinit(); self.proto.deinit(alloc); - - // TODO the client must be shared between requests. - self.cli.deinit(); } pub fn get_readyState(self: *XMLHttpRequest) u16 { diff --git a/vendor/zig-js-runtime b/vendor/zig-js-runtime index fff1a677..d4a2eaef 160000 --- a/vendor/zig-js-runtime +++ b/vendor/zig-js-runtime @@ -1 +1 @@ -Subproject commit fff1a6778dfc91d71d77e5c593283b2aba78432f +Subproject commit d4a2eaefd8390b9483e5ad58d2992dc381632559