mirror of
				https://github.com/lightpanda-io/browser.git
				synced 2025-10-29 15:13:28 +00:00 
			
		
		
		
	Merge pull request #345 from lightpanda-io/modules
browser: support for modules
This commit is contained in:
		| @@ -29,6 +29,7 @@ const Mime = @import("mime.zig"); | ||||
| const jsruntime = @import("jsruntime"); | ||||
| const Loop = jsruntime.Loop; | ||||
| const Env = jsruntime.Env; | ||||
| const Module = jsruntime.Module; | ||||
|  | ||||
| const apiweb = @import("../apiweb.zig"); | ||||
|  | ||||
| @@ -125,6 +126,21 @@ pub const Session = struct { | ||||
|         try self.env.load(&self.jstypes); | ||||
|     } | ||||
|  | ||||
|     fn fetchModule(ctx: *anyopaque, referrer: ?jsruntime.Module, specifier: []const u8) !jsruntime.Module { | ||||
|         _ = referrer; | ||||
|  | ||||
|         const self: *Session = @ptrCast(@alignCast(ctx)); | ||||
|  | ||||
|         if (self.page == null) return error.NoPage; | ||||
|  | ||||
|         log.debug("fetch module: specifier: {s}", .{specifier}); | ||||
|         const alloc = self.arena.allocator(); | ||||
|         const body = try self.page.?.fetchData(alloc, specifier); | ||||
|         defer alloc.free(body); | ||||
|  | ||||
|         return self.env.compileModule(body, specifier); | ||||
|     } | ||||
|  | ||||
|     fn deinit(self: *Session) void { | ||||
|         if (self.page) |*p| p.end(); | ||||
|  | ||||
| @@ -362,6 +378,9 @@ pub const Page = struct { | ||||
|         log.debug("start js env", .{}); | ||||
|         try self.session.env.start(); | ||||
|  | ||||
|         // register the module loader | ||||
|         try self.session.env.setModuleLoadFn(self.session, Session.fetchModule); | ||||
|  | ||||
|         // load polyfills | ||||
|         try polyfill.load(alloc, self.session.env); | ||||
|  | ||||
| @@ -388,7 +407,7 @@ pub const Page = struct { | ||||
|         // sasync stores scripts which can be run asynchronously. | ||||
|         // for now they are just run after the non-async one in order to | ||||
|         // dispatch DOMContentLoaded the sooner as possible. | ||||
|         var sasync = std.ArrayList(*parser.Element).init(alloc); | ||||
|         var sasync = std.ArrayList(Script).init(alloc); | ||||
|         defer sasync.deinit(); | ||||
|  | ||||
|         const root = parser.documentToNode(doc); | ||||
| @@ -403,21 +422,10 @@ pub const Page = struct { | ||||
|             } | ||||
|  | ||||
|             const e = parser.nodeToElement(next.?); | ||||
|             const tag = try parser.elementHTMLGetTagType(@as(*parser.ElementHTML, @ptrCast(e))); | ||||
|  | ||||
|             // ignore non-script tags | ||||
|             if (tag != .script) continue; | ||||
|  | ||||
|             // ignore non-js script. | ||||
|             // > type | ||||
|             // > Attribute is not set (default), an empty string, or a JavaScript MIME | ||||
|             // > type indicates that the script is a "classic script", containing | ||||
|             // > JavaScript code. | ||||
|             // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attribute_is_not_set_default_an_empty_string_or_a_javascript_mime_type | ||||
|             const stype = try parser.elementGetAttribute(e, "type"); | ||||
|             if (!isJS(stype)) { | ||||
|                 continue; | ||||
|             } | ||||
|             const script = try Script.init(e) orelse continue; | ||||
|             if (script.kind == .unknown) continue; | ||||
|  | ||||
|             // Ignore the defer attribute b/c we analyze all script | ||||
|             // after the document has been parsed. | ||||
| @@ -431,8 +439,8 @@ pub const Page = struct { | ||||
|             // > then the classic script will be fetched in parallel to | ||||
|             // > parsing and evaluated as soon as it is available. | ||||
|             // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#async | ||||
|             if (try parser.elementGetAttribute(e, "async") != null) { | ||||
|                 try sasync.append(e); | ||||
|             if (script.isasync) { | ||||
|                 try sasync.append(script); | ||||
|                 continue; | ||||
|             } | ||||
|  | ||||
| @@ -455,7 +463,7 @@ pub const Page = struct { | ||||
|             // > page. | ||||
|             // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#notes | ||||
|             try parser.documentHTMLSetCurrentScript(html_doc, @ptrCast(e)); | ||||
|             self.evalScript(e) catch |err| log.warn("evaljs: {any}", .{err}); | ||||
|             self.evalScript(script) catch |err| log.warn("evaljs: {any}", .{err}); | ||||
|             try parser.documentHTMLSetCurrentScript(html_doc, null); | ||||
|         } | ||||
|  | ||||
| @@ -472,9 +480,9 @@ pub const Page = struct { | ||||
|         _ = try parser.eventTargetDispatchEvent(parser.toEventTarget(parser.DocumentHTML, html_doc), evt); | ||||
|  | ||||
|         // eval async scripts. | ||||
|         for (sasync.items) |e| { | ||||
|             try parser.documentHTMLSetCurrentScript(html_doc, @ptrCast(e)); | ||||
|             self.evalScript(e) catch |err| log.warn("evaljs: {any}", .{err}); | ||||
|         for (sasync.items) |s| { | ||||
|             try parser.documentHTMLSetCurrentScript(html_doc, @ptrCast(s.element)); | ||||
|             self.evalScript(s) catch |err| log.warn("evaljs: {any}", .{err}); | ||||
|             try parser.documentHTMLSetCurrentScript(html_doc, null); | ||||
|         } | ||||
|  | ||||
| @@ -496,15 +504,15 @@ pub const Page = struct { | ||||
|     // evalScript evaluates the src in priority. | ||||
|     // if no src is present, we evaluate the text source. | ||||
|     // https://html.spec.whatwg.org/multipage/scripting.html#script-processing-model | ||||
|     fn evalScript(self: *Page, e: *parser.Element) !void { | ||||
|     fn evalScript(self: *Page, s: Script) !void { | ||||
|         const alloc = self.arena.allocator(); | ||||
|  | ||||
|         // https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-classic-script | ||||
|         const opt_src = try parser.elementGetAttribute(e, "src"); | ||||
|         const opt_src = try parser.elementGetAttribute(s.element, "src"); | ||||
|         if (opt_src) |src| { | ||||
|             log.debug("starting GET {s}", .{src}); | ||||
|  | ||||
|             self.fetchScript(src) catch |err| { | ||||
|             self.fetchScript(s) catch |err| { | ||||
|                 switch (err) { | ||||
|                     FetchError.BadStatusCode => return err, | ||||
|  | ||||
| @@ -523,26 +531,10 @@ pub const Page = struct { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         var try_catch: jsruntime.TryCatch = undefined; | ||||
|         try_catch.init(self.session.env); | ||||
|         defer try_catch.deinit(); | ||||
|  | ||||
|         const opt_text = try parser.nodeTextContent(parser.elementToNode(e)); | ||||
|         // TODO handle charset attribute | ||||
|         const opt_text = try parser.nodeTextContent(parser.elementToNode(s.element)); | ||||
|         if (opt_text) |text| { | ||||
|             // TODO handle charset attribute | ||||
|             const res = self.session.env.exec(text, "") catch { | ||||
|                 if (try try_catch.err(alloc, self.session.env)) |msg| { | ||||
|                     defer alloc.free(msg); | ||||
|                     log.info("eval inline {s}: {s}", .{ text, msg }); | ||||
|                 } | ||||
|                 return; | ||||
|             }; | ||||
|  | ||||
|             if (builtin.mode == .Debug) { | ||||
|                 const msg = try res.toString(alloc, self.session.env); | ||||
|                 defer alloc.free(msg); | ||||
|                 log.debug("eval inline {s}", .{msg}); | ||||
|             } | ||||
|             try s.eval(alloc, self.session.env, text); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
| @@ -557,12 +549,9 @@ pub const Page = struct { | ||||
|         JsErr, | ||||
|     }; | ||||
|  | ||||
|     // fetchScript senf a GET request to the src and execute the script | ||||
|     // received. | ||||
|     fn fetchScript(self: *Page, src: []const u8) !void { | ||||
|         const alloc = self.arena.allocator(); | ||||
|  | ||||
|         log.debug("starting fetch script {s}", .{src}); | ||||
|     // the caller owns the returned string | ||||
|     fn fetchData(self: *Page, alloc: std.mem.Allocator, src: []const u8) ![]const u8 { | ||||
|         log.debug("starting fetch {s}", .{src}); | ||||
|  | ||||
|         var buffer: [1024]u8 = undefined; | ||||
|         var b: []u8 = buffer[0..]; | ||||
| @@ -573,46 +562,91 @@ pub const Page = struct { | ||||
|  | ||||
|         const resp = fetchres.req.response; | ||||
|  | ||||
|         log.info("fetch script {any}: {d}", .{ u, resp.status }); | ||||
|         log.info("fetch {any}: {d}", .{ u, resp.status }); | ||||
|  | ||||
|         if (resp.status != .ok) return FetchError.BadStatusCode; | ||||
|  | ||||
|         // TODO check content-type | ||||
|         const body = try fetchres.req.reader().readAllAlloc(alloc, 16 * 1024 * 1024); | ||||
|         defer alloc.free(body); | ||||
|  | ||||
|         // check no body | ||||
|         if (body.len == 0) return FetchError.NoBody; | ||||
|  | ||||
|         var try_catch: jsruntime.TryCatch = undefined; | ||||
|         try_catch.init(self.session.env); | ||||
|         defer try_catch.deinit(); | ||||
|         return body; | ||||
|     } | ||||
|  | ||||
|         const res = self.session.env.exec(body, src) catch { | ||||
|             if (try try_catch.err(alloc, self.session.env)) |msg| { | ||||
|                 defer alloc.free(msg); | ||||
|                 log.info("eval remote {s}: {s}", .{ src, msg }); | ||||
|             } | ||||
|             return FetchError.JsErr; | ||||
|     // fetchScript senf a GET request to the src and execute the script | ||||
|     // received. | ||||
|     fn fetchScript(self: *Page, s: Script) !void { | ||||
|         const alloc = self.arena.allocator(); | ||||
|         const body = try self.fetchData(alloc, s.src); | ||||
|         defer alloc.free(body); | ||||
|  | ||||
|         try s.eval(alloc, self.session.env, body); | ||||
|     } | ||||
|  | ||||
|     const Script = struct { | ||||
|         element: *parser.Element, | ||||
|         kind: Kind, | ||||
|         isasync: bool, | ||||
|  | ||||
|         src: []const u8, | ||||
|  | ||||
|         const Kind = enum { | ||||
|             unknown, | ||||
|             javascript, | ||||
|             module, | ||||
|         }; | ||||
|  | ||||
|         if (builtin.mode == .Debug) { | ||||
|             const msg = try res.toString(alloc, self.session.env); | ||||
|             defer alloc.free(msg); | ||||
|             log.debug("eval remote {s}: {s}", .{ src, msg }); | ||||
|         fn init(e: *parser.Element) !?Script { | ||||
|             // ignore non-script tags | ||||
|             const tag = try parser.elementHTMLGetTagType(@as(*parser.ElementHTML, @ptrCast(e))); | ||||
|             if (tag != .script) return null; | ||||
|  | ||||
|             return .{ | ||||
|                 .element = e, | ||||
|                 .kind = kind(try parser.elementGetAttribute(e, "type")), | ||||
|                 .isasync = try parser.elementGetAttribute(e, "async") != null, | ||||
|  | ||||
|                 .src = try parser.elementGetAttribute(e, "src") orelse "inline", | ||||
|             }; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // > type | ||||
|     // > Attribute is not set (default), an empty string, or a JavaScript MIME | ||||
|     // > type indicates that the script is a "classic script", containing | ||||
|     // > JavaScript code. | ||||
|     // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attribute_is_not_set_default_an_empty_string_or_a_javascript_mime_type | ||||
|     fn isJS(stype: ?[]const u8) bool { | ||||
|         if (stype == null or stype.?.len == 0) return true; | ||||
|         if (std.mem.eql(u8, stype.?, "application/javascript")) return true; | ||||
|         if (!std.mem.eql(u8, stype.?, "module")) return true; | ||||
|         // > type | ||||
|         // > Attribute is not set (default), an empty string, or a JavaScript MIME | ||||
|         // > type indicates that the script is a "classic script", containing | ||||
|         // > JavaScript code. | ||||
|         // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attribute_is_not_set_default_an_empty_string_or_a_javascript_mime_type | ||||
|         fn kind(stype: ?[]const u8) Kind { | ||||
|             if (stype == null or stype.?.len == 0) return .javascript; | ||||
|             if (std.mem.eql(u8, stype.?, "application/javascript")) return .javascript; | ||||
|             if (std.mem.eql(u8, stype.?, "module")) return .module; | ||||
|  | ||||
|         return false; | ||||
|     } | ||||
|             return .unknown; | ||||
|         } | ||||
|  | ||||
|         fn eval(self: Script, alloc: std.mem.Allocator, env: Env, body: []const u8) !void { | ||||
|             var try_catch: jsruntime.TryCatch = undefined; | ||||
|             try_catch.init(env); | ||||
|             defer try_catch.deinit(); | ||||
|  | ||||
|             const res = switch (self.kind) { | ||||
|                 .unknown => return error.UnknownScript, | ||||
|                 .javascript => env.exec(body, self.src), | ||||
|                 .module => env.module(body, self.src), | ||||
|             } catch { | ||||
|                 if (try try_catch.err(alloc, env)) |msg| { | ||||
|                     defer alloc.free(msg); | ||||
|                     log.info("eval script {s}: {s}", .{ self.src, msg }); | ||||
|                 } | ||||
|                 return FetchError.JsErr; | ||||
|             }; | ||||
|  | ||||
|             if (builtin.mode == .Debug) { | ||||
|                 const msg = try res.toString(alloc, env); | ||||
|                 defer alloc.free(msg); | ||||
|                 log.debug("eval script {s}: {s}", .{ self.src, msg }); | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
| }; | ||||
|   | ||||
							
								
								
									
										104
									
								
								src/html/history.zig
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								src/html/history.zig
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,104 @@ | ||||
| // Copyright (C) 2023-2024  Lightpanda (Selecy SAS) | ||||
| // | ||||
| // Francis Bouvier <francis@lightpanda.io> | ||||
| // Pierre Tachoire <pierre@lightpanda.io> | ||||
| // | ||||
| // This program is free software: you can redistribute it and/or modify | ||||
| // it under the terms of the GNU Affero General Public License as | ||||
| // published by the Free Software Foundation, either version 3 of the | ||||
| // License, or (at your option) any later version. | ||||
| // | ||||
| // This program is distributed in the hope that it will be useful, | ||||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| // GNU Affero General Public License for more details. | ||||
| // | ||||
| // You should have received a copy of the GNU Affero General Public License | ||||
| // along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||
|  | ||||
| const std = @import("std"); | ||||
|  | ||||
| const builtin = @import("builtin"); | ||||
| const jsruntime = @import("jsruntime"); | ||||
|  | ||||
| const Case = jsruntime.test_utils.Case; | ||||
| const checkCases = jsruntime.test_utils.checkCases; | ||||
|  | ||||
| // https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-history-interface | ||||
| pub const History = struct { | ||||
|     pub const mem_guarantied = true; | ||||
|  | ||||
|     const ScrollRestaurationMode = enum { | ||||
|         auto, | ||||
|         manual, | ||||
|     }; | ||||
|  | ||||
|     scrollRestauration: ScrollRestaurationMode = .audio, | ||||
|  | ||||
|     pub fn get_length(_: *History) u64 { | ||||
|         return 0; | ||||
|     } | ||||
|  | ||||
|     pub fn get_appCodeName(_: *Navigator) []const u8 { | ||||
|         return "Mozilla"; | ||||
|     } | ||||
|     pub fn get_appName(_: *Navigator) []const u8 { | ||||
|         return "Netscape"; | ||||
|     } | ||||
|     pub fn get_appVersion(self: *Navigator) []const u8 { | ||||
|         return self.version; | ||||
|     } | ||||
|     pub fn get_platform(self: *Navigator) []const u8 { | ||||
|         return self.platform; | ||||
|     } | ||||
|     pub fn get_product(_: *Navigator) []const u8 { | ||||
|         return "Gecko"; | ||||
|     } | ||||
|     pub fn get_productSub(_: *Navigator) []const u8 { | ||||
|         return "20030107"; | ||||
|     } | ||||
|     pub fn get_vendor(self: *Navigator) []const u8 { | ||||
|         return self.vendor; | ||||
|     } | ||||
|     pub fn get_vendorSub(_: *Navigator) []const u8 { | ||||
|         return ""; | ||||
|     } | ||||
|     pub fn get_language(self: *Navigator) []const u8 { | ||||
|         return self.language; | ||||
|     } | ||||
|     // TODO wait for arrays. | ||||
|     //pub fn get_languages(self: *Navigator) [][]const u8 { | ||||
|     //    return .{self.language}; | ||||
|     //} | ||||
|     pub fn get_online(_: *Navigator) bool { | ||||
|         return true; | ||||
|     } | ||||
|     pub fn _registerProtocolHandler(_: *Navigator, scheme: []const u8, url: []const u8) void { | ||||
|         _ = scheme; | ||||
|         _ = url; | ||||
|     } | ||||
|     pub fn _unregisterProtocolHandler(_: *Navigator, scheme: []const u8, url: []const u8) void { | ||||
|         _ = scheme; | ||||
|         _ = url; | ||||
|     } | ||||
|  | ||||
|     pub fn get_cookieEnabled(_: *Navigator) bool { | ||||
|         return true; | ||||
|     } | ||||
| }; | ||||
|  | ||||
| // Tests | ||||
| // ----- | ||||
|  | ||||
| pub fn testExecFn( | ||||
|     _: std.mem.Allocator, | ||||
|     js_env: *jsruntime.Env, | ||||
| ) anyerror!void { | ||||
|     var navigator = [_]Case{ | ||||
|         .{ .src = "navigator.userAgent", .ex = "Lightpanda/1.0" }, | ||||
|         .{ .src = "navigator.appVersion", .ex = "1.0" }, | ||||
|         .{ .src = "navigator.language", .ex = "en-US" }, | ||||
|     }; | ||||
|     try checkCases(js_env, &navigator); | ||||
| } | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 Pierre Tachoire
					Pierre Tachoire