commit 497a1119f8cc995be48d8cc200db296f9a5282e8 Author: Francis Bouvier Date: Tue Feb 7 16:22:01 2023 +0100 Initial commit Signed-off-by: Francis Bouvier diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..4f05ca29 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +zig-cache +zig-out +vendor \ No newline at end of file diff --git a/build.zig b/build.zig new file mode 100644 index 00000000..2fb538ef --- /dev/null +++ b/build.zig @@ -0,0 +1,77 @@ +const std = @import("std"); + +const jsruntime_path: []const u8 = "vendor/jsruntime-lib/"; +const jsruntime_pkgs = @import("vendor/jsruntime-lib/build.zig").packages(jsruntime_path); + +pub fn build(b: *std.build.Builder) !void { + const target = b.standardTargetOptions(.{}); + const mode = b.standardReleaseOptions(); + + // browser + // ------- + + // compile and install + const exe = b.addExecutable("browsercore", "src/main.zig"); + try common(exe, mode, target); + exe.install(); + + // run + const run_cmd = exe.run(); + run_cmd.step.dependOn(b.getInstallStep()); + if (b.args) |args| { + run_cmd.addArgs(args); + } + + // step + const run_step = b.step("run", "Run the app"); + run_step.dependOn(&run_cmd.step); + + // shell + // ----- + + // compile and install + const shell = b.addExecutable("browsercore-shell", "src/main_shell.zig"); + try common(shell, mode, target); + try jsruntime_pkgs.add_shell(shell, mode); + // do not install shell binary + shell.install(); + + // run + const shell_cmd = shell.run(); + shell_cmd.step.dependOn(b.getInstallStep()); + if (b.args) |args| { + shell_cmd.addArgs(args); + } + + // step + const shell_step = b.step("shell", "Run JS shell"); + shell_step.dependOn(&shell_cmd.step); + + // test + // ---- + + // compile + const exe_tests = b.addTest("src/run_tests.zig"); + try common(exe_tests, mode, target); + + // step + const test_step = b.step("test", "Run unit tests"); + test_step.dependOn(&exe_tests.step); +} + +fn common( + step: *std.build.LibExeObjStep, + mode: std.builtin.Mode, + target: std.zig.CrossTarget, +) !void { + step.setTarget(target); + step.setBuildMode(mode); + try jsruntime_pkgs.add(step, mode); + linkLexbor(step); +} + +fn linkLexbor(step: *std.build.LibExeObjStep) void { + const lib_path = "../lexbor/liblexbor_static.a"; + step.addObjectFile(lib_path); + step.addIncludePath("../lexbor/source"); +} diff --git a/src/dom.zig b/src/dom.zig new file mode 100644 index 00000000..d87594f1 --- /dev/null +++ b/src/dom.zig @@ -0,0 +1,24 @@ +const Console = @import("jsruntime").Console; + +pub const EventTarget = @import("dom/event_target.zig").EventTarget; +pub const Node = @import("dom/node.zig").Node; + +pub const Element = @import("dom/element.zig").Element; +pub const HTMLElement = @import("dom/element.zig").HTMLElement; +pub const HTMLBodyElement = @import("dom/element.zig").HTMLBodyElement; + +pub const Document = @import("dom/document.zig").Document; +pub const HTMLDocument = @import("dom/document.zig").HTMLDocument; + +pub const Interfaces = .{ + Console, + EventTarget, + Node, + + Element, + HTMLElement, + HTMLBodyElement, + + Document, + HTMLDocument, +}; diff --git a/src/dom/document.zig b/src/dom/document.zig new file mode 100644 index 00000000..584595dc --- /dev/null +++ b/src/dom/document.zig @@ -0,0 +1,122 @@ +const std = @import("std"); + +const jsruntime = @import("jsruntime"); +const Case = jsruntime.test_utils.Case; +const checkCases = jsruntime.test_utils.checkCases; + +const parser = @import("../parser.zig"); + +const DOM = @import("../dom.zig"); +const Node = DOM.Node; +const Element = DOM.Element; +const HTMLElement = DOM.HTMLElement; +const HTMLBodyElement = DOM.HTMLBodyElement; + +pub const Document = struct { + proto: Node, + base: ?*parser.Document, + + pub const prototype = *Node; + + pub fn init(base: ?*parser.Document) Document { + return .{ + .proto = Node.init(null), + .base = base, + }; + } + + pub fn constructor() Document { + return Document.init(null); + } + + fn getElementById(self: Document, elem_dom: *parser.Element, id: []const u8) ?Element { + if (self.base == null) { + return null; + } + const collection = parser.collectionInit(self.base.?, 1); + defer parser.collectionDeinit(collection); + const case_sensitve = true; + parser.elementsByAttr(elem_dom, collection, "id", id, case_sensitve) catch |err| { + std.debug.print("getElementById error: {s}\n", .{@errorName(err)}); + return null; + }; + if (collection.array.length == 0) { + // no results + return null; + } + const element_base = parser.collectionElement(collection, 0); + return Element.init(element_base); + } + + // JS funcs + // -------- + + pub fn get_body(_: Document) ?void { + // TODO + return null; + } + + pub fn _getElementById(_: Document, _: []u8) ?Element { + // TODO + return null; + } +}; + +pub const HTMLDocument = struct { + proto: Document, + base: *parser.DocumentHTML, + + pub const prototype = *Document; + + pub fn init() HTMLDocument { + return .{ + .proto = Document.init(null), + .base = parser.documentHTMLInit(), + }; + } + + pub fn deinit(self: HTMLDocument) void { + parser.documentHTMLDeinit(self.base); + } + + pub fn parse(self: *HTMLDocument, html: []const u8) !void { + try parser.documentHTMLParse(self.base, html); + self.proto.base = parser.documentHTMLToDocument(self.base); + } + + // JS funcs + // -------- + + pub fn get_body(self: HTMLDocument) ?HTMLBodyElement { + const body_dom = parser.documentHTMLBody(self.base); + return HTMLBodyElement.init(body_dom); + } + + pub fn _getElementById(self: HTMLDocument, id: []u8) ?HTMLElement { + const body_dom = parser.documentHTMLBody(self.base); + if (self.proto.getElementById(body_dom, id)) |elem| { + return HTMLElement.init(elem.base); + } + return null; + } +}; + +pub fn testExecFn( + js_env: *jsruntime.Env, + comptime _: []jsruntime.API, +) !void { + var constructor = [_]Case{ + .{ .src = "document.__proto__.constructor.name", .ex = "HTMLDocument" }, + .{ .src = "document.__proto__.__proto__.constructor.name", .ex = "Document" }, + .{ .src = "document.__proto__.__proto__.__proto__.constructor.name", .ex = "Node" }, + .{ .src = "document.__proto__.__proto__.__proto__.__proto__.constructor.name", .ex = "EventTarget" }, + }; + try checkCases(js_env, &constructor); + + var getElementById = [_]Case{ + .{ .src = "let getElementById = document.getElementById('content')", .ex = "undefined" }, + .{ .src = "getElementById.constructor.name", .ex = "HTMLElement" }, + .{ .src = "getElementById.localName", .ex = "main" }, + }; + try checkCases(js_env, &getElementById); +} diff --git a/src/dom/element.zig b/src/dom/element.zig new file mode 100644 index 00000000..70b0d5ab --- /dev/null +++ b/src/dom/element.zig @@ -0,0 +1,54 @@ +const std = @import("std"); + +const jsruntime = @import("jsruntime"); +const Case = jsruntime.test_utils.Case; +const checkCases = jsruntime.test_utils.checkCases; + +const parser = @import("../parser.zig"); + +const DOM = @import("../dom.zig"); +const Node = DOM.Node; + +pub const Element = struct { + proto: Node, + base: *parser.Element, + + pub const prototype = *Node; + + pub fn init(base: *parser.Element) Element { + return .{ + .proto = Node.init(null), + .base = base, + }; + } + + // JS funcs + // -------- + + pub fn get_localName(self: Element) []const u8 { + return parser.elementLocalName(self.base); + } +}; + +// HTML elements +// ------------- + +pub const HTMLElement = struct { + proto: Element, + + pub const prototype = *Element; + + pub fn init(elem_base: *parser.Element) HTMLElement { + return .{ .proto = Element.init(elem_base) }; + } +}; + +pub const HTMLBodyElement = struct { + proto: HTMLElement, + + pub const prototype = *HTMLElement; + + pub fn init(elem_base: *parser.Element) HTMLBodyElement { + return .{ .proto = HTMLElement.init(elem_base) }; + } +}; diff --git a/src/dom/event_target.zig b/src/dom/event_target.zig new file mode 100644 index 00000000..a423cb81 --- /dev/null +++ b/src/dom/event_target.zig @@ -0,0 +1,13 @@ +const parser = @import("../parser.zig"); + +pub const EventTarget = struct { + base: ?*parser.EventTarget = null, + + pub fn init(base: ?*parser.EventTarget) EventTarget { + return .{ .base = base }; + } + + pub fn constructor() EventTarget { + return .{}; + } +}; diff --git a/src/dom/node.zig b/src/dom/node.zig new file mode 100644 index 00000000..ef732071 --- /dev/null +++ b/src/dom/node.zig @@ -0,0 +1,36 @@ +const std = @import("std"); + +const parser = @import("../parser.zig"); + +const EventTarget = @import("event_target.zig").EventTarget; + +pub fn create_tree(node: ?*parser.Node, _: ?*anyopaque) callconv(.C) parser.Action { + if (node == null) { + return parser.ActionStop; + } + const node_type = parser.nodeType(node.?); + const node_name = parser.nodeName(node.?); + std.debug.print("type: {any}, name: {s}\n", .{ node_type, node_name }); + if (node_type == parser.NodeType.element) { + std.debug.print("yes\n", .{}); + } + return parser.ActionOk; +} + +pub const Node = struct { + proto: EventTarget, + base: ?*parser.Node = null, + + pub const prototype = *EventTarget; + + pub fn init(base: ?*parser.Node) Node { + return .{ .proto = EventTarget.init(null), .base = base }; + } + + pub fn make_tree(self: Node) !void { + if (self.base) |node| { + try parser.nodeWalk(node, create_tree); + } + return error.NodeParserNull; + } +}; diff --git a/src/html.zig b/src/html.zig new file mode 100644 index 00000000..6b230399 --- /dev/null +++ b/src/html.zig @@ -0,0 +1,6 @@ +pub const html: []const u8 = + \\
+ \\OK + \\

blah-blah-blah

+ \\
+; diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 00000000..08420024 --- /dev/null +++ b/src/main.zig @@ -0,0 +1,27 @@ +const std = @import("std"); + +const runtime = @import("jsruntime"); + +const EventTarget = @import("dom/event_target.zig").EventTarget; +const Node = @import("dom/node.zig").Node; +const Document = @import("dom/document.zig").Document; + +pub fn main() !void { + + // // generate APIs + // _ = comptime runtime.compile(.{ EventTarget, Node, Document }); + + // // create v8 vm + // const vm = runtime.VM.init(); + // defer vm.deinit(); + + // // document + // var doc = Document.init(); + // defer doc.deinit(); + // var html: []const u8 = "
OK

blah-blah-blah

"; + // try doc.parse(html); + + // try doc.proto.make_tree(); + + std.debug.print("ok\n", .{}); +} diff --git a/src/main_shell.zig b/src/main_shell.zig new file mode 100644 index 00000000..ff536faf --- /dev/null +++ b/src/main_shell.zig @@ -0,0 +1,51 @@ +const std = @import("std"); + +const jsruntime = @import("jsruntime"); +const Console = @import("jsruntime").Console; + +const DOM = @import("dom.zig"); + +const html = @import("html.zig").html; + +var doc: DOM.HTMLDocument = undefined; + +fn execJS( + alloc: std.mem.Allocator, + js_env: *jsruntime.Env, + comptime apis: []jsruntime.API, +) !void { + + // start JS env + js_env.start(); + defer js_env.stop(); + + // add document object + try js_env.addObject(apis, doc, "document"); + + // launch shellExec + try jsruntime.shellExec(alloc, js_env, apis); +} + +pub fn main() !void { + + // generate APIs + const apis = jsruntime.compile(DOM.Interfaces); + + // document + var base_doc = DOM.Document.init(); + defer base_doc.deinit(); + try base_doc.parse(html); + doc = DOM.HTMLDocument{ .proto = base_doc }; + + // create JS vm + const vm = jsruntime.VM.init(); + defer vm.deinit(); + + // alloc + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + const alloc = gpa.allocator(); + + // launch shell + try jsruntime.shell(alloc, false, apis, execJS, .{ .app_name = "browsercore" }); +} diff --git a/src/parser.zig b/src/parser.zig new file mode 100644 index 00000000..9258734e --- /dev/null +++ b/src/parser.zig @@ -0,0 +1,232 @@ +const std = @import("std"); + +const c = @cImport({ + @cInclude("../../lexbor/source/lexbor/html/html.h"); +}); + +// Public API +// ---------- + +// EventTarget + +pub const EventTarget = c.lxb_dom_event_target_t; + +// Node + +pub const Node = c.lxb_dom_node_t; + +pub const NodeType = enum(u4) { + undef, + element, + attribute, + text, + cdata_section, + entity_reference, + entity, + processing_instruction, + comment, + document, + document_type, + document_fragment, + notation, + last_entry, +}; + +pub fn nodeEventTarget(node: *Node) *EventTarget { + return c.lxb_dom_interface_event_target(node); +} + +pub const nodeWalker = (fn (node: ?*Node, _: ?*anyopaque) callconv(.C) Action); + +pub fn nodeName(node: *Node) [*c]const u8 { + var s: usize = undefined; + return c.lxb_dom_node_name(node, &s); +} + +pub fn nodeType(node: *Node) NodeType { + return @intToEnum(NodeType, node.*.type); +} + +pub fn nodeWalk(node: *Node, comptime walker: nodeWalker) !void { + c.lxb_dom_node_simple_walk(node, walker, null); +} + +// Element + +pub const Element = c.lxb_dom_element_t; + +pub fn elementNode(element: *Element) *Node { + return c.lxb_dom_interface_node(element); +} + +pub fn elementLocalName(element: *Element) []const u8 { + const local_name = c.lxb_dom_element_local_name(element, null); + return std.mem.sliceTo(local_name, 0); +} + +pub fn elementsByAttr( + element: *Element, + collection: *Collection, + attr: []const u8, + value: []const u8, + case_sensitve: bool, +) !void { + const status = c.lxb_dom_elements_by_attr( + element, + collection, + attr.ptr, + attr.len, + value.ptr, + value.len, + case_sensitve, + ); + if (status != 0) { + return error.ElementsByAttr; + } +} + +// DocumentHTML + +pub const DocumentHTML = c.lxb_html_document_t; + +pub fn documentHTMLInit() *DocumentHTML { + return c.lxb_html_document_create(); +} + +pub fn documentHTMLDeinit(document_html: *DocumentHTML) void { + _ = c.lxb_html_document_destroy(document_html); +} + +pub fn documentHTMLParse(document_html: *DocumentHTML, html: []const u8) !void { + const status = c.lxb_html_document_parse(document_html, html.ptr, html.len - 1); + if (status != 0) { + return error.DocumentHTMLParse; + } +} + +pub fn documentHTMLToNode(document_html: *DocumentHTML) *Node { + return c.lxb_dom_interface_node(document_html); +} + +pub fn documentHTMLToDocument(document_html: *DocumentHTML) *Document { + return &document_html.dom_document; +} + +pub fn documentHTMLBody(document_html: *DocumentHTML) *Element { + return c.lxb_dom_interface_element(document_html.body); +} + +// Document + +pub const Document = c.lxb_dom_document_t; + +// Collection + +pub const Collection = c.lxb_dom_collection_t; + +pub fn collectionInit(document: *Document, size: usize) *Collection { + return c.lxb_dom_collection_make(document, size); +} + +pub fn collectionDeinit(collection: *Collection) void { + _ = c.lxb_dom_collection_destroy(collection, true); +} + +pub fn collectionElement(collection: *Collection, index: usize) *Element { + return c.lxb_dom_collection_element(collection, index); +} + +// Base + +pub const Action = c.lexbor_action_t; + +// TODO: use enum? +pub const ActionStop = c.LEXBOR_ACTION_STOP; +pub const ActionNext = c.LEXBOR_ACTION_NEXT; +pub const ActionOk = c.LEXBOR_ACTION_OK; + +// Playground +// ---------- + +fn serialize_callback(_: [*c]const u8, _: usize, _: ?*anyopaque) callconv(.C) c_uint { + return 0; +} + +fn walker_play(nn: ?*c.lxb_dom_node_t, _: ?*anyopaque) callconv(.C) c.lexbor_action_t { + if (nn == null) { + return c.LEXBOR_ACTION_STOP; + } + const n = nn.?; + + var s: usize = undefined; + const name = c.lxb_dom_node_name(n, &s); + + std.debug.print("type: {d}, name: {s}\n", .{ n.*.type, name }); + if (n.*.local_name == c.LXB_TAG_A) { + const element = c.lxb_dom_interface_element(n); + const attr = element.*.first_attr; + std.debug.print("link, attr: {any}\n", .{attr.*.upper_name}); + } + return c.LEXBOR_ACTION_OK; +} + +pub fn parse_document() void { + const html = "
OK

blah-blah-blah

"; + const html_len = html.len - 1; + + // parse + const doc = c.lxb_html_document_create(); + const status_parse = c.lxb_html_document_parse(doc, html, html_len); + std.debug.print("status parse: {any}\n", .{status_parse}); + + // tree + const document_node = c.lxb_dom_interface_node(doc); + std.debug.print("document node is empty: {any}\n", .{c.lxb_dom_node_is_empty(document_node)}); + std.debug.print("document node type: {any}\n", .{document_node.*.type}); + std.debug.print("document node name: {any}\n", .{document_node.*.local_name}); + + c.lxb_dom_node_simple_walk(document_node, walker_play, null); + + const first_child = c.lxb_dom_node_last_child(document_node); + if (first_child == null) { + std.debug.print("hummm is null\n", .{}); + } + std.debug.print("first child type: {any}\n", .{first_child.*.type}); + std.debug.print("first child name: {any}\n", .{first_child.*.local_name}); + + const tt = c.lxb_dom_node_first_child(first_child); + std.debug.print("tt type: {any}\n", .{tt.*.type}); + std.debug.print("tt name: {any}\n", .{tt.*.local_name}); + std.debug.print("{any}\n", .{c.LXB_DOM_NODE_TYPE_TEXT}); + + var s: usize = undefined; + const tt_name = c.lxb_dom_node_name(tt, &s); + std.debug.print("tt name: {s}\n", .{tt_name}); + + const nn = tt.*.first_child; + if (nn == null) { + std.debug.print("is null\n", .{}); + } + + // text + var text_len: usize = undefined; + var text = c.lxb_dom_node_text_content(tt, &text_len); + std.debug.print("size: {d}\n", .{text_len}); + std.debug.print("text: {s}\n", .{text}); + + // serialize + const status_serialize = c.lxb_html_serialize_pretty_tree_cb( + document_node, + c.LXB_HTML_SERIALIZE_OPT_UNDEF, + 0, + serialize_callback, + null, + ); + std.debug.print("status serialize: {any}\n", .{status_serialize}); + + // destroy + _ = c.lxb_html_document_destroy(doc); + // _ = c.lxb_dom_document_destroy_text(first_child.*.owner_document, &text); + // _ = c.lxb_dom_document_destroy_text(c.lxb_dom_interface_document(document), text); + std.debug.print("text2: {s}\n", .{text}); // should not work +} diff --git a/src/run_tests.zig b/src/run_tests.zig new file mode 100644 index 00000000..6133ab29 --- /dev/null +++ b/src/run_tests.zig @@ -0,0 +1,46 @@ +const std = @import("std"); + +const jsruntime = @import("jsruntime"); + +const DOM = @import("dom.zig"); +const document = @import("dom/document.zig"); +const element = @import("dom/element.zig"); + +const html = @import("html.zig").html; + +var doc: DOM.HTMLDocument = undefined; + +fn testsExecFn( + _: std.mem.Allocator, + js_env: *jsruntime.Env, + comptime apis: []jsruntime.API, +) !void { + + // start JS env + js_env.start(); + defer js_env.stop(); + + // add document object + try js_env.addObject(apis, doc, "document"); + + // run tests + try document.testExecFn(js_env, apis); +} + +test { + // generate APIs + const apis = jsruntime.compile(DOM.Interfaces); + + // document + doc = DOM.HTMLDocument.init(); + defer doc.deinit(); + try doc.parse(html); + + // create JS vm + const vm = jsruntime.VM.init(); + defer vm.deinit(); + + var alloc = jsruntime.bench_allocator(std.testing.allocator); + + try jsruntime.loadEnv(alloc.allocator(), false, testsExecFn, apis); +}