├── .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(""); 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 | --------------------------------------------------------------------------------