Initial commit

Signed-off-by: Francis Bouvier <francis.bouvier@gmail.com>
This commit is contained in:
Francis Bouvier
2023-02-07 16:22:01 +01:00
commit 497a1119f8
12 changed files with 691 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
zig-cache
zig-out
vendor

77
build.zig Normal file
View File

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

24
src/dom.zig Normal file
View File

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

122
src/dom/document.zig Normal file
View File

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

54
src/dom/element.zig Normal file
View File

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

13
src/dom/event_target.zig Normal file
View File

@@ -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 .{};
}
};

36
src/dom/node.zig Normal file
View File

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

6
src/html.zig Normal file
View File

@@ -0,0 +1,6 @@
pub const html: []const u8 =
\\<main id='content'>
\\<a href='foo'>OK</a>
\\<p>blah-blah-blah</p>
\\</main>
;

27
src/main.zig Normal file
View File

@@ -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 = "<div><a href='foo'>OK</a><p>blah-blah-blah</p></div>";
// try doc.parse(html);
// try doc.proto.make_tree();
std.debug.print("ok\n", .{});
}

51
src/main_shell.zig Normal file
View File

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

232
src/parser.zig Normal file
View File

@@ -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 = "<div><a href='foo'>OK</a><p>blah-blah-blah</p></div>";
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
}

46
src/run_tests.zig Normal file
View File

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