Merge pull request #216 from lightpanda-io/usrctx

Add user context
This commit is contained in:
Pierre Tachoire
2024-05-22 15:07:02 +02:00
committed by GitHub
19 changed files with 240 additions and 42 deletions

View File

@@ -39,3 +39,5 @@ pub const Interfaces = generate.Tuple(.{
Storage.Interfaces,
URL.Interfaces,
});
pub const UserContext = @import("user_context.zig").UserContext;

View File

@@ -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);

View File

@@ -15,13 +15,45 @@
//
// 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 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);
}

View File

@@ -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);

View File

@@ -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 thiss node
// > document to current global objects 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);
}

View File

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

View File

@@ -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);

View File

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

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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();

View File

@@ -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

View File

@@ -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);

View File

@@ -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" {

View File

@@ -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();

8
src/user_context.zig Normal file
View File

@@ -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,
};

View File

@@ -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);

View File

@@ -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 {