├── .gitignore
├── test.html
├── src
├── user_context.zig
├── html_test.zig
├── cdp
│ ├── css.zig
│ ├── log.zig
│ ├── fetch.zig
│ ├── inspector.zig
│ ├── security.zig
│ ├── performance.zig
│ ├── network.zig
│ ├── emulation.zig
│ ├── browser.zig
│ ├── runtime.zig
│ └── testing.zig
├── dom
│ ├── cdata_section.zig
│ ├── dom.zig
│ ├── document_type.zig
│ ├── document_fragment.zig
│ ├── comment.zig
│ ├── css.zig
│ ├── processing_instruction.zig
│ ├── text.zig
│ ├── namednodemap.zig
│ ├── walker.zig
│ ├── attribute.zig
│ ├── implementation.zig
│ ├── token_list.zig
│ ├── nodelist.zig
│ └── character_data.zig
├── html
│ ├── html.zig
│ ├── navigator.zig
│ ├── location.zig
│ ├── history.zig
│ └── window.zig
├── iterator
│ └── iterator.zig
├── apiweb.zig
├── polyfill
│ ├── fetch.zig
│ └── polyfill.zig
├── wpt
│ ├── fileloader.zig
│ └── run.zig
├── xmlserializer
│ └── xmlserializer.zig
├── mimalloc
│ └── mimalloc.zig
├── main_shell.zig
├── xhr
│ ├── progress_event.zig
│ └── event_target.zig
├── browser
│ ├── loader.zig
│ └── dump.zig
├── css
│ ├── libdom.zig
│ ├── README.md
│ └── css.zig
├── str
│ └── parser.zig
├── id.zig
└── generate.zig
├── CONTRIBUTING.md
├── LICENSING.md
├── tests
└── html
│ └── bug-html-parsing-4.html
├── .github
├── workflows
│ ├── cla.yml
│ ├── zig-fmt.yml
│ ├── build.yml
│ ├── wpt.yml
│ ├── e2e-test.yml
│ └── zig-test.yml
└── actions
│ └── install
│ └── action.yml
├── .gitmodules
├── Dockerfile
├── CLA.md
└── Makefile
/.gitignore:
--------------------------------------------------------------------------------
1 | zig-cache
2 | /.zig-cache/
3 | zig-out
4 | /vendor/netsurf/out
5 | /vendor/libiconv/
6 |
--------------------------------------------------------------------------------
/test.html:
--------------------------------------------------------------------------------
1 |
2 |
OK
3 |
4 |
5 |
6 |
And
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/user_context.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 | const parser = @import("netsurf");
3 | const Client = @import("asyncio").Client;
4 |
5 | pub const UserContext = struct {
6 | document: *parser.DocumentHTML,
7 | httpClient: *Client,
8 | };
9 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Lightpanda accepts pull requests through GitHub.
4 |
5 | You have to sign our [CLA](CLA.md) during your first pull request process
6 | otherwise we're not able to accept your contributions.
7 |
8 | The process signature uses the [CLA assistant
9 | lite](https://github.com/marketplace/actions/cla-assistant-lite). You can see
10 | an example of the process in [#303](https://github.com/lightpanda-io/browser/pull/303).
11 |
--------------------------------------------------------------------------------
/LICENSING.md:
--------------------------------------------------------------------------------
1 | # Licensing
2 |
3 | License names used in this document are as per [SPDX License
4 | List](https://spdx.org/licenses/).
5 |
6 | The default license for this project is [AGPL-3.0-only](LICENSE).
7 |
8 | ## MIT
9 |
10 | The following files are licensed under MIT:
11 |
12 | ```
13 | src/http/Client.zig
14 | src/polyfill/fetch.js
15 | ```
16 |
17 | The following directories and their subdirectories are licensed under their
18 | original upstream licenses:
19 |
20 | ```
21 | vendor/
22 | tests/wpt/
23 | ```
24 |
--------------------------------------------------------------------------------
/tests/html/bug-html-parsing-4.html:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/src/html_test.zig:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
2 | //
3 | // Francis Bouvier
4 | // Pierre Tachoire
5 | //
6 | // This program is free software: you can redistribute it and/or modify
7 | // it under the terms of the GNU Affero General Public License as
8 | // published by the Free Software Foundation, either version 3 of the
9 | // License, or (at your option) any later version.
10 | //
11 | // This program is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | // GNU Affero General Public License for more details.
15 | //
16 | // You should have received a copy of the GNU Affero General Public License
17 | // along with this program. If not, see .
18 |
19 | pub const html: []const u8 =
20 | \\
21 | \\OK
22 | \\blah-blah-blah
23 | \\
24 | ;
25 |
--------------------------------------------------------------------------------
/.github/workflows/cla.yml:
--------------------------------------------------------------------------------
1 | name: "CLA Assistant"
2 | on:
3 | issue_comment:
4 | types: [created]
5 | pull_request_target:
6 | types: [opened,closed,synchronize]
7 |
8 | permissions:
9 | actions: write
10 | contents: read
11 | pull-requests: write
12 | statuses: write
13 |
14 | jobs:
15 | CLAAssistant:
16 | runs-on: ubuntu-latest
17 | steps:
18 | - name: "CLA Assistant"
19 | if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
20 | uses: contributor-assistant/github-action@v2.6.1
21 | env:
22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
23 | PERSONAL_ACCESS_TOKEN: ${{ secrets.CLA_GH_PAT }}
24 | with:
25 | path-to-signatures: 'signatures/browser/version1/cla.json'
26 | path-to-document: 'https://github.com/lightpanda-io/browser/blob/main/CLA.md'
27 | # branch should not be protected
28 | branch: 'main'
29 | allowlist: krichprollsch,francisbouvier,katie-lpd
30 |
31 | remote-organization-name: lightpanda-io
32 | remote-repository-name: cla
33 |
--------------------------------------------------------------------------------
/src/cdp/css.zig:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
2 | //
3 | // Francis Bouvier
4 | // Pierre Tachoire
5 | //
6 | // This program is free software: you can redistribute it and/or modify
7 | // it under the terms of the GNU Affero General Public License as
8 | // published by the Free Software Foundation, either version 3 of the
9 | // License, or (at your option) any later version.
10 | //
11 | // This program is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | // GNU Affero General Public License for more details.
15 | //
16 | // You should have received a copy of the GNU Affero General Public License
17 | // along with this program. If not, see .
18 |
19 | const std = @import("std");
20 | const cdp = @import("cdp.zig");
21 |
22 | pub fn processMessage(cmd: anytype) !void {
23 | const action = std.meta.stringToEnum(enum {
24 | enable,
25 | }, cmd.action) orelse return error.UnknownMethod;
26 |
27 | switch (action) {
28 | .enable => return cmd.sendResult(null, .{}),
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/cdp/log.zig:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
2 | //
3 | // Francis Bouvier
4 | // Pierre Tachoire
5 | //
6 | // This program is free software: you can redistribute it and/or modify
7 | // it under the terms of the GNU Affero General Public License as
8 | // published by the Free Software Foundation, either version 3 of the
9 | // License, or (at your option) any later version.
10 | //
11 | // This program is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | // GNU Affero General Public License for more details.
15 | //
16 | // You should have received a copy of the GNU Affero General Public License
17 | // along with this program. If not, see .
18 |
19 | const std = @import("std");
20 | const cdp = @import("cdp.zig");
21 |
22 | pub fn processMessage(cmd: anytype) !void {
23 | const action = std.meta.stringToEnum(enum {
24 | enable,
25 | }, cmd.action) orelse return error.UnknownMethod;
26 |
27 | switch (action) {
28 | .enable => return cmd.sendResult(null, .{}),
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/dom/cdata_section.zig:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
2 | //
3 | // Francis Bouvier
4 | // Pierre Tachoire
5 | //
6 | // This program is free software: you can redistribute it and/or modify
7 | // it under the terms of the GNU Affero General Public License as
8 | // published by the Free Software Foundation, either version 3 of the
9 | // License, or (at your option) any later version.
10 | //
11 | // This program is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | // GNU Affero General Public License for more details.
15 | //
16 | // You should have received a copy of the GNU Affero General Public License
17 | // along with this program. If not, see .
18 |
19 | const std = @import("std");
20 |
21 | const parser = @import("netsurf");
22 |
23 | const Text = @import("text.zig").Text;
24 |
25 | // https://dom.spec.whatwg.org/#cdatasection
26 | pub const CDATASection = struct {
27 | pub const Self = parser.CDATASection;
28 | pub const prototype = *Text;
29 | pub const mem_guarantied = true;
30 | };
31 |
--------------------------------------------------------------------------------
/src/cdp/fetch.zig:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
2 | //
3 | // Francis Bouvier
4 | // Pierre Tachoire
5 | //
6 | // This program is free software: you can redistribute it and/or modify
7 | // it under the terms of the GNU Affero General Public License as
8 | // published by the Free Software Foundation, either version 3 of the
9 | // License, or (at your option) any later version.
10 | //
11 | // This program is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | // GNU Affero General Public License for more details.
15 | //
16 | // You should have received a copy of the GNU Affero General Public License
17 | // along with this program. If not, see .
18 |
19 | const std = @import("std");
20 | const cdp = @import("cdp.zig");
21 |
22 | pub fn processMessage(cmd: anytype) !void {
23 | const action = std.meta.stringToEnum(enum {
24 | disable,
25 | }, cmd.action) orelse return error.UnknownMethod;
26 |
27 | switch (action) {
28 | .disable => return cmd.sendResult(null, .{}),
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/cdp/inspector.zig:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
2 | //
3 | // Francis Bouvier
4 | // Pierre Tachoire
5 | //
6 | // This program is free software: you can redistribute it and/or modify
7 | // it under the terms of the GNU Affero General Public License as
8 | // published by the Free Software Foundation, either version 3 of the
9 | // License, or (at your option) any later version.
10 | //
11 | // This program is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | // GNU Affero General Public License for more details.
15 | //
16 | // You should have received a copy of the GNU Affero General Public License
17 | // along with this program. If not, see .
18 |
19 | const std = @import("std");
20 | const cdp = @import("cdp.zig");
21 |
22 | pub fn processMessage(cmd: anytype) !void {
23 | const action = std.meta.stringToEnum(enum {
24 | enable,
25 | }, cmd.action) orelse return error.UnknownMethod;
26 |
27 | switch (action) {
28 | .enable => return cmd.sendResult(null, .{}),
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/cdp/security.zig:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
2 | //
3 | // Francis Bouvier
4 | // Pierre Tachoire
5 | //
6 | // This program is free software: you can redistribute it and/or modify
7 | // it under the terms of the GNU Affero General Public License as
8 | // published by the Free Software Foundation, either version 3 of the
9 | // License, or (at your option) any later version.
10 | //
11 | // This program is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | // GNU Affero General Public License for more details.
15 | //
16 | // You should have received a copy of the GNU Affero General Public License
17 | // along with this program. If not, see .
18 |
19 | const std = @import("std");
20 | const cdp = @import("cdp.zig");
21 |
22 | pub fn processMessage(cmd: anytype) !void {
23 | const action = std.meta.stringToEnum(enum {
24 | enable,
25 | }, cmd.action) orelse return error.UnknownMethod;
26 |
27 | switch (action) {
28 | .enable => return cmd.sendResult(null, .{}),
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/cdp/performance.zig:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
2 | //
3 | // Francis Bouvier
4 | // Pierre Tachoire
5 | //
6 | // This program is free software: you can redistribute it and/or modify
7 | // it under the terms of the GNU Affero General Public License as
8 | // published by the Free Software Foundation, either version 3 of the
9 | // License, or (at your option) any later version.
10 | //
11 | // This program is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | // GNU Affero General Public License for more details.
15 | //
16 | // You should have received a copy of the GNU Affero General Public License
17 | // along with this program. If not, see .
18 |
19 | const std = @import("std");
20 | const cdp = @import("cdp.zig");
21 | const asUint = @import("../str/parser.zig").asUint;
22 |
23 | pub fn processMessage(cmd: anytype) !void {
24 | const action = std.meta.stringToEnum(enum {
25 | enable,
26 | }, cmd.action) orelse return error.UnknownMethod;
27 |
28 | switch (action) {
29 | .enable => return cmd.sendResult(null, .{}),
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "vendor/zig-js-runtime"]
2 | path = vendor/zig-js-runtime
3 | url = https://github.com/lightpanda-io/zig-js-runtime.git/
4 | [submodule "vendor/netsurf/libwapcaplet"]
5 | path = vendor/netsurf/libwapcaplet
6 | url = https://github.com/lightpanda-io/libwapcaplet.git/
7 | [submodule "vendor/netsurf/libparserutils"]
8 | path = vendor/netsurf/libparserutils
9 | url = https://github.com/lightpanda-io/libparserutils.git/
10 | [submodule "vendor/netsurf/libdom"]
11 | path = vendor/netsurf/libdom
12 | url = https://github.com/lightpanda-io/libdom.git/
13 | [submodule "vendor/netsurf/share/netsurf-buildsystem"]
14 | path = vendor/netsurf/share/netsurf-buildsystem
15 | url = https://source.netsurf-browser.org/buildsystem.git
16 | [submodule "vendor/netsurf/libhubbub"]
17 | path = vendor/netsurf/libhubbub
18 | url = https://github.com/lightpanda-io/libhubbub.git/
19 | [submodule "tests/wpt"]
20 | path = tests/wpt
21 | url = https://github.com/lightpanda-io/wpt
22 | [submodule "vendor/mimalloc"]
23 | path = vendor/mimalloc
24 | url = https://github.com/microsoft/mimalloc.git/
25 | [submodule "vendor/tls.zig"]
26 | path = vendor/tls.zig
27 | url = https://github.com/ianic/tls.zig.git/
28 | [submodule "vendor/zig-async-io"]
29 | path = vendor/zig-async-io
30 | url = https://github.com/lightpanda-io/zig-async-io.git/
31 |
--------------------------------------------------------------------------------
/src/cdp/network.zig:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
2 | //
3 | // Francis Bouvier
4 | // Pierre Tachoire
5 | //
6 | // This program is free software: you can redistribute it and/or modify
7 | // it under the terms of the GNU Affero General Public License as
8 | // published by the Free Software Foundation, either version 3 of the
9 | // License, or (at your option) any later version.
10 | //
11 | // This program is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | // GNU Affero General Public License for more details.
15 | //
16 | // You should have received a copy of the GNU Affero General Public License
17 | // along with this program. If not, see .
18 |
19 | const std = @import("std");
20 | const cdp = @import("cdp.zig");
21 |
22 | pub fn processMessage(cmd: anytype) !void {
23 | const action = std.meta.stringToEnum(enum {
24 | enable,
25 | setCacheDisabled,
26 | }, cmd.action) orelse return error.UnknownMethod;
27 |
28 | switch (action) {
29 | .enable => return cmd.sendResult(null, .{}),
30 | .setCacheDisabled => return cmd.sendResult(null, .{}),
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/html/html.zig:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
2 | //
3 | // Francis Bouvier
4 | // Pierre Tachoire
5 | //
6 | // This program is free software: you can redistribute it and/or modify
7 | // it under the terms of the GNU Affero General Public License as
8 | // published by the Free Software Foundation, either version 3 of the
9 | // License, or (at your option) any later version.
10 | //
11 | // This program is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | // GNU Affero General Public License for more details.
15 | //
16 | // You should have received a copy of the GNU Affero General Public License
17 | // along with this program. If not, see .
18 |
19 | const generate = @import("../generate.zig");
20 |
21 | const HTMLDocument = @import("document.zig").HTMLDocument;
22 | const HTMLElem = @import("elements.zig");
23 | const Window = @import("window.zig").Window;
24 | const Navigator = @import("navigator.zig").Navigator;
25 | const History = @import("history.zig").History;
26 | const Location = @import("location.zig").Location;
27 |
28 | pub const Interfaces = .{
29 | HTMLDocument,
30 | HTMLElem.HTMLElement,
31 | HTMLElem.HTMLMediaElement,
32 | HTMLElem.Interfaces,
33 | Window,
34 | Navigator,
35 | History,
36 | Location,
37 | };
38 |
--------------------------------------------------------------------------------
/src/iterator/iterator.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 |
3 | pub const Interfaces = .{
4 | U32Iterator,
5 | };
6 |
7 | pub const U32Iterator = struct {
8 | pub const mem_guarantied = true;
9 |
10 | length: u32,
11 | index: u32 = 0,
12 |
13 | pub const Return = struct {
14 | value: u32,
15 | done: bool,
16 | };
17 |
18 | pub fn _next(self: *U32Iterator) Return {
19 | const i = self.index;
20 | if (i >= self.length) {
21 | return .{
22 | .value = 0,
23 | .done = true,
24 | };
25 | }
26 |
27 | self.index = i + 1;
28 | return .{
29 | .value = i,
30 | .done = false,
31 | };
32 | }
33 | };
34 |
35 | const testing = std.testing;
36 | test "U32Iterator" {
37 | const Return = U32Iterator.Return;
38 |
39 | {
40 | var it = U32Iterator{ .length = 0 };
41 | try testing.expectEqual(Return{ .value = 0, .done = true }, it._next());
42 | try testing.expectEqual(Return{ .value = 0, .done = true }, it._next());
43 | }
44 |
45 | {
46 | var it = U32Iterator{ .length = 3 };
47 | try testing.expectEqual(Return{ .value = 0, .done = false }, it._next());
48 | try testing.expectEqual(Return{ .value = 1, .done = false }, it._next());
49 | try testing.expectEqual(Return{ .value = 2, .done = false }, it._next());
50 | try testing.expectEqual(Return{ .value = 0, .done = true }, it._next());
51 | try testing.expectEqual(Return{ .value = 0, .done = true }, it._next());
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/dom/dom.zig:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
2 | //
3 | // Francis Bouvier
4 | // Pierre Tachoire
5 | //
6 | // This program is free software: you can redistribute it and/or modify
7 | // it under the terms of the GNU Affero General Public License as
8 | // published by the Free Software Foundation, either version 3 of the
9 | // License, or (at your option) any later version.
10 | //
11 | // This program is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | // GNU Affero General Public License for more details.
15 | //
16 | // You should have received a copy of the GNU Affero General Public License
17 | // along with this program. If not, see .
18 |
19 | const DOMException = @import("exceptions.zig").DOMException;
20 | const EventTarget = @import("event_target.zig").EventTarget;
21 | const DOMImplementation = @import("implementation.zig").DOMImplementation;
22 | const NamedNodeMap = @import("namednodemap.zig").NamedNodeMap;
23 | const DOMTokenList = @import("token_list.zig").DOMTokenList;
24 | const NodeList = @import("nodelist.zig");
25 | const Nod = @import("node.zig");
26 | const MutationObserver = @import("mutation_observer.zig");
27 |
28 | pub const Interfaces = .{
29 | DOMException,
30 | EventTarget,
31 | DOMImplementation,
32 | NamedNodeMap,
33 | DOMTokenList,
34 | NodeList.Interfaces,
35 | Nod.Node,
36 | Nod.Interfaces,
37 | MutationObserver.Interfaces,
38 | };
39 |
--------------------------------------------------------------------------------
/src/dom/document_type.zig:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
2 | //
3 | // Francis Bouvier
4 | // Pierre Tachoire
5 | //
6 | // This program is free software: you can redistribute it and/or modify
7 | // it under the terms of the GNU Affero General Public License as
8 | // published by the Free Software Foundation, either version 3 of the
9 | // License, or (at your option) any later version.
10 | //
11 | // This program is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | // GNU Affero General Public License for more details.
15 | //
16 | // You should have received a copy of the GNU Affero General Public License
17 | // along with this program. If not, see .
18 |
19 | const std = @import("std");
20 |
21 | const parser = @import("netsurf");
22 |
23 | const Node = @import("node.zig").Node;
24 |
25 | // WEB IDL https://dom.spec.whatwg.org/#documenttype
26 | pub const DocumentType = struct {
27 | pub const Self = parser.DocumentType;
28 | pub const prototype = *Node;
29 | pub const mem_guarantied = true;
30 |
31 | pub fn get_name(self: *parser.DocumentType) ![]const u8 {
32 | return try parser.documentTypeGetName(self);
33 | }
34 |
35 | pub fn get_publicId(self: *parser.DocumentType) ![]const u8 {
36 | return try parser.documentTypeGetPublicId(self);
37 | }
38 |
39 | pub fn get_systemId(self: *parser.DocumentType) ![]const u8 {
40 | return try parser.documentTypeGetSystemId(self);
41 | }
42 | };
43 |
--------------------------------------------------------------------------------
/src/apiweb.zig:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
2 | //
3 | // Francis Bouvier
4 | // Pierre Tachoire
5 | //
6 | // This program is free software: you can redistribute it and/or modify
7 | // it under the terms of the GNU Affero General Public License as
8 | // published by the Free Software Foundation, either version 3 of the
9 | // License, or (at your option) any later version.
10 | //
11 | // This program is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | // GNU Affero General Public License for more details.
15 | //
16 | // You should have received a copy of the GNU Affero General Public License
17 | // along with this program. If not, see .
18 |
19 | const generate = @import("generate.zig");
20 |
21 | const Console = @import("jsruntime").Console;
22 |
23 | const DOM = @import("dom/dom.zig");
24 | const HTML = @import("html/html.zig");
25 | const Events = @import("events/event.zig");
26 | const XHR = @import("xhr/xhr.zig");
27 | const Storage = @import("storage/storage.zig");
28 | const URL = @import("url/url.zig");
29 | const Iterators = @import("iterator/iterator.zig");
30 | const XMLSerializer = @import("xmlserializer/xmlserializer.zig");
31 |
32 | pub const HTMLDocument = @import("html/document.zig").HTMLDocument;
33 |
34 | // Interfaces
35 | pub const Interfaces = generate.Tuple(.{
36 | Console,
37 | DOM.Interfaces,
38 | Events.Interfaces,
39 | HTML.Interfaces,
40 | XHR.Interfaces,
41 | Storage.Interfaces,
42 | URL.Interfaces,
43 | Iterators.Interfaces,
44 | XMLSerializer.Interfaces,
45 | }){};
46 |
47 | pub const UserContext = @import("user_context.zig").UserContext;
48 |
--------------------------------------------------------------------------------
/src/polyfill/fetch.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 | const jsruntime = @import("jsruntime");
3 | const Case = jsruntime.test_utils.Case;
4 | const checkCases = jsruntime.test_utils.checkCases;
5 |
6 | // fetch.js code comes from
7 | // https://github.com/JakeChampion/fetch/blob/main/fetch.js
8 | //
9 | // The original code source is available in MIT license.
10 | //
11 | // The script comes from the built version from npm.
12 | // You can get the package with the command:
13 | //
14 | // wget $(npm view whatwg-fetch dist.tarball)
15 | //
16 | // The source is the content of `package/dist/fetch.umd.js` file.
17 | pub const source = @embedFile("fetch.js");
18 |
19 | pub fn testExecFn(
20 | alloc: std.mem.Allocator,
21 | js_env: *jsruntime.Env,
22 | ) anyerror!void {
23 | try @import("polyfill.zig").load(alloc, js_env.*);
24 |
25 | var fetch = [_]Case{
26 | .{
27 | .src =
28 | \\var ok = false;
29 | \\const request = new Request("https://httpbin.io/json");
30 | \\fetch(request)
31 | \\ .then((response) => { ok = response.ok; });
32 | \\false;
33 | ,
34 | .ex = "false",
35 | },
36 | // all events have been resolved.
37 | .{ .src = "ok", .ex = "true" },
38 | };
39 | try checkCases(js_env, &fetch);
40 |
41 | var fetch2 = [_]Case{
42 | .{
43 | .src =
44 | \\var ok2 = false;
45 | \\const request2 = new Request("https://httpbin.io/json");
46 | \\(async function () { resp = await fetch(request2); ok2 = resp.ok; }());
47 | \\false;
48 | ,
49 | .ex = "false",
50 | },
51 | // all events have been resolved.
52 | .{ .src = "ok2", .ex = "true" },
53 | };
54 | try checkCases(js_env, &fetch2);
55 | }
56 |
--------------------------------------------------------------------------------
/src/polyfill/polyfill.zig:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
2 | //
3 | // Francis Bouvier
4 | // Pierre Tachoire
5 | //
6 | // This program is free software: you can redistribute it and/or modify
7 | // it under the terms of the GNU Affero General Public License as
8 | // published by the Free Software Foundation, either version 3 of the
9 | // License, or (at your option) any later version.
10 | //
11 | // This program is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | // GNU Affero General Public License for more details.
15 | //
16 | // You should have received a copy of the GNU Affero General Public License
17 | // along with this program. If not, see .
18 |
19 | const std = @import("std");
20 | const builtin = @import("builtin");
21 |
22 | const jsruntime = @import("jsruntime");
23 | const Env = jsruntime.Env;
24 |
25 | const fetch = @import("fetch.zig").fetch_polyfill;
26 |
27 | const log = std.log.scoped(.polyfill);
28 |
29 | const modules = [_]struct {
30 | name: []const u8,
31 | source: []const u8,
32 | }{
33 | .{ .name = "polyfill-fetch", .source = @import("fetch.zig").source },
34 | };
35 |
36 | pub fn load(alloc: std.mem.Allocator, env: Env) !void {
37 | var try_catch: jsruntime.TryCatch = undefined;
38 | try_catch.init(env);
39 | defer try_catch.deinit();
40 |
41 | for (modules) |m| {
42 | const res = env.exec(m.source, m.name) catch {
43 | if (try try_catch.err(alloc, env)) |msg| {
44 | defer alloc.free(msg);
45 | log.err("load {s}: {s}", .{ m.name, msg });
46 | }
47 | return;
48 | };
49 |
50 | if (builtin.mode == .Debug) {
51 | const msg = try res.toString(alloc, env);
52 | defer alloc.free(msg);
53 | log.debug("load {s}: {s}", .{ m.name, msg });
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/.github/workflows/zig-fmt.yml:
--------------------------------------------------------------------------------
1 | name: zig-fmt
2 |
3 | env:
4 | ZIG_VERSION: 0.13.0
5 |
6 | on:
7 | pull_request:
8 |
9 | # By default GH trigger on types opened, synchronize and reopened.
10 | # see https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request
11 | # Since we skip the job when the PR is in draft state, we want to force CI
12 | # running when the PR is marked ready_for_review w/o other change.
13 | # see https://github.com/orgs/community/discussions/25722#discussioncomment-3248917
14 | types: [opened, synchronize, reopened, ready_for_review]
15 |
16 | paths:
17 | - ".github/**"
18 | - "build.zig"
19 | - "src/**/*.zig"
20 | - "src/*.zig"
21 | # Allows you to run this workflow manually from the Actions tab
22 | workflow_dispatch:
23 |
24 | jobs:
25 | zig-fmt:
26 | name: zig fmt
27 |
28 | # Don't run the CI with draft PR.
29 | if: github.event.pull_request.draft == false
30 |
31 | runs-on: ubuntu-latest
32 |
33 | steps:
34 | - uses: mlugg/setup-zig@v1
35 | with:
36 | version: ${{ env.ZIG_VERSION }}
37 |
38 | - uses: actions/checkout@v4
39 | with:
40 | fetch-depth: 0
41 |
42 | - name: Run zig fmt
43 | id: fmt
44 | run: |
45 | zig fmt --check ./*.zig ./**/*.zig 2> zig-fmt.err > zig-fmt.err2 || echo "Failed"
46 | delimiter="$(openssl rand -hex 8)"
47 | echo "zig_fmt_errs<<${delimiter}" >> "${GITHUB_OUTPUT}"
48 |
49 | if [ -s zig-fmt.err ]; then
50 | echo "// The following errors occurred:" >> "${GITHUB_OUTPUT}"
51 | cat zig-fmt.err >> "${GITHUB_OUTPUT}"
52 | fi
53 |
54 | if [ -s zig-fmt.err2 ]; then
55 | echo "// The following files were not formatted:" >> "${GITHUB_OUTPUT}"
56 | cat zig-fmt.err2 >> "${GITHUB_OUTPUT}"
57 | fi
58 |
59 | echo "${delimiter}" >> "${GITHUB_OUTPUT}"
60 | - name: Fail the job
61 | if: steps.fmt.outputs.zig_fmt_errs != ''
62 | run: exit 1
63 |
--------------------------------------------------------------------------------
/src/dom/document_fragment.zig:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
2 | //
3 | // Francis Bouvier
4 | // Pierre Tachoire
5 | //
6 | // This program is free software: you can redistribute it and/or modify
7 | // it under the terms of the GNU Affero General Public License as
8 | // published by the Free Software Foundation, either version 3 of the
9 | // License, or (at your option) any later version.
10 | //
11 | // This program is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | // GNU Affero General Public License for more details.
15 | //
16 | // You should have received a copy of the GNU Affero General Public License
17 | // along with this program. If not, see .
18 |
19 | const std = @import("std");
20 |
21 | const parser = @import("netsurf");
22 |
23 | const jsruntime = @import("jsruntime");
24 | const Case = jsruntime.test_utils.Case;
25 | const checkCases = jsruntime.test_utils.checkCases;
26 |
27 | const Node = @import("node.zig").Node;
28 |
29 | const UserContext = @import("../user_context.zig").UserContext;
30 |
31 | // WEB IDL https://dom.spec.whatwg.org/#documentfragment
32 | pub const DocumentFragment = struct {
33 | pub const Self = parser.DocumentFragment;
34 | pub const prototype = *Node;
35 | pub const mem_guarantied = true;
36 |
37 | pub fn constructor(userctx: UserContext) !*parser.DocumentFragment {
38 | return parser.documentCreateDocumentFragment(
39 | parser.documentHTMLToDocument(userctx.document),
40 | );
41 | }
42 | };
43 |
44 | // Tests
45 | // -----
46 |
47 | pub fn testExecFn(
48 | _: std.mem.Allocator,
49 | js_env: *jsruntime.Env,
50 | ) anyerror!void {
51 | var constructor = [_]Case{
52 | .{ .src = "const dc = new DocumentFragment()", .ex = "undefined" },
53 | .{ .src = "dc.constructor.name", .ex = "DocumentFragment" },
54 | };
55 | try checkCases(js_env, &constructor);
56 | }
57 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: nightly build
2 |
3 | on:
4 | schedule:
5 | - cron: "2 2 * * *"
6 |
7 | # Allows you to run this workflow manually from the Actions tab
8 | workflow_dispatch:
9 |
10 | permissions:
11 | contents: write
12 |
13 | jobs:
14 | build-linux-x86_64:
15 | env:
16 | ARCH: x86_64
17 | OS: linux
18 |
19 | runs-on: ubuntu-22.04
20 |
21 | steps:
22 | - uses: actions/checkout@v4
23 | with:
24 | fetch-depth: 0
25 | # fetch submodules recusively, to get zig-js-runtime submodules also.
26 | submodules: recursive
27 |
28 | - uses: ./.github/actions/install
29 |
30 | - name: zig build
31 | run: zig build --release=safe -Doptimize=ReleaseSafe -Dengine=v8 -Dcpu=x86_64 -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
32 |
33 | - name: Rename binary
34 | run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
35 |
36 | - name: Upload the build
37 | uses: ncipollo/release-action@v1
38 | with:
39 | allowUpdates: true
40 | artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
41 | tag: nightly
42 |
43 | build-macos-aarch64:
44 | env:
45 | ARCH: aarch64
46 | OS: macos
47 |
48 | runs-on: macos-latest
49 |
50 | steps:
51 | - uses: actions/checkout@v4
52 | with:
53 | fetch-depth: 0
54 | # fetch submodules recusively, to get zig-js-runtime submodules also.
55 | submodules: recursive
56 |
57 | - uses: ./.github/actions/install
58 | with:
59 | os: ${{env.OS}}
60 | arch: ${{env.ARCH}}
61 |
62 | - name: zig build
63 | run: zig build --release=safe -Doptimize=ReleaseSafe -Dengine=v8 -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
64 |
65 | - name: Rename binary
66 | run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
67 |
68 | - name: Upload the build
69 | uses: ncipollo/release-action@v1
70 | with:
71 | allowUpdates: true
72 | artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
73 | tag: nightly
74 |
--------------------------------------------------------------------------------
/src/dom/comment.zig:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
2 | //
3 | // Francis Bouvier
4 | // Pierre Tachoire
5 | //
6 | // This program is free software: you can redistribute it and/or modify
7 | // it under the terms of the GNU Affero General Public License as
8 | // published by the Free Software Foundation, either version 3 of the
9 | // License, or (at your option) any later version.
10 | //
11 | // This program is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | // GNU Affero General Public License for more details.
15 | //
16 | // You should have received a copy of the GNU Affero General Public License
17 | // along with this program. If not, see .
18 | const std = @import("std");
19 |
20 | const parser = @import("netsurf");
21 |
22 | const jsruntime = @import("jsruntime");
23 | const Case = jsruntime.test_utils.Case;
24 | const checkCases = jsruntime.test_utils.checkCases;
25 |
26 | const CharacterData = @import("character_data.zig").CharacterData;
27 |
28 | const UserContext = @import("../user_context.zig").UserContext;
29 |
30 | // https://dom.spec.whatwg.org/#interface-comment
31 | pub const Comment = struct {
32 | pub const Self = parser.Comment;
33 | pub const prototype = *CharacterData;
34 | pub const mem_guarantied = true;
35 |
36 | pub fn constructor(userctx: UserContext, data: ?[]const u8) !*parser.Comment {
37 | return parser.documentCreateComment(
38 | parser.documentHTMLToDocument(userctx.document),
39 | data orelse "",
40 | );
41 | }
42 | };
43 |
44 | // Tests
45 | // -----
46 |
47 | pub fn testExecFn(
48 | _: std.mem.Allocator,
49 | js_env: *jsruntime.Env,
50 | ) anyerror!void {
51 | var constructor = [_]Case{
52 | .{ .src = "let comment = new Comment('foo')", .ex = "undefined" },
53 | .{ .src = "comment.data", .ex = "foo" },
54 |
55 | .{ .src = "let emptycomment = new Comment()", .ex = "undefined" },
56 | .{ .src = "emptycomment.data", .ex = "" },
57 | };
58 | try checkCases(js_env, &constructor);
59 | }
60 |
--------------------------------------------------------------------------------
/src/wpt/fileloader.zig:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
2 | //
3 | // Francis Bouvier
4 | // Pierre Tachoire
5 | //
6 | // This program is free software: you can redistribute it and/or modify
7 | // it under the terms of the GNU Affero General Public License as
8 | // published by the Free Software Foundation, either version 3 of the
9 | // License, or (at your option) any later version.
10 | //
11 | // This program is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | // GNU Affero General Public License for more details.
15 | //
16 | // You should have received a copy of the GNU Affero General Public License
17 | // along with this program. If not, see .
18 |
19 | const std = @import("std");
20 | const fspath = std.fs.path;
21 |
22 | // FileLoader loads files content from the filesystem.
23 | pub const FileLoader = struct {
24 | const FilesMap = std.StringHashMap([]const u8);
25 |
26 | files: FilesMap,
27 | path: []const u8,
28 | alloc: std.mem.Allocator,
29 |
30 | pub fn init(alloc: std.mem.Allocator, path: []const u8) FileLoader {
31 | const files = FilesMap.init(alloc);
32 |
33 | return FileLoader{
34 | .path = path,
35 | .alloc = alloc,
36 | .files = files,
37 | };
38 | }
39 | pub fn get(self: *FileLoader, name: []const u8) ![]const u8 {
40 | if (!self.files.contains(name)) {
41 | try self.load(name);
42 | }
43 | return self.files.get(name).?;
44 | }
45 | pub fn load(self: *FileLoader, name: []const u8) !void {
46 | const filename = try fspath.join(self.alloc, &.{ self.path, name });
47 | defer self.alloc.free(filename);
48 | var file = try std.fs.cwd().openFile(filename, .{});
49 | defer file.close();
50 |
51 | const file_size = try file.getEndPos();
52 | const content = try file.readToEndAlloc(self.alloc, file_size);
53 | const namedup = try self.alloc.dupe(u8, name);
54 | try self.files.put(namedup, content);
55 | }
56 | pub fn deinit(self: *FileLoader) void {
57 | var iter = self.files.iterator();
58 | while (iter.next()) |entry| {
59 | self.alloc.free(entry.key_ptr.*);
60 | self.alloc.free(entry.value_ptr.*);
61 | }
62 | self.files.deinit();
63 | }
64 | };
65 |
--------------------------------------------------------------------------------
/src/cdp/emulation.zig:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
2 | //
3 | // Francis Bouvier
4 | // Pierre Tachoire
5 | //
6 | // This program is free software: you can redistribute it and/or modify
7 | // it under the terms of the GNU Affero General Public License as
8 | // published by the Free Software Foundation, either version 3 of the
9 | // License, or (at your option) any later version.
10 | //
11 | // This program is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | // GNU Affero General Public License for more details.
15 | //
16 | // You should have received a copy of the GNU Affero General Public License
17 | // along with this program. If not, see .
18 |
19 | const std = @import("std");
20 | const cdp = @import("cdp.zig");
21 | const Runtime = @import("runtime.zig");
22 |
23 | pub fn processMessage(cmd: anytype) !void {
24 | const action = std.meta.stringToEnum(enum {
25 | setEmulatedMedia,
26 | setFocusEmulationEnabled,
27 | setDeviceMetricsOverride,
28 | setTouchEmulationEnabled,
29 | }, cmd.action) orelse return error.UnknownMethod;
30 |
31 | switch (action) {
32 | .setEmulatedMedia => return setEmulatedMedia(cmd),
33 | .setFocusEmulationEnabled => return setFocusEmulationEnabled(cmd),
34 | .setDeviceMetricsOverride => return setDeviceMetricsOverride(cmd),
35 | .setTouchEmulationEnabled => return setTouchEmulationEnabled(cmd),
36 | }
37 | }
38 |
39 | // TODO: noop method
40 | fn setEmulatedMedia(cmd: anytype) !void {
41 | // const input = (try const incoming.params(struct {
42 | // media: ?[]const u8 = null,
43 | // features: ?[]struct{
44 | // name: []const u8,
45 | // value: [] const u8
46 | // } = null,
47 | // })) orelse return error.InvalidParams;
48 |
49 | return cmd.sendResult(null, .{});
50 | }
51 |
52 | // TODO: noop method
53 | fn setFocusEmulationEnabled(cmd: anytype) !void {
54 | // const input = (try const incoming.params(struct {
55 | // enabled: bool,
56 | // })) orelse return error.InvalidParams;
57 | return cmd.sendResult(null, .{});
58 | }
59 |
60 | // TODO: noop method
61 | fn setDeviceMetricsOverride(cmd: anytype) !void {
62 | return cmd.sendResult(null, .{});
63 | }
64 |
65 | // TODO: noop method
66 | fn setTouchEmulationEnabled(cmd: anytype) !void {
67 | return cmd.sendResult(null, .{});
68 | }
69 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM ubuntu:22.04
2 |
3 | ARG ZIG=0.13.0
4 | ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U
5 | ARG OS=linux
6 | ARG ARCH=x86_64
7 | ARG V8=11.1.134
8 | ARG ZIG_V8=v0.1.11
9 |
10 | RUN apt-get update -yq && \
11 | apt-get install -yq xz-utils \
12 | python3 ca-certificates git \
13 | pkg-config libglib2.0-dev \
14 | gperf libexpat1-dev \
15 | cmake clang \
16 | curl git
17 |
18 | # install minisig
19 | RUN curl -L -O https://github.com/jedisct1/minisign/releases/download/0.11/minisign-0.11-linux.tar.gz && \
20 | tar xvzf minisign-0.11-linux.tar.gz
21 |
22 | # install zig
23 | RUN curl -O https://ziglang.org/download/${ZIG}/zig-linux-x86_64-${ZIG}.tar.xz && \
24 | curl -O https://ziglang.org/download/${ZIG}/zig-linux-x86_64-${ZIG}.tar.xz.minisig
25 |
26 | RUN minisign-linux/x86_64/minisign -Vm zig-linux-x86_64-${ZIG}.tar.xz -P ${ZIG_MINISIG}
27 |
28 | # clean minisg
29 | RUN rm -fr minisign-0.11-linux.tar.gz minisign-linux
30 |
31 | # install zig
32 | RUN tar xvf zig-linux-x86_64-${ZIG}.tar.xz && \
33 | mv zig-linux-x86_64-${ZIG} /usr/local/lib && \
34 | ln -s /usr/local/lib/zig-linux-x86_64-${ZIG}/zig /usr/local/bin/zig
35 |
36 | # clean up zig install
37 | RUN rm -fr zig-linux-x86_64-${ZIG}.tar.xz zig-linux-x86_64-${ZIG}.tar.xz.minisig
38 |
39 | # force use of http instead of ssh with github
40 | RUN cat < /root/.gitconfig
41 | [url "https://github.com/"]
42 | insteadOf="git@github.com:"
43 | EOF
44 |
45 | # clone lightpanda
46 | RUN git clone git@github.com:lightpanda-io/browser.git
47 |
48 | WORKDIR /browser
49 |
50 | # install deps
51 | RUN git submodule init && \
52 | git submodule update --recursive
53 |
54 | RUN cd vendor/zig-js-runtime && \
55 | git submodule init && \
56 | git submodule update --recursive
57 |
58 | RUN make install-libiconv && \
59 | make install-netsurf && \
60 | make install-mimalloc
61 |
62 | # download and install v8
63 | RUN curl -L -o libc_v8.a https://github.com/lightpanda-io/zig-v8-fork/releases/download/${ZIG_V8}/libc_v8_${V8}_${OS}_${ARCH}.a && \
64 | mkdir -p vendor/zig-js-runtime/vendor/v8/${ARCH}-${OS}/release && \
65 | mv libc_v8.a vendor/zig-js-runtime/vendor/v8/${ARCH}-${OS}/release/libc_v8.a
66 |
67 | # build release
68 | RUN make build
69 |
70 | FROM ubuntu:22.04
71 |
72 | # copy ca certificates
73 | COPY --from=0 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
74 |
75 | COPY --from=0 /browser/zig-out/bin/lightpanda /bin/lightpanda
76 |
77 | EXPOSE 9222/tcp
78 |
79 | CMD ["/bin/lightpanda", "serve", "--host", "0.0.0.0", "--port", "9222"]
80 |
--------------------------------------------------------------------------------
/src/dom/css.zig:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
2 | //
3 | // Francis Bouvier
4 | // Pierre Tachoire
5 | //
6 | // This program is free software: you can redistribute it and/or modify
7 | // it under the terms of the GNU Affero General Public License as
8 | // published by the Free Software Foundation, either version 3 of the
9 | // License, or (at your option) any later version.
10 | //
11 | // This program is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | // GNU Affero General Public License for more details.
15 | //
16 | // You should have received a copy of the GNU Affero General Public License
17 | // along with this program. If not, see .
18 |
19 | const std = @import("std");
20 |
21 | const parser = @import("netsurf");
22 |
23 | const css = @import("../css/css.zig");
24 | const Node = @import("../css/libdom.zig").Node;
25 | const NodeList = @import("nodelist.zig").NodeList;
26 |
27 | const MatchFirst = struct {
28 | n: ?*parser.Node = null,
29 |
30 | pub fn match(m: *MatchFirst, n: Node) !void {
31 | m.n = n.node;
32 | }
33 | };
34 |
35 | pub fn querySelector(alloc: std.mem.Allocator, n: *parser.Node, selector: []const u8) !?*parser.Node {
36 | const ps = try css.parse(alloc, selector, .{ .accept_pseudo_elts = true });
37 | defer ps.deinit(alloc);
38 |
39 | var m = MatchFirst{};
40 |
41 | _ = try css.matchFirst(ps, Node{ .node = n }, &m);
42 | return m.n;
43 | }
44 |
45 | const MatchAll = struct {
46 | alloc: std.mem.Allocator,
47 | nl: NodeList,
48 |
49 | fn init(alloc: std.mem.Allocator) MatchAll {
50 | return .{
51 | .alloc = alloc,
52 | .nl = NodeList.init(),
53 | };
54 | }
55 |
56 | fn deinit(m: *MatchAll) void {
57 | m.nl.deinit(m.alloc);
58 | }
59 |
60 | pub fn match(m: *MatchAll, n: Node) !void {
61 | try m.nl.append(m.alloc, n.node);
62 | }
63 |
64 | fn toOwnedList(m: *MatchAll) NodeList {
65 | defer m.nl = NodeList.init();
66 | return m.nl;
67 | }
68 | };
69 |
70 | pub fn querySelectorAll(alloc: std.mem.Allocator, n: *parser.Node, selector: []const u8) !NodeList {
71 | const ps = try css.parse(alloc, selector, .{ .accept_pseudo_elts = true });
72 | defer ps.deinit(alloc);
73 |
74 | var m = MatchAll.init(alloc);
75 | defer m.deinit();
76 |
77 | try css.matchAll(ps, Node{ .node = n }, &m);
78 | return m.toOwnedList();
79 | }
80 |
--------------------------------------------------------------------------------
/.github/actions/install/action.yml:
--------------------------------------------------------------------------------
1 | name: "Browsercore install"
2 | description: "Install deps for the project browsercore"
3 |
4 | inputs:
5 | zig:
6 | description: 'Zig version to install'
7 | required: false
8 | default: '0.13.0'
9 | arch:
10 | description: 'CPU arch used to select the v8 lib'
11 | required: false
12 | default: 'x86_64'
13 | os:
14 | description: 'OS used to select the v8 lib'
15 | required: false
16 | default: 'linux'
17 | zig-v8:
18 | description: 'zig v8 version to install'
19 | required: false
20 | default: 'v0.1.11'
21 | v8:
22 | description: 'v8 version to install'
23 | required: false
24 | default: '11.1.134'
25 | cache-dir:
26 | description: 'cache dir to use'
27 | required: false
28 | default: '~/.cache'
29 |
30 | runs:
31 | using: "composite"
32 |
33 | steps:
34 | - name: Install apt deps
35 | if: ${{ inputs.os == 'linux' }}
36 | shell: bash
37 | run: sudo apt-get install -y wget xz-utils python3 ca-certificates git pkg-config libglib2.0-dev gperf libexpat1-dev cmake clang
38 |
39 | - uses: mlugg/setup-zig@v1
40 | with:
41 | version: ${{ inputs.zig }}
42 |
43 | - name: Cache v8
44 | id: cache-v8
45 | uses: actions/cache@v4
46 | env:
47 | cache-name: cache-v8
48 | with:
49 | path: ${{ inputs.cache-dir }}/v8
50 | key: libc_v8_${{ inputs.v8 }}_${{ inputs.os }}_${{ inputs.arch }}_${{ inputs.zig-v8 }}.a
51 |
52 | - if: ${{ steps.cache-v8.outputs.cache-hit != 'true' }}
53 | shell: bash
54 | run: |
55 | mkdir -p ${{ inputs.cache-dir }}/v8
56 |
57 | wget -O ${{ inputs.cache-dir }}/v8/libc_v8.a https://github.com/lightpanda-io/zig-v8-fork/releases/download/${{ inputs.zig-v8 }}/libc_v8_${{ inputs.v8 }}_${{ inputs.os }}_${{ inputs.arch }}.a
58 |
59 | - name: install v8
60 | shell: bash
61 | run: |
62 | mkdir -p vendor/zig-js-runtime/vendor/v8/${{inputs.arch}}-${{inputs.os}}/debug
63 | ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a vendor/zig-js-runtime/vendor/v8/${{inputs.arch}}-${{inputs.os}}/debug/libc_v8.a
64 |
65 | mkdir -p vendor/zig-js-runtime/vendor/v8/${{inputs.arch}}-${{inputs.os}}/release
66 | ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a vendor/zig-js-runtime/vendor/v8/${{inputs.arch}}-${{inputs.os}}/release/libc_v8.a
67 |
68 | - name: libiconv
69 | shell: bash
70 | run: make install-libiconv
71 |
72 | - name: build mimalloc
73 | shell: bash
74 | run: make install-mimalloc
75 |
76 | - name: build netsurf
77 | shell: bash
78 | run: make install-netsurf
79 |
--------------------------------------------------------------------------------
/src/xmlserializer/xmlserializer.zig:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
2 | //
3 | // Francis Bouvier
4 | // Pierre Tachoire
5 | //
6 | // This program is free software: you can redistribute it and/or modify
7 | // it under the terms of the GNU Affero General Public License as
8 | // published by the Free Software Foundation, either version 3 of the
9 | // License, or (at your option) any later version.
10 | //
11 | // This program is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | // GNU Affero General Public License for more details.
15 | //
16 | // You should have received a copy of the GNU Affero General Public License
17 | // along with this program. If not, see .
18 | //
19 | const std = @import("std");
20 |
21 | const jsruntime = @import("jsruntime");
22 | const Case = jsruntime.test_utils.Case;
23 | const checkCases = jsruntime.test_utils.checkCases;
24 |
25 | const DOMError = @import("netsurf").DOMError;
26 |
27 | const parser = @import("netsurf");
28 | const dump = @import("../browser/dump.zig");
29 |
30 | pub const Interfaces = .{
31 | XMLSerializer,
32 | };
33 |
34 | // https://w3c.github.io/DOM-Parsing/#dom-xmlserializer-constructor
35 | pub const XMLSerializer = struct {
36 | pub const mem_guarantied = true;
37 |
38 | pub fn constructor() !XMLSerializer {
39 | return .{};
40 | }
41 |
42 | pub fn deinit(_: *XMLSerializer, _: std.mem.Allocator) void {}
43 |
44 | pub fn _serializeToString(_: XMLSerializer, alloc: std.mem.Allocator, root: *parser.Node) ![]const u8 {
45 | var buf = std.ArrayList(u8).init(alloc);
46 | defer buf.deinit();
47 |
48 | if (try parser.nodeType(root) == .document) {
49 | try dump.writeHTML(@as(*parser.Document, @ptrCast(root)), buf.writer());
50 | } else {
51 | try dump.writeNode(root, buf.writer());
52 | }
53 | // TODO express the caller owned the slice.
54 | // https://github.com/lightpanda-io/jsruntime-lib/issues/195
55 | return try buf.toOwnedSlice();
56 | }
57 | };
58 |
59 | // Tests
60 | // -----
61 |
62 | pub fn testExecFn(
63 | _: std.mem.Allocator,
64 | js_env: *jsruntime.Env,
65 | ) anyerror!void {
66 | var serializer = [_]Case{
67 | .{ .src = "const s = new XMLSerializer()", .ex = "undefined" },
68 | .{ .src = "s.serializeToString(document.getElementById('para'))", .ex = " And
" },
69 | };
70 | try checkCases(js_env, &serializer);
71 | }
72 |
--------------------------------------------------------------------------------
/src/mimalloc/mimalloc.zig:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
2 | //
3 | // Francis Bouvier
4 | // Pierre Tachoire
5 | //
6 | // This program is free software: you can redistribute it and/or modify
7 | // it under the terms of the GNU Affero General Public License as
8 | // published by the Free Software Foundation, either version 3 of the
9 | // License, or (at your option) any later version.
10 | //
11 | // This program is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | // GNU Affero General Public License for more details.
15 | //
16 | // You should have received a copy of the GNU Affero General Public License
17 | // along with this program. If not, see .
18 |
19 | // This file makes the glue between mimalloc heap allocation and libdom memory
20 | // management.
21 | // We replace the libdom default usage of allocations with mimalloc heap
22 | // allocation to be able to free all memory used at once, like an arena usage.
23 |
24 | const std = @import("std");
25 | const c = @cImport({
26 | @cInclude("mimalloc.h");
27 | });
28 |
29 | const Error = error{
30 | HeapNotNull,
31 | HeapNull,
32 | };
33 |
34 | var heap: ?*c.mi_heap_t = null;
35 |
36 | pub fn create() Error!void {
37 | if (heap != null) return Error.HeapNotNull;
38 | heap = c.mi_heap_new();
39 | if (heap == null) return Error.HeapNull;
40 | }
41 |
42 | pub fn destroy() void {
43 | if (heap == null) return;
44 | c.mi_heap_destroy(heap.?);
45 | heap = null;
46 | }
47 |
48 | pub export fn m_alloc(size: usize) callconv(.C) ?*anyopaque {
49 | if (heap == null) return null;
50 | return c.mi_heap_malloc(heap.?, size);
51 | }
52 |
53 | pub export fn re_alloc(ptr: ?*anyopaque, size: usize) callconv(.C) ?*anyopaque {
54 | if (heap == null) return null;
55 | return c.mi_heap_realloc(heap.?, ptr, size);
56 | }
57 |
58 | pub export fn c_alloc(nmemb: usize, size: usize) callconv(.C) ?*anyopaque {
59 | if (heap == null) return null;
60 | return c.mi_heap_calloc(heap.?, nmemb, size);
61 | }
62 |
63 | pub export fn str_dup(s: [*c]const u8) callconv(.C) [*c]u8 {
64 | if (heap == null) return null;
65 | return c.mi_heap_strdup(heap.?, s);
66 | }
67 |
68 | pub export fn strn_dup(s: [*c]const u8, size: usize) callconv(.C) [*c]u8 {
69 | if (heap == null) return null;
70 | return c.mi_heap_strndup(heap.?, s, size);
71 | }
72 |
73 | // NOOP, use destroy to clear all the memory allocated at once.
74 | pub export fn f_ree(_: ?*anyopaque) callconv(.C) void {
75 | return;
76 | }
77 |
--------------------------------------------------------------------------------
/src/dom/processing_instruction.zig:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
2 | //
3 | // Francis Bouvier
4 | // Pierre Tachoire
5 | //
6 | // This program is free software: you can redistribute it and/or modify
7 | // it under the terms of the GNU Affero General Public License as
8 | // published by the Free Software Foundation, either version 3 of the
9 | // License, or (at your option) any later version.
10 | //
11 | // This program is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | // GNU Affero General Public License for more details.
15 | //
16 | // You should have received a copy of the GNU Affero General Public License
17 | // along with this program. If not, see .
18 |
19 | const std = @import("std");
20 |
21 | const jsruntime = @import("jsruntime");
22 | const Case = jsruntime.test_utils.Case;
23 | const checkCases = jsruntime.test_utils.checkCases;
24 |
25 | const parser = @import("netsurf");
26 | const Node = @import("node.zig").Node;
27 |
28 | // https://dom.spec.whatwg.org/#processinginstruction
29 | pub const ProcessingInstruction = struct {
30 | pub const Self = parser.ProcessingInstruction;
31 |
32 | // TODO for libdom processing instruction inherit from node.
33 | // But the spec says it must inherit from CDATA.
34 | pub const prototype = *Node;
35 | pub const mem_guarantied = true;
36 |
37 | pub fn get_target(self: *parser.ProcessingInstruction) ![]const u8 {
38 | // libdom stores the ProcessingInstruction target in the node's name.
39 | return try parser.nodeName(parser.processingInstructionToNode(self));
40 | }
41 |
42 | pub fn _cloneNode(self: *parser.ProcessingInstruction, _: ?bool) !*parser.ProcessingInstruction {
43 | return try parser.processInstructionCopy(self);
44 | }
45 |
46 | pub fn get_data(self: *parser.ProcessingInstruction) !?[]const u8 {
47 | return try parser.nodeValue(parser.processingInstructionToNode(self));
48 | }
49 |
50 | pub fn set_data(self: *parser.ProcessingInstruction, data: []u8) !void {
51 | try parser.nodeSetValue(parser.processingInstructionToNode(self), data);
52 | }
53 | };
54 |
55 | pub fn testExecFn(
56 | _: std.mem.Allocator,
57 | js_env: *jsruntime.Env,
58 | ) anyerror!void {
59 | var createProcessingInstruction = [_]Case{
60 | .{ .src = "let pi = document.createProcessingInstruction('foo', 'bar')", .ex = "undefined" },
61 | .{ .src = "pi.target", .ex = "foo" },
62 | .{ .src = "pi.data", .ex = "bar" },
63 | .{ .src = "pi.data = 'foo'", .ex = "foo" },
64 | .{ .src = "pi.data", .ex = "foo" },
65 |
66 | .{ .src = "let pi2 = pi.cloneNode()", .ex = "undefined" },
67 | };
68 | try checkCases(js_env, &createProcessingInstruction);
69 | }
70 |
--------------------------------------------------------------------------------
/src/main_shell.zig:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
2 | //
3 | // Francis Bouvier
4 | // Pierre Tachoire
5 | //
6 | // This program is free software: you can redistribute it and/or modify
7 | // it under the terms of the GNU Affero General Public License as
8 | // published by the Free Software Foundation, either version 3 of the
9 | // License, or (at your option) any later version.
10 | //
11 | // This program is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | // GNU Affero General Public License for more details.
15 | //
16 | // You should have received a copy of the GNU Affero General Public License
17 | // along with this program. If not, see .
18 |
19 | const std = @import("std");
20 |
21 | const jsruntime = @import("jsruntime");
22 |
23 | const parser = @import("netsurf");
24 | const apiweb = @import("apiweb.zig");
25 | const Window = @import("html/window.zig").Window;
26 | const storage = @import("storage/storage.zig");
27 | const Client = @import("asyncio").Client;
28 |
29 | const html_test = @import("html_test.zig").html;
30 |
31 | pub const Types = jsruntime.reflect(apiweb.Interfaces);
32 | pub const UserContext = apiweb.UserContext;
33 | pub const IO = @import("asyncio").Wrapper(jsruntime.Loop);
34 |
35 | var doc: *parser.DocumentHTML = undefined;
36 |
37 | fn execJS(
38 | alloc: std.mem.Allocator,
39 | js_env: *jsruntime.Env,
40 | ) anyerror!void {
41 | // start JS env
42 | try js_env.start();
43 | defer js_env.stop();
44 |
45 | var cli = Client{ .allocator = alloc };
46 | defer cli.deinit();
47 |
48 | try js_env.setUserContext(UserContext{
49 | .document = doc,
50 | .httpClient = &cli,
51 | });
52 |
53 | var storageShelf = storage.Shelf.init(alloc);
54 | defer storageShelf.deinit();
55 |
56 | // alias global as self and window
57 | var window = Window.create(null, null);
58 | try window.replaceDocument(doc);
59 | window.setStorageShelf(&storageShelf);
60 | try js_env.bindGlobal(window);
61 |
62 | // launch shellExec
63 | try jsruntime.shellExec(alloc, js_env);
64 | }
65 |
66 | pub fn main() !void {
67 |
68 | // allocator
69 | var gpa = std.heap.GeneralPurposeAllocator(.{}){};
70 | defer _ = gpa.deinit();
71 | var arena = std.heap.ArenaAllocator.init(gpa.allocator());
72 | defer arena.deinit();
73 |
74 | try parser.init();
75 | defer parser.deinit();
76 |
77 | // document
78 | const file = try std.fs.cwd().openFile("test.html", .{});
79 | defer file.close();
80 |
81 | doc = try parser.documentHTMLParse(file.reader(), "UTF-8");
82 | defer parser.documentHTMLClose(doc) catch |err| {
83 | std.debug.print("documentHTMLClose error: {s}\n", .{@errorName(err)});
84 | };
85 |
86 | // create JS vm
87 | const vm = jsruntime.VM.init();
88 | defer vm.deinit();
89 |
90 | // launch shell
91 | try jsruntime.shell(&arena, execJS, .{ .app_name = "lightpanda-shell" });
92 | }
93 |
--------------------------------------------------------------------------------
/.github/workflows/wpt.yml:
--------------------------------------------------------------------------------
1 | name: wpt
2 |
3 | env:
4 | AWS_ACCESS_KEY_ID: ${{ vars.LPD_PERF_AWS_ACCESS_KEY_ID }}
5 | AWS_SECRET_ACCESS_KEY: ${{ secrets.LPD_PERF_AWS_SECRET_ACCESS_KEY }}
6 | AWS_BUCKET: ${{ vars.LPD_PERF_AWS_BUCKET }}
7 | AWS_REGION: ${{ vars.LPD_PERF_AWS_REGION }}
8 |
9 | on:
10 | push:
11 | branches:
12 | - main
13 | paths:
14 | - "build.zig"
15 | - "src/**/*.zig"
16 | - "src/*.zig"
17 | - "tests/wpt/**"
18 | - "vendor/**"
19 | - ".github/**"
20 | pull_request:
21 |
22 | # By default GH trigger on types opened, synchronize and reopened.
23 | # see https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request
24 | # Since we skip the job when the PR is in draft state, we want to force CI
25 | # running when the PR is marked ready_for_review w/o other change.
26 | # see https://github.com/orgs/community/discussions/25722#discussioncomment-3248917
27 | types: [opened, synchronize, reopened, ready_for_review]
28 |
29 | paths:
30 | - ".github/**"
31 | - "build.zig"
32 | - "src/**/*.zig"
33 | - "src/*.zig"
34 | - "tests/wpt/**"
35 | - "vendor/**"
36 | - ".github/**"
37 | # Allows you to run this workflow manually from the Actions tab
38 | workflow_dispatch:
39 |
40 | jobs:
41 | wpt:
42 | name: web platform tests
43 |
44 | # Don't run the CI with draft PR.
45 | if: github.event.pull_request.draft == false
46 |
47 | runs-on: ubuntu-latest
48 |
49 | steps:
50 | - uses: actions/checkout@v4
51 | with:
52 | fetch-depth: 0
53 | # fetch submodules recusively, to get zig-js-runtime submodules also.
54 | submodules: recursive
55 |
56 | - uses: ./.github/actions/install
57 |
58 | - run: zig build wpt -Dengine=v8 -- --safe --summary
59 |
60 | # For now WPT tests doesn't pass at all.
61 | # We accept then to continue the job on failure.
62 | # TODO remove the continue-on-error when tests will pass.
63 | continue-on-error: true
64 |
65 | - name: json output
66 | run: zig build wpt -Dengine=v8 -- --safe --json > wpt.json
67 |
68 | - name: write commit
69 | run: |
70 | echo "${{github.sha}}" > commit.txt
71 |
72 | - name: upload artifact
73 | uses: actions/upload-artifact@v4
74 | with:
75 | name: wpt-results
76 | path: |
77 | wpt.json
78 | commit.txt
79 | retention-days: 10
80 |
81 | perf-fmt:
82 | name: perf-fmt
83 | needs: wpt
84 |
85 | # Don't execute on PR
86 | if: github.event_name != 'pull_request'
87 |
88 | runs-on: ubuntu-latest
89 | container:
90 | image: ghcr.io/lightpanda-io/perf-fmt:latest
91 | credentials:
92 | username: ${{ github.actor }}
93 | password: ${{ secrets.GITHUB_TOKEN }}
94 |
95 | steps:
96 | - name: download artifact
97 | uses: actions/download-artifact@v4
98 | with:
99 | name: wpt-results
100 |
101 | - name: format and send json result
102 | run: /perf-fmt wpt ${{ github.sha }} wpt.json
103 |
--------------------------------------------------------------------------------
/src/dom/text.zig:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
2 | //
3 | // Francis Bouvier
4 | // Pierre Tachoire
5 | //
6 | // This program is free software: you can redistribute it and/or modify
7 | // it under the terms of the GNU Affero General Public License as
8 | // published by the Free Software Foundation, either version 3 of the
9 | // License, or (at your option) any later version.
10 | //
11 | // This program is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | // GNU Affero General Public License for more details.
15 | //
16 | // You should have received a copy of the GNU Affero General Public License
17 | // along with this program. If not, see .
18 |
19 | const std = @import("std");
20 |
21 | const jsruntime = @import("jsruntime");
22 | const Case = jsruntime.test_utils.Case;
23 | const checkCases = jsruntime.test_utils.checkCases;
24 |
25 | const parser = @import("netsurf");
26 |
27 | const CharacterData = @import("character_data.zig").CharacterData;
28 | const CDATASection = @import("cdata_section.zig").CDATASection;
29 |
30 | const UserContext = @import("../user_context.zig").UserContext;
31 |
32 | // Text interfaces
33 | pub const Interfaces = .{
34 | CDATASection,
35 | };
36 |
37 | pub const Text = struct {
38 | pub const Self = parser.Text;
39 | pub const prototype = *CharacterData;
40 | pub const mem_guarantied = true;
41 |
42 | pub fn constructor(userctx: UserContext, data: ?[]const u8) !*parser.Text {
43 | return parser.documentCreateTextNode(
44 | parser.documentHTMLToDocument(userctx.document),
45 | data orelse "",
46 | );
47 | }
48 |
49 | // JS funcs
50 | // --------
51 |
52 | // Read attributes
53 |
54 | pub fn get_wholeText(self: *parser.Text) ![]const u8 {
55 | return try parser.textWholdeText(self);
56 | }
57 |
58 | // JS methods
59 | // ----------
60 |
61 | pub fn _splitText(self: *parser.Text, offset: u32) !*parser.Text {
62 | return try parser.textSplitText(self, offset);
63 | }
64 | };
65 |
66 | // Tests
67 | // -----
68 |
69 | pub fn testExecFn(
70 | _: std.mem.Allocator,
71 | js_env: *jsruntime.Env,
72 | ) anyerror!void {
73 | var constructor = [_]Case{
74 | .{ .src = "let t = new Text('foo')", .ex = "undefined" },
75 | .{ .src = "t.data", .ex = "foo" },
76 |
77 | .{ .src = "let emptyt = new Text()", .ex = "undefined" },
78 | .{ .src = "emptyt.data", .ex = "" },
79 | };
80 | try checkCases(js_env, &constructor);
81 |
82 | var get_whole_text = [_]Case{
83 | .{ .src = "let text = document.getElementById('link').firstChild", .ex = "undefined" },
84 | .{ .src = "text.wholeText === 'OK'", .ex = "true" },
85 | };
86 | try checkCases(js_env, &get_whole_text);
87 |
88 | var split_text = [_]Case{
89 | .{ .src = "text.data = 'OK modified'", .ex = "OK modified" },
90 | .{ .src = "let split = text.splitText('OK'.length)", .ex = "undefined" },
91 | .{ .src = "split.data === ' modified'", .ex = "true" },
92 | .{ .src = "text.data === 'OK'", .ex = "true" },
93 | };
94 | try checkCases(js_env, &split_text);
95 | }
96 |
--------------------------------------------------------------------------------
/src/xhr/progress_event.zig:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
2 | //
3 | // Francis Bouvier
4 | // Pierre Tachoire
5 | //
6 | // This program is free software: you can redistribute it and/or modify
7 | // it under the terms of the GNU Affero General Public License as
8 | // published by the Free Software Foundation, either version 3 of the
9 | // License, or (at your option) any later version.
10 | //
11 | // This program is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | // GNU Affero General Public License for more details.
15 | //
16 | // You should have received a copy of the GNU Affero General Public License
17 | // along with this program. If not, see .
18 |
19 | const std = @import("std");
20 |
21 | const jsruntime = @import("jsruntime");
22 | const Case = jsruntime.test_utils.Case;
23 | const checkCases = jsruntime.test_utils.checkCases;
24 |
25 | const parser = @import("netsurf");
26 | const Event = @import("../events/event.zig").Event;
27 |
28 | const DOMException = @import("../dom/exceptions.zig").DOMException;
29 |
30 | pub const ProgressEvent = struct {
31 | pub const prototype = *Event;
32 | pub const Exception = DOMException;
33 | pub const mem_guarantied = true;
34 |
35 | pub const EventInit = struct {
36 | lengthComputable: bool = false,
37 | loaded: u64 = 0,
38 | total: u64 = 0,
39 | };
40 |
41 | proto: parser.Event,
42 | lengthComputable: bool,
43 | loaded: u64 = 0,
44 | total: u64 = 0,
45 |
46 | pub fn constructor(eventType: []const u8, opts: ?EventInit) !ProgressEvent {
47 | const event = try parser.eventCreate();
48 | defer parser.eventDestroy(event);
49 | try parser.eventInit(event, eventType, .{});
50 | try parser.eventSetInternalType(event, .progress_event);
51 |
52 | const o = opts orelse EventInit{};
53 |
54 | return .{
55 | .proto = event.*,
56 | .lengthComputable = o.lengthComputable,
57 | .loaded = o.loaded,
58 | .total = o.total,
59 | };
60 | }
61 |
62 | pub fn get_lengthComputable(self: ProgressEvent) bool {
63 | return self.lengthComputable;
64 | }
65 |
66 | pub fn get_loaded(self: ProgressEvent) u64 {
67 | return self.loaded;
68 | }
69 |
70 | pub fn get_total(self: ProgressEvent) u64 {
71 | return self.total;
72 | }
73 | };
74 |
75 | pub fn testExecFn(
76 | _: std.mem.Allocator,
77 | js_env: *jsruntime.Env,
78 | ) anyerror!void {
79 | var progress_event = [_]Case{
80 | .{ .src = "let pevt = new ProgressEvent('foo');", .ex = "undefined" },
81 | .{ .src = "pevt.loaded", .ex = "0" },
82 | .{ .src = "pevt instanceof ProgressEvent", .ex = "true" },
83 | .{ .src = "var nnb = 0; var eevt = null; function ccbk(event) { nnb ++; eevt = event; }", .ex = "undefined" },
84 | .{ .src = "document.addEventListener('foo', ccbk)", .ex = "undefined" },
85 | .{ .src = "document.dispatchEvent(pevt)", .ex = "true" },
86 | .{ .src = "eevt.type", .ex = "foo" },
87 | .{ .src = "eevt instanceof ProgressEvent", .ex = "true" },
88 | };
89 | try checkCases(js_env, &progress_event);
90 | }
91 |
--------------------------------------------------------------------------------
/src/browser/loader.zig:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
2 | //
3 | // Francis Bouvier
4 | // Pierre Tachoire
5 | //
6 | // This program is free software: you can redistribute it and/or modify
7 | // it under the terms of the GNU Affero General Public License as
8 | // published by the Free Software Foundation, either version 3 of the
9 | // License, or (at your option) any later version.
10 | //
11 | // This program is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | // GNU Affero General Public License for more details.
15 | //
16 | // You should have received a copy of the GNU Affero General Public License
17 | // along with this program. If not, see .
18 |
19 | const std = @import("std");
20 | const Client = @import("../http/Client.zig");
21 |
22 | const user_agent = @import("browser.zig").user_agent;
23 |
24 | pub const Loader = struct {
25 | client: Client,
26 | // use 64KB for headers buffer size.
27 | server_header_buffer: [1024 * 64]u8 = undefined,
28 |
29 | pub const Response = struct {
30 | alloc: std.mem.Allocator,
31 | req: *Client.Request,
32 |
33 | pub fn deinit(self: *Response) void {
34 | self.req.deinit();
35 | self.alloc.destroy(self.req);
36 | }
37 | };
38 |
39 | pub fn init(alloc: std.mem.Allocator) Loader {
40 | return Loader{
41 | .client = Client{
42 | .allocator = alloc,
43 | },
44 | };
45 | }
46 |
47 | pub fn deinit(self: *Loader) void {
48 | self.client.deinit();
49 | }
50 |
51 | // see
52 | // https://ziglang.org/documentation/master/std/#A;std:http.Client.fetch
53 | // for reference.
54 | // The caller is responsible for calling `deinit()` on the `Response`.
55 | pub fn get(self: *Loader, alloc: std.mem.Allocator, uri: std.Uri) !Response {
56 | var resp = Response{
57 | .alloc = alloc,
58 | .req = try alloc.create(Client.Request),
59 | };
60 | errdefer alloc.destroy(resp.req);
61 |
62 | resp.req.* = try self.client.open(.GET, uri, .{
63 | .headers = .{
64 | .user_agent = .{ .override = user_agent },
65 | },
66 | .extra_headers = &.{
67 | .{ .name = "Accept", .value = "*/*" },
68 | .{ .name = "Accept-Language", .value = "en-US,en;q=0.5" },
69 | },
70 | .server_header_buffer = &self.server_header_buffer,
71 | });
72 | errdefer resp.req.deinit();
73 |
74 | try resp.req.send();
75 | try resp.req.finish();
76 | try resp.req.wait();
77 |
78 | return resp;
79 | }
80 | };
81 |
82 | test "loader: get" {
83 | const alloc = std.testing.allocator;
84 | var loader = Loader.init(alloc);
85 | defer loader.deinit();
86 |
87 | const uri = try std.Uri.parse("http://localhost:9582/loader");
88 | var result = try loader.get(alloc, uri);
89 | defer result.deinit();
90 |
91 | try std.testing.expectEqual(.ok, result.req.response.status);
92 |
93 | var res: [128]u8 = undefined;
94 | const size = try result.req.readAll(&res);
95 | try std.testing.expectEqual(6, size);
96 | try std.testing.expectEqualStrings("Hello!", res[0..6]);
97 | }
98 |
--------------------------------------------------------------------------------
/src/css/libdom.zig:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
2 | //
3 | // Francis Bouvier
4 | // Pierre Tachoire
5 | //
6 | // This program is free software: you can redistribute it and/or modify
7 | // it under the terms of the GNU Affero General Public License as
8 | // published by the Free Software Foundation, either version 3 of the
9 | // License, or (at your option) any later version.
10 | //
11 | // This program is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | // GNU Affero General Public License for more details.
15 | //
16 | // You should have received a copy of the GNU Affero General Public License
17 | // along with this program. If not, see .
18 |
19 | const std = @import("std");
20 |
21 | const parser = @import("netsurf");
22 |
23 | // Node implementation with Netsurf Libdom C lib.
24 | pub const Node = struct {
25 | node: *parser.Node,
26 |
27 | pub fn firstChild(n: Node) !?Node {
28 | const c = try parser.nodeFirstChild(n.node);
29 | if (c) |cc| return .{ .node = cc };
30 |
31 | return null;
32 | }
33 |
34 | pub fn lastChild(n: Node) !?Node {
35 | const c = try parser.nodeLastChild(n.node);
36 | if (c) |cc| return .{ .node = cc };
37 |
38 | return null;
39 | }
40 |
41 | pub fn nextSibling(n: Node) !?Node {
42 | const c = try parser.nodeNextSibling(n.node);
43 | if (c) |cc| return .{ .node = cc };
44 |
45 | return null;
46 | }
47 |
48 | pub fn prevSibling(n: Node) !?Node {
49 | const c = try parser.nodePreviousSibling(n.node);
50 | if (c) |cc| return .{ .node = cc };
51 |
52 | return null;
53 | }
54 |
55 | pub fn parent(n: Node) !?Node {
56 | const c = try parser.nodeParentNode(n.node);
57 | if (c) |cc| return .{ .node = cc };
58 |
59 | return null;
60 | }
61 |
62 | pub fn isElement(n: Node) bool {
63 | const t = parser.nodeType(n.node) catch return false;
64 | return t == .element;
65 | }
66 |
67 | pub fn isDocument(n: Node) bool {
68 | const t = parser.nodeType(n.node) catch return false;
69 | return t == .document;
70 | }
71 |
72 | pub fn isComment(n: Node) bool {
73 | const t = parser.nodeType(n.node) catch return false;
74 | return t == .comment;
75 | }
76 |
77 | pub fn isText(n: Node) bool {
78 | const t = parser.nodeType(n.node) catch return false;
79 | return t == .text;
80 | }
81 |
82 | pub fn isEmptyText(n: Node) !bool {
83 | const data = try parser.nodeTextContent(n.node);
84 | if (data == null) return true;
85 | if (data.?.len == 0) return true;
86 |
87 | return std.mem.trim(u8, data.?, &std.ascii.whitespace).len == 0;
88 | }
89 |
90 | pub fn tag(n: Node) ![]const u8 {
91 | return try parser.nodeName(n.node);
92 | }
93 |
94 | pub fn attr(n: Node, key: []const u8) !?[]const u8 {
95 | if (!n.isElement()) return null;
96 | return try parser.elementGetAttribute(parser.nodeToElement(n.node), key);
97 | }
98 |
99 | pub fn eql(a: Node, b: Node) bool {
100 | return a.node == b.node;
101 | }
102 | };
103 |
--------------------------------------------------------------------------------
/.github/workflows/e2e-test.yml:
--------------------------------------------------------------------------------
1 | name: e2e-test
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | paths:
8 | - "build.zig"
9 | - "src/**/*.zig"
10 | - "src/*.zig"
11 | - "vendor/zig-js-runtime"
12 | - ".github/**"
13 | - "vendor/**"
14 | pull_request:
15 |
16 | # By default GH trigger on types opened, synchronize and reopened.
17 | # see https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request
18 | # Since we skip the job when the PR is in draft state, we want to force CI
19 | # running when the PR is marked ready_for_review w/o other change.
20 | # see https://github.com/orgs/community/discussions/25722#discussioncomment-3248917
21 | types: [opened, synchronize, reopened, ready_for_review]
22 |
23 | paths:
24 | - ".github/**"
25 | - "build.zig"
26 | - "src/**/*.zig"
27 | - "src/*.zig"
28 | - "vendor/**"
29 | - ".github/**"
30 | - "vendor/**"
31 | # Allows you to run this workflow manually from the Actions tab
32 | workflow_dispatch:
33 |
34 | jobs:
35 | zig-build-release:
36 | name: zig build release
37 |
38 | runs-on: ubuntu-latest
39 |
40 | steps:
41 | - uses: actions/checkout@v4
42 | with:
43 | fetch-depth: 0
44 | # fetch submodules recusively, to get zig-js-runtime submodules also.
45 | submodules: recursive
46 |
47 | - uses: ./.github/actions/install
48 |
49 | - name: zig build release
50 | run: zig build -Doptimize=ReleaseSafe -Dengine=v8
51 |
52 | - name: upload artifact
53 | uses: actions/upload-artifact@v4
54 | with:
55 | name: lightpanda-build-release
56 | path: |
57 | zig-out/bin/lightpanda
58 | retention-days: 1
59 |
60 | puppeteer:
61 | name: puppeteer
62 | needs: zig-build-release
63 |
64 | env:
65 | MAX_MEMORY: 28000
66 | MAX_AVG_DURATION: 24
67 |
68 | runs-on: ubuntu-latest
69 |
70 | steps:
71 | - uses: actions/checkout@v4
72 | with:
73 | repository: 'lightpanda-io/demo'
74 | fetch-depth: 0
75 |
76 | - run: npm install
77 |
78 | - name: download artifact
79 | uses: actions/download-artifact@v4
80 | with:
81 | name: lightpanda-build-release
82 |
83 | - run: chmod a+x ./lightpanda
84 |
85 | - name: run puppeteer
86 | run: |
87 | python3 -m http.server 1234 -d ./public & echo $! > PYTHON.pid
88 | ./lightpanda serve & echo $! > LPD.pid
89 | RUNS=100 npm run bench-puppeteer-cdp > puppeteer.out || exit 1
90 | cat /proc/`cat LPD.pid`/status |grep VmHWM|grep -oP '\d+' > LPD.VmHWM
91 | kill `cat LPD.pid` `cat PYTHON.pid`
92 |
93 | - name: puppeteer result
94 | run: cat puppeteer.out
95 |
96 | - name: memory regression
97 | run: |
98 | export LPD_VmHWM=`cat LPD.VmHWM`
99 | echo "Peak resident set size: $LPD_VmHWM"
100 | test "$LPD_VmHWM" -le "$MAX_MEMORY"
101 |
102 | - name: duration regression
103 | run: |
104 | export PUPPETEER_AVG_DURATION=`cat puppeteer.out|grep 'avg run'|sed 's/avg run duration (ms) //'`
105 | echo "puppeteer avg duration: $PUPPETEER_AVG_DURATION"
106 | test "$PUPPETEER_AVG_DURATION" -le "$MAX_AVG_DURATION"
107 |
108 |
--------------------------------------------------------------------------------
/src/html/navigator.zig:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
2 | //
3 | // Francis Bouvier
4 | // Pierre Tachoire
5 | //
6 | // This program is free software: you can redistribute it and/or modify
7 | // it under the terms of the GNU Affero General Public License as
8 | // published by the Free Software Foundation, either version 3 of the
9 | // License, or (at your option) any later version.
10 | //
11 | // This program is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | // GNU Affero General Public License for more details.
15 | //
16 | // You should have received a copy of the GNU Affero General Public License
17 | // along with this program. If not, see .
18 |
19 | const std = @import("std");
20 |
21 | const builtin = @import("builtin");
22 | const jsruntime = @import("jsruntime");
23 |
24 | const Case = jsruntime.test_utils.Case;
25 | const checkCases = jsruntime.test_utils.checkCases;
26 |
27 | // https://html.spec.whatwg.org/multipage/system-state.html#navigator
28 | pub const Navigator = struct {
29 | pub const mem_guarantied = true;
30 |
31 | agent: []const u8 = "Lightpanda/1.0",
32 | version: []const u8 = "1.0",
33 | vendor: []const u8 = "",
34 | platform: []const u8 = std.fmt.comptimePrint("{any} {any}", .{ builtin.os.tag, builtin.cpu.arch }),
35 |
36 | language: []const u8 = "en-US",
37 |
38 | pub fn get_userAgent(self: *Navigator) []const u8 {
39 | return self.agent;
40 | }
41 | pub fn get_appCodeName(_: *Navigator) []const u8 {
42 | return "Mozilla";
43 | }
44 | pub fn get_appName(_: *Navigator) []const u8 {
45 | return "Netscape";
46 | }
47 | pub fn get_appVersion(self: *Navigator) []const u8 {
48 | return self.version;
49 | }
50 | pub fn get_platform(self: *Navigator) []const u8 {
51 | return self.platform;
52 | }
53 | pub fn get_product(_: *Navigator) []const u8 {
54 | return "Gecko";
55 | }
56 | pub fn get_productSub(_: *Navigator) []const u8 {
57 | return "20030107";
58 | }
59 | pub fn get_vendor(self: *Navigator) []const u8 {
60 | return self.vendor;
61 | }
62 | pub fn get_vendorSub(_: *Navigator) []const u8 {
63 | return "";
64 | }
65 | pub fn get_language(self: *Navigator) []const u8 {
66 | return self.language;
67 | }
68 | // TODO wait for arrays.
69 | //pub fn get_languages(self: *Navigator) [][]const u8 {
70 | // return .{self.language};
71 | //}
72 | pub fn get_online(_: *Navigator) bool {
73 | return true;
74 | }
75 | pub fn _registerProtocolHandler(_: *Navigator, scheme: []const u8, url: []const u8) void {
76 | _ = scheme;
77 | _ = url;
78 | }
79 | pub fn _unregisterProtocolHandler(_: *Navigator, scheme: []const u8, url: []const u8) void {
80 | _ = scheme;
81 | _ = url;
82 | }
83 |
84 | pub fn get_cookieEnabled(_: *Navigator) bool {
85 | return true;
86 | }
87 | };
88 |
89 | // Tests
90 | // -----
91 |
92 | pub fn testExecFn(
93 | _: std.mem.Allocator,
94 | js_env: *jsruntime.Env,
95 | ) anyerror!void {
96 | var navigator = [_]Case{
97 | .{ .src = "navigator.userAgent", .ex = "Lightpanda/1.0" },
98 | .{ .src = "navigator.appVersion", .ex = "1.0" },
99 | .{ .src = "navigator.language", .ex = "en-US" },
100 | };
101 | try checkCases(js_env, &navigator);
102 | }
103 |
--------------------------------------------------------------------------------
/src/dom/namednodemap.zig:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
2 | //
3 | // Francis Bouvier
4 | // Pierre Tachoire
5 | //
6 | // This program is free software: you can redistribute it and/or modify
7 | // it under the terms of the GNU Affero General Public License as
8 | // published by the Free Software Foundation, either version 3 of the
9 | // License, or (at your option) any later version.
10 | //
11 | // This program is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | // GNU Affero General Public License for more details.
15 | //
16 | // You should have received a copy of the GNU Affero General Public License
17 | // along with this program. If not, see .
18 |
19 | const std = @import("std");
20 |
21 | const parser = @import("netsurf");
22 |
23 | const jsruntime = @import("jsruntime");
24 | const Case = jsruntime.test_utils.Case;
25 | const checkCases = jsruntime.test_utils.checkCases;
26 |
27 | const DOMException = @import("exceptions.zig").DOMException;
28 |
29 | // WEB IDL https://dom.spec.whatwg.org/#namednodemap
30 | pub const NamedNodeMap = struct {
31 | pub const Self = parser.NamedNodeMap;
32 | pub const mem_guarantied = true;
33 |
34 | pub const Exception = DOMException;
35 |
36 | // TODO implement LegacyUnenumerableNamedProperties.
37 | // https://webidl.spec.whatwg.org/#LegacyUnenumerableNamedProperties
38 |
39 | pub fn get_length(self: *parser.NamedNodeMap) !u32 {
40 | return try parser.namedNodeMapGetLength(self);
41 | }
42 |
43 | pub fn _item(self: *parser.NamedNodeMap, index: u32) !?*parser.Attribute {
44 | return try parser.namedNodeMapItem(self, index);
45 | }
46 |
47 | pub fn _getNamedItem(self: *parser.NamedNodeMap, qname: []const u8) !?*parser.Attribute {
48 | return try parser.namedNodeMapGetNamedItem(self, qname);
49 | }
50 |
51 | pub fn _getNamedItemNS(
52 | self: *parser.NamedNodeMap,
53 | namespace: []const u8,
54 | localname: []const u8,
55 | ) !?*parser.Attribute {
56 | return try parser.namedNodeMapGetNamedItemNS(self, namespace, localname);
57 | }
58 |
59 | pub fn _setNamedItem(self: *parser.NamedNodeMap, attr: *parser.Attribute) !?*parser.Attribute {
60 | return try parser.namedNodeMapSetNamedItem(self, attr);
61 | }
62 |
63 | pub fn _setNamedItemNS(self: *parser.NamedNodeMap, attr: *parser.Attribute) !?*parser.Attribute {
64 | return try parser.namedNodeMapSetNamedItemNS(self, attr);
65 | }
66 |
67 | pub fn _removeNamedItem(self: *parser.NamedNodeMap, qname: []const u8) !*parser.Attribute {
68 | return try parser.namedNodeMapRemoveNamedItem(self, qname);
69 | }
70 |
71 | pub fn _removeNamedItemNS(
72 | self: *parser.NamedNodeMap,
73 | namespace: []const u8,
74 | localname: []const u8,
75 | ) !*parser.Attribute {
76 | return try parser.namedNodeMapRemoveNamedItemNS(self, namespace, localname);
77 | }
78 | };
79 |
80 | // Tests
81 | // -----
82 |
83 | pub fn testExecFn(
84 | _: std.mem.Allocator,
85 | js_env: *jsruntime.Env,
86 | ) anyerror!void {
87 | var setItem = [_]Case{
88 | .{ .src = "let a = document.getElementById('content').attributes", .ex = "undefined" },
89 | .{ .src = "a.length", .ex = "1" },
90 | .{ .src = "a.item(0)", .ex = "[object Attr]" },
91 | .{ .src = "a.item(1)", .ex = "null" },
92 | .{ .src = "a.getNamedItem('id')", .ex = "[object Attr]" },
93 | .{ .src = "a.getNamedItem('foo')", .ex = "null" },
94 | .{ .src = "a.setNamedItem(a.getNamedItem('id'))", .ex = "[object Attr]" },
95 | };
96 | try checkCases(js_env, &setItem);
97 | }
98 |
--------------------------------------------------------------------------------
/.github/workflows/zig-test.yml:
--------------------------------------------------------------------------------
1 | name: zig-test
2 |
3 | env:
4 | AWS_ACCESS_KEY_ID: ${{ vars.LPD_PERF_AWS_ACCESS_KEY_ID }}
5 | AWS_SECRET_ACCESS_KEY: ${{ secrets.LPD_PERF_AWS_SECRET_ACCESS_KEY }}
6 | AWS_BUCKET: ${{ vars.LPD_PERF_AWS_BUCKET }}
7 | AWS_REGION: ${{ vars.LPD_PERF_AWS_REGION }}
8 |
9 | on:
10 | push:
11 | branches:
12 | - main
13 | paths:
14 | - "build.zig"
15 | - "src/**/*.zig"
16 | - "src/*.zig"
17 | - "vendor/zig-js-runtime"
18 | - ".github/**"
19 | - "vendor/**"
20 | pull_request:
21 |
22 | # By default GH trigger on types opened, synchronize and reopened.
23 | # see https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request
24 | # Since we skip the job when the PR is in draft state, we want to force CI
25 | # running when the PR is marked ready_for_review w/o other change.
26 | # see https://github.com/orgs/community/discussions/25722#discussioncomment-3248917
27 | types: [opened, synchronize, reopened, ready_for_review]
28 |
29 | paths:
30 | - ".github/**"
31 | - "build.zig"
32 | - "src/**/*.zig"
33 | - "src/*.zig"
34 | - "vendor/**"
35 | - ".github/**"
36 | - "vendor/**"
37 | # Allows you to run this workflow manually from the Actions tab
38 | workflow_dispatch:
39 |
40 | jobs:
41 | zig-build-dev:
42 | name: zig build dev
43 |
44 | # Don't run the CI with draft PR.
45 | if: github.event.pull_request.draft == false
46 |
47 | runs-on: ubuntu-latest
48 |
49 | steps:
50 | - uses: actions/checkout@v4
51 | with:
52 | fetch-depth: 0
53 | # fetch submodules recusively, to get zig-js-runtime submodules also.
54 | submodules: recursive
55 |
56 | - uses: ./.github/actions/install
57 |
58 | - name: zig build debug
59 | run: zig build -Dengine=v8
60 |
61 | - name: upload artifact
62 | uses: actions/upload-artifact@v4
63 | with:
64 | name: lightpanda-build-dev
65 | path: |
66 | zig-out/bin/lightpanda
67 | retention-days: 1
68 |
69 | zig-test:
70 | name: zig test
71 |
72 | # Don't run the CI with draft PR.
73 | if: github.event.pull_request.draft == false
74 |
75 | runs-on: ubuntu-latest
76 |
77 | steps:
78 | - uses: actions/checkout@v4
79 | with:
80 | fetch-depth: 0
81 | # fetch submodules recusively, to get zig-js-runtime submodules also.
82 | submodules: recursive
83 |
84 | - uses: ./.github/actions/install
85 |
86 | - name: zig build unittest
87 | run: zig build unittest -freference-trace --summary all
88 |
89 | - name: zig build test
90 | run: zig build test -Dengine=v8 -- --json > bench.json
91 |
92 | - name: write commit
93 | run: |
94 | echo "${{github.sha}}" > commit.txt
95 |
96 | - name: upload artifact
97 | uses: actions/upload-artifact@v4
98 | with:
99 | name: bench-results
100 | path: |
101 | bench.json
102 | commit.txt
103 | retention-days: 10
104 |
105 | bench-fmt:
106 | name: perf-fmt
107 | needs: zig-test
108 |
109 | # Don't execute on PR
110 | if: github.event_name != 'pull_request'
111 |
112 | runs-on: ubuntu-latest
113 | container:
114 | image: ghcr.io/lightpanda-io/perf-fmt:latest
115 | credentials:
116 | username: ${{ github.actor }}
117 | password: ${{ secrets.GITHUB_TOKEN }}
118 |
119 | steps:
120 | - name: download artifact
121 | uses: actions/download-artifact@v4
122 | with:
123 | name: bench-results
124 |
125 | - name: format and send json result
126 | run: /perf-fmt bench-browser ${{ github.sha }} bench.json
127 |
--------------------------------------------------------------------------------
/src/dom/walker.zig:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
2 | //
3 | // Francis Bouvier
4 | // Pierre Tachoire
5 | //
6 | // This program is free software: you can redistribute it and/or modify
7 | // it under the terms of the GNU Affero General Public License as
8 | // published by the Free Software Foundation, either version 3 of the
9 | // License, or (at your option) any later version.
10 | //
11 | // This program is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | // GNU Affero General Public License for more details.
15 | //
16 | // You should have received a copy of the GNU Affero General Public License
17 | // along with this program. If not, see .
18 |
19 | const std = @import("std");
20 |
21 | const parser = @import("netsurf");
22 |
23 | pub const Walker = union(enum) {
24 | walkerDepthFirst: WalkerDepthFirst,
25 | walkerChildren: WalkerChildren,
26 | walkerNone: WalkerNone,
27 |
28 | pub fn get_next(self: Walker, root: *parser.Node, cur: ?*parser.Node) !?*parser.Node {
29 | switch (self) {
30 | inline else => |case| return case.get_next(root, cur),
31 | }
32 | }
33 | };
34 |
35 | // WalkerDepthFirst iterates over the DOM tree to return the next following
36 | // node or null at the end.
37 | //
38 | // This implementation is a zig version of Netsurf code.
39 | // http://source.netsurf-browser.org/libdom.git/tree/src/html/html_collection.c#n177
40 | //
41 | // The iteration is a depth first as required by the specification.
42 | // https://dom.spec.whatwg.org/#htmlcollection
43 | // https://dom.spec.whatwg.org/#concept-tree-order
44 | pub const WalkerDepthFirst = struct {
45 | pub fn get_next(_: WalkerDepthFirst, root: *parser.Node, cur: ?*parser.Node) !?*parser.Node {
46 | var n = cur orelse root;
47 |
48 | // TODO deinit next
49 | if (try parser.nodeFirstChild(n)) |next| {
50 | return next;
51 | }
52 |
53 | // TODO deinit next
54 | if (try parser.nodeNextSibling(n)) |next| {
55 | return next;
56 | }
57 |
58 | // TODO deinit parent
59 | // Back to the parent of cur.
60 | // If cur has no parent, then the iteration is over.
61 | var parent = try parser.nodeParentNode(n) orelse return null;
62 |
63 | // TODO deinit lastchild
64 | var lastchild = try parser.nodeLastChild(parent);
65 | while (n != root and n == lastchild) {
66 | n = parent;
67 |
68 | // TODO deinit parent
69 | // Back to the prev's parent.
70 | // If prev has no parent, then the loop must stop.
71 | parent = try parser.nodeParentNode(n) orelse break;
72 |
73 | // TODO deinit lastchild
74 | lastchild = try parser.nodeLastChild(parent);
75 | }
76 |
77 | if (n == root) {
78 | return null;
79 | }
80 |
81 | return try parser.nodeNextSibling(n);
82 | }
83 | };
84 |
85 | // WalkerChildren iterates over the root's children only.
86 | pub const WalkerChildren = struct {
87 | pub fn get_next(_: WalkerChildren, root: *parser.Node, cur: ?*parser.Node) !?*parser.Node {
88 | // On walk start, we return the first root's child.
89 | if (cur == null) return try parser.nodeFirstChild(root);
90 |
91 | // If cur is root, then return null.
92 | // This is a special case, if the root is included in the walk, we
93 | // don't want to go further to find children.
94 | if (root == cur.?) return null;
95 |
96 | return try parser.nodeNextSibling(cur.?);
97 | }
98 | };
99 |
100 | pub const WalkerNone = struct {
101 | pub fn get_next(_: WalkerNone, _: *parser.Node, _: ?*parser.Node) !?*parser.Node {
102 | return null;
103 | }
104 | };
105 |
--------------------------------------------------------------------------------
/src/dom/attribute.zig:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
2 | //
3 | // Francis Bouvier
4 | // Pierre Tachoire
5 | //
6 | // This program is free software: you can redistribute it and/or modify
7 | // it under the terms of the GNU Affero General Public License as
8 | // published by the Free Software Foundation, either version 3 of the
9 | // License, or (at your option) any later version.
10 | //
11 | // This program is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | // GNU Affero General Public License for more details.
15 | //
16 | // You should have received a copy of the GNU Affero General Public License
17 | // along with this program. If not, see .
18 |
19 | const std = @import("std");
20 |
21 | const jsruntime = @import("jsruntime");
22 | const Case = jsruntime.test_utils.Case;
23 | const checkCases = jsruntime.test_utils.checkCases;
24 |
25 | const parser = @import("netsurf");
26 |
27 | const Node = @import("node.zig").Node;
28 | const DOMException = @import("exceptions.zig").DOMException;
29 |
30 | // WEB IDL https://dom.spec.whatwg.org/#attr
31 | pub const Attr = struct {
32 | pub const Self = parser.Attribute;
33 | pub const prototype = *Node;
34 | pub const mem_guarantied = true;
35 |
36 | pub fn get_namespaceURI(self: *parser.Attribute) !?[]const u8 {
37 | return try parser.nodeGetNamespace(parser.attributeToNode(self));
38 | }
39 |
40 | pub fn get_prefix(self: *parser.Attribute) !?[]const u8 {
41 | return try parser.nodeGetPrefix(parser.attributeToNode(self));
42 | }
43 |
44 | pub fn get_localName(self: *parser.Attribute) ![]const u8 {
45 | return try parser.nodeLocalName(parser.attributeToNode(self));
46 | }
47 |
48 | pub fn get_name(self: *parser.Attribute) ![]const u8 {
49 | return try parser.attributeGetName(self);
50 | }
51 |
52 | pub fn get_value(self: *parser.Attribute) !?[]const u8 {
53 | return try parser.attributeGetValue(self);
54 | }
55 |
56 | pub fn set_value(self: *parser.Attribute, v: []const u8) !?[]const u8 {
57 | try parser.attributeSetValue(self, v);
58 | return v;
59 | }
60 |
61 | pub fn get_ownerElement(self: *parser.Attribute) !?*parser.Element {
62 | return try parser.attributeGetOwnerElement(self);
63 | }
64 |
65 | pub fn get_specified(_: *parser.Attribute) bool {
66 | return true;
67 | }
68 | };
69 |
70 | // Tests
71 | // -----
72 |
73 | pub fn testExecFn(
74 | _: std.mem.Allocator,
75 | js_env: *jsruntime.Env,
76 | ) anyerror!void {
77 | var getters = [_]Case{
78 | .{ .src = "let a = document.createAttributeNS('foo', 'bar')", .ex = "undefined" },
79 | .{ .src = "a.namespaceURI", .ex = "foo" },
80 | .{ .src = "a.prefix", .ex = "null" },
81 | .{ .src = "a.localName", .ex = "bar" },
82 | .{ .src = "a.name", .ex = "bar" },
83 | .{ .src = "a.value", .ex = "" },
84 | // TODO: libdom has a bug here: the created attr has no parent, it
85 | // causes a panic w/ libdom when setting the value.
86 | //.{ .src = "a.value = 'nok'", .ex = "nok" },
87 | .{ .src = "a.ownerElement", .ex = "null" },
88 | };
89 | try checkCases(js_env, &getters);
90 |
91 | var attr = [_]Case{
92 | .{ .src = "let b = document.getElementById('link').getAttributeNode('class')", .ex = "undefined" },
93 | .{ .src = "b.name", .ex = "class" },
94 | .{ .src = "b.value", .ex = "ok" },
95 | .{ .src = "b.value = 'nok'", .ex = "nok" },
96 | .{ .src = "b.value", .ex = "nok" },
97 | .{ .src = "b.value = null", .ex = "null" },
98 | .{ .src = "b.value", .ex = "null" },
99 | .{ .src = "b.value = 'ok'", .ex = "ok" },
100 | .{ .src = "b.ownerElement.id", .ex = "link" },
101 | };
102 | try checkCases(js_env, &attr);
103 | }
104 |
--------------------------------------------------------------------------------
/src/dom/implementation.zig:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
2 | //
3 | // Francis Bouvier
4 | // Pierre Tachoire
5 | //
6 | // This program is free software: you can redistribute it and/or modify
7 | // it under the terms of the GNU Affero General Public License as
8 | // published by the Free Software Foundation, either version 3 of the
9 | // License, or (at your option) any later version.
10 | //
11 | // This program is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | // GNU Affero General Public License for more details.
15 | //
16 | // You should have received a copy of the GNU Affero General Public License
17 | // along with this program. If not, see .
18 |
19 | const std = @import("std");
20 |
21 | const parser = @import("netsurf");
22 |
23 | const jsruntime = @import("jsruntime");
24 | const Case = jsruntime.test_utils.Case;
25 | const checkCases = jsruntime.test_utils.checkCases;
26 |
27 | const Document = @import("document.zig").Document;
28 | const DocumentType = @import("document_type.zig").DocumentType;
29 | const DOMException = @import("exceptions.zig").DOMException;
30 |
31 | // WEB IDL https://dom.spec.whatwg.org/#domimplementation
32 | pub const DOMImplementation = struct {
33 | pub const mem_guarantied = true;
34 |
35 | pub const Exception = DOMException;
36 |
37 | pub fn _createDocumentType(
38 | _: *DOMImplementation,
39 | alloc: std.mem.Allocator,
40 | qname: []const u8,
41 | publicId: []const u8,
42 | systemId: []const u8,
43 | ) !*parser.DocumentType {
44 | const cqname = try alloc.dupeZ(u8, qname);
45 | defer alloc.free(cqname);
46 |
47 | const cpublicId = try alloc.dupeZ(u8, publicId);
48 | defer alloc.free(cpublicId);
49 |
50 | const csystemId = try alloc.dupeZ(u8, systemId);
51 | defer alloc.free(csystemId);
52 |
53 | return try parser.domImplementationCreateDocumentType(cqname, cpublicId, csystemId);
54 | }
55 |
56 | pub fn _createDocument(
57 | _: *DOMImplementation,
58 | alloc: std.mem.Allocator,
59 | namespace: ?[]const u8,
60 | qname: ?[]const u8,
61 | doctype: ?*parser.DocumentType,
62 | ) !*parser.Document {
63 | var cnamespace: ?[:0]const u8 = null;
64 | if (namespace) |ns| {
65 | cnamespace = try alloc.dupeZ(u8, ns);
66 | }
67 | defer if (cnamespace) |v| alloc.free(v);
68 |
69 | var cqname: ?[:0]const u8 = null;
70 | if (qname) |qn| {
71 | cqname = try alloc.dupeZ(u8, qn);
72 | }
73 | defer if (cqname) |v| alloc.free(v);
74 |
75 | return try parser.domImplementationCreateDocument(cnamespace, cqname, doctype);
76 | }
77 |
78 | pub fn _createHTMLDocument(_: *DOMImplementation, title: ?[]const u8) !*parser.DocumentHTML {
79 | return try parser.domImplementationCreateHTMLDocument(title);
80 | }
81 |
82 | pub fn _hasFeature(_: *DOMImplementation) bool {
83 | return true;
84 | }
85 |
86 | pub fn deinit(_: *DOMImplementation, _: std.mem.Allocator) void {}
87 | };
88 |
89 | // Tests
90 | // -----
91 |
92 | pub fn testExecFn(
93 | _: std.mem.Allocator,
94 | js_env: *jsruntime.Env,
95 | ) anyerror!void {
96 | var getImplementation = [_]Case{
97 | .{ .src = "let impl = document.implementation", .ex = "undefined" },
98 | .{ .src = "impl.createHTMLDocument();", .ex = "[object HTMLDocument]" },
99 | .{ .src = "impl.createHTMLDocument('foo');", .ex = "[object HTMLDocument]" },
100 | .{ .src = "impl.createDocument(null, 'foo');", .ex = "[object Document]" },
101 | .{ .src = "impl.createDocumentType('foo', 'bar', 'baz')", .ex = "[object DocumentType]" },
102 | .{ .src = "impl.hasFeature()", .ex = "true" },
103 | };
104 | try checkCases(js_env, &getImplementation);
105 | }
106 |
--------------------------------------------------------------------------------
/src/cdp/browser.zig:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
2 | //
3 | // Francis Bouvier
4 | // Pierre Tachoire
5 | //
6 | // This program is free software: you can redistribute it and/or modify
7 | // it under the terms of the GNU Affero General Public License as
8 | // published by the Free Software Foundation, either version 3 of the
9 | // License, or (at your option) any later version.
10 | //
11 | // This program is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | // GNU Affero General Public License for more details.
15 | //
16 | // You should have received a copy of the GNU Affero General Public License
17 | // along with this program. If not, see .
18 |
19 | const std = @import("std");
20 | const cdp = @import("cdp.zig");
21 |
22 | // TODO: hard coded data
23 | const PROTOCOL_VERSION = "1.3";
24 | const PRODUCT = "Chrome/124.0.6367.29";
25 | const REVISION = "@9e6ded5ac1ff5e38d930ae52bd9aec09bd1a68e4";
26 | const USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36";
27 | const JS_VERSION = "12.4.254.8";
28 | const DEV_TOOLS_WINDOW_ID = 1923710101;
29 |
30 | pub fn processMessage(cmd: anytype) !void {
31 | const action = std.meta.stringToEnum(enum {
32 | getVersion,
33 | setDownloadBehavior,
34 | getWindowForTarget,
35 | setWindowBounds,
36 | }, cmd.action) orelse return error.UnknownMethod;
37 |
38 | switch (action) {
39 | .getVersion => return getVersion(cmd),
40 | .setDownloadBehavior => return setDownloadBehavior(cmd),
41 | .getWindowForTarget => return getWindowForTarget(cmd),
42 | .setWindowBounds => return setWindowBounds(cmd),
43 | }
44 | }
45 |
46 | fn getVersion(cmd: anytype) !void {
47 | // TODO: pre-serialize?
48 | return cmd.sendResult(.{
49 | .protocolVersion = PROTOCOL_VERSION,
50 | .product = PRODUCT,
51 | .revision = REVISION,
52 | .userAgent = USER_AGENT,
53 | .jsVersion = JS_VERSION,
54 | }, .{ .include_session_id = false });
55 | }
56 |
57 | // TODO: noop method
58 | fn setDownloadBehavior(cmd: anytype) !void {
59 | // const params = (try cmd.params(struct {
60 | // behavior: []const u8,
61 | // browserContextId: ?[]const u8 = null,
62 | // downloadPath: ?[]const u8 = null,
63 | // eventsEnabled: ?bool = null,
64 | // })) orelse return error.InvalidParams;
65 |
66 | return cmd.sendResult(null, .{ .include_session_id = false });
67 | }
68 |
69 | fn getWindowForTarget(cmd: anytype) !void {
70 | // const params = (try cmd.params(struct {
71 | // targetId: ?[]const u8 = null,
72 | // })) orelse return error.InvalidParams;
73 |
74 | return cmd.sendResult(.{ .windowId = DEV_TOOLS_WINDOW_ID, .bounds = .{
75 | .windowState = "normal",
76 | } }, .{});
77 | }
78 |
79 | // TODO: noop method
80 | fn setWindowBounds(cmd: anytype) !void {
81 | return cmd.sendResult(null, .{});
82 | }
83 |
84 | const testing = @import("testing.zig");
85 | test "cdp.browser: getVersion" {
86 | var ctx = testing.context();
87 | defer ctx.deinit();
88 |
89 | try ctx.processMessage(.{
90 | .id = 32,
91 | .sessionID = "leto",
92 | .method = "Browser.getVersion",
93 | });
94 |
95 | try ctx.expectSentCount(1);
96 | try ctx.expectSentResult(.{
97 | .protocolVersion = PROTOCOL_VERSION,
98 | .product = PRODUCT,
99 | .revision = REVISION,
100 | .userAgent = USER_AGENT,
101 | .jsVersion = JS_VERSION,
102 | }, .{ .id = 32, .index = 0 });
103 | }
104 |
105 | test "cdp.browser: getWindowForTarget" {
106 | var ctx = testing.context();
107 | defer ctx.deinit();
108 |
109 | try ctx.processMessage(.{
110 | .id = 33,
111 | .sessionId = "leto",
112 | .method = "Browser.getWindowForTarget",
113 | });
114 |
115 | try ctx.expectSentCount(1);
116 | try ctx.expectSentResult(.{
117 | .windowId = DEV_TOOLS_WINDOW_ID,
118 | .bounds = .{ .windowState = "normal" },
119 | }, .{ .id = 33, .index = 0, .session_id = "leto" });
120 | }
121 |
--------------------------------------------------------------------------------
/src/html/location.zig:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
2 | //
3 | // Francis Bouvier
4 | // Pierre Tachoire
5 | //
6 | // This program is free software: you can redistribute it and/or modify
7 | // it under the terms of the GNU Affero General Public License as
8 | // published by the Free Software Foundation, either version 3 of the
9 | // License, or (at your option) any later version.
10 | //
11 | // This program is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | // GNU Affero General Public License for more details.
15 | //
16 | // You should have received a copy of the GNU Affero General Public License
17 | // along with this program. If not, see .
18 |
19 | const std = @import("std");
20 |
21 | const builtin = @import("builtin");
22 | const jsruntime = @import("jsruntime");
23 |
24 | const URL = @import("../url/url.zig").URL;
25 |
26 | const Case = jsruntime.test_utils.Case;
27 | const checkCases = jsruntime.test_utils.checkCases;
28 |
29 | // https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-location-interface
30 | pub const Location = struct {
31 | pub const mem_guarantied = true;
32 |
33 | url: ?*URL = null,
34 |
35 | pub fn deinit(_: *Location, _: std.mem.Allocator) void {}
36 |
37 | pub fn get_href(self: *Location, alloc: std.mem.Allocator) ![]const u8 {
38 | if (self.url) |u| return u.get_href(alloc);
39 |
40 | return "";
41 | }
42 |
43 | pub fn get_protocol(self: *Location, alloc: std.mem.Allocator) ![]const u8 {
44 | if (self.url) |u| return u.get_protocol(alloc);
45 |
46 | return "";
47 | }
48 |
49 | pub fn get_host(self: *Location, alloc: std.mem.Allocator) ![]const u8 {
50 | if (self.url) |u| return u.get_host(alloc);
51 |
52 | return "";
53 | }
54 |
55 | pub fn get_hostname(self: *Location) []const u8 {
56 | if (self.url) |u| return u.get_hostname();
57 |
58 | return "";
59 | }
60 |
61 | pub fn get_port(self: *Location, alloc: std.mem.Allocator) ![]const u8 {
62 | if (self.url) |u| return u.get_port(alloc);
63 |
64 | return "";
65 | }
66 |
67 | pub fn get_pathname(self: *Location) []const u8 {
68 | if (self.url) |u| return u.get_pathname();
69 |
70 | return "";
71 | }
72 |
73 | pub fn get_search(self: *Location, alloc: std.mem.Allocator) ![]const u8 {
74 | if (self.url) |u| return u.get_search(alloc);
75 |
76 | return "";
77 | }
78 |
79 | pub fn get_hash(self: *Location, alloc: std.mem.Allocator) ![]const u8 {
80 | if (self.url) |u| return u.get_hash(alloc);
81 |
82 | return "";
83 | }
84 |
85 | pub fn get_origin(self: *Location, alloc: std.mem.Allocator) ![]const u8 {
86 | if (self.url) |u| return u.get_origin(alloc);
87 |
88 | return "";
89 | }
90 |
91 | // TODO
92 | pub fn _assign(_: *Location, url: []const u8) !void {
93 | _ = url;
94 | }
95 |
96 | // TODO
97 | pub fn _replace(_: *Location, url: []const u8) !void {
98 | _ = url;
99 | }
100 |
101 | // TODO
102 | pub fn _reload(_: *Location) !void {}
103 |
104 | pub fn _toString(self: *Location, alloc: std.mem.Allocator) ![]const u8 {
105 | return try self.get_href(alloc);
106 | }
107 | };
108 |
109 | // Tests
110 | // -----
111 |
112 | pub fn testExecFn(
113 | _: std.mem.Allocator,
114 | js_env: *jsruntime.Env,
115 | ) anyerror!void {
116 | var location = [_]Case{
117 | .{ .src = "location.href", .ex = "https://lightpanda.io/opensource-browser/" },
118 | .{ .src = "document.location.href", .ex = "https://lightpanda.io/opensource-browser/" },
119 |
120 | .{ .src = "location.host", .ex = "lightpanda.io" },
121 | .{ .src = "location.hostname", .ex = "lightpanda.io" },
122 | .{ .src = "location.origin", .ex = "https://lightpanda.io" },
123 | .{ .src = "location.pathname", .ex = "/opensource-browser/" },
124 | .{ .src = "location.hash", .ex = "" },
125 | .{ .src = "location.port", .ex = "" },
126 | .{ .src = "location.search", .ex = "" },
127 | };
128 | try checkCases(js_env, &location);
129 | }
130 |
--------------------------------------------------------------------------------
/src/cdp/runtime.zig:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
2 | //
3 | // Francis Bouvier
4 | // Pierre Tachoire
5 | //
6 | // This program is free software: you can redistribute it and/or modify
7 | // it under the terms of the GNU Affero General Public License as
8 | // published by the Free Software Foundation, either version 3 of the
9 | // License, or (at your option) any later version.
10 | //
11 | // This program is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | // GNU Affero General Public License for more details.
15 | //
16 | // You should have received a copy of the GNU Affero General Public License
17 | // along with this program. If not, see .
18 |
19 | const std = @import("std");
20 | const cdp = @import("cdp.zig");
21 |
22 | pub fn processMessage(cmd: anytype) !void {
23 | const action = std.meta.stringToEnum(enum {
24 | enable,
25 | runIfWaitingForDebugger,
26 | evaluate,
27 | addBinding,
28 | callFunctionOn,
29 | releaseObject,
30 | }, cmd.action) orelse return error.UnknownMethod;
31 |
32 | switch (action) {
33 | .runIfWaitingForDebugger => return cmd.sendResult(null, .{}),
34 | else => return sendInspector(cmd, action),
35 | }
36 | }
37 |
38 | fn sendInspector(cmd: anytype, action: anytype) !void {
39 | // save script in file at debug mode
40 | if (std.log.defaultLogEnabled(.debug)) {
41 | try logInspector(cmd, action);
42 | }
43 |
44 | if (cmd.session_id) |s| {
45 | cmd.cdp.session_id = try cdp.SessionID.parse(s);
46 | }
47 |
48 | // remove awaitPromise true params
49 | // TODO: delete when Promise are correctly handled by zig-js-runtime
50 | if (action == .callFunctionOn or action == .evaluate) {
51 | const json = cmd.json;
52 | if (std.mem.indexOf(u8, json, "\"awaitPromise\":true")) |_| {
53 | // +1 because we'll be turning a true -> false
54 | const buf = try cmd.arena.alloc(u8, json.len + 1);
55 | _ = std.mem.replace(u8, json, "\"awaitPromise\":true", "\"awaitPromise\":false", buf);
56 | cmd.session.callInspector(buf);
57 | return;
58 | }
59 | }
60 |
61 | cmd.session.callInspector(cmd.json);
62 |
63 | if (cmd.id != null) {
64 | return cmd.sendResult(null, .{});
65 | }
66 | }
67 |
68 | pub const ExecutionContextCreated = struct {
69 | id: u64,
70 | origin: []const u8,
71 | name: []const u8,
72 | uniqueId: []const u8,
73 | auxData: ?AuxData = null,
74 |
75 | pub const AuxData = struct {
76 | isDefault: bool = true,
77 | type: []const u8 = "default",
78 | frameId: []const u8 = cdp.FRAME_ID,
79 | };
80 | };
81 |
82 | fn logInspector(cmd: anytype, action: anytype) !void {
83 | const script = switch (action) {
84 | .evaluate => blk: {
85 | const params = (try cmd.params(struct {
86 | expression: []const u8,
87 | // contextId: ?u8 = null,
88 | // returnByValue: ?bool = null,
89 | // awaitPromise: ?bool = null,
90 | // userGesture: ?bool = null,
91 | })) orelse return error.InvalidParams;
92 |
93 | break :blk params.expression;
94 | },
95 | .callFunctionOn => blk: {
96 | const params = (try cmd.params(struct {
97 | functionDeclaration: []const u8,
98 | // objectId: ?[]const u8 = null,
99 | // executionContextId: ?u8 = null,
100 | // arguments: ?[]struct {
101 | // value: ?[]const u8 = null,
102 | // objectId: ?[]const u8 = null,
103 | // } = null,
104 | // returnByValue: ?bool = null,
105 | // awaitPromise: ?bool = null,
106 | // userGesture: ?bool = null,
107 | })) orelse return error.InvalidParams;
108 |
109 | break :blk params.functionDeclaration;
110 | },
111 | else => return,
112 | };
113 | const id = cmd.id orelse return error.RequiredId;
114 | const name = try std.fmt.allocPrint(cmd.arena, "id_{d}.js", .{id});
115 |
116 | var dir = try std.fs.cwd().makeOpenPath("zig-cache/tmp", .{});
117 | defer dir.close();
118 |
119 | const f = try dir.createFile(name, .{});
120 | defer f.close();
121 | try f.writeAll(script);
122 | }
123 |
--------------------------------------------------------------------------------
/src/str/parser.zig:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
2 | //
3 | // Francis Bouvier
4 | // Pierre Tachoire
5 | //
6 | // This program is free software: you can redistribute it and/or modify
7 | // it under the terms of the GNU Affero General Public License as
8 | // published by the Free Software Foundation, either version 3 of the
9 | // License, or (at your option) any later version.
10 | //
11 | // This program is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | // GNU Affero General Public License for more details.
15 | //
16 | // You should have received a copy of the GNU Affero General Public License
17 | // along with this program. If not, see .
18 |
19 | // some utils to parser strings.
20 | const std = @import("std");
21 |
22 | pub const Reader = struct {
23 | pos: usize = 0,
24 | data: []const u8,
25 |
26 | pub fn until(self: *Reader, c: u8) []const u8 {
27 | const pos = self.pos;
28 | const data = self.data;
29 |
30 | const index = std.mem.indexOfScalarPos(u8, data, pos, c) orelse data.len;
31 | self.pos = index;
32 | return data[pos..index];
33 | }
34 |
35 | pub fn tail(self: *Reader) []const u8 {
36 | const pos = self.pos;
37 | const data = self.data;
38 | if (pos > data.len) {
39 | return "";
40 | }
41 | self.pos = data.len;
42 | return data[pos..];
43 | }
44 |
45 | pub fn skip(self: *Reader) bool {
46 | const pos = self.pos;
47 | if (pos >= self.data.len) {
48 | return false;
49 | }
50 | self.pos = pos + 1;
51 | return true;
52 | }
53 | };
54 |
55 | // converts a comptime-known string (i.e. null terminated) to an uint
56 | pub fn asUint(comptime string: anytype) AsUintReturn(string) {
57 | const byteLength = @bitSizeOf(@TypeOf(string.*)) / 8 - 1;
58 | const expectedType = *const [byteLength:0]u8;
59 | if (@TypeOf(string) != expectedType) {
60 | @compileError("expected : " ++ @typeName(expectedType) ++
61 | ", got: " ++ @typeName(@TypeOf(string)));
62 | }
63 |
64 | return @bitCast(@as(*const [byteLength]u8, string).*);
65 | }
66 |
67 | fn AsUintReturn(comptime string: anytype) type {
68 | return @Type(.{
69 | .Int = .{
70 | .bits = @bitSizeOf(@TypeOf(string.*)) - 8, // (- 8) to exclude sentinel 0
71 | .signedness = .unsigned,
72 | },
73 | });
74 | }
75 |
76 | const testing = std.testing;
77 | test "parser.Reader: skip" {
78 | var r = Reader{ .data = "foo" };
79 | try testing.expectEqual(true, r.skip());
80 | try testing.expectEqual(true, r.skip());
81 | try testing.expectEqual(true, r.skip());
82 | try testing.expectEqual(false, r.skip());
83 | try testing.expectEqual(false, r.skip());
84 | }
85 |
86 | test "parser.Reader: tail" {
87 | var r = Reader{ .data = "foo" };
88 | try testing.expectEqualStrings("foo", r.tail());
89 | try testing.expectEqualStrings("", r.tail());
90 | try testing.expectEqualStrings("", r.tail());
91 | }
92 |
93 | test "parser.Reader: until" {
94 | var r = Reader{ .data = "foo.bar.baz" };
95 | try testing.expectEqualStrings("foo", r.until('.'));
96 | _ = r.skip();
97 | try testing.expectEqualStrings("bar", r.until('.'));
98 | _ = r.skip();
99 | try testing.expectEqualStrings("baz", r.until('.'));
100 |
101 | r = Reader{ .data = "foo" };
102 | try testing.expectEqualStrings("foo", r.until('.'));
103 | try testing.expectEqualStrings("", r.tail());
104 |
105 | r = Reader{ .data = "" };
106 | try testing.expectEqualStrings("", r.until('.'));
107 | try testing.expectEqualStrings("", r.tail());
108 | }
109 |
110 | test "parser: asUint" {
111 | const ASCII_x = @as(u8, @bitCast([1]u8{'x'}));
112 | const ASCII_ab = @as(u16, @bitCast([2]u8{ 'a', 'b' }));
113 | const ASCII_xyz = @as(u24, @bitCast([3]u8{ 'x', 'y', 'z' }));
114 | const ASCII_abcd = @as(u32, @bitCast([4]u8{ 'a', 'b', 'c', 'd' }));
115 |
116 | try testing.expectEqual(ASCII_x, asUint("x"));
117 | try testing.expectEqual(ASCII_ab, asUint("ab"));
118 | try testing.expectEqual(ASCII_xyz, asUint("xyz"));
119 | try testing.expectEqual(ASCII_abcd, asUint("abcd"));
120 |
121 | try testing.expectEqual(u8, @TypeOf(asUint("x")));
122 | try testing.expectEqual(u16, @TypeOf(asUint("ab")));
123 | try testing.expectEqual(u24, @TypeOf(asUint("xyz")));
124 | try testing.expectEqual(u32, @TypeOf(asUint("abcd")));
125 | }
126 |
--------------------------------------------------------------------------------
/src/html/history.zig:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
2 | //
3 | // Francis Bouvier
4 | // Pierre Tachoire
5 | //
6 | // This program is free software: you can redistribute it and/or modify
7 | // it under the terms of the GNU Affero General Public License as
8 | // published by the Free Software Foundation, either version 3 of the
9 | // License, or (at your option) any later version.
10 | //
11 | // This program is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | // GNU Affero General Public License for more details.
15 | //
16 | // You should have received a copy of the GNU Affero General Public License
17 | // along with this program. If not, see .
18 |
19 | const std = @import("std");
20 |
21 | const builtin = @import("builtin");
22 | const jsruntime = @import("jsruntime");
23 |
24 | const Case = jsruntime.test_utils.Case;
25 | const checkCases = jsruntime.test_utils.checkCases;
26 |
27 | // https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-history-interface
28 | pub const History = struct {
29 | pub const mem_guarantied = true;
30 |
31 | const ScrollRestorationMode = enum {
32 | auto,
33 | manual,
34 | };
35 |
36 | scrollRestoration: ScrollRestorationMode = .auto,
37 | state: std.json.Value = .null,
38 |
39 | // count tracks the history length until we implement correctly pushstate.
40 | count: u32 = 0,
41 |
42 | pub fn get_length(self: *History) u32 {
43 | // TODO return the real history length value.
44 | return self.count;
45 | }
46 |
47 | pub fn get_scrollRestoration(self: *History) []const u8 {
48 | return switch (self.scrollRestoration) {
49 | .auto => "auto",
50 | .manual => "manual",
51 | };
52 | }
53 |
54 | pub fn set_scrollRestoration(self: *History, mode: []const u8) void {
55 | if (std.mem.eql(u8, "manual", mode)) self.scrollRestoration = .manual;
56 | if (std.mem.eql(u8, "auto", mode)) self.scrollRestoration = .auto;
57 | }
58 |
59 | pub fn get_state(self: *History) std.json.Value {
60 | return self.state;
61 | }
62 |
63 | // TODO implement the function
64 | // data must handle any argument. We could expect a std.json.Value but
65 | // https://github.com/lightpanda-io/zig-js-runtime/issues/267 is missing.
66 | pub fn _pushState(self: *History, data: []const u8, _: ?[]const u8, url: ?[]const u8) void {
67 | self.count += 1;
68 | _ = url;
69 | _ = data;
70 | }
71 |
72 | // TODO implement the function
73 | // data must handle any argument. We could expect a std.json.Value but
74 | // https://github.com/lightpanda-io/zig-js-runtime/issues/267 is missing.
75 | pub fn _replaceState(self: *History, data: []const u8, _: ?[]const u8, url: ?[]const u8) void {
76 | _ = self;
77 | _ = url;
78 | _ = data;
79 | }
80 |
81 | // TODO implement the function
82 | pub fn _go(self: *History, delta: ?i32) void {
83 | _ = self;
84 | _ = delta;
85 | }
86 |
87 | // TODO implement the function
88 | pub fn _back(self: *History) void {
89 | _ = self;
90 | }
91 |
92 | // TODO implement the function
93 | pub fn _forward(self: *History) void {
94 | _ = self;
95 | }
96 | };
97 |
98 | // Tests
99 | // -----
100 |
101 | pub fn testExecFn(
102 | _: std.mem.Allocator,
103 | js_env: *jsruntime.Env,
104 | ) anyerror!void {
105 | var history = [_]Case{
106 | .{ .src = "history.scrollRestoration", .ex = "auto" },
107 | .{ .src = "history.scrollRestoration = 'manual'", .ex = "manual" },
108 | .{ .src = "history.scrollRestoration = 'foo'", .ex = "foo" },
109 | .{ .src = "history.scrollRestoration", .ex = "manual" },
110 | .{ .src = "history.scrollRestoration = 'auto'", .ex = "auto" },
111 | .{ .src = "history.scrollRestoration", .ex = "auto" },
112 |
113 | .{ .src = "history.state", .ex = "null" },
114 |
115 | .{ .src = "history.pushState({}, null, '')", .ex = "undefined" },
116 |
117 | .{ .src = "history.replaceState({}, null, '')", .ex = "undefined" },
118 |
119 | .{ .src = "history.go()", .ex = "undefined" },
120 | .{ .src = "history.go(1)", .ex = "undefined" },
121 | .{ .src = "history.go(-1)", .ex = "undefined" },
122 |
123 | .{ .src = "history.forward()", .ex = "undefined" },
124 |
125 | .{ .src = "history.back()", .ex = "undefined" },
126 | };
127 | try checkCases(js_env, &history);
128 | }
129 |
--------------------------------------------------------------------------------
/src/css/README.md:
--------------------------------------------------------------------------------
1 | # css
2 |
3 | Lightpanda css implements CSS selectors parsing and matching in Zig.
4 | This package is a port of the Go lib [andybalholm/cascadia](https://github.com/andybalholm/cascadia).
5 |
6 | ## Usage
7 |
8 | ### Query parser
9 |
10 | ```zig
11 | const css = @import("css.zig");
12 |
13 | const selector = try css.parse(alloc, "h1", .{});
14 | defer selector.deinit(alloc);
15 | ```
16 |
17 | ### DOM tree match
18 |
19 | The lib expects a `Node` interface implementation to match your DOM tree.
20 |
21 | ```zig
22 | pub const Node = struct {
23 | pub fn firstChild(_: Node) !?Node {
24 | return error.TODO;
25 | }
26 |
27 | pub fn lastChild(_: Node) !?Node {
28 | return error.TODO;
29 | }
30 |
31 | pub fn nextSibling(_: Node) !?Node {
32 | return error.TODO;
33 | }
34 |
35 | pub fn prevSibling(_: Node) !?Node {
36 | return error.TODO;
37 | }
38 |
39 | pub fn parent(_: Node) !?Node {
40 | return error.TODO;
41 | }
42 |
43 | pub fn isElement(_: Node) bool {
44 | return false;
45 | }
46 |
47 | pub fn isDocument(_: Node) bool {
48 | return false;
49 | }
50 |
51 | pub fn isComment(_: Node) bool {
52 | return false;
53 | }
54 |
55 | pub fn isText(_: Node) bool {
56 | return false;
57 | }
58 |
59 | pub fn isEmptyText(_: Node) !bool {
60 | return error.TODO;
61 | }
62 |
63 | pub fn tag(_: Node) ![]const u8 {
64 | return error.TODO;
65 | }
66 |
67 | pub fn attr(_: Node, _: []const u8) !?[]const u8 {
68 | return error.TODO;
69 | }
70 |
71 | pub fn eql(_: Node, _: Node) bool {
72 | return false;
73 | }
74 | };
75 | ```
76 |
77 | You also need do define a `Matcher` implementing a `match` function to
78 | accumulate the results.
79 |
80 | ```zig
81 | const Matcher = struct {
82 | const Nodes = std.ArrayList(Node);
83 |
84 | nodes: Nodes,
85 |
86 | fn init(alloc: std.mem.Allocator) Matcher {
87 | return .{ .nodes = Nodes.init(alloc) };
88 | }
89 |
90 | fn deinit(m: *Matcher) void {
91 | m.nodes.deinit();
92 | }
93 |
94 | pub fn match(m: *Matcher, n: Node) !void {
95 | try m.nodes.append(n);
96 | }
97 | };
98 | ```
99 |
100 | Then you can use the lib itself.
101 |
102 | ```zig
103 | var matcher = Matcher.init(alloc);
104 | defer matcher.deinit();
105 |
106 | try css.matchAll(selector, node, &matcher);
107 | _ = try css.matchFirst(selector, node, &matcher); // returns true if a node matched.
108 | ```
109 |
110 | ## Features
111 |
112 | * [x] parse query selector
113 | * [x] `matchAll`
114 | * [x] `matchFirst`
115 | * [ ] specificity
116 |
117 | ### Selectors implemented
118 |
119 | #### Selectors
120 |
121 | * [x] Class selectors
122 | * [x] Id selectors
123 | * [x] Type selectors
124 | * [x] Universal selectors
125 | * [ ] Nesting selectors
126 |
127 | #### Combinators
128 |
129 | * [x] Child combinator
130 | * [ ] Column combinator
131 | * [x] Descendant combinator
132 | * [ ] Namespace combinator
133 | * [x] Next-sibling combinator
134 | * [x] Selector list combinator
135 | * [x] Subsequent-sibling combinator
136 |
137 | #### Attribute
138 |
139 | * [x] `[attr]`
140 | * [x] `[attr=value]`
141 | * [x] `[attr|=value]`
142 | * [x] `[attr^=value]`
143 | * [x] `[attr$=value]`
144 | * [ ] `[attr*=value]`
145 | * [x] `[attr operator value i]`
146 | * [ ] `[attr operator value s]`
147 |
148 | #### Pseudo classes
149 |
150 | * [ ] `:active`
151 | * [ ] `:any-link`
152 | * [ ] `:autofill`
153 | * [ ] `:blank Experimental`
154 | * [x] `:checked`
155 | * [ ] `:current Experimental`
156 | * [ ] `:default`
157 | * [ ] `:defined`
158 | * [ ] `:dir() Experimental`
159 | * [x] `:disabled`
160 | * [x] `:empty`
161 | * [x] `:enabled`
162 | * [ ] `:first`
163 | * [x] `:first-child`
164 | * [x] `:first-of-type`
165 | * [ ] `:focus`
166 | * [ ] `:focus-visible`
167 | * [ ] `:focus-within`
168 | * [ ] `:fullscreen`
169 | * [ ] `:future Experimental`
170 | * [x] `:has() Experimental`
171 | * [ ] `:host`
172 | * [ ] `:host()`
173 | * [ ] `:host-context() Experimental`
174 | * [ ] `:hover`
175 | * [ ] `:indeterminate`
176 | * [ ] `:in-range`
177 | * [ ] `:invalid`
178 | * [ ] `:is()`
179 | * [x] `:lang()`
180 | * [x] `:last-child`
181 | * [x] `:last-of-type`
182 | * [ ] `:left`
183 | * [x] `:link`
184 | * [ ] `:local-link Experimental`
185 | * [ ] `:modal`
186 | * [x] `:not()`
187 | * [x] `:nth-child()`
188 | * [x] `:nth-last-child()`
189 | * [x] `:nth-last-of-type()`
190 | * [x] `:nth-of-type()`
191 | * [x] `:only-child`
192 | * [x] `:only-of-type`
193 | * [ ] `:optional`
194 | * [ ] `:out-of-range`
195 | * [ ] `:past Experimental`
196 | * [ ] `:paused`
197 | * [ ] `:picture-in-picture`
198 | * [ ] `:placeholder-shown`
199 | * [ ] `:playing`
200 | * [ ] `:read-only`
201 | * [ ] `:read-write`
202 | * [ ] `:required`
203 | * [ ] `:right`
204 | * [x] `:root`
205 | * [ ] `:scope`
206 | * [ ] `:state() Experimental`
207 | * [ ] `:target`
208 | * [ ] `:target-within Experimental`
209 | * [ ] `:user-invalid Experimental`
210 | * [ ] `:valid`
211 | * [ ] `:visited`
212 | * [ ] `:where()`
213 | * [ ] `:contains()`
214 | * [ ] `:containsown()`
215 | * [ ] `:matched()`
216 | * [ ] `:matchesown()`
217 | * [x] `:root`
218 |
219 |
--------------------------------------------------------------------------------
/src/html/window.zig:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
2 | //
3 | // Francis Bouvier
4 | // Pierre Tachoire
5 | //
6 | // This program is free software: you can redistribute it and/or modify
7 | // it under the terms of the GNU Affero General Public License as
8 | // published by the Free Software Foundation, either version 3 of the
9 | // License, or (at your option) any later version.
10 | //
11 | // This program is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | // GNU Affero General Public License for more details.
15 | //
16 | // You should have received a copy of the GNU Affero General Public License
17 | // along with this program. If not, see .
18 |
19 | const std = @import("std");
20 |
21 | const parser = @import("netsurf");
22 | const jsruntime = @import("jsruntime");
23 | const Callback = jsruntime.Callback;
24 | const CallbackArg = jsruntime.CallbackArg;
25 | const Loop = jsruntime.Loop;
26 |
27 | const EventTarget = @import("../dom/event_target.zig").EventTarget;
28 | const Navigator = @import("navigator.zig").Navigator;
29 | const History = @import("history.zig").History;
30 | const Location = @import("location.zig").Location;
31 |
32 | const storage = @import("../storage/storage.zig");
33 |
34 | var emptyLocation = Location{};
35 |
36 | // https://dom.spec.whatwg.org/#interface-window-extensions
37 | // https://html.spec.whatwg.org/multipage/nav-history-apis.html#window
38 | pub const Window = struct {
39 | pub const prototype = *EventTarget;
40 | pub const mem_guarantied = true;
41 | pub const global_type = true;
42 |
43 | // Extend libdom event target for pure zig struct.
44 | base: parser.EventTargetTBase = parser.EventTargetTBase{},
45 |
46 | document: ?*parser.DocumentHTML = null,
47 | target: []const u8,
48 | history: History = .{},
49 | location: *Location = &emptyLocation,
50 |
51 | storageShelf: ?*storage.Shelf = null,
52 |
53 | // store a map between internal timeouts ids and pointers to uint.
54 | // the maximum number of possible timeouts is fixed.
55 | timeoutid: u32 = 0,
56 | timeoutids: [512]u64 = undefined,
57 |
58 | navigator: Navigator,
59 |
60 | pub fn create(target: ?[]const u8, navigator: ?Navigator) Window {
61 | return Window{
62 | .target = target orelse "",
63 | .navigator = navigator orelse .{},
64 | };
65 | }
66 |
67 | pub fn replaceLocation(self: *Window, loc: *Location) !void {
68 | self.location = loc;
69 |
70 | if (self.document != null) {
71 | try parser.documentHTMLSetLocation(Location, self.document.?, self.location);
72 | }
73 | }
74 |
75 | pub fn replaceDocument(self: *Window, doc: *parser.DocumentHTML) !void {
76 | self.document = doc;
77 | try parser.documentHTMLSetLocation(Location, doc, self.location);
78 | }
79 |
80 | pub fn setStorageShelf(self: *Window, shelf: *storage.Shelf) void {
81 | self.storageShelf = shelf;
82 | }
83 |
84 | pub fn get_window(self: *Window) *Window {
85 | return self;
86 | }
87 |
88 | pub fn get_navigator(self: *Window) *Navigator {
89 | return &self.navigator;
90 | }
91 |
92 | pub fn get_location(self: *Window) *Location {
93 | return self.location;
94 | }
95 |
96 | pub fn get_self(self: *Window) *Window {
97 | return self;
98 | }
99 |
100 | pub fn get_parent(self: *Window) *Window {
101 | return self;
102 | }
103 |
104 | pub fn get_document(self: *Window) ?*parser.DocumentHTML {
105 | return self.document;
106 | }
107 |
108 | pub fn get_history(self: *Window) *History {
109 | return &self.history;
110 | }
111 |
112 | pub fn get_name(self: *Window) []const u8 {
113 | return self.target;
114 | }
115 |
116 | pub fn get_localStorage(self: *Window) !*storage.Bottle {
117 | if (self.storageShelf == null) return parser.DOMError.NotSupported;
118 | return &self.storageShelf.?.bucket.local;
119 | }
120 |
121 | pub fn get_sessionStorage(self: *Window) !*storage.Bottle {
122 | if (self.storageShelf == null) return parser.DOMError.NotSupported;
123 | return &self.storageShelf.?.bucket.session;
124 | }
125 |
126 | // TODO handle callback arguments.
127 | pub fn _setTimeout(self: *Window, loop: *Loop, cbk: Callback, delay: ?u32) !u32 {
128 | if (self.timeoutid >= self.timeoutids.len) return error.TooMuchTimeout;
129 |
130 | const ddelay: u63 = delay orelse 0;
131 | const id = loop.timeout(ddelay * std.time.ns_per_ms, cbk);
132 |
133 | self.timeoutids[self.timeoutid] = id;
134 | defer self.timeoutid += 1;
135 |
136 | return self.timeoutid;
137 | }
138 |
139 | pub fn _clearTimeout(self: *Window, loop: *Loop, id: u32) void {
140 | // I do would prefer return an error in this case, but it seems some JS
141 | // uses invalid id, in particular id 0.
142 | // So we silently ignore invalid id for now.
143 | if (id >= self.timeoutid) return;
144 |
145 | loop.cancel(self.timeoutids[id], null);
146 | }
147 | };
148 |
--------------------------------------------------------------------------------
/src/cdp/testing.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 |
3 | const json = std.json;
4 | const Allocator = std.mem.Allocator;
5 |
6 | const Testing = @This();
7 |
8 | const cdp = @import("cdp.zig");
9 | const parser = @import("netsurf");
10 |
11 | pub const expectEqual = std.testing.expectEqual;
12 | pub const expectError = std.testing.expectError;
13 | pub const expectString = std.testing.expectEqualStrings;
14 |
15 | const Browser = struct {
16 | session: ?Session = null,
17 |
18 | pub fn init(_: Allocator, loop: anytype) Browser {
19 | _ = loop;
20 | return .{};
21 | }
22 |
23 | pub fn deinit(_: *const Browser) void {}
24 |
25 | pub fn newSession(self: *Browser, ctx: anytype) !*Session {
26 | _ = ctx;
27 |
28 | self.session = .{};
29 | return &self.session.?;
30 | }
31 | };
32 |
33 | const Session = struct {
34 | page: ?Page = null,
35 |
36 | pub fn currentPage(self: *Session) ?*Page {
37 | return &(self.page orelse return null);
38 | }
39 |
40 | pub fn createPage(self: *Session) !*Page {
41 | self.page = .{};
42 | return &self.page.?;
43 | }
44 |
45 | pub fn callInspector(self: *Session, msg: []const u8) void {
46 | _ = self;
47 | _ = msg;
48 | }
49 | };
50 |
51 | const Page = struct {
52 | doc: ?*parser.Document = null,
53 |
54 | pub fn navigate(self: *Page, url: []const u8, aux_data: []const u8) !void {
55 | _ = self;
56 | _ = url;
57 | _ = aux_data;
58 | }
59 |
60 | pub fn start(self: *Page, aux_data: []const u8) !void {
61 | _ = self;
62 | _ = aux_data;
63 | }
64 |
65 | pub fn end(self: *Page) void {
66 | _ = self;
67 | }
68 | };
69 |
70 | const Client = struct {
71 | allocator: Allocator,
72 | sent: std.ArrayListUnmanaged([]const u8) = .{},
73 |
74 | fn init(allocator: Allocator) Client {
75 | return .{
76 | .allocator = allocator,
77 | };
78 | }
79 |
80 | pub fn sendJSON(self: *Client, message: anytype, opts: json.StringifyOptions) !void {
81 | const serialized = try json.stringifyAlloc(self.allocator, message, opts);
82 | try self.sent.append(self.allocator, serialized);
83 | }
84 | };
85 |
86 | const TestCDP = cdp.CDPT(struct {
87 | pub const Browser = Testing.Browser;
88 | pub const Session = Testing.Session;
89 | pub const Client = Testing.Client;
90 | });
91 |
92 | const TestContext = struct {
93 | client: ?Client = null,
94 | cdp_: ?TestCDP = null,
95 | arena: std.heap.ArenaAllocator,
96 |
97 | pub fn deinit(self: *TestContext) void {
98 | if (self.cdp_) |*c| {
99 | c.deinit();
100 | }
101 | self.arena.deinit();
102 | }
103 |
104 | pub fn cdp(self: *TestContext) *TestCDP {
105 | if (self.cdp_ == null) {
106 | self.client = Client.init(self.arena.allocator());
107 | // Don't use the arena here. We want to detect leaks in CDP.
108 | // The arena is only for test-specific stuff
109 | self.cdp_ = TestCDP.init(std.testing.allocator, &self.client.?, "dummy-loop");
110 | }
111 | return &self.cdp_.?;
112 | }
113 |
114 | pub fn processMessage(self: *TestContext, msg: anytype) !void {
115 | var json_message: []const u8 = undefined;
116 | if (@typeInfo(@TypeOf(msg)) != .Pointer) {
117 | json_message = try std.json.stringifyAlloc(self.arena.allocator(), msg, .{});
118 | } else {
119 | // assume this is a string we want to send as-is, if it isn't, we'll
120 | // get a compile error, so no big deal.
121 | json_message = msg;
122 | }
123 | return self.cdp().processMessage(json_message);
124 | }
125 |
126 | pub fn expectSentCount(self: *TestContext, expected: usize) !void {
127 | try expectEqual(expected, self.client.?.sent.items.len);
128 | }
129 |
130 | const ExpectResultOpts = struct {
131 | id: ?usize = null,
132 | index: ?usize = null,
133 | session_id: ?[]const u8 = null,
134 | };
135 |
136 | pub fn expectSentResult(self: *TestContext, expected: anytype, opts: ExpectResultOpts) !void {
137 | const expected_result = .{
138 | .id = opts.id,
139 | .result = expected,
140 | .sessionId = opts.session_id,
141 | };
142 |
143 | const serialized = try json.stringifyAlloc(self.arena.allocator(), expected_result, .{
144 | .emit_null_optional_fields = false,
145 | });
146 |
147 | for (self.client.?.sent.items, 0..) |sent, i| {
148 | if (std.mem.eql(u8, sent, serialized) == false) {
149 | continue;
150 | }
151 | if (opts.index) |expected_index| {
152 | if (expected_index != i) {
153 | return error.MessageAtWrongIndex;
154 | }
155 | return;
156 | }
157 | }
158 | std.debug.print("Message not found. Expecting:\n{s}\n\nGot:\n", .{serialized});
159 | for (self.client.?.sent.items, 0..) |sent, i| {
160 | std.debug.print("#{d}\n{s}\n\n", .{ i, sent });
161 | }
162 | return error.MessageNotFound;
163 | }
164 | };
165 |
166 | pub fn context() TestContext {
167 | return .{
168 | .arena = std.heap.ArenaAllocator.init(std.testing.allocator),
169 | };
170 | }
171 |
--------------------------------------------------------------------------------
/src/xhr/event_target.zig:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
2 | //
3 | // Francis Bouvier
4 | // Pierre Tachoire
5 | //
6 | // This program is free software: you can redistribute it and/or modify
7 | // it under the terms of the GNU Affero General Public License as
8 | // published by the Free Software Foundation, either version 3 of the
9 | // License, or (at your option) any later version.
10 | //
11 | // This program is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | // GNU Affero General Public License for more details.
15 | //
16 | // You should have received a copy of the GNU Affero General Public License
17 | // along with this program. If not, see .
18 |
19 | const std = @import("std");
20 |
21 | const jsruntime = @import("jsruntime");
22 | const Callback = jsruntime.Callback;
23 |
24 | const EventTarget = @import("../dom/event_target.zig").EventTarget;
25 | const EventHandler = @import("../events/event.zig").EventHandler;
26 |
27 | const parser = @import("netsurf");
28 |
29 | const log = std.log.scoped(.xhr);
30 |
31 | pub const XMLHttpRequestEventTarget = struct {
32 | pub const prototype = *EventTarget;
33 | pub const mem_guarantied = true;
34 |
35 | // Extend libdom event target for pure zig struct.
36 | base: parser.EventTargetTBase = parser.EventTargetTBase{},
37 |
38 | onloadstart_cbk: ?Callback = null,
39 | onprogress_cbk: ?Callback = null,
40 | onabort_cbk: ?Callback = null,
41 | onload_cbk: ?Callback = null,
42 | ontimeout_cbk: ?Callback = null,
43 | onloadend_cbk: ?Callback = null,
44 |
45 | fn register(
46 | self: *XMLHttpRequestEventTarget,
47 | alloc: std.mem.Allocator,
48 | typ: []const u8,
49 | cbk: Callback,
50 | ) !void {
51 | try parser.eventTargetAddEventListener(
52 | @as(*parser.EventTarget, @ptrCast(self)),
53 | alloc,
54 | typ,
55 | EventHandler,
56 | .{ .cbk = cbk },
57 | false,
58 | );
59 | }
60 | fn unregister(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, typ: []const u8, cbk: Callback) !void {
61 | const et = @as(*parser.EventTarget, @ptrCast(self));
62 | // check if event target has already this listener
63 | const lst = try parser.eventTargetHasListener(et, typ, false, cbk.id());
64 | if (lst == null) {
65 | return;
66 | }
67 |
68 | // remove listener
69 | try parser.eventTargetRemoveEventListener(et, alloc, typ, lst.?, false);
70 | }
71 |
72 | pub fn get_onloadstart(self: *XMLHttpRequestEventTarget) ?Callback {
73 | return self.onloadstart_cbk;
74 | }
75 | pub fn get_onprogress(self: *XMLHttpRequestEventTarget) ?Callback {
76 | return self.onprogress_cbk;
77 | }
78 | pub fn get_onabort(self: *XMLHttpRequestEventTarget) ?Callback {
79 | return self.onabort_cbk;
80 | }
81 | pub fn get_onload(self: *XMLHttpRequestEventTarget) ?Callback {
82 | return self.onload_cbk;
83 | }
84 | pub fn get_ontimeout(self: *XMLHttpRequestEventTarget) ?Callback {
85 | return self.ontimeout_cbk;
86 | }
87 | pub fn get_onloadend(self: *XMLHttpRequestEventTarget) ?Callback {
88 | return self.onloadend_cbk;
89 | }
90 |
91 | pub fn set_onloadstart(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void {
92 | if (self.onloadstart_cbk) |cbk| try self.unregister(alloc, "loadstart", cbk);
93 | try self.register(alloc, "loadstart", handler);
94 | self.onloadstart_cbk = handler;
95 | }
96 | pub fn set_onprogress(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void {
97 | if (self.onprogress_cbk) |cbk| try self.unregister(alloc, "progress", cbk);
98 | try self.register(alloc, "progress", handler);
99 | self.onprogress_cbk = handler;
100 | }
101 | pub fn set_onabort(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void {
102 | if (self.onabort_cbk) |cbk| try self.unregister(alloc, "abort", cbk);
103 | try self.register(alloc, "abort", handler);
104 | self.onabort_cbk = handler;
105 | }
106 | pub fn set_onload(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void {
107 | if (self.onload_cbk) |cbk| try self.unregister(alloc, "load", cbk);
108 | try self.register(alloc, "load", handler);
109 | self.onload_cbk = handler;
110 | }
111 | pub fn set_ontimeout(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void {
112 | if (self.ontimeout_cbk) |cbk| try self.unregister(alloc, "timeout", cbk);
113 | try self.register(alloc, "timeout", handler);
114 | self.ontimeout_cbk = handler;
115 | }
116 | pub fn set_onloadend(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void {
117 | if (self.onloadend_cbk) |cbk| try self.unregister(alloc, "loadend", cbk);
118 | try self.register(alloc, "loadend", handler);
119 | self.onloadend_cbk = handler;
120 | }
121 |
122 | pub fn deinit(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator) void {
123 | parser.eventTargetRemoveAllEventListeners(@as(*parser.EventTarget, @ptrCast(self)), alloc) catch |e| {
124 | log.err("remove all listeners: {any}", .{e});
125 | };
126 | }
127 | };
128 |
--------------------------------------------------------------------------------
/src/id.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 |
3 | // Generates incrementing prefixed integers, i.e. CTX-1, CTX-2, CTX-3.
4 | // Wraps to 0 on overflow.
5 | // Many caveats for using this:
6 | // - Not thread-safe.
7 | // - Information leaking
8 | // - The slice returned by next() is only valid:
9 | // - while incrementor is valid
10 | // - until the next call to next()
11 | // On the positive, it's zero allocation
12 | fn Incrementing(comptime T: type, comptime prefix: []const u8) type {
13 | // +1 for the '-' separator
14 | const NUMERIC_START = prefix.len + 1;
15 | const MAX_BYTES = NUMERIC_START + switch (T) {
16 | u8 => 3,
17 | u16 => 5,
18 | u32 => 10,
19 | u64 => 20,
20 | else => @compileError("Incrementing must be given an unsigned int type, got: " ++ @typeName(T)),
21 | };
22 |
23 | const buffer = blk: {
24 | var b = [_]u8{0} ** MAX_BYTES;
25 | @memcpy(b[0..prefix.len], prefix);
26 | b[prefix.len] = '-';
27 | break :blk b;
28 | };
29 |
30 | const PrefixIntType = @Type(.{ .Int = .{
31 | .bits = NUMERIC_START * 8,
32 | .signedness = .unsigned,
33 | } });
34 |
35 | const PREFIX_INT_CODE: PrefixIntType = @bitCast(buffer[0..NUMERIC_START].*);
36 |
37 | return struct {
38 | current: T = 0,
39 | buffer: [MAX_BYTES]u8 = buffer,
40 |
41 | const Self = @This();
42 |
43 | pub fn next(self: *Self) []const u8 {
44 | const current = self.current;
45 | const n = current +% 1;
46 | defer self.current = n;
47 |
48 | const size = std.fmt.formatIntBuf(self.buffer[NUMERIC_START..], n, 10, .lower, .{});
49 | return self.buffer[0 .. NUMERIC_START + size];
50 | }
51 |
52 | // extracts the numeric portion from an ID
53 | pub fn parse(str: []const u8) !T {
54 | if (str.len <= NUMERIC_START) {
55 | return error.InvalidId;
56 | }
57 |
58 | if (@as(PrefixIntType, @bitCast(str[0..NUMERIC_START].*)) != PREFIX_INT_CODE) {
59 | return error.InvalidId;
60 | }
61 |
62 | return std.fmt.parseInt(T, str[NUMERIC_START..], 10) catch {
63 | return error.InvalidId;
64 | };
65 | }
66 | };
67 | }
68 |
69 | fn uuidv4(hex: []u8) void {
70 | std.debug.assert(hex.len == 36);
71 |
72 | var bin: [16]u8 = undefined;
73 | std.crypto.random.bytes(&bin);
74 | bin[6] = (bin[6] & 0x0f) | 0x40;
75 | bin[8] = (bin[8] & 0x3f) | 0x80;
76 |
77 | const alphabet = "0123456789abcdef";
78 |
79 | hex[8] = '-';
80 | hex[13] = '-';
81 | hex[18] = '-';
82 | hex[23] = '-';
83 |
84 | const encoded_pos = [16]u8{ 0, 2, 4, 6, 9, 11, 14, 16, 19, 21, 24, 26, 28, 30, 32, 34 };
85 | inline for (encoded_pos, 0..) |i, j| {
86 | hex[i + 0] = alphabet[bin[j] >> 4];
87 | hex[i + 1] = alphabet[bin[j] & 0x0f];
88 | }
89 | }
90 |
91 | const hex_to_nibble = [_]u8{0xff} ** 48 ++ [_]u8{
92 | 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
93 | 0x08, 0x09, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
94 | 0xff, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0xff,
95 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
96 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
97 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
98 | 0xff, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0xff,
99 | } ++ [_]u8{0xff} ** 152;
100 |
101 | const testing = std.testing;
102 | test "id: Incrementing.next" {
103 | var id = Incrementing(u16, "IDX"){};
104 | try testing.expectEqualStrings("IDX-1", id.next());
105 | try testing.expectEqualStrings("IDX-2", id.next());
106 | try testing.expectEqualStrings("IDX-3", id.next());
107 |
108 | // force a wrap
109 | id.current = 65533;
110 | try testing.expectEqualStrings("IDX-65534", id.next());
111 | try testing.expectEqualStrings("IDX-65535", id.next());
112 | try testing.expectEqualStrings("IDX-0", id.next());
113 | }
114 |
115 | test "id: Incrementing.parse" {
116 | const ReqId = Incrementing(u32, "REQ");
117 | try testing.expectError(error.InvalidId, ReqId.parse(""));
118 | try testing.expectError(error.InvalidId, ReqId.parse("R"));
119 | try testing.expectError(error.InvalidId, ReqId.parse("RE"));
120 | try testing.expectError(error.InvalidId, ReqId.parse("REQ"));
121 | try testing.expectError(error.InvalidId, ReqId.parse("REQ-"));
122 | try testing.expectError(error.InvalidId, ReqId.parse("REQ--1"));
123 | try testing.expectError(error.InvalidId, ReqId.parse("REQ--"));
124 | try testing.expectError(error.InvalidId, ReqId.parse("REQ-Nope"));
125 | try testing.expectError(error.InvalidId, ReqId.parse("REQ-4294967296"));
126 |
127 | try testing.expectEqual(0, try ReqId.parse("REQ-0"));
128 | try testing.expectEqual(99, try ReqId.parse("REQ-99"));
129 | try testing.expectEqual(4294967295, try ReqId.parse("REQ-4294967295"));
130 | }
131 |
132 | test "id: uuiv4" {
133 | const expectUUID = struct {
134 | fn expect(uuid: [36]u8) !void {
135 | for (uuid, 0..) |b, i| {
136 | switch (b) {
137 | '0'...'9', 'a'...'z' => {},
138 | '-' => {
139 | if (i != 8 and i != 13 and i != 18 and i != 23) {
140 | return error.InvalidEncoding;
141 | }
142 | },
143 | else => return error.InvalidHexEncoding,
144 | }
145 | }
146 | }
147 | }.expect;
148 |
149 | var arena = std.heap.ArenaAllocator.init(testing.allocator);
150 | defer arena.deinit();
151 | const allocator = arena.allocator();
152 |
153 | var seen = std.StringHashMapUnmanaged(void){};
154 | for (0..100) |_| {
155 | var hex: [36]u8 = undefined;
156 | uuidv4(&hex);
157 | try expectUUID(hex);
158 | try seen.put(allocator, try allocator.dupe(u8, &hex), {});
159 | }
160 | try testing.expectEqual(100, seen.count());
161 | }
162 |
--------------------------------------------------------------------------------
/CLA.md:
--------------------------------------------------------------------------------
1 | # Lightpanda (Selecy SAS) Grant and Contributor License Agreement (“Agreement”)
2 |
3 | This agreement is based on the Apache Software Foundation Contributor License
4 | Agreement. (v r190612)
5 |
6 | Thank you for your interest in software projects stewarded by Lightpanda
7 | (Selecy SAS) (“Lightpanda”). In order to clarify the intellectual property
8 | license granted with Contributions from any person or entity, Lightpanda must
9 | have a Contributor License Agreement (CLA) on file that has been agreed to by
10 | each Contributor, indicating agreement to the license terms below. This license
11 | is for your protection as a Contributor as well as the protection of Lightpanda
12 | and its users; it does not change your rights to use your own Contributions for
13 | any other purpose. This Agreement allows an individual to contribute to
14 | Lightpanda on that individual’s own behalf, or an entity (the “Corporation”) to
15 | submit Contributions to Lightpanda, to authorize Contributions submitted by its
16 | designated employees to Lightpanda, and to grant copyright and patent licenses
17 | thereto.
18 |
19 | You accept and agree to the following terms and conditions for Your present and
20 | future Contributions submitted to Lightpanda. Except for the license granted
21 | herein to Lightpanda and recipients of software distributed by Lightpanda, You
22 | reserve all right, title, and interest in and to Your Contributions.
23 |
24 | 1. Definitions. “You” (or “Your”) shall mean the copyright owner or legal
25 | entity authorized by the copyright owner that is making this Agreement with
26 | Lightpanda. For legal entities, the entity making a Contribution and all
27 | other entities that control, are controlled by, or are under common control
28 | with that entity are considered to be a single Contributor. For the purposes
29 | of this definition, “control” means (i) the power, direct or indirect, to
30 | cause the direction or management of such entity, whether by contract or
31 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
32 | outstanding shares, or (iii) beneficial ownership of such entity.
33 | “Contribution” shall mean any work, as well as any modifications or
34 | additions to an existing work, that is intentionally submitted by You to
35 | Lightpanda for inclusion in, or documentation of, any of the products owned
36 | or managed by Lightpanda (the “Work”). For the purposes of this definition,
37 | “submitted” means any form of electronic, verbal, or written communication
38 | sent to Lightpanda or its representatives, including but not limited to
39 | communication on electronic mailing lists, source code control systems (such
40 | as GitHub), and issue tracking systems that are managed by, or on behalf of,
41 | Lightpanda for the purpose of discussing and improving the Work, but
42 | excluding communication that is conspicuously marked or otherwise designated
43 | in writing by You as “Not a Contribution.”
44 |
45 | 2. Grant of Copyright License. Subject to the terms and conditions of this
46 | Agreement, You hereby grant to Lightpanda and to recipients of software
47 | distributed by Lightpanda a perpetual, worldwide, non-exclusive, no-charge,
48 | royalty-free, irrevocable copyright license to reproduce, prepare derivative
49 | works of, publicly display, publicly perform, sublicense, and distribute
50 | Your Contributions and such derivative works.
51 |
52 | 3. Grant of Patent License. Subject to the terms and conditions of this
53 | Agreement, You hereby grant to Lightpanda and to recipients of software
54 | distributed by Lightpanda a perpetual, worldwide, non-exclusive, no-charge,
55 | royalty-free, irrevocable (except as stated in this section) patent license
56 | to make, have made, use, offer to sell, sell, import, and otherwise transfer
57 | the Work, where such license applies only to those patent claims licensable
58 | by You that are necessarily infringed by Your Contribution(s) alone or by
59 | combination of Your Contribution(s) with the Work to which such
60 | Contribution(s) were submitted. If any entity institutes patent litigation
61 | against You or any other entity (including a cross-claim or counterclaim in
62 | a lawsuit) alleging that your Contribution, or the Work to which you have
63 | contributed, constitutes direct or contributory patent infringement, then
64 | any patent licenses granted to that entity under this Agreement for that
65 | Contribution or Work shall terminate as of the date such litigation is
66 | filed.
67 |
68 | 4. You represent that You are legally entitled to grant the above license. If
69 | You are an individual, and if Your employer(s) has rights to intellectual
70 | property that you create that includes Your Contributions, you represent
71 | that You have received permission to make Contributions on behalf of that
72 | employer, or that Your employer has waived such rights for your
73 | Contributions to Lightpanda. If You are a Corporation, any individual who
74 | makes a contribution from an account associated with You will be considered
75 | authorized to Contribute on Your behalf.
76 |
77 | 5. You represent that each of Your Contributions is Your original creation (see
78 | section 7 for submissions on behalf of others).
79 |
80 | 6. You are not expected to provide support for Your Contributions,except to the
81 | extent You desire to provide support. You may provide support for free, for
82 | a fee, or not at all. Unless required by applicable law or agreed to in
83 | writing, You provide Your Contributions on an “AS IS” BASIS, WITHOUT
84 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including,
85 | without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT,
86 | MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE.
87 |
88 | 7. Should You wish to submit work that is not Your original creation, You may
89 | submit it to Lightpanda separately from any Contribution, identifying the
90 | complete details of its source and of any license or other restriction
91 | (including, but not limited to, related patents, trademarks, and license
92 | agreements) of which you are personally aware, and conspicuously marking the
93 | work as “Submitted on behalf of a third-party: [named here]”.
94 |
--------------------------------------------------------------------------------
/src/css/css.zig:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
2 | //
3 | // Francis Bouvier
4 | // Pierre Tachoire
5 | //
6 | // This program is free software: you can redistribute it and/or modify
7 | // it under the terms of the GNU Affero General Public License as
8 | // published by the Free Software Foundation, either version 3 of the
9 | // License, or (at your option) any later version.
10 | //
11 | // This program is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | // GNU Affero General Public License for more details.
15 | //
16 | // You should have received a copy of the GNU Affero General Public License
17 | // along with this program. If not, see .
18 |
19 | // CSS Selector parser and query
20 | // This package is a rewrite in Zig of Cascadia CSS Selector parser.
21 | // see https://github.com/andybalholm/cascadia
22 | const std = @import("std");
23 | const Selector = @import("selector.zig").Selector;
24 | const parser = @import("parser.zig");
25 |
26 | // parse parse a selector string and returns the parsed result or an error.
27 | pub fn parse(alloc: std.mem.Allocator, s: []const u8, opts: parser.ParseOptions) parser.ParseError!Selector {
28 | var p = parser.Parser{ .s = s, .i = 0, .opts = opts };
29 | return p.parse(alloc);
30 | }
31 |
32 | // matchFirst call m.match with the first node that matches the selector s, from the
33 | // descendants of n and returns true. If none matches, it returns false.
34 | pub fn matchFirst(s: Selector, node: anytype, m: anytype) !bool {
35 | var c = try node.firstChild();
36 | while (true) {
37 | if (c == null) break;
38 |
39 | if (try s.match(c.?)) {
40 | try m.match(c.?);
41 | return true;
42 | }
43 |
44 | if (try matchFirst(s, c.?, m)) return true;
45 | c = try c.?.nextSibling();
46 | }
47 | return false;
48 | }
49 |
50 | // matchAll call m.match with the all the nodes that matches the selector s, from the
51 | // descendants of n.
52 | pub fn matchAll(s: Selector, node: anytype, m: anytype) !void {
53 | var c = try node.firstChild();
54 | while (true) {
55 | if (c == null) break;
56 |
57 | if (try s.match(c.?)) try m.match(c.?);
58 | try matchAll(s, c.?, m);
59 | c = try c.?.nextSibling();
60 | }
61 | }
62 |
63 | test "parse" {
64 | const alloc = std.testing.allocator;
65 |
66 | const testcases = [_][]const u8{
67 | "address",
68 | "*",
69 | "#foo",
70 | "li#t1",
71 | "*#t4",
72 | ".t1",
73 | "p.t1",
74 | "div.teST",
75 | ".t1.fail",
76 | "p.t1.t2",
77 | "p.--t1",
78 | "p.--t1.--t2",
79 | "p[title]",
80 | "div[class=\"red\" i]",
81 | "address[title=\"foo\"]",
82 | "address[title=\"FoOIgnoRECaSe\" i]",
83 | "address[title!=\"foo\"]",
84 | "address[title!=\"foo\" i]",
85 | "p[title!=\"FooBarUFoo\" i]",
86 | "[ \t title ~= foo ]",
87 | "p[title~=\"FOO\" i]",
88 | "p[title~=toofoo i]",
89 | "[title~=\"hello world\"]",
90 | "[title~=\"hello\" i]",
91 | "[title~=\"hello\" I]",
92 | "[lang|=\"en\"]",
93 | "[lang|=\"EN\" i]",
94 | "[lang|=\"EN\" i]",
95 | "[title^=\"foo\"]",
96 | "[title^=\"foo\" i]",
97 | "[title$=\"bar\"]",
98 | "[title$=\"BAR\" i]",
99 | "[title*=\"bar\"]",
100 | "[title*=\"BaRu\" i]",
101 | "[title*=\"BaRu\" I]",
102 | "p[class$=\" \"]",
103 | "p[class$=\"\"]",
104 | "p[class^=\" \"]",
105 | "p[class^=\"\"]",
106 | "p[class*=\" \"]",
107 | "p[class*=\"\"]",
108 | "input[name=Sex][value=F]",
109 | "table[border=\"0\"][cellpadding=\"0\"][cellspacing=\"0\"]",
110 | ".t1:not(.t2)",
111 | "div:not(.t1)",
112 | "div:not([class=\"t2\"])",
113 | "li:nth-child(odd)",
114 | "li:nth-child(even)",
115 | "li:nth-child(-n+2)",
116 | "li:nth-child(3n+1)",
117 | "li:nth-last-child(odd)",
118 | "li:nth-last-child(even)",
119 | "li:nth-last-child(-n+2)",
120 | "li:nth-last-child(3n+1)",
121 | "span:first-child",
122 | "span:last-child",
123 | "p:nth-of-type(2)",
124 | "p:nth-last-of-type(2)",
125 | "p:last-of-type",
126 | "p:first-of-type",
127 | "p:only-child",
128 | "p:only-of-type",
129 | ":empty",
130 | "div p",
131 | "div table p",
132 | "div > p",
133 | "p ~ p",
134 | "p + p",
135 | "li, p",
136 | "p +/*This is a comment*/ p",
137 | "p:contains(\"that wraps\")",
138 | "p:containsOwn(\"that wraps\")",
139 | ":containsOwn(\"inner\")",
140 | "p:containsOwn(\"block\")",
141 | "div:has(#p1)",
142 | "div:has(:containsOwn(\"2\"))",
143 | "body :has(:containsOwn(\"2\"))",
144 | "body :haschild(:containsOwn(\"2\"))",
145 | "p:matches([\\d])",
146 | "p:matches([a-z])",
147 | "p:matches([a-zA-Z])",
148 | "p:matches([^\\d])",
149 | "p:matches(^(0|a))",
150 | "p:matches(^\\d+$)",
151 | "p:not(:matches(^\\d+$))",
152 | "div :matchesOwn(^\\d+$)",
153 | "[href#=(fina)]:not([href#=(\\/\\/[^\\/]+untrusted)])",
154 | "[href#=(^https:\\/\\/[^\\/]*\\/?news)]",
155 | ":input",
156 | ":root",
157 | "*:root",
158 | "html:nth-child(1)",
159 | "*:root:first-child",
160 | "*:root:nth-child(1)",
161 | "a:not(:root)",
162 | "body > *:nth-child(3n+2)",
163 | "input:disabled",
164 | ":disabled",
165 | ":enabled",
166 | "div.class1, div.class2",
167 | };
168 |
169 | for (testcases) |tc| {
170 | const s = parse(alloc, tc, .{}) catch |e| {
171 | std.debug.print("query {s}", .{tc});
172 | return e;
173 | };
174 | defer s.deinit(alloc);
175 | }
176 | }
177 |
--------------------------------------------------------------------------------
/src/dom/token_list.zig:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
2 | //
3 | // Francis Bouvier
4 | // Pierre Tachoire
5 | //
6 | // This program is free software: you can redistribute it and/or modify
7 | // it under the terms of the GNU Affero General Public License as
8 | // published by the Free Software Foundation, either version 3 of the
9 | // License, or (at your option) any later version.
10 | //
11 | // This program is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | // GNU Affero General Public License for more details.
15 | //
16 | // You should have received a copy of the GNU Affero General Public License
17 | // along with this program. If not, see .
18 |
19 | const std = @import("std");
20 |
21 | const parser = @import("netsurf");
22 |
23 | const jsruntime = @import("jsruntime");
24 | const Case = jsruntime.test_utils.Case;
25 | const checkCases = jsruntime.test_utils.checkCases;
26 | const Variadic = jsruntime.Variadic;
27 |
28 | const DOMException = @import("exceptions.zig").DOMException;
29 |
30 | // https://dom.spec.whatwg.org/#domtokenlist
31 | pub const DOMTokenList = struct {
32 | pub const Self = parser.TokenList;
33 | pub const Exception = DOMException;
34 | pub const mem_guarantied = true;
35 |
36 | pub fn get_length(self: *parser.TokenList) !u32 {
37 | return parser.tokenListGetLength(self);
38 | }
39 |
40 | pub fn _item(self: *parser.TokenList, index: u32) !?[]const u8 {
41 | return parser.tokenListItem(self, index);
42 | }
43 |
44 | pub fn _contains(self: *parser.TokenList, token: []const u8) !bool {
45 | return parser.tokenListContains(self, token);
46 | }
47 |
48 | pub fn _add(self: *parser.TokenList, tokens: ?Variadic([]const u8)) !void {
49 | if (tokens == null) return;
50 | for (tokens.?.slice) |token| {
51 | try parser.tokenListAdd(self, token);
52 | }
53 | }
54 |
55 | pub fn _remove(self: *parser.TokenList, tokens: ?Variadic([]const u8)) !void {
56 | if (tokens == null) return;
57 | for (tokens.?.slice) |token| {
58 | try parser.tokenListRemove(self, token);
59 | }
60 | }
61 |
62 | /// If token is the empty string, then throw a "SyntaxError" DOMException.
63 | /// If token contains any ASCII whitespace, then throw an
64 | /// "InvalidCharacterError" DOMException.
65 | fn validateToken(token: []const u8) !void {
66 | if (token.len == 0) {
67 | return parser.DOMError.Syntax;
68 | }
69 | for (token) |c| {
70 | if (std.ascii.isWhitespace(c)) return parser.DOMError.InvalidCharacter;
71 | }
72 | }
73 |
74 | pub fn _toggle(self: *parser.TokenList, token: []const u8, force: ?bool) !bool {
75 | try validateToken(token);
76 | const exists = try parser.tokenListContains(self, token);
77 | if (exists) {
78 | if (force == null or force.? == false) {
79 | try parser.tokenListRemove(self, token);
80 | return false;
81 | }
82 | return true;
83 | }
84 |
85 | if (force == null or force.? == true) {
86 | try parser.tokenListAdd(self, token);
87 | return true;
88 | }
89 | return false;
90 | }
91 |
92 | pub fn _replace(self: *parser.TokenList, token: []const u8, new: []const u8) !bool {
93 | try validateToken(token);
94 | try validateToken(new);
95 | const exists = try parser.tokenListContains(self, token);
96 | if (!exists) return false;
97 | try parser.tokenListRemove(self, token);
98 | try parser.tokenListAdd(self, new);
99 | return true;
100 | }
101 |
102 | // TODO to implement.
103 | pub fn _supports(_: *parser.TokenList, token: []const u8) !bool {
104 | try validateToken(token);
105 | return error.TypeError;
106 | }
107 |
108 | pub fn get_value(self: *parser.TokenList) !?[]const u8 {
109 | return try parser.tokenListGetValue(self);
110 | }
111 | };
112 |
113 | // Tests
114 | // -----
115 |
116 | pub fn testExecFn(
117 | _: std.mem.Allocator,
118 | js_env: *jsruntime.Env,
119 | ) anyerror!void {
120 | var dynamiclist = [_]Case{
121 | .{ .src = "let gs = document.getElementById('para-empty')", .ex = "undefined" },
122 | .{ .src = "let cl = gs.classList", .ex = "undefined" },
123 | .{ .src = "gs.className", .ex = "ok empty" },
124 | .{ .src = "cl.value", .ex = "ok empty" },
125 | .{ .src = "cl.length", .ex = "2" },
126 | .{ .src = "gs.className = 'foo bar baz'", .ex = "foo bar baz" },
127 | .{ .src = "gs.className", .ex = "foo bar baz" },
128 | .{ .src = "cl.length", .ex = "3" },
129 | .{ .src = "gs.className = 'ok empty'", .ex = "ok empty" },
130 | .{ .src = "cl.length", .ex = "2" },
131 | };
132 | try checkCases(js_env, &dynamiclist);
133 |
134 | var testcases = [_]Case{
135 | .{ .src = "let cl2 = gs.classList", .ex = "undefined" },
136 | .{ .src = "cl2.length", .ex = "2" },
137 | .{ .src = "cl2.item(0)", .ex = "ok" },
138 | .{ .src = "cl2.item(1)", .ex = "empty" },
139 | .{ .src = "cl2.contains('ok')", .ex = "true" },
140 | .{ .src = "cl2.contains('nok')", .ex = "false" },
141 | .{ .src = "cl2.add('foo', 'bar', 'baz')", .ex = "undefined" },
142 | .{ .src = "cl2.length", .ex = "5" },
143 | .{ .src = "cl2.remove('foo', 'bar', 'baz')", .ex = "undefined" },
144 | .{ .src = "cl2.length", .ex = "2" },
145 | };
146 | try checkCases(js_env, &testcases);
147 |
148 | var toogle = [_]Case{
149 | .{ .src = "let cl3 = gs.classList", .ex = "undefined" },
150 | .{ .src = "cl3.toggle('ok')", .ex = "false" },
151 | .{ .src = "cl3.toggle('ok')", .ex = "true" },
152 | .{ .src = "cl3.length", .ex = "2" },
153 | };
154 | try checkCases(js_env, &toogle);
155 |
156 | var replace = [_]Case{
157 | .{ .src = "let cl4 = gs.classList", .ex = "undefined" },
158 | .{ .src = "cl4.replace('ok', 'nok')", .ex = "true" },
159 | .{ .src = "cl4.value", .ex = "empty nok" },
160 | .{ .src = "cl4.replace('nok', 'ok')", .ex = "true" },
161 | .{ .src = "cl4.value", .ex = "empty ok" },
162 | };
163 | try checkCases(js_env, &replace);
164 | }
165 |
--------------------------------------------------------------------------------
/src/dom/nodelist.zig:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
2 | //
3 | // Francis Bouvier
4 | // Pierre Tachoire
5 | //
6 | // This program is free software: you can redistribute it and/or modify
7 | // it under the terms of the GNU Affero General Public License as
8 | // published by the Free Software Foundation, either version 3 of the
9 | // License, or (at your option) any later version.
10 | //
11 | // This program is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | // GNU Affero General Public License for more details.
15 | //
16 | // You should have received a copy of the GNU Affero General Public License
17 | // along with this program. If not, see .
18 |
19 | const std = @import("std");
20 |
21 | const parser = @import("netsurf");
22 |
23 | const jsruntime = @import("jsruntime");
24 | const Callback = jsruntime.Callback;
25 | const CallbackResult = jsruntime.CallbackResult;
26 | const Case = jsruntime.test_utils.Case;
27 | const checkCases = jsruntime.test_utils.checkCases;
28 |
29 | const NodeUnion = @import("node.zig").Union;
30 | const Node = @import("node.zig").Node;
31 |
32 | const U32Iterator = @import("../iterator/iterator.zig").U32Iterator;
33 |
34 | const log = std.log.scoped(.nodelist);
35 |
36 | const DOMException = @import("exceptions.zig").DOMException;
37 |
38 | pub const Interfaces = .{
39 | NodeListIterator,
40 | NodeList,
41 | };
42 |
43 | pub const NodeListIterator = struct {
44 | pub const mem_guarantied = true;
45 |
46 | coll: *NodeList,
47 | index: u32 = 0,
48 |
49 | pub const Return = struct {
50 | value: ?NodeUnion,
51 | done: bool,
52 | };
53 |
54 | pub fn _next(self: *NodeListIterator) !Return {
55 | const e = try self.coll._item(self.index);
56 | if (e == null) {
57 | return Return{
58 | .value = null,
59 | .done = true,
60 | };
61 | }
62 |
63 | self.index += 1;
64 | return Return{
65 | .value = e,
66 | .done = false,
67 | };
68 | }
69 | };
70 |
71 | pub const NodeListEntriesIterator = struct {
72 | pub const mem_guarantied = true;
73 |
74 | coll: *NodeList,
75 | index: u32 = 0,
76 |
77 | pub const Return = struct {
78 | value: ?NodeUnion,
79 | done: bool,
80 | };
81 |
82 | pub fn _next(self: *NodeListEntriesIterator) !Return {
83 | const e = try self.coll._item(self.index);
84 | if (e == null) {
85 | return Return{
86 | .value = null,
87 | .done = true,
88 | };
89 | }
90 |
91 | self.index += 1;
92 | return Return{
93 | .value = e,
94 | .done = false,
95 | };
96 | }
97 | };
98 |
99 | // Nodelist is implemented in pure Zig b/c libdom's NodeList doesn't allow to
100 | // append nodes.
101 | // WEB IDL https://dom.spec.whatwg.org/#nodelist
102 | //
103 | // TODO: a Nodelist can be either static or live. But the current
104 | // implementation allows only static nodelist.
105 | // see https://dom.spec.whatwg.org/#old-style-collections
106 | pub const NodeList = struct {
107 | pub const mem_guarantied = true;
108 | pub const Exception = DOMException;
109 |
110 | const NodesArrayList = std.ArrayListUnmanaged(*parser.Node);
111 |
112 | nodes: NodesArrayList,
113 |
114 | pub fn init() NodeList {
115 | return NodeList{
116 | .nodes = NodesArrayList{},
117 | };
118 | }
119 |
120 | pub fn deinit(self: *NodeList, alloc: std.mem.Allocator) void {
121 | // TODO unref all nodes
122 | self.nodes.deinit(alloc);
123 | }
124 |
125 | pub fn append(self: *NodeList, alloc: std.mem.Allocator, node: *parser.Node) !void {
126 | try self.nodes.append(alloc, node);
127 | }
128 |
129 | pub fn get_length(self: *NodeList) u32 {
130 | return @intCast(self.nodes.items.len);
131 | }
132 |
133 | pub fn _item(self: *NodeList, index: u32) !?NodeUnion {
134 | if (index >= self.nodes.items.len) {
135 | return null;
136 | }
137 |
138 | const n = self.nodes.items[index];
139 | return try Node.toInterface(n);
140 | }
141 |
142 | pub fn _forEach(self: *NodeList, alloc: std.mem.Allocator, cbk: Callback) !void { // TODO handle thisArg
143 | var res = CallbackResult.init(alloc);
144 | defer res.deinit();
145 |
146 | for (self.nodes.items, 0..) |n, i| {
147 | const ii: u32 = @intCast(i);
148 | cbk.trycall(.{ n, ii, self }, &res) catch |e| {
149 | log.err("callback error: {s}", .{res.result orelse "unknown"});
150 | log.debug("{s}", .{res.stack orelse "no stack trace"});
151 |
152 | return e;
153 | };
154 | }
155 | }
156 |
157 | pub fn _keys(self: *NodeList) U32Iterator {
158 | return .{
159 | .length = self.get_length(),
160 | };
161 | }
162 |
163 | pub fn _values(self: *NodeList) NodeListIterator {
164 | return .{
165 | .coll = self,
166 | };
167 | }
168 |
169 | pub fn _symbol_iterator(self: *NodeList) NodeListIterator {
170 | return self._values();
171 | }
172 |
173 | // TODO entries() https://developer.mozilla.org/en-US/docs/Web/API/NodeList/entries
174 |
175 | pub fn postAttach(self: *NodeList, alloc: std.mem.Allocator, js_obj: jsruntime.JSObject) !void {
176 | const ln = self.get_length();
177 | var i: u32 = 0;
178 | while (i < ln) {
179 | defer i += 1;
180 | const k = try std.fmt.allocPrint(alloc, "{d}", .{i});
181 |
182 | const node = try self._item(i) orelse unreachable;
183 | try js_obj.set(k, node);
184 | }
185 | }
186 | };
187 |
188 | // Tests
189 | // -----
190 |
191 | pub fn testExecFn(
192 | _: std.mem.Allocator,
193 | js_env: *jsruntime.Env,
194 | ) anyerror!void {
195 | var childnodes = [_]Case{
196 | .{ .src = "let list = document.getElementById('content').childNodes", .ex = "undefined" },
197 | .{ .src = "list.length", .ex = "9" },
198 | .{ .src = "list[0].__proto__.constructor.name", .ex = "Text" },
199 | .{ .src =
200 | \\let i = 0;
201 | \\list.forEach(function (n, idx) {
202 | \\ i += idx;
203 | \\});
204 | \\i;
205 | , .ex = "36" },
206 | };
207 | try checkCases(js_env, &childnodes);
208 | }
209 |
--------------------------------------------------------------------------------
/src/dom/character_data.zig:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
2 | //
3 | // Francis Bouvier
4 | // Pierre Tachoire
5 | //
6 | // This program is free software: you can redistribute it and/or modify
7 | // it under the terms of the GNU Affero General Public License as
8 | // published by the Free Software Foundation, either version 3 of the
9 | // License, or (at your option) any later version.
10 | //
11 | // This program is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | // GNU Affero General Public License for more details.
15 | //
16 | // You should have received a copy of the GNU Affero General Public License
17 | // along with this program. If not, see .
18 |
19 | const std = @import("std");
20 |
21 | const jsruntime = @import("jsruntime");
22 | const Case = jsruntime.test_utils.Case;
23 | const checkCases = jsruntime.test_utils.checkCases;
24 |
25 | const parser = @import("netsurf");
26 |
27 | const Node = @import("node.zig").Node;
28 | const Comment = @import("comment.zig").Comment;
29 | const Text = @import("text.zig");
30 | const ProcessingInstruction = @import("processing_instruction.zig").ProcessingInstruction;
31 | const HTMLElem = @import("../html/elements.zig");
32 |
33 | // CharacterData interfaces
34 | pub const Interfaces = .{
35 | Comment,
36 | Text.Text,
37 | Text.Interfaces,
38 | ProcessingInstruction,
39 | };
40 |
41 | // CharacterData implementation
42 | pub const CharacterData = struct {
43 | pub const Self = parser.CharacterData;
44 | pub const prototype = *Node;
45 | pub const mem_guarantied = true;
46 |
47 | // JS funcs
48 | // --------
49 |
50 | // Read attributes
51 |
52 | pub fn get_length(self: *parser.CharacterData) !u32 {
53 | return try parser.characterDataLength(self);
54 | }
55 |
56 | pub fn get_nextElementSibling(self: *parser.CharacterData) !?HTMLElem.Union {
57 | const res = try parser.nodeNextElementSibling(parser.characterDataToNode(self));
58 | if (res == null) {
59 | return null;
60 | }
61 | return try HTMLElem.toInterface(HTMLElem.Union, res.?);
62 | }
63 |
64 | pub fn get_previousElementSibling(self: *parser.CharacterData) !?HTMLElem.Union {
65 | const res = try parser.nodePreviousElementSibling(parser.characterDataToNode(self));
66 | if (res == null) {
67 | return null;
68 | }
69 | return try HTMLElem.toInterface(HTMLElem.Union, res.?);
70 | }
71 |
72 | // Read/Write attributes
73 |
74 | pub fn get_data(self: *parser.CharacterData) ![]const u8 {
75 | return try parser.characterDataData(self);
76 | }
77 |
78 | pub fn set_data(self: *parser.CharacterData, data: []const u8) !void {
79 | return try parser.characterDataSetData(self, data);
80 | }
81 |
82 | // JS methods
83 | // ----------
84 |
85 | pub fn _appendData(self: *parser.CharacterData, data: []const u8) !void {
86 | return try parser.characterDataAppendData(self, data);
87 | }
88 |
89 | pub fn _deleteData(self: *parser.CharacterData, offset: u32, count: u32) !void {
90 | return try parser.characterDataDeleteData(self, offset, count);
91 | }
92 |
93 | pub fn _insertData(self: *parser.CharacterData, offset: u32, data: []const u8) !void {
94 | return try parser.characterDataInsertData(self, offset, data);
95 | }
96 |
97 | pub fn _replaceData(self: *parser.CharacterData, offset: u32, count: u32, data: []const u8) !void {
98 | return try parser.characterDataReplaceData(self, offset, count, data);
99 | }
100 |
101 | pub fn _substringData(self: *parser.CharacterData, offset: u32, count: u32) ![]const u8 {
102 | return try parser.characterDataSubstringData(self, offset, count);
103 | }
104 | };
105 |
106 | // Tests
107 | // -----
108 |
109 | pub fn testExecFn(
110 | _: std.mem.Allocator,
111 | js_env: *jsruntime.Env,
112 | ) anyerror!void {
113 | var get_data = [_]Case{
114 | .{ .src = "let link = document.getElementById('link')", .ex = "undefined" },
115 | .{ .src = "let cdata = link.firstChild", .ex = "undefined" },
116 | .{ .src = "cdata.data", .ex = "OK" },
117 | };
118 | try checkCases(js_env, &get_data);
119 |
120 | var set_data = [_]Case{
121 | .{ .src = "cdata.data = 'OK modified'", .ex = "OK modified" },
122 | .{ .src = "cdata.data === 'OK modified'", .ex = "true" },
123 | .{ .src = "cdata.data = 'OK'", .ex = "OK" },
124 | };
125 | try checkCases(js_env, &set_data);
126 |
127 | var get_length = [_]Case{
128 | .{ .src = "cdata.length === 2", .ex = "true" },
129 | };
130 | try checkCases(js_env, &get_length);
131 |
132 | var get_next_elem_sibling = [_]Case{
133 | .{ .src = "cdata.nextElementSibling === null", .ex = "true" },
134 | // create a next element
135 | .{ .src = "let next = document.createElement('a')", .ex = "undefined" },
136 | .{ .src = "link.appendChild(next, cdata) !== undefined", .ex = "true" },
137 | .{ .src = "cdata.nextElementSibling.localName === 'a' ", .ex = "true" },
138 | };
139 | try checkCases(js_env, &get_next_elem_sibling);
140 |
141 | var get_prev_elem_sibling = [_]Case{
142 | .{ .src = "cdata.previousElementSibling === null", .ex = "true" },
143 | // create a prev element
144 | .{ .src = "let prev = document.createElement('div')", .ex = "undefined" },
145 | .{ .src = "link.insertBefore(prev, cdata) !== undefined", .ex = "true" },
146 | .{ .src = "cdata.previousElementSibling.localName === 'div' ", .ex = "true" },
147 | };
148 | try checkCases(js_env, &get_prev_elem_sibling);
149 |
150 | var append_data = [_]Case{
151 | .{ .src = "cdata.appendData(' modified')", .ex = "undefined" },
152 | .{ .src = "cdata.data === 'OK modified' ", .ex = "true" },
153 | };
154 | try checkCases(js_env, &append_data);
155 |
156 | var delete_data = [_]Case{
157 | .{ .src = "cdata.deleteData('OK'.length, ' modified'.length)", .ex = "undefined" },
158 | .{ .src = "cdata.data == 'OK'", .ex = "true" },
159 | };
160 | try checkCases(js_env, &delete_data);
161 |
162 | var insert_data = [_]Case{
163 | .{ .src = "cdata.insertData('OK'.length-1, 'modified')", .ex = "undefined" },
164 | .{ .src = "cdata.data == 'OmodifiedK'", .ex = "true" },
165 | };
166 | try checkCases(js_env, &insert_data);
167 |
168 | var replace_data = [_]Case{
169 | .{ .src = "cdata.replaceData('OK'.length-1, 'modified'.length, 'replaced')", .ex = "undefined" },
170 | .{ .src = "cdata.data == 'OreplacedK'", .ex = "true" },
171 | };
172 | try checkCases(js_env, &replace_data);
173 |
174 | var substring_data = [_]Case{
175 | .{ .src = "cdata.substringData('OK'.length-1, 'replaced'.length) == 'replaced'", .ex = "true" },
176 | .{ .src = "cdata.substringData('OK'.length-1, 0) == ''", .ex = "true" },
177 | };
178 | try checkCases(js_env, &substring_data);
179 | }
180 |
--------------------------------------------------------------------------------
/src/wpt/run.zig:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
2 | //
3 | // Francis Bouvier
4 | // Pierre Tachoire
5 | //
6 | // This program is free software: you can redistribute it and/or modify
7 | // it under the terms of the GNU Affero General Public License as
8 | // published by the Free Software Foundation, either version 3 of the
9 | // License, or (at your option) any later version.
10 | //
11 | // This program is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | // GNU Affero General Public License for more details.
15 | //
16 | // You should have received a copy of the GNU Affero General Public License
17 | // along with this program. If not, see .
18 |
19 | const std = @import("std");
20 | const fspath = std.fs.path;
21 |
22 | const FileLoader = @import("fileloader.zig").FileLoader;
23 |
24 | const parser = @import("netsurf");
25 |
26 | const jsruntime = @import("jsruntime");
27 | const Loop = jsruntime.Loop;
28 | const Env = jsruntime.Env;
29 | const Window = @import("../html/window.zig").Window;
30 | const storage = @import("../storage/storage.zig");
31 | const Client = @import("asyncio").Client;
32 |
33 | const Types = @import("../main_wpt.zig").Types;
34 | const UserContext = @import("../main_wpt.zig").UserContext;
35 |
36 | const polyfill = @import("../polyfill/polyfill.zig");
37 |
38 | // runWPT parses the given HTML file, starts a js env and run the first script
39 | // tags containing javascript sources.
40 | // It loads first the js libs files.
41 | pub fn run(arena: *std.heap.ArenaAllocator, comptime dir: []const u8, f: []const u8, loader: *FileLoader) !Res {
42 | const alloc = arena.allocator();
43 | try parser.init();
44 | defer parser.deinit();
45 |
46 | // document
47 | const file = try std.fs.cwd().openFile(f, .{});
48 | defer file.close();
49 |
50 | const html_doc = try parser.documentHTMLParse(file.reader(), "UTF-8");
51 |
52 | const dirname = fspath.dirname(f[dir.len..]) orelse unreachable;
53 |
54 | // create JS env
55 | var loop = try Loop.init(alloc);
56 | defer loop.deinit();
57 |
58 | var cli = Client{ .allocator = alloc };
59 | defer cli.deinit();
60 |
61 | var js_env: Env = undefined;
62 | Env.init(&js_env, alloc, &loop, UserContext{
63 | .document = html_doc,
64 | .httpClient = &cli,
65 | });
66 | defer js_env.deinit();
67 |
68 | var storageShelf = storage.Shelf.init(alloc);
69 | defer storageShelf.deinit();
70 |
71 | // load user-defined types in JS env
72 | var js_types: [Types.len]usize = undefined;
73 | try js_env.load(&js_types);
74 |
75 | // start JS env
76 | try js_env.start();
77 | defer js_env.stop();
78 |
79 | // load polyfills
80 | try polyfill.load(alloc, js_env);
81 |
82 | // display console logs
83 | defer {
84 | const res = evalJS(js_env, alloc, "console.join('\\n');", "console") catch unreachable;
85 | defer res.deinit(alloc);
86 |
87 | if (res.msg != null and res.msg.?.len > 0) {
88 | std.debug.print("-- CONSOLE LOG\n{s}\n--\n", .{res.msg.?});
89 | }
90 | }
91 |
92 | // setup global env vars.
93 | var window = Window.create(null, null);
94 | try window.replaceDocument(html_doc);
95 | window.setStorageShelf(&storageShelf);
96 | try js_env.bindGlobal(&window);
97 |
98 | const init =
99 | \\console = [];
100 | \\console.log = function () {
101 | \\ console.push(...arguments);
102 | \\};
103 | \\console.debug = function () {
104 | \\ console.push("debug", ...arguments);
105 | \\};
106 | ;
107 | var res = try evalJS(js_env, alloc, init, "init");
108 | if (!res.ok) return res;
109 | res.deinit(alloc);
110 |
111 | // loop hover the scripts.
112 | const doc = parser.documentHTMLToDocument(html_doc);
113 | const scripts = try parser.documentGetElementsByTagName(doc, "script");
114 | const slen = try parser.nodeListLength(scripts);
115 | for (0..slen) |i| {
116 | const s = (try parser.nodeListItem(scripts, @intCast(i))).?;
117 |
118 | // If the script contains an src attribute, load it.
119 | if (try parser.elementGetAttribute(@as(*parser.Element, @ptrCast(s)), "src")) |src| {
120 | var path = src;
121 | if (!std.mem.startsWith(u8, src, "/")) {
122 | // no need to free path, thanks to the arena.
123 | path = try fspath.join(alloc, &.{ "/", dirname, path });
124 | }
125 |
126 | res = try evalJS(js_env, alloc, try loader.get(path), src);
127 | if (!res.ok) return res;
128 | res.deinit(alloc);
129 | }
130 |
131 | // If the script as a source text, execute it.
132 | const src = try parser.nodeTextContent(s) orelse continue;
133 | res = try evalJS(js_env, alloc, src, "");
134 | if (!res.ok) return res;
135 | res.deinit(alloc);
136 | }
137 |
138 | // Mark tests as ready to run.
139 | const loadevt = try parser.eventCreate();
140 | defer parser.eventDestroy(loadevt);
141 |
142 | try parser.eventInit(loadevt, "load", .{});
143 | _ = try parser.eventTargetDispatchEvent(
144 | parser.toEventTarget(Window, &window),
145 | loadevt,
146 | );
147 |
148 | // wait for all async executions
149 | var try_catch: jsruntime.TryCatch = undefined;
150 | try_catch.init(js_env);
151 | defer try_catch.deinit();
152 | js_env.wait() catch {
153 | return .{
154 | .ok = false,
155 | .msg = try try_catch.err(alloc, js_env),
156 | };
157 | };
158 |
159 | // Check the final test status.
160 | res = try evalJS(js_env, alloc, "report.status;", "teststatus");
161 | if (!res.ok) return res;
162 | res.deinit(alloc);
163 |
164 | // return the detailed result.
165 | return try evalJS(js_env, alloc, "report.log", "teststatus");
166 | }
167 |
168 | pub const Res = struct {
169 | ok: bool,
170 | msg: ?[]const u8,
171 |
172 | pub fn deinit(res: Res, alloc: std.mem.Allocator) void {
173 | if (res.msg) |msg| {
174 | alloc.free(msg);
175 | }
176 | }
177 | };
178 |
179 | fn evalJS(env: jsruntime.Env, alloc: std.mem.Allocator, script: []const u8, name: ?[]const u8) !Res {
180 | var try_catch: jsruntime.TryCatch = undefined;
181 | try_catch.init(env);
182 | defer try_catch.deinit();
183 |
184 | const v = env.exec(script, name) catch {
185 | return .{
186 | .ok = false,
187 | .msg = try try_catch.err(alloc, env),
188 | };
189 | };
190 |
191 | return .{
192 | .ok = true,
193 | .msg = try v.toString(alloc, env),
194 | };
195 | }
196 |
197 | // browse the path to find the tests list.
198 | pub fn find(allocator: std.mem.Allocator, comptime path: []const u8, list: *std.ArrayList([]const u8)) !void {
199 | var dir = try std.fs.cwd().openDir(path, .{ .iterate = true, .no_follow = true });
200 | defer dir.close();
201 |
202 | var walker = try dir.walk(allocator);
203 | defer walker.deinit();
204 |
205 | while (try walker.next()) |entry| {
206 | if (entry.kind != .file) {
207 | continue;
208 | }
209 | if (!std.mem.endsWith(u8, entry.basename, ".html") and !std.mem.endsWith(u8, entry.basename, ".htm")) {
210 | continue;
211 | }
212 |
213 | try list.append(try fspath.join(allocator, &.{ path, entry.path }));
214 | }
215 | }
216 |
--------------------------------------------------------------------------------
/src/generate.zig:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
2 | //
3 | // Francis Bouvier
4 | // Pierre Tachoire
5 | //
6 | // This program is free software: you can redistribute it and/or modify
7 | // it under the terms of the GNU Affero General Public License as
8 | // published by the Free Software Foundation, either version 3 of the
9 | // License, or (at your option) any later version.
10 | //
11 | // This program is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | // GNU Affero General Public License for more details.
15 | //
16 | // You should have received a copy of the GNU Affero General Public License
17 | // along with this program. If not, see .
18 |
19 | const std = @import("std");
20 |
21 | // ----
22 | const Type = std.builtin.Type;
23 |
24 | // Union
25 | // -----
26 |
27 | // Generate a flatten tagged Union from a Tuple
28 | pub fn Union(interfaces: anytype) type {
29 | // @setEvalBranchQuota(10000);
30 | const tuple = Tuple(interfaces){};
31 | const fields = std.meta.fields(@TypeOf(tuple));
32 |
33 | const tag_type = switch (fields.len) {
34 | 0 => unreachable,
35 | 1 => u0,
36 | 2 => u1,
37 | 3...4 => u2,
38 | 5...8 => u3,
39 | 9...16 => u4,
40 | 17...32 => u5,
41 | 33...64 => u6,
42 | 65...128 => u7,
43 | 129...256 => u8,
44 | else => @compileError("Too many interfaces to generate union"),
45 | };
46 |
47 | // second iteration to generate tags
48 | var enum_fields: [fields.len]Type.EnumField = undefined;
49 | for (fields, 0..) |field, index| {
50 | const member = @field(tuple, field.name);
51 | const full_name = @typeName(member);
52 | const separator = std.mem.lastIndexOfScalar(u8, full_name, '.') orelse unreachable;
53 | const name = full_name[separator + 1 ..];
54 | enum_fields[index] = .{
55 | .name = name ++ "",
56 | .value = index,
57 | };
58 | }
59 |
60 | const enum_info = Type.Enum{
61 | .tag_type = tag_type,
62 | .fields = &enum_fields,
63 | .decls = &.{},
64 | .is_exhaustive = true,
65 | };
66 | const enum_T = @Type(.{ .Enum = enum_info });
67 |
68 | // third iteration to generate union type
69 | var union_fields: [fields.len]Type.UnionField = undefined;
70 | for (fields, enum_fields, 0..) |field, e, index| {
71 | var FT = @field(tuple, field.name);
72 | if (@hasDecl(FT, "Self")) {
73 | FT = *(@field(FT, "Self"));
74 | }
75 | union_fields[index] = .{
76 | .type = FT,
77 | .name = e.name,
78 | .alignment = @alignOf(FT),
79 | };
80 | }
81 |
82 | return @Type(.{ .Union = .{
83 | .layout = .auto,
84 | .tag_type = enum_T,
85 | .fields = &union_fields,
86 | .decls = &.{},
87 | } });
88 | }
89 |
90 | // Tuple
91 | // -----
92 |
93 | // Flattens and depuplicates a list of nested tuples. For example
94 | // input: {A, B, {C, B, D}, {A, E}}
95 | // output {A, B, C, D, E}
96 | pub fn Tuple(args: anytype) type {
97 | @setEvalBranchQuota(100000);
98 |
99 | const count = countInterfaces(args, 0);
100 | var interfaces: [count]type = undefined;
101 | _ = flattenInterfaces(args, &interfaces, 0);
102 |
103 | const unfiltered_count, const filter_set = filterMap(count, interfaces);
104 |
105 | var field_index: usize = 0;
106 | var fields: [unfiltered_count]Type.StructField = undefined;
107 |
108 | for (filter_set, 0..) |filter, i| {
109 | if (filter) {
110 | continue;
111 | }
112 | fields[field_index] = .{
113 | .name = std.fmt.comptimePrint("{d}", .{field_index}),
114 | .type = type,
115 | // has to be true in order to properly capture the default value
116 | .is_comptime = true,
117 | .alignment = @alignOf(type),
118 | .default_value = @ptrCast(&interfaces[i]),
119 | };
120 | field_index += 1;
121 | }
122 |
123 | return @Type(.{ .Struct = .{
124 | .layout = .auto,
125 | .fields = &fields,
126 | .decls = &.{},
127 | .is_tuple = true,
128 | } });
129 | }
130 |
131 | fn countInterfaces(args: anytype, count: usize) usize {
132 | var new_count = count;
133 | for (@typeInfo(@TypeOf(args)).Struct.fields) |f| {
134 | const member = @field(args, f.name);
135 | if (@TypeOf(member) == type) {
136 | new_count += 1;
137 | } else {
138 | new_count = countInterfaces(member, new_count);
139 | }
140 | }
141 | return new_count;
142 | }
143 |
144 | fn flattenInterfaces(args: anytype, interfaces: []type, index: usize) usize {
145 | var new_index = index;
146 | for (@typeInfo(@TypeOf(args)).Struct.fields) |f| {
147 | const member = @field(args, f.name);
148 | if (@TypeOf(member) == type) {
149 | interfaces[new_index] = member;
150 | new_index += 1;
151 | } else {
152 | new_index = flattenInterfaces(member, interfaces, new_index);
153 | }
154 | }
155 | return new_index;
156 | }
157 |
158 | fn filterMap(comptime count: usize, interfaces: [count]type) struct { usize, [count]bool } {
159 | var map: [count]bool = undefined;
160 | var unfiltered_count: usize = 0;
161 | outer: for (interfaces, 0..) |iface, i| {
162 | for (interfaces[i + 1 ..]) |check| {
163 | if (iface == check) {
164 | map[i] = true;
165 | continue :outer;
166 | }
167 | }
168 | map[i] = false;
169 | unfiltered_count += 1;
170 | }
171 | return .{ unfiltered_count, map };
172 | }
173 |
174 | test "generate.Union" {
175 | const Astruct = struct {
176 | pub const Self = Other;
177 | const Other = struct {};
178 | };
179 |
180 | const Bstruct = struct {
181 | value: u8 = 0,
182 | };
183 |
184 | const Cstruct = struct {
185 | value: u8 = 0,
186 | };
187 |
188 | const value = Union(.{ Astruct, Bstruct, .{Cstruct} });
189 | const ti = @typeInfo(value).Union;
190 | try std.testing.expectEqual(3, ti.fields.len);
191 | try std.testing.expectEqualStrings("*generate.test.generate.Union.Astruct.Other", @typeName(ti.fields[0].type));
192 | try std.testing.expectEqualStrings(ti.fields[0].name, "Astruct");
193 | try std.testing.expectEqual(Bstruct, ti.fields[1].type);
194 | try std.testing.expectEqualStrings(ti.fields[1].name, "Bstruct");
195 | try std.testing.expectEqual(Cstruct, ti.fields[2].type);
196 | try std.testing.expectEqualStrings(ti.fields[2].name, "Cstruct");
197 | }
198 |
199 | test "generate.Tuple" {
200 | const Astruct = struct {};
201 |
202 | const Bstruct = struct {
203 | value: u8 = 0,
204 | };
205 |
206 | const Cstruct = struct {
207 | value: u8 = 0,
208 | };
209 |
210 | {
211 | const tuple = Tuple(.{ Astruct, Bstruct }){};
212 | const ti = @typeInfo(@TypeOf(tuple)).Struct;
213 | try std.testing.expectEqual(true, ti.is_tuple);
214 | try std.testing.expectEqual(2, ti.fields.len);
215 | try std.testing.expectEqual(Astruct, tuple.@"0");
216 | try std.testing.expectEqual(Bstruct, tuple.@"1");
217 | }
218 |
219 | {
220 | // dedupe
221 | const tuple = Tuple(.{ Cstruct, Astruct, .{Astruct}, Bstruct, .{ Astruct, .{ Astruct, Bstruct } } }){};
222 | const ti = @typeInfo(@TypeOf(tuple)).Struct;
223 | try std.testing.expectEqual(true, ti.is_tuple);
224 | try std.testing.expectEqual(3, ti.fields.len);
225 | try std.testing.expectEqual(Cstruct, tuple.@"0");
226 | try std.testing.expectEqual(Astruct, tuple.@"1");
227 | try std.testing.expectEqual(Bstruct, tuple.@"2");
228 | }
229 | }
230 |
--------------------------------------------------------------------------------
/src/browser/dump.zig:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
2 | //
3 | // Francis Bouvier
4 | // Pierre Tachoire
5 | //
6 | // This program is free software: you can redistribute it and/or modify
7 | // it under the terms of the GNU Affero General Public License as
8 | // published by the Free Software Foundation, either version 3 of the
9 | // License, or (at your option) any later version.
10 | //
11 | // This program is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | // GNU Affero General Public License for more details.
15 | //
16 | // You should have received a copy of the GNU Affero General Public License
17 | // along with this program. If not, see .
18 |
19 | const std = @import("std");
20 | const File = std.fs.File;
21 |
22 | const parser = @import("netsurf");
23 | const Walker = @import("../dom/walker.zig").WalkerChildren;
24 |
25 | // writer must be a std.io.Writer
26 | pub fn writeHTML(doc: *parser.Document, writer: anytype) !void {
27 | try writer.writeAll("\n");
28 | try writeChildren(parser.documentToNode(doc), writer);
29 | try writer.writeAll("\n");
30 | }
31 |
32 | pub fn writeNode(node: *parser.Node, writer: anytype) anyerror!void {
33 | switch (try parser.nodeType(node)) {
34 | .element => {
35 | // open the tag
36 | const tag = try parser.nodeLocalName(node);
37 | try writer.writeAll("<");
38 | try writer.writeAll(tag);
39 |
40 | // write the attributes
41 | const map = try parser.nodeGetAttributes(node);
42 | const ln = try parser.namedNodeMapGetLength(map);
43 | var i: u32 = 0;
44 | while (i < ln) {
45 | const attr = try parser.namedNodeMapItem(map, i) orelse break;
46 | try writer.writeAll(" ");
47 | try writer.writeAll(try parser.attributeGetName(attr));
48 | try writer.writeAll("=\"");
49 | const attribute_value = try parser.attributeGetValue(attr) orelse "";
50 | try writeEscapedAttributeValue(writer, attribute_value);
51 | try writer.writeAll("\"");
52 | i += 1;
53 | }
54 |
55 | try writer.writeAll(">");
56 |
57 | // void elements can't have any content.
58 | if (try isVoid(parser.nodeToElement(node))) return;
59 |
60 | // write the children
61 | // TODO avoid recursion
62 | try writeChildren(node, writer);
63 |
64 | // close the tag
65 | try writer.writeAll("");
66 | try writer.writeAll(tag);
67 | try writer.writeAll(">");
68 | },
69 | .text => {
70 | const v = try parser.nodeValue(node) orelse return;
71 | try writeEscapedTextNode(writer, v);
72 | },
73 | .cdata_section => {
74 | const v = try parser.nodeValue(node) orelse return;
75 | try writer.writeAll("");
78 | },
79 | .comment => {
80 | const v = try parser.nodeValue(node) orelse return;
81 | try writer.writeAll("");
84 | },
85 | // TODO handle processing instruction dump
86 | .processing_instruction => return,
87 | // document fragment is outside of the main document DOM, so we
88 | // don't output it.
89 | .document_fragment => return,
90 | // document will never be called, but required for completeness.
91 | .document => return,
92 | // done globally instead, but required for completeness.
93 | .document_type => return,
94 | // deprecated
95 | .attribute => return,
96 | .entity_reference => return,
97 | .entity => return,
98 | .notation => return,
99 | }
100 | }
101 |
102 | // writer must be a std.io.Writer
103 | pub fn writeChildren(root: *parser.Node, writer: anytype) !void {
104 | const walker = Walker{};
105 | var next: ?*parser.Node = null;
106 | while (true) {
107 | next = try walker.get_next(root, next) orelse break;
108 | try writeNode(next.?, writer);
109 | }
110 | }
111 |
112 | // area, base, br, col, embed, hr, img, input, link, meta, source, track, wbr
113 | // https://html.spec.whatwg.org/#void-elements
114 | fn isVoid(elem: *parser.Element) !bool {
115 | const tag = try parser.elementHTMLGetTagType(@as(*parser.ElementHTML, @ptrCast(elem)));
116 | return switch (tag) {
117 | .area, .base, .br, .col, .embed, .hr, .img, .input, .link => true,
118 | .meta, .source, .track, .wbr => true,
119 | else => false,
120 | };
121 | }
122 |
123 | fn writeEscapedTextNode(writer: anytype, value: []const u8) !void {
124 | var v = value;
125 | while (v.len > 0) {
126 | const index = std.mem.indexOfAnyPos(u8, v, 0, &.{ '&', '<', '>' }) orelse {
127 | return writer.writeAll(v);
128 | };
129 | try writer.writeAll(v[0..index]);
130 | switch (v[index]) {
131 | '&' => try writer.writeAll("&"),
132 | '<' => try writer.writeAll("<"),
133 | '>' => try writer.writeAll(">"),
134 | else => unreachable,
135 | }
136 | v = v[index + 1 ..];
137 | }
138 | }
139 |
140 | fn writeEscapedAttributeValue(writer: anytype, value: []const u8) !void {
141 | var v = value;
142 | while (v.len > 0) {
143 | const index = std.mem.indexOfAnyPos(u8, v, 0, &.{ '&', '<', '>', '"' }) orelse {
144 | return writer.writeAll(v);
145 | };
146 | try writer.writeAll(v[0..index]);
147 | switch (v[index]) {
148 | '&' => try writer.writeAll("&"),
149 | '<' => try writer.writeAll("<"),
150 | '>' => try writer.writeAll(">"),
151 | '"' => try writer.writeAll("""),
152 | else => unreachable,
153 | }
154 | v = v[index + 1 ..];
155 | }
156 | }
157 |
158 | const testing = std.testing;
159 | test "dump.writeHTML" {
160 | try testWriteHTML(
161 | "Over 9000!
",
162 | "Over 9000!
",
163 | );
164 |
165 | try testWriteHTML(
166 | "",
167 | "",
168 | );
169 |
170 | try testWriteHTML(
171 | "< > &
",
172 | "< > &
",
173 | );
174 |
175 | try testWriteHTML(
176 | "wat?
",
177 | "wat?
",
178 | );
179 |
180 | try testWriteFullHTML(
181 | \\
182 | \\It's over what?
183 | \\9000
184 | \\
185 | , "It's over what?\n9000");
186 | }
187 |
188 | fn testWriteHTML(comptime expected_body: []const u8, src: []const u8) !void {
189 | const expected =
190 | "\n" ++
191 | expected_body ++
192 | "\n";
193 | return testWriteFullHTML(expected, src);
194 | }
195 |
196 | fn testWriteFullHTML(comptime expected: []const u8, src: []const u8) !void {
197 | var buf = std.ArrayListUnmanaged(u8){};
198 | defer buf.deinit(testing.allocator);
199 |
200 | const doc_html = try parser.documentHTMLParseFromStr(src);
201 | defer parser.documentHTMLClose(doc_html) catch {};
202 |
203 | const doc = parser.documentHTMLToDocument(doc_html);
204 | try writeHTML(doc, buf.writer(testing.allocator));
205 | try testing.expectEqualStrings(expected, buf.items);
206 | }
207 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | # Variables
2 | # ---------
3 |
4 | ZIG := zig
5 | BC := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
6 | # option test filter make unittest F="server"
7 | F=
8 |
9 | # OS and ARCH
10 | kernel = $(shell uname -ms)
11 | ifeq ($(kernel), Darwin arm64)
12 | OS := macos
13 | ARCH := aarch64
14 | else ifeq ($(kernel), Linux aarch64)
15 | OS := linux
16 | ARCH := aarch64
17 | else ifeq ($(kernel), Linux arm64)
18 | OS := linux
19 | ARCH := aarch64
20 | else ifeq ($(kernel), Linux x86_64)
21 | OS := linux
22 | ARCH := x86_64
23 | else
24 | $(error "Unhandled kernel: $(kernel)")
25 | endif
26 |
27 |
28 | # Infos
29 | # -----
30 | .PHONY: help
31 |
32 | ## Display this help screen
33 | help:
34 | @printf "\e[36m%-35s %s\e[0m\n" "Command" "Usage"
35 | @sed -n -e '/^## /{'\
36 | -e 's/## //g;'\
37 | -e 'h;'\
38 | -e 'n;'\
39 | -e 's/:.*//g;'\
40 | -e 'G;'\
41 | -e 's/\n/ /g;'\
42 | -e 'p;}' Makefile | awk '{printf "\033[33m%-35s\033[0m%s\n", $$1, substr($$0,length($$1)+1)}'
43 |
44 |
45 | # $(ZIG) commands
46 | # ------------
47 | .PHONY: build build-dev run run-release shell test bench download-zig wpt unittest
48 |
49 | zig_version = $(shell grep 'recommended_zig_version = "' "vendor/zig-js-runtime/build.zig" | cut -d'"' -f2)
50 |
51 | ## Download the zig recommended version
52 | download-zig:
53 | $(eval url = "https://ziglang.org/download/$(zig_version)/zig-$(OS)-$(ARCH)-$(zig_version).tar.xz")
54 | $(eval dest = "/tmp/zig-$(OS)-$(ARCH)-$(zig_version).tar.xz")
55 | @printf "\e[36mDownload zig version $(zig_version)...\e[0m\n"
56 | @curl -o "$(dest)" -L "$(url)" || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
57 | @printf "\e[33mDownloaded $(dest)\e[0m\n"
58 |
59 | ## Build in release-safe mode
60 | build:
61 | @printf "\e[36mBuilding (release safe)...\e[0m\n"
62 | $(ZIG) build -Doptimize=ReleaseSafe -Dengine=v8 -Dgit_commit=$$(git rev-parse --short HEAD) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
63 | @printf "\e[33mBuild OK\e[0m\n"
64 |
65 | ## Build in debug mode
66 | build-dev:
67 | @printf "\e[36mBuilding (debug)...\e[0m\n"
68 | @$(ZIG) build -Dengine=v8 -Dgit_commit=$$(git rev-parse --short HEAD) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
69 | @printf "\e[33mBuild OK\e[0m\n"
70 |
71 | ## Run the server in debug mode
72 | run: build
73 | @printf "\e[36mRunning...\e[0m\n"
74 | @./zig-out/bin/lightpanda || (printf "\e[33mRun ERROR\e[0m\n"; exit 1;)
75 |
76 | ## Run a JS shell in debug mode
77 | shell:
78 | @printf "\e[36mBuilding shell...\e[0m\n"
79 | @$(ZIG) build shell -Dengine=v8 || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
80 |
81 | ## Run WPT tests
82 | wpt:
83 | @printf "\e[36mBuilding wpt...\e[0m\n"
84 | @$(ZIG) build wpt -Dengine=v8 -- --safe $(filter-out $@,$(MAKECMDGOALS)) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
85 |
86 | wpt-summary:
87 | @printf "\e[36mBuilding wpt...\e[0m\n"
88 | @$(ZIG) build wpt -Dengine=v8 -- --safe --summary $(filter-out $@,$(MAKECMDGOALS)) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
89 |
90 | ## Test
91 | test:
92 | @printf "\e[36mTesting...\e[0m\n"
93 | @$(ZIG) build test -Dengine=v8 || (printf "\e[33mTest ERROR\e[0m\n"; exit 1;)
94 | @printf "\e[33mTest OK\e[0m\n"
95 |
96 | unittest:
97 | @TEST_FILTER='${F}' $(ZIG) build unittest -freference-trace --summary all
98 |
99 | # Install and build required dependencies commands
100 | # ------------
101 | .PHONY: install-submodule
102 | .PHONY: install-zig-js-runtime install-zig-js-runtime-dev install-libiconv
103 | .PHONY: _install-netsurf install-netsurf clean-netsurf test-netsurf install-netsurf-dev
104 | .PHONY: install-mimalloc install-mimalloc-dev clean-mimalloc
105 | .PHONY: install-dev install
106 |
107 | ## Install and build dependencies for release
108 | install: install-submodule install-zig-js-runtime install-libiconv install-netsurf install-mimalloc
109 |
110 | ## Install and build dependencies for dev
111 | install-dev: install-submodule install-zig-js-runtime-dev install-libiconv install-netsurf-dev install-mimalloc-dev
112 |
113 | install-netsurf-dev: _install-netsurf
114 | install-netsurf-dev: OPTCFLAGS := -O0 -g -DNDEBUG
115 |
116 | install-netsurf: _install-netsurf
117 | install-netsurf: OPTCFLAGS := -DNDEBUG
118 |
119 | BC_NS := $(BC)vendor/netsurf/out/$(OS)-$(ARCH)
120 | ICONV := $(BC)vendor/libiconv/out/$(OS)-$(ARCH)
121 | # TODO: add Linux iconv path (I guess it depends on the distro)
122 | # TODO: this way of linking libiconv is not ideal. We should have a more generic way
123 | # and stick to a specif version. Maybe build from source. Anyway not now.
124 | _install-netsurf: clean-netsurf
125 | @printf "\e[36mInstalling NetSurf...\e[0m\n" && \
126 | ls $(ICONV)/lib/libiconv.a 1> /dev/null || (printf "\e[33mERROR: you need to execute 'make install-libiconv'\e[0m\n"; exit 1;) && \
127 | mkdir -p $(BC_NS) && \
128 | cp -R vendor/netsurf/share $(BC_NS) && \
129 | export PREFIX=$(BC_NS) && \
130 | export OPTLDFLAGS="-L$(ICONV)/lib" && \
131 | export OPTCFLAGS="$(OPTCFLAGS) -I$(ICONV)/include" && \
132 | printf "\e[33mInstalling libwapcaplet...\e[0m\n" && \
133 | cd vendor/netsurf/libwapcaplet && \
134 | BUILDDIR=$(BC_NS)/build/libwapcaplet make install && \
135 | cd ../libparserutils && \
136 | printf "\e[33mInstalling libparserutils...\e[0m\n" && \
137 | BUILDDIR=$(BC_NS)/build/libparserutils make install && \
138 | cd ../libhubbub && \
139 | printf "\e[33mInstalling libhubbub...\e[0m\n" && \
140 | BUILDDIR=$(BC_NS)/build/libhubbub make install && \
141 | rm src/treebuilder/autogenerated-element-type.c && \
142 | cd ../libdom && \
143 | printf "\e[33mInstalling libdom...\e[0m\n" && \
144 | BUILDDIR=$(BC_NS)/build/libdom make install && \
145 | printf "\e[33mRunning libdom example...\e[0m\n" && \
146 | cd examples && \
147 | $(ZIG) cc \
148 | -I$(ICONV)/include \
149 | -I$(BC_NS)/include \
150 | -L$(ICONV)/lib \
151 | -L$(BC_NS)/lib \
152 | -liconv \
153 | -ldom \
154 | -lhubbub \
155 | -lparserutils \
156 | -lwapcaplet \
157 | -o a.out \
158 | dom-structure-dump.c \
159 | $(ICONV)/lib/libiconv.a && \
160 | ./a.out > /dev/null && \
161 | rm a.out && \
162 | printf "\e[36mDone NetSurf $(OS)\e[0m\n"
163 |
164 | clean-netsurf:
165 | @printf "\e[36mCleaning NetSurf build...\e[0m\n" && \
166 | rm -Rf $(BC_NS)
167 |
168 | test-netsurf:
169 | @printf "\e[36mTesting NetSurf...\e[0m\n" && \
170 | export PREFIX=$(BC_NS) && \
171 | export LDFLAGS="-L$(ICONV)/lib -L$(BC_NS)/lib" && \
172 | export CFLAGS="-I$(ICONV)/include -I$(BC_NS)/include" && \
173 | cd vendor/netsurf/libdom && \
174 | BUILDDIR=$(BC_NS)/build/libdom make test
175 |
176 | download-libiconv:
177 | ifeq ("$(wildcard vendor/libiconv/libiconv-1.17)","")
178 | @mkdir -p vendor/libiconv
179 | @cd vendor/libiconv && \
180 | curl https://ftp.gnu.org/pub/gnu/libiconv/libiconv-1.17.tar.gz | tar -xvzf -
181 | endif
182 |
183 | install-libiconv: download-libiconv clean-libiconv
184 | @cd vendor/libiconv/libiconv-1.17 && \
185 | ./configure --prefix=$(ICONV) --enable-static && \
186 | make && make install
187 |
188 | clean-libiconv:
189 | ifneq ("$(wildcard vendor/libiconv/libiconv-1.17/Makefile)","")
190 | @cd vendor/libiconv/libiconv-1.17 && \
191 | make clean
192 | endif
193 |
194 | install-zig-js-runtime-dev:
195 | @cd vendor/zig-js-runtime && \
196 | make install-dev
197 |
198 | install-zig-js-runtime:
199 | @cd vendor/zig-js-runtime && \
200 | make install
201 |
202 | .PHONY: _build_mimalloc
203 |
204 | MIMALLOC := $(BC)vendor/mimalloc/out/$(OS)-$(ARCH)
205 | _build_mimalloc: clean-mimalloc
206 | @mkdir -p $(MIMALLOC)/build && \
207 | cd $(MIMALLOC)/build && \
208 | cmake -DMI_BUILD_SHARED=OFF -DMI_BUILD_OBJECT=OFF -DMI_BUILD_TESTS=OFF -DMI_OVERRIDE=OFF $(OPTS) ../../.. && \
209 | make && \
210 | mkdir -p $(MIMALLOC)/lib
211 |
212 | install-mimalloc-dev: _build_mimalloc
213 | install-mimalloc-dev: OPTS=-DCMAKE_BUILD_TYPE=Debug
214 | install-mimalloc-dev:
215 | @cd $(MIMALLOC) && \
216 | mv build/libmimalloc-debug.a lib/libmimalloc.a
217 |
218 | install-mimalloc: _build_mimalloc
219 | install-mimalloc:
220 | @cd $(MIMALLOC) && \
221 | mv build/libmimalloc.a lib/libmimalloc.a
222 |
223 | clean-mimalloc:
224 | @rm -Rf $(MIMALLOC)/build
225 |
226 | ## Init and update git submodule
227 | install-submodule:
228 | @git submodule init && \
229 | git submodule update
230 |
--------------------------------------------------------------------------------