Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add user context #216

Merged
merged 14 commits into from
May 22, 2024
Merged
2 changes: 2 additions & 0 deletions src/apiweb.zig
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,5 @@ pub const Interfaces = generate.Tuple(.{
Storage.Interfaces,
URL.Interfaces,
});

pub const UserContext = @import("user_context.zig").UserContext;
15 changes: 14 additions & 1 deletion src/browser/browser.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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,

Expand All @@ -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;
Expand All @@ -122,6 +128,7 @@ pub const Session = struct {
self.loader.deinit();
self.loop.deinit();
self.storageShed.deinit();
self.httpClient.deinit();
self.alloc.destroy(self);
}

Expand Down Expand Up @@ -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);
Expand Down
32 changes: 32 additions & 0 deletions src/dom/comment.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
23 changes: 21 additions & 2 deletions src/dom/document.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);

Expand Down
31 changes: 24 additions & 7 deletions src/dom/document_fragment.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
5 changes: 3 additions & 2 deletions src/dom/implementation.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand All @@ -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" },
Expand Down
31 changes: 27 additions & 4 deletions src/dom/node.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
}
Expand All @@ -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);
Expand Down
18 changes: 18 additions & 0 deletions src/dom/text.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
// --------

Expand All @@ -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" },
Expand Down
3 changes: 2 additions & 1 deletion src/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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);
}
1 change: 1 addition & 0 deletions src/main_get.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
10 changes: 10 additions & 0 deletions src/main_shell.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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;

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

Expand Down
1 change: 1 addition & 0 deletions src/main_wpt.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading