From b047cb6dc166fca24325947e50a10be0c072f865 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Sat, 18 Oct 2025 07:49:15 +0800 Subject: [PATCH 01/16] remove libdom --- .github/actions/install/action.yml | 2 +- .github/workflows/zig-fmt.yml | 2 +- .gitignore | 7 +- .gitmodules | 18 - Dockerfile | 2 +- Makefile | 131 +- README.md | 2 +- build.zig | 160 +- flake.nix | 2 +- src/Scheduler.zig | 88 + src/TestHTTPServer.zig | 1 + src/app.zig | 152 +- src/browser/DataURI.zig | 52 - src/browser/EventManager.zig | 297 ++ src/browser/Factory.zig | 367 ++ src/browser/Mime.zig | 518 +++ src/browser/Renderer.zig | 109 + src/browser/Scheduler.zig | 166 +- src/browser/ScriptManager.zig | 264 +- src/browser/SlotChangeMonitor.zig | 189 - src/browser/State.zig | 77 - src/browser/URL.zig | 264 ++ src/browser/browser.zig | 154 +- src/browser/console/console.zig | 177 - src/browser/crypto/crypto.zig | 71 - src/browser/css/README.md | 218 -- src/browser/css/css.zig | 191 - src/browser/css/libdom.zig | 423 --- src/browser/css/parser.zig | 996 ------ src/browser/css/selector.zig | 1417 -------- src/browser/cssom/CSSParser.zig | 289 -- src/browser/cssom/CSSRule.zig | 41 - src/browser/cssom/CSSRuleList.zig | 51 - src/browser/cssom/CSSStyleDeclaration.zig | 958 ----- src/browser/cssom/CSSStyleSheet.zig | 95 - src/browser/cssom/StyleSheet.zig | 55 - src/browser/cssom/cssom.zig | 25 - src/browser/dom/Animation.zig | 107 - src/browser/dom/IntersectionObserver.zig | 329 -- src/browser/dom/MessageChannel.zig | 288 -- src/browser/dom/attribute.zig | 75 - src/browser/dom/cdata_section.zig | 28 - src/browser/dom/character_data.zig | 134 - src/browser/dom/comment.zig | 45 - src/browser/dom/css.zig | 80 - src/browser/dom/document.zig | 321 -- src/browser/dom/document_fragment.zig | 96 - src/browser/dom/document_type.zig | 67 - src/browser/dom/dom.zig | 56 - src/browser/dom/dom_parser.zig | 41 - src/browser/dom/element.zig | 686 ---- src/browser/dom/event_target.zig | 168 - src/browser/dom/exceptions.zig | 224 -- src/browser/dom/html_collection.zig | 454 --- src/browser/dom/implementation.zig | 56 - src/browser/dom/mutation_observer.zig | 407 --- src/browser/dom/namednodemap.zig | 121 - src/browser/dom/node.zig | 637 ---- src/browser/dom/node_filter.zig | 83 - src/browser/dom/node_iterator.zig | 302 -- src/browser/dom/nodelist.zig | 188 - src/browser/dom/performance.zig | 206 -- src/browser/dom/performance_observer.zig | 58 - src/browser/dom/processing_instruction.zig | 92 - src/browser/dom/range.zig | 390 --- src/browser/dom/resize_observer.zig | 54 - src/browser/dom/shadow_root.zig | 101 - src/browser/dom/text.zig | 62 - src/browser/dom/token_list.zig | 174 - src/browser/dom/tree_walker.zig | 315 -- src/browser/dom/walker.zig | 102 - src/browser/dump.zig | 373 +- src/browser/encoding/TextDecoder.zig | 102 - src/browser/encoding/TextEncoder.zig | 48 - src/browser/encoding/encoding.zig | 22 - src/browser/events/custom_event.zig | 86 - src/browser/events/event.zig | 402 --- src/browser/events/keyboard_event.zig | 159 - src/browser/events/mouse_event.zig | 111 - src/browser/fetch/Headers.zig | 225 -- src/browser/fetch/Request.zig | 283 -- src/browser/fetch/Response.zig | 209 -- src/browser/fetch/fetch.zig | 243 -- src/browser/html/AbortController.zig | 143 - src/browser/html/DataSet.zig | 82 - src/browser/html/History.zig | 215 -- src/browser/html/document.zig | 322 -- src/browser/html/elements.zig | 1361 -------- src/browser/html/error_event.zig | 86 - src/browser/html/form.zig | 37 - src/browser/html/html.zig | 43 - src/browser/html/iframe.zig | 28 - src/browser/html/location.zig | 96 - src/browser/html/media_query_list.zig | 45 - src/browser/html/navigator.zig | 86 - src/browser/html/screen.zig | 103 - src/browser/html/select.zig | 204 -- src/browser/html/svg_elements.zig | 36 - src/browser/html/window.zig | 497 --- src/browser/iterator/iterator.zig | 226 -- src/browser/js/Caller.zig | 349 +- src/browser/js/Context.zig | 386 ++- src/browser/js/Env.zig | 466 +-- src/browser/js/ExecutionWorld.zig | 129 +- src/browser/js/Function.zig | 25 +- src/browser/js/Inspector.zig | 18 + src/browser/js/Object.zig | 38 +- src/browser/js/Platform.zig | 18 + src/browser/js/This.zig | 18 + src/browser/js/TryCatch.zig | 18 + src/browser/js/bridge.zig | 471 +++ src/browser/js/generate.zig | 231 -- src/browser/js/js.zig | 65 +- src/browser/js/types.zig | 183 - src/browser/key_value.zig | 284 -- src/browser/mimalloc.zig | 110 - src/browser/mime.zig | 519 --- src/browser/netsurf.zig | 3083 ----------------- src/browser/page.zig | 2192 ++++++------ src/browser/parser/Parser.zig | 243 ++ src/browser/parser/html5ever.zig | 134 + src/browser/polyfill/polyfill.zig | 2 +- src/browser/polyfill/webcomponents.zig | 2 +- src/browser/reflect.zig | 46 + src/browser/renderer.zig | 116 - src/browser/session.zig | 298 +- src/browser/storage/storage.zig | 238 -- src/browser/streams/ReadableStream.zig | 205 -- .../ReadableStreamDefaultController.zig | 79 - .../streams/ReadableStreamDefaultReader.zig | 79 - src/browser/streams/streams.zig | 24 - src/browser/tests/cdata/data.html | 10 + src/browser/tests/crypto.html | 58 + src/browser/tests/document/collections.html | 23 + .../tests/document/create_element.html | 13 + .../tests/document/create_element_ns.html | 32 + src/browser/tests/document/document.html | 41 + .../tests/document/get_element_by_id.html | 35 + .../document/get_elements_by_class_name.html | 98 + .../document/get_elements_by_tag_name.html | 155 + .../tests/document/query_selector.html | 271 ++ .../tests/document/query_selector_all.html | 378 ++ .../document/query_selector_attributes.html | 113 + .../document/query_selector_edge_cases.html | 202 ++ .../tests/document/query_selector_not.html | 119 + .../document_fragment/document_fragment.html | 102 + src/browser/tests/document_head_body.html | 9 + src/browser/tests/element/append.html | 29 + src/browser/tests/element/attributes.html | 85 + src/browser/tests/element/class_list.html | 334 ++ .../tests/element/css_style_properties.html | 133 + src/browser/tests/element/element.html | 54 + .../element/get_elements_by_class_name.html | 187 + .../element/get_elements_by_tag_name.html | 186 + src/browser/tests/element/html/anchor.html | 13 + src/browser/tests/element/html/button.html | 55 + src/browser/tests/element/html/input.html | 246 ++ .../tests/element/html/input_radio.html | 140 + src/browser/tests/element/html/option.html | 67 + .../tests/element/html/script/dynamic.html | 43 + .../tests/element/html/script/dynamic1.js | 1 + .../tests/element/html/script/dynamic2.js | 1 + src/browser/tests/element/html/select.html | 83 + src/browser/tests/element/html/textarea.html | 78 + src/browser/tests/element/inner.html | 131 + src/browser/tests/element/inner.js | 1 + src/browser/tests/element/query_selector.html | 65 + .../tests/element/query_selector_all.html | 188 + src/browser/tests/element/remove.html | 26 + src/browser/tests/element/styles.html | 129 + src/browser/tests/element/svg/svg.html | 28 + src/browser/tests/encoding/text_decoder.html | 64 + src/browser/tests/encoding/text_encoder.html | 10 + src/browser/tests/event/abort_controller.html | 213 ++ src/browser/tests/event/error.html | 60 + src/browser/tests/events.html | 283 ++ src/browser/tests/navigator.html | 29 + src/browser/tests/net/form_data.html | 252 ++ src/browser/tests/net/url_search_params.html | 354 ++ src/browser/tests/net/xhr.html | 10 + src/browser/tests/node/append_child.html | 30 + src/browser/tests/node/child_nodes.html | 88 + src/browser/tests/node/clone_node.html | 292 ++ .../tests/node/compare_document_position.html | 259 ++ src/browser/tests/node/insert_before.html | 42 + src/browser/tests/node/node.html | 191 + src/browser/tests/node/node_iterator.html | 473 +++ src/browser/tests/node/normalize.html | 30 + src/browser/tests/node/remove_child.html | 18 + src/browser/tests/node/replace_child.html | 40 + src/browser/tests/node/text_content.html | 35 + src/browser/tests/node/tree.html | 25 + src/browser/tests/node/tree_walker.html | 385 ++ src/browser/tests/page/load_event.html | 18 + src/browser/tests/page/meta.html | 12 + src/browser/tests/page/mod1.js | 2 + src/browser/tests/page/module.html | 159 + src/browser/tests/page/modules/base.js | 1 + src/browser/tests/page/modules/circular-a.js | 7 + src/browser/tests/page/modules/circular-b.js | 11 + .../tests/page/modules/dynamic-chain-a.js | 6 + .../tests/page/modules/dynamic-chain-b.js | 6 + .../tests/page/modules/dynamic-chain-c.js | 1 + .../tests/page/modules/dynamic-circular-x.js | 6 + .../tests/page/modules/dynamic-circular-y.js | 6 + src/browser/tests/page/modules/importer.js | 4 + .../page/modules/mixed-circular-dynamic.js | 7 + .../page/modules/mixed-circular-static.js | 6 + src/browser/tests/page/modules/re-exporter.js | 2 + src/browser/tests/page/modules/shared.js | 9 + .../tests/page/modules/syntax-error.js | 2 + src/browser/tests/page/modules/test-404.js | 2 + .../tests/page/modules/test-syntax-error.js | 2 + src/browser/tests/storage.html | 62 + src/browser/tests/testing.js | 201 ++ src/browser/tests/url.html | 316 ++ src/browser/tests/window/body_onload1.html | 17 + src/browser/tests/window/body_onload2.html | 15 + src/browser/tests/window/location.html | 7 + src/browser/tests/window/navigator.html | 70 + src/browser/tests/window/report_error.html | 187 + src/browser/tests/window/timers.html | 24 + src/browser/tests/window/window.html | 95 + src/browser/url/url.zig | 516 --- src/browser/webapi/AbortController.zig | 44 + src/browser/webapi/AbortSignal.zig | 101 + src/browser/webapi/CData.zig | 70 + src/browser/webapi/Console.zig | 53 + src/browser/webapi/Crypto.zig | 64 + src/browser/webapi/DOMException.zig | 71 + src/browser/webapi/DOMNodeIterator.zig | 169 + src/browser/webapi/DOMTreeWalker.zig | 263 ++ src/browser/webapi/Document.zig | 252 ++ src/browser/webapi/DocumentFragment.zig | 147 + src/browser/webapi/Element.zig | 714 ++++ src/browser/webapi/Event.zig | 131 + src/browser/webapi/EventTarget.zig | 80 + src/browser/webapi/Location.zig | 67 + src/browser/webapi/Navigator.zig | 108 + src/browser/webapi/Node.zig | 692 ++++ src/browser/webapi/NodeFilter.zig | 89 + src/browser/webapi/TreeWalker.zig | 123 + src/browser/webapi/URL.zig | 255 ++ src/browser/webapi/Window.zig | 275 ++ src/browser/webapi/cdata/Comment.zig | 17 + src/browser/webapi/cdata/Text.zig | 23 + src/browser/webapi/children.zig | 39 + src/browser/webapi/collections.zig | 16 + src/browser/webapi/collections/ChildNodes.zig | 116 + .../webapi/collections/DOMTokenList.zig | 216 ++ .../webapi/collections/HTMLCollection.zig | 98 + src/browser/webapi/collections/NodeList.zig | 82 + src/browser/webapi/collections/iterator.zig | 92 + src/browser/webapi/collections/node_live.zig | 225 ++ .../webapi/css/CSSStyleDeclaration.zig | 223 ++ src/browser/webapi/css/CSSStyleProperties.zig | 179 + src/browser/webapi/element/Attribute.zig | 467 +++ src/browser/webapi/element/Html.zig | 153 + src/browser/webapi/element/Svg.zig | 61 + src/browser/webapi/element/html/Anchor.zig | 40 + src/browser/webapi/element/html/BR.zig | 25 + src/browser/webapi/element/html/Body.zig | 40 + src/browser/webapi/element/html/Button.zig | 81 + src/browser/webapi/element/html/Custom.zig | 28 + src/browser/webapi/element/html/Div.zig | 24 + src/browser/webapi/element/html/Form.zig | 117 + src/browser/webapi/element/html/Generic.zig | 28 + src/browser/webapi/element/html/HR.zig | 24 + src/browser/webapi/element/html/Head.zig | 24 + src/browser/webapi/element/html/Heading.zig | 29 + src/browser/webapi/element/html/Html.zig | 24 + src/browser/webapi/element/html/Image.zig | 24 + src/browser/webapi/element/html/Input.zig | 259 ++ src/browser/webapi/element/html/LI.zig | 24 + src/browser/webapi/element/html/Link.zig | 24 + src/browser/webapi/element/html/Meta.zig | 28 + src/browser/webapi/element/html/OL.zig | 24 + src/browser/webapi/element/html/Option.zig | 116 + src/browser/webapi/element/html/Paragraph.zig | 24 + src/browser/webapi/element/html/Script.zig | 95 + src/browser/webapi/element/html/Select.zig | 143 + src/browser/webapi/element/html/Style.zig | 24 + src/browser/webapi/element/html/TextArea.zig | 110 + src/browser/webapi/element/html/Title.zig | 25 + src/browser/webapi/element/html/UL.zig | 24 + src/browser/webapi/element/html/Unknown.zig | 28 + src/browser/webapi/element/svg/Generic.zig | 29 + src/browser/webapi/element/svg/Rect.zig | 28 + src/browser/webapi/encoding/TextDecoder.zig | 100 + src/browser/webapi/encoding/TextEncoder.zig | 40 + src/browser/webapi/event/ErrorEvent.zig | 93 + src/browser/webapi/event/ProgressEvent.zig | 48 + src/browser/webapi/net/Fetch.zig | 22 + src/browser/webapi/net/Request.zig | 39 + src/browser/webapi/net/Response.zig | 53 + src/browser/webapi/net/URLSearchParams.zig | 346 ++ src/browser/webapi/net/XMLHttpRequest.zig | 335 ++ .../webapi/net/XMLHttpRequestEventTarget.zig | 167 + src/browser/webapi/selector/List.zig | 722 ++++ src/browser/webapi/selector/Parser.zig | 1154 ++++++ src/browser/webapi/selector/Selector.zig | 175 + src/browser/{ => webapi}/storage/cookie.zig | 10 +- src/browser/webapi/storage/storage.zig | 107 + src/browser/xhr/File.zig | 34 - src/browser/xhr/event_target.zig | 137 - src/browser/xhr/form_data.zig | 301 -- src/browser/xhr/progress_event.zig | 72 - src/browser/xhr/xhr.zig | 759 ---- src/browser/xmlserializer/xmlserializer.zig | 50 - src/datetime.zig | 41 +- src/html5ever/Cargo.lock | 478 +++ src/html5ever/Cargo.toml | 20 + src/html5ever/lib.rs | 260 ++ src/html5ever/sink.rs | 226 ++ src/html5ever/types.rs | 119 + src/http/Client.zig | 29 +- src/lightpanda.zig | 53 + src/log.zig | 27 +- src/main.zig | 281 +- src/notification.zig | 364 +- src/server.zig | 239 +- src/string.zig | 207 ++ src/telemetry/telemetry.zig | 4 +- src/test_runner.zig | 426 +-- src/testing.zig | 254 +- src/tests/browser.html | 6 - src/tests/crypto.html | 26 - src/tests/css.html | 6 - src/tests/cssom/css_rule_list.html | 8 - src/tests/cssom/css_style_declaration.html | 102 - src/tests/cssom/css_stylesheet.html | 16 - src/tests/dom/animation.html | 15 - src/tests/dom/attribute.html | 33 - src/tests/dom/character_data.html | 48 - src/tests/dom/comment.html | 9 - src/tests/dom/document.html | 190 - src/tests/dom/document_fragment.html | 34 - src/tests/dom/document_type.html | 13 - src/tests/dom/dom_parser.html | 7 - src/tests/dom/element.html | 341 -- src/tests/dom/event_target.html | 116 - src/tests/dom/exceptions.html | 40 - src/tests/dom/html_collection.html | 67 - src/tests/dom/implementation.html | 14 - src/tests/dom/intersection_observer.html | 163 - src/tests/dom/message_channel.html | 60 - src/tests/dom/mutation_observer.html | 76 - src/tests/dom/named_node_map.html | 19 - src/tests/dom/node.html | 245 -- src/tests/dom/node_filter.html | 219 -- src/tests/dom/node_iterator.html | 62 - src/tests/dom/node_list.html | 19 - src/tests/dom/node_owner.html | 34 - src/tests/dom/performance.html | 16 - src/tests/dom/performance_observer.html | 5 - src/tests/dom/processing_instruction.html | 22 - src/tests/dom/range.html | 41 - src/tests/dom/shadow_root.html | 49 - src/tests/dom/text.html | 19 - src/tests/dom/token_list.html | 64 - src/tests/encoding/decoder.html | 60 - src/tests/encoding/encoder.html | 14 - src/tests/events/custom.html | 25 - src/tests/events/event.html | 139 - src/tests/events/keyboard.html | 88 - src/tests/events/mouse.html | 34 - src/tests/fetch/fetch.html | 34 - src/tests/fetch/headers.html | 102 - src/tests/fetch/request.html | 22 - src/tests/fetch/response.html | 50 - src/tests/html/abort_controller.html | 41 - src/tests/html/dataset.html | 30 - src/tests/html/document.html | 85 - src/tests/html/element.html | 53 - src/tests/html/error_event.html | 25 - src/tests/html/history.html | 41 - src/tests/html/image.html | 32 - src/tests/html/input.html | 111 - src/tests/html/link.html | 60 - src/tests/html/location.html | 15 - src/tests/html/navigator.html | 8 - src/tests/html/screen.html | 21 - src/tests/html/script/dynamic_import.html | 32 - src/tests/html/script/import.html | 15 - src/tests/html/script/import.js | 2 - src/tests/html/script/import2.js | 2 - src/tests/html/script/importmap.html | 24 - src/tests/html/script/inline_defer.html | 28 - src/tests/html/script/inline_defer.js | 1 - src/tests/html/script/script.html | 21 - src/tests/html/select.html | 80 - src/tests/html/slot.html | 179 - src/tests/html/style.html | 8 - src/tests/html/svg.html | 38 - src/tests/html/template.html | 22 - src/tests/polyfill/webcomponents.html | 23 - src/tests/storage/local_storage.html | 29 - src/tests/streams/readable_stream.html | 134 - src/tests/testing.js | 223 -- src/tests/url/url.html | 83 - src/tests/url/url_search_params.html | 94 - src/tests/window/frames.html | 13 - src/tests/window/window.html | 151 - src/tests/xhr/file.html | 6 - src/tests/xhr/form_data.html | 130 - src/tests/xhr/progress_event.html | 17 - src/tests/xhr/xhr.html | 110 - src/tests/xmlserializer.html | 8 - src/url.zig | 555 --- vendor/mimalloc | 1 - vendor/netsurf/libdom | 1 - vendor/netsurf/libhubbub | 1 - vendor/netsurf/libparserutils | 1 - vendor/netsurf/libwapcaplet | 1 - vendor/netsurf/share/netsurf-buildsystem | 1 - 415 files changed, 26294 insertions(+), 33558 deletions(-) create mode 100644 src/Scheduler.zig delete mode 100644 src/browser/DataURI.zig create mode 100644 src/browser/EventManager.zig create mode 100644 src/browser/Factory.zig create mode 100644 src/browser/Mime.zig create mode 100644 src/browser/Renderer.zig delete mode 100644 src/browser/SlotChangeMonitor.zig delete mode 100644 src/browser/State.zig create mode 100644 src/browser/URL.zig delete mode 100644 src/browser/console/console.zig delete mode 100644 src/browser/crypto/crypto.zig delete mode 100644 src/browser/css/README.md delete mode 100644 src/browser/css/css.zig delete mode 100644 src/browser/css/libdom.zig delete mode 100644 src/browser/css/parser.zig delete mode 100644 src/browser/css/selector.zig delete mode 100644 src/browser/cssom/CSSParser.zig delete mode 100644 src/browser/cssom/CSSRule.zig delete mode 100644 src/browser/cssom/CSSRuleList.zig delete mode 100644 src/browser/cssom/CSSStyleDeclaration.zig delete mode 100644 src/browser/cssom/CSSStyleSheet.zig delete mode 100644 src/browser/cssom/StyleSheet.zig delete mode 100644 src/browser/cssom/cssom.zig delete mode 100644 src/browser/dom/Animation.zig delete mode 100644 src/browser/dom/IntersectionObserver.zig delete mode 100644 src/browser/dom/MessageChannel.zig delete mode 100644 src/browser/dom/attribute.zig delete mode 100644 src/browser/dom/cdata_section.zig delete mode 100644 src/browser/dom/character_data.zig delete mode 100644 src/browser/dom/comment.zig delete mode 100644 src/browser/dom/css.zig delete mode 100644 src/browser/dom/document.zig delete mode 100644 src/browser/dom/document_fragment.zig delete mode 100644 src/browser/dom/document_type.zig delete mode 100644 src/browser/dom/dom.zig delete mode 100644 src/browser/dom/dom_parser.zig delete mode 100644 src/browser/dom/element.zig delete mode 100644 src/browser/dom/event_target.zig delete mode 100644 src/browser/dom/exceptions.zig delete mode 100644 src/browser/dom/html_collection.zig delete mode 100644 src/browser/dom/implementation.zig delete mode 100644 src/browser/dom/mutation_observer.zig delete mode 100644 src/browser/dom/namednodemap.zig delete mode 100644 src/browser/dom/node.zig delete mode 100644 src/browser/dom/node_filter.zig delete mode 100644 src/browser/dom/node_iterator.zig delete mode 100644 src/browser/dom/nodelist.zig delete mode 100644 src/browser/dom/performance.zig delete mode 100644 src/browser/dom/performance_observer.zig delete mode 100644 src/browser/dom/processing_instruction.zig delete mode 100644 src/browser/dom/range.zig delete mode 100644 src/browser/dom/resize_observer.zig delete mode 100644 src/browser/dom/shadow_root.zig delete mode 100644 src/browser/dom/text.zig delete mode 100644 src/browser/dom/token_list.zig delete mode 100644 src/browser/dom/tree_walker.zig delete mode 100644 src/browser/dom/walker.zig delete mode 100644 src/browser/encoding/TextDecoder.zig delete mode 100644 src/browser/encoding/TextEncoder.zig delete mode 100644 src/browser/encoding/encoding.zig delete mode 100644 src/browser/events/custom_event.zig delete mode 100644 src/browser/events/event.zig delete mode 100644 src/browser/events/keyboard_event.zig delete mode 100644 src/browser/events/mouse_event.zig delete mode 100644 src/browser/fetch/Headers.zig delete mode 100644 src/browser/fetch/Request.zig delete mode 100644 src/browser/fetch/Response.zig delete mode 100644 src/browser/fetch/fetch.zig delete mode 100644 src/browser/html/AbortController.zig delete mode 100644 src/browser/html/DataSet.zig delete mode 100644 src/browser/html/History.zig delete mode 100644 src/browser/html/document.zig delete mode 100644 src/browser/html/elements.zig delete mode 100644 src/browser/html/error_event.zig delete mode 100644 src/browser/html/form.zig delete mode 100644 src/browser/html/html.zig delete mode 100644 src/browser/html/iframe.zig delete mode 100644 src/browser/html/location.zig delete mode 100644 src/browser/html/media_query_list.zig delete mode 100644 src/browser/html/navigator.zig delete mode 100644 src/browser/html/screen.zig delete mode 100644 src/browser/html/select.zig delete mode 100644 src/browser/html/svg_elements.zig delete mode 100644 src/browser/html/window.zig delete mode 100644 src/browser/iterator/iterator.zig create mode 100644 src/browser/js/bridge.zig delete mode 100644 src/browser/js/generate.zig delete mode 100644 src/browser/js/types.zig delete mode 100644 src/browser/key_value.zig delete mode 100644 src/browser/mimalloc.zig delete mode 100644 src/browser/mime.zig delete mode 100644 src/browser/netsurf.zig create mode 100644 src/browser/parser/Parser.zig create mode 100644 src/browser/parser/html5ever.zig create mode 100644 src/browser/reflect.zig delete mode 100644 src/browser/renderer.zig delete mode 100644 src/browser/storage/storage.zig delete mode 100644 src/browser/streams/ReadableStream.zig delete mode 100644 src/browser/streams/ReadableStreamDefaultController.zig delete mode 100644 src/browser/streams/ReadableStreamDefaultReader.zig delete mode 100644 src/browser/streams/streams.zig create mode 100644 src/browser/tests/cdata/data.html create mode 100644 src/browser/tests/crypto.html create mode 100644 src/browser/tests/document/collections.html create mode 100644 src/browser/tests/document/create_element.html create mode 100644 src/browser/tests/document/create_element_ns.html create mode 100644 src/browser/tests/document/document.html create mode 100644 src/browser/tests/document/get_element_by_id.html create mode 100644 src/browser/tests/document/get_elements_by_class_name.html create mode 100644 src/browser/tests/document/get_elements_by_tag_name.html create mode 100644 src/browser/tests/document/query_selector.html create mode 100644 src/browser/tests/document/query_selector_all.html create mode 100644 src/browser/tests/document/query_selector_attributes.html create mode 100644 src/browser/tests/document/query_selector_edge_cases.html create mode 100644 src/browser/tests/document/query_selector_not.html create mode 100644 src/browser/tests/document_fragment/document_fragment.html create mode 100644 src/browser/tests/document_head_body.html create mode 100644 src/browser/tests/element/append.html create mode 100644 src/browser/tests/element/attributes.html create mode 100644 src/browser/tests/element/class_list.html create mode 100644 src/browser/tests/element/css_style_properties.html create mode 100644 src/browser/tests/element/element.html create mode 100644 src/browser/tests/element/get_elements_by_class_name.html create mode 100644 src/browser/tests/element/get_elements_by_tag_name.html create mode 100644 src/browser/tests/element/html/anchor.html create mode 100644 src/browser/tests/element/html/button.html create mode 100644 src/browser/tests/element/html/input.html create mode 100644 src/browser/tests/element/html/input_radio.html create mode 100644 src/browser/tests/element/html/option.html create mode 100644 src/browser/tests/element/html/script/dynamic.html create mode 100644 src/browser/tests/element/html/script/dynamic1.js create mode 100644 src/browser/tests/element/html/script/dynamic2.js create mode 100644 src/browser/tests/element/html/select.html create mode 100644 src/browser/tests/element/html/textarea.html create mode 100644 src/browser/tests/element/inner.html create mode 100644 src/browser/tests/element/inner.js create mode 100644 src/browser/tests/element/query_selector.html create mode 100644 src/browser/tests/element/query_selector_all.html create mode 100644 src/browser/tests/element/remove.html create mode 100644 src/browser/tests/element/styles.html create mode 100644 src/browser/tests/element/svg/svg.html create mode 100644 src/browser/tests/encoding/text_decoder.html create mode 100644 src/browser/tests/encoding/text_encoder.html create mode 100644 src/browser/tests/event/abort_controller.html create mode 100644 src/browser/tests/event/error.html create mode 100644 src/browser/tests/events.html create mode 100644 src/browser/tests/navigator.html create mode 100644 src/browser/tests/net/form_data.html create mode 100644 src/browser/tests/net/url_search_params.html create mode 100644 src/browser/tests/net/xhr.html create mode 100644 src/browser/tests/node/append_child.html create mode 100644 src/browser/tests/node/child_nodes.html create mode 100644 src/browser/tests/node/clone_node.html create mode 100644 src/browser/tests/node/compare_document_position.html create mode 100644 src/browser/tests/node/insert_before.html create mode 100644 src/browser/tests/node/node.html create mode 100644 src/browser/tests/node/node_iterator.html create mode 100644 src/browser/tests/node/normalize.html create mode 100644 src/browser/tests/node/remove_child.html create mode 100644 src/browser/tests/node/replace_child.html create mode 100644 src/browser/tests/node/text_content.html create mode 100644 src/browser/tests/node/tree.html create mode 100644 src/browser/tests/node/tree_walker.html create mode 100644 src/browser/tests/page/load_event.html create mode 100644 src/browser/tests/page/meta.html create mode 100644 src/browser/tests/page/mod1.js create mode 100644 src/browser/tests/page/module.html create mode 100644 src/browser/tests/page/modules/base.js create mode 100644 src/browser/tests/page/modules/circular-a.js create mode 100644 src/browser/tests/page/modules/circular-b.js create mode 100644 src/browser/tests/page/modules/dynamic-chain-a.js create mode 100644 src/browser/tests/page/modules/dynamic-chain-b.js create mode 100644 src/browser/tests/page/modules/dynamic-chain-c.js create mode 100644 src/browser/tests/page/modules/dynamic-circular-x.js create mode 100644 src/browser/tests/page/modules/dynamic-circular-y.js create mode 100644 src/browser/tests/page/modules/importer.js create mode 100644 src/browser/tests/page/modules/mixed-circular-dynamic.js create mode 100644 src/browser/tests/page/modules/mixed-circular-static.js create mode 100644 src/browser/tests/page/modules/re-exporter.js create mode 100644 src/browser/tests/page/modules/shared.js create mode 100644 src/browser/tests/page/modules/syntax-error.js create mode 100644 src/browser/tests/page/modules/test-404.js create mode 100644 src/browser/tests/page/modules/test-syntax-error.js create mode 100644 src/browser/tests/storage.html create mode 100644 src/browser/tests/testing.js create mode 100644 src/browser/tests/url.html create mode 100644 src/browser/tests/window/body_onload1.html create mode 100644 src/browser/tests/window/body_onload2.html create mode 100644 src/browser/tests/window/location.html create mode 100644 src/browser/tests/window/navigator.html create mode 100644 src/browser/tests/window/report_error.html create mode 100644 src/browser/tests/window/timers.html create mode 100644 src/browser/tests/window/window.html delete mode 100644 src/browser/url/url.zig create mode 100644 src/browser/webapi/AbortController.zig create mode 100644 src/browser/webapi/AbortSignal.zig create mode 100644 src/browser/webapi/CData.zig create mode 100644 src/browser/webapi/Console.zig create mode 100644 src/browser/webapi/Crypto.zig create mode 100644 src/browser/webapi/DOMException.zig create mode 100644 src/browser/webapi/DOMNodeIterator.zig create mode 100644 src/browser/webapi/DOMTreeWalker.zig create mode 100644 src/browser/webapi/Document.zig create mode 100644 src/browser/webapi/DocumentFragment.zig create mode 100644 src/browser/webapi/Element.zig create mode 100644 src/browser/webapi/Event.zig create mode 100644 src/browser/webapi/EventTarget.zig create mode 100644 src/browser/webapi/Location.zig create mode 100644 src/browser/webapi/Navigator.zig create mode 100644 src/browser/webapi/Node.zig create mode 100644 src/browser/webapi/NodeFilter.zig create mode 100644 src/browser/webapi/TreeWalker.zig create mode 100644 src/browser/webapi/URL.zig create mode 100644 src/browser/webapi/Window.zig create mode 100644 src/browser/webapi/cdata/Comment.zig create mode 100644 src/browser/webapi/cdata/Text.zig create mode 100644 src/browser/webapi/children.zig create mode 100644 src/browser/webapi/collections.zig create mode 100644 src/browser/webapi/collections/ChildNodes.zig create mode 100644 src/browser/webapi/collections/DOMTokenList.zig create mode 100644 src/browser/webapi/collections/HTMLCollection.zig create mode 100644 src/browser/webapi/collections/NodeList.zig create mode 100644 src/browser/webapi/collections/iterator.zig create mode 100644 src/browser/webapi/collections/node_live.zig create mode 100644 src/browser/webapi/css/CSSStyleDeclaration.zig create mode 100644 src/browser/webapi/css/CSSStyleProperties.zig create mode 100644 src/browser/webapi/element/Attribute.zig create mode 100644 src/browser/webapi/element/Html.zig create mode 100644 src/browser/webapi/element/Svg.zig create mode 100644 src/browser/webapi/element/html/Anchor.zig create mode 100644 src/browser/webapi/element/html/BR.zig create mode 100644 src/browser/webapi/element/html/Body.zig create mode 100644 src/browser/webapi/element/html/Button.zig create mode 100644 src/browser/webapi/element/html/Custom.zig create mode 100644 src/browser/webapi/element/html/Div.zig create mode 100644 src/browser/webapi/element/html/Form.zig create mode 100644 src/browser/webapi/element/html/Generic.zig create mode 100644 src/browser/webapi/element/html/HR.zig create mode 100644 src/browser/webapi/element/html/Head.zig create mode 100644 src/browser/webapi/element/html/Heading.zig create mode 100644 src/browser/webapi/element/html/Html.zig create mode 100644 src/browser/webapi/element/html/Image.zig create mode 100644 src/browser/webapi/element/html/Input.zig create mode 100644 src/browser/webapi/element/html/LI.zig create mode 100644 src/browser/webapi/element/html/Link.zig create mode 100644 src/browser/webapi/element/html/Meta.zig create mode 100644 src/browser/webapi/element/html/OL.zig create mode 100644 src/browser/webapi/element/html/Option.zig create mode 100644 src/browser/webapi/element/html/Paragraph.zig create mode 100644 src/browser/webapi/element/html/Script.zig create mode 100644 src/browser/webapi/element/html/Select.zig create mode 100644 src/browser/webapi/element/html/Style.zig create mode 100644 src/browser/webapi/element/html/TextArea.zig create mode 100644 src/browser/webapi/element/html/Title.zig create mode 100644 src/browser/webapi/element/html/UL.zig create mode 100644 src/browser/webapi/element/html/Unknown.zig create mode 100644 src/browser/webapi/element/svg/Generic.zig create mode 100644 src/browser/webapi/element/svg/Rect.zig create mode 100644 src/browser/webapi/encoding/TextDecoder.zig create mode 100644 src/browser/webapi/encoding/TextEncoder.zig create mode 100644 src/browser/webapi/event/ErrorEvent.zig create mode 100644 src/browser/webapi/event/ProgressEvent.zig create mode 100644 src/browser/webapi/net/Fetch.zig create mode 100644 src/browser/webapi/net/Request.zig create mode 100644 src/browser/webapi/net/Response.zig create mode 100644 src/browser/webapi/net/URLSearchParams.zig create mode 100644 src/browser/webapi/net/XMLHttpRequest.zig create mode 100644 src/browser/webapi/net/XMLHttpRequestEventTarget.zig create mode 100644 src/browser/webapi/selector/List.zig create mode 100644 src/browser/webapi/selector/Parser.zig create mode 100644 src/browser/webapi/selector/Selector.zig rename src/browser/{ => webapi}/storage/cookie.zig (99%) create mode 100644 src/browser/webapi/storage/storage.zig delete mode 100644 src/browser/xhr/File.zig delete mode 100644 src/browser/xhr/event_target.zig delete mode 100644 src/browser/xhr/form_data.zig delete mode 100644 src/browser/xhr/progress_event.zig delete mode 100644 src/browser/xhr/xhr.zig delete mode 100644 src/browser/xmlserializer/xmlserializer.zig create mode 100644 src/html5ever/Cargo.lock create mode 100644 src/html5ever/Cargo.toml create mode 100644 src/html5ever/lib.rs create mode 100644 src/html5ever/sink.rs create mode 100644 src/html5ever/types.rs create mode 100644 src/lightpanda.zig create mode 100644 src/string.zig delete mode 100644 src/tests/browser.html delete mode 100644 src/tests/crypto.html delete mode 100644 src/tests/css.html delete mode 100644 src/tests/cssom/css_rule_list.html delete mode 100644 src/tests/cssom/css_style_declaration.html delete mode 100644 src/tests/cssom/css_stylesheet.html delete mode 100644 src/tests/dom/animation.html delete mode 100644 src/tests/dom/attribute.html delete mode 100644 src/tests/dom/character_data.html delete mode 100644 src/tests/dom/comment.html delete mode 100644 src/tests/dom/document.html delete mode 100644 src/tests/dom/document_fragment.html delete mode 100644 src/tests/dom/document_type.html delete mode 100644 src/tests/dom/dom_parser.html delete mode 100644 src/tests/dom/element.html delete mode 100644 src/tests/dom/event_target.html delete mode 100644 src/tests/dom/exceptions.html delete mode 100644 src/tests/dom/html_collection.html delete mode 100644 src/tests/dom/implementation.html delete mode 100644 src/tests/dom/intersection_observer.html delete mode 100644 src/tests/dom/message_channel.html delete mode 100644 src/tests/dom/mutation_observer.html delete mode 100644 src/tests/dom/named_node_map.html delete mode 100644 src/tests/dom/node.html delete mode 100644 src/tests/dom/node_filter.html delete mode 100644 src/tests/dom/node_iterator.html delete mode 100644 src/tests/dom/node_list.html delete mode 100644 src/tests/dom/node_owner.html delete mode 100644 src/tests/dom/performance.html delete mode 100644 src/tests/dom/performance_observer.html delete mode 100644 src/tests/dom/processing_instruction.html delete mode 100644 src/tests/dom/range.html delete mode 100644 src/tests/dom/shadow_root.html delete mode 100644 src/tests/dom/text.html delete mode 100644 src/tests/dom/token_list.html delete mode 100644 src/tests/encoding/decoder.html delete mode 100644 src/tests/encoding/encoder.html delete mode 100644 src/tests/events/custom.html delete mode 100644 src/tests/events/event.html delete mode 100644 src/tests/events/keyboard.html delete mode 100644 src/tests/events/mouse.html delete mode 100644 src/tests/fetch/fetch.html delete mode 100644 src/tests/fetch/headers.html delete mode 100644 src/tests/fetch/request.html delete mode 100644 src/tests/fetch/response.html delete mode 100644 src/tests/html/abort_controller.html delete mode 100644 src/tests/html/dataset.html delete mode 100644 src/tests/html/document.html delete mode 100644 src/tests/html/element.html delete mode 100644 src/tests/html/error_event.html delete mode 100644 src/tests/html/history.html delete mode 100644 src/tests/html/image.html delete mode 100644 src/tests/html/input.html delete mode 100644 src/tests/html/link.html delete mode 100644 src/tests/html/location.html delete mode 100644 src/tests/html/navigator.html delete mode 100644 src/tests/html/screen.html delete mode 100644 src/tests/html/script/dynamic_import.html delete mode 100644 src/tests/html/script/import.html delete mode 100644 src/tests/html/script/import.js delete mode 100644 src/tests/html/script/import2.js delete mode 100644 src/tests/html/script/importmap.html delete mode 100644 src/tests/html/script/inline_defer.html delete mode 100644 src/tests/html/script/inline_defer.js delete mode 100644 src/tests/html/script/script.html delete mode 100644 src/tests/html/select.html delete mode 100644 src/tests/html/slot.html delete mode 100644 src/tests/html/style.html delete mode 100644 src/tests/html/svg.html delete mode 100644 src/tests/html/template.html delete mode 100644 src/tests/polyfill/webcomponents.html delete mode 100644 src/tests/storage/local_storage.html delete mode 100644 src/tests/streams/readable_stream.html delete mode 100644 src/tests/testing.js delete mode 100644 src/tests/url/url.html delete mode 100644 src/tests/url/url_search_params.html delete mode 100644 src/tests/window/frames.html delete mode 100644 src/tests/window/window.html delete mode 100644 src/tests/xhr/file.html delete mode 100644 src/tests/xhr/form_data.html delete mode 100644 src/tests/xhr/progress_event.html delete mode 100644 src/tests/xhr/xhr.html delete mode 100644 src/tests/xmlserializer.html delete mode 100644 src/url.zig delete mode 160000 vendor/mimalloc delete mode 160000 vendor/netsurf/libdom delete mode 160000 vendor/netsurf/libhubbub delete mode 160000 vendor/netsurf/libparserutils delete mode 160000 vendor/netsurf/libwapcaplet delete mode 160000 vendor/netsurf/share/netsurf-buildsystem diff --git a/.github/actions/install/action.yml b/.github/actions/install/action.yml index 17c027593..e9864c01d 100644 --- a/.github/actions/install/action.yml +++ b/.github/actions/install/action.yml @@ -5,7 +5,7 @@ inputs: zig: description: 'Zig version to install' required: false - default: '0.15.1' + default: '0.15.2' arch: description: 'CPU arch used to select the v8 lib' required: false diff --git a/.github/workflows/zig-fmt.yml b/.github/workflows/zig-fmt.yml index 2a1fdd527..106e557a1 100644 --- a/.github/workflows/zig-fmt.yml +++ b/.github/workflows/zig-fmt.yml @@ -1,7 +1,7 @@ name: zig-fmt env: - ZIG_VERSION: 0.15.1 + ZIG_VERSION: 0.15.2 on: pull_request: diff --git a/.gitignore b/.gitignore index ad9ae7b45..9a7968b9a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ -zig-cache /.zig-cache/ -zig-out -/vendor/netsurf/out -/vendor/libiconv/ +/zig-out/ lightpanda.id /v8/ +/build/ +src/html5ever/target/ diff --git a/.gitmodules b/.gitmodules index 717d079bb..3358b9a3e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,24 +1,6 @@ -[submodule "vendor/netsurf/libwapcaplet"] - path = vendor/netsurf/libwapcaplet - url = https://github.com/lightpanda-io/libwapcaplet.git/ -[submodule "vendor/netsurf/libparserutils"] - path = vendor/netsurf/libparserutils - url = https://github.com/lightpanda-io/libparserutils.git/ -[submodule "vendor/netsurf/libdom"] - path = vendor/netsurf/libdom - url = https://github.com/lightpanda-io/libdom.git/ -[submodule "vendor/netsurf/share/netsurf-buildsystem"] - path = vendor/netsurf/share/netsurf-buildsystem - url = https://github.com/lightpanda-io/netsurf-buildsystem.git -[submodule "vendor/netsurf/libhubbub"] - path = vendor/netsurf/libhubbub - url = https://github.com/lightpanda-io/libhubbub.git/ [submodule "tests/wpt"] path = tests/wpt url = https://github.com/lightpanda-io/wpt -[submodule "vendor/mimalloc"] - path = vendor/mimalloc - url = https://github.com/microsoft/mimalloc.git/ [submodule "vendor/nghttp2"] path = vendor/nghttp2 url = https://github.com/nghttp2/nghttp2.git diff --git a/Dockerfile b/Dockerfile index bcb613f7f..919a9a658 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ FROM debian:stable ARG MINISIG=0.12 -ARG ZIG=0.15.1 +ARG ZIG=0.15.2 ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U ARG V8=14.0.365.4 ARG ZIG_V8=v0.1.33 diff --git a/Makefile b/Makefile index b0ae69015..957705e2b 100644 --- a/Makefile +++ b/Makefile @@ -96,9 +96,16 @@ wpt-summary: @printf "\e[36mBuilding wpt...\e[0m\n" @$(ZIG) build wpt -- --summary $(filter-out $@,$(MAKECMDGOALS)) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;) -## Test +## Test - `grep` is used to filter out the huge compile command on build +ifeq ($(OS), macos) test: - @TEST_FILTER='${F}' $(ZIG) build test -freference-trace --summary all + @script -q /dev/null sh -c 'TEST_FILTER="${F}" $(ZIG) build test -freference-trace --summary all' 2>&1 \ + | grep --line-buffered -v "^/.*zig test -freference-trace" +else +test: + @script -qec 'TEST_FILTER="${F}" $(ZIG) build test -freference-trace --summary all' /dev/null 2>&1 \ + | grep --line-buffered -v "^/.*zig test -freference-trace" +endif ## Run demo/runner end to end tests end2end: @@ -120,128 +127,24 @@ build-v8: # Install and build required dependencies commands # ------------ -.PHONY: install-submodule -.PHONY: install-libiconv -.PHONY: _install-netsurf install-netsurf clean-netsurf test-netsurf install-netsurf-dev -.PHONY: install-mimalloc install-mimalloc-dev clean-mimalloc -.PHONY: install-dev install +.PHONY: install-html5ever install-html5ever-dev +.PHONY: install install-dev ## Install and build dependencies for release -install: install-submodule install-libiconv install-netsurf install-mimalloc +install: install-submodule install-html5ever ## Install and build dependencies for dev -install-dev: install-submodule install-libiconv install-netsurf-dev install-mimalloc-dev - -install-netsurf-dev: _install-netsurf -install-netsurf-dev: OPTCFLAGS := -O0 -g -DNDEBUG - -install-netsurf: _install-netsurf -install-netsurf: OPTCFLAGS := -DNDEBUG - -BC_NS := $(BC)vendor/netsurf/out/$(OS)-$(ARCH) -ICONV := $(BC)vendor/libiconv/out/$(OS)-$(ARCH) -# TODO: add Linux iconv path (I guess it depends on the distro) -# TODO: this way of linking libiconv is not ideal. We should have a more generic way -# and stick to a specif version. Maybe build from source. Anyway not now. -_install-netsurf: clean-netsurf - @printf "\e[36mInstalling NetSurf...\e[0m\n" && \ - ls $(ICONV)/lib/libiconv.a 1> /dev/null || (printf "\e[33mERROR: you need to execute 'make install-libiconv'\e[0m\n"; exit 1;) && \ - mkdir -p $(BC_NS) && \ - cp -R vendor/netsurf/share $(BC_NS) && \ - export PREFIX=$(BC_NS) && \ - export OPTLDFLAGS="-L$(ICONV)/lib" && \ - export OPTCFLAGS="$(OPTCFLAGS) -I$(ICONV)/include" && \ - printf "\e[33mInstalling libwapcaplet...\e[0m\n" && \ - cd vendor/netsurf/libwapcaplet && \ - BUILDDIR=$(BC_NS)/build/libwapcaplet make install && \ - cd ../libparserutils && \ - printf "\e[33mInstalling libparserutils...\e[0m\n" && \ - BUILDDIR=$(BC_NS)/build/libparserutils make install && \ - cd ../libhubbub && \ - printf "\e[33mInstalling libhubbub...\e[0m\n" && \ - BUILDDIR=$(BC_NS)/build/libhubbub make install && \ - rm src/treebuilder/autogenerated-element-type.c && \ - cd ../libdom && \ - printf "\e[33mInstalling libdom...\e[0m\n" && \ - BUILDDIR=$(BC_NS)/build/libdom make install && \ - printf "\e[33mRunning libdom example...\e[0m\n" && \ - cd examples && \ - $(ZIG) cc \ - -I$(ICONV)/include \ - -I$(BC_NS)/include \ - -L$(ICONV)/lib \ - -L$(BC_NS)/lib \ - -liconv \ - -ldom \ - -lhubbub \ - -lparserutils \ - -lwapcaplet \ - -o a.out \ - dom-structure-dump.c \ - $(ICONV)/lib/libiconv.a && \ - ./a.out > /dev/null && \ - rm a.out && \ - printf "\e[36mDone NetSurf $(OS)\e[0m\n" - -clean-netsurf: - @printf "\e[36mCleaning NetSurf build...\e[0m\n" && \ - rm -Rf $(BC_NS) - -test-netsurf: - @printf "\e[36mTesting NetSurf...\e[0m\n" && \ - export PREFIX=$(BC_NS) && \ - export LDFLAGS="-L$(ICONV)/lib -L$(BC_NS)/lib" && \ - export CFLAGS="-I$(ICONV)/include -I$(BC_NS)/include" && \ - cd vendor/netsurf/libdom && \ - BUILDDIR=$(BC_NS)/build/libdom make test - -download-libiconv: -ifeq ("$(wildcard vendor/libiconv/libiconv-1.17)","") - @mkdir -p vendor/libiconv - @cd vendor/libiconv && \ - curl -L https://github.com/lightpanda-io/libiconv/releases/download/1.17/libiconv-1.17.tar.gz | tar -xvzf - -endif +install-dev: install-submodule install-html5ever-dev -build-libiconv: clean-libiconv - @cd vendor/libiconv/libiconv-1.17 && \ - ./configure --prefix=$(ICONV) --enable-static && \ - make && make install +install-html5ever: + cd src/html5ever && cargo build --release --target-dir ../../build/html5ever/ -install-libiconv: download-libiconv build-libiconv - -clean-libiconv: -ifneq ("$(wildcard vendor/libiconv/libiconv-1.17/Makefile)","") - @cd vendor/libiconv/libiconv-1.17 && \ - make clean -endif +install-html5ever-dev: + cd src/html5ever && cargo build --target-dir ../../build/html5ever/ data: cd src/data && go run public_suffix_list_gen.go > public_suffix_list.zig -.PHONY: _build_mimalloc - -MIMALLOC := $(BC)vendor/mimalloc/out/$(OS)-$(ARCH) -_build_mimalloc: clean-mimalloc - @mkdir -p $(MIMALLOC)/build && \ - cd $(MIMALLOC)/build && \ - cmake -DMI_BUILD_SHARED=OFF -DMI_BUILD_OBJECT=OFF -DMI_BUILD_TESTS=OFF -DMI_OVERRIDE=OFF $(OPTS) ../../.. && \ - make && \ - mkdir -p $(MIMALLOC)/lib - -install-mimalloc-dev: _build_mimalloc -install-mimalloc-dev: OPTS=-DCMAKE_BUILD_TYPE=Debug -install-mimalloc-dev: - @cd $(MIMALLOC) && \ - mv build/libmimalloc-debug.a lib/libmimalloc.a - -install-mimalloc: _build_mimalloc -install-mimalloc: - @cd $(MIMALLOC) && \ - mv build/libmimalloc.a lib/libmimalloc.a - -clean-mimalloc: - @rm -Rf $(MIMALLOC)/build - ## Init and update git submodule install-submodule: @git submodule init && \ diff --git a/README.md b/README.md index a1009e7f1..87c393a52 100644 --- a/README.md +++ b/README.md @@ -164,7 +164,7 @@ You can also follow the progress of our Javascript support in our dedicated [zig ### Prerequisites -Lightpanda is written with [Zig](https://ziglang.org/) `0.15.1`. You have to +Lightpanda is written with [Zig](https://ziglang.org/) `0.15.2`. You have to install it with the right version in order to build the project. Lightpanda also depends on diff --git a/build.zig b/build.zig index 3437dfad0..d7effb26b 100644 --- a/build.zig +++ b/build.zig @@ -23,7 +23,7 @@ const Build = std.Build; /// Do not rename this constant. It is scanned by some scripts to determine /// which zig version to install. -const recommended_zig_version = "0.15.1"; +const recommended_zig_version = "0.15.2"; pub fn build(b: *Build) !void { switch (comptime builtin.zig_version.order(std.SemanticVersion.parse(recommended_zig_version) catch unreachable)) { @@ -49,87 +49,93 @@ pub fn build(b: *Build) !void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); - // We're still using llvm because the new x86 backend seems to crash - // with v8. This can be reproduced in zig-v8-fork. + const enable_tsan = b.option(bool, "tsan", "Enable Thread Sanitizer"); + const enable_csan = b.option(std.zig.SanitizeC, "csan", "Enable C Sanitizers"); - const lightpanda_module = b.addModule("lightpanda", .{ - .root_source_file = b.path("src/main.zig"), - .target = target, - .optimize = optimize, - .link_libc = true, - .link_libcpp = true, - }); - try addDependencies(b, lightpanda_module, opts); + const lightpanda_module = blk: { + const mod = b.addModule("lightpanda", .{ + .root_source_file = b.path("src/lightpanda.zig"), + .target = target, + .optimize = optimize, + .link_libc = true, + .link_libcpp = true, + .sanitize_c = enable_csan, + .sanitize_thread = enable_tsan, + }); + + try addDependencies(b, mod, opts); + + if (optimize == .ReleaseFast or optimize == .ReleaseSmall) { + mod.addLibraryPath(b.path("build/html5ever/release")); + } else { + mod.addLibraryPath(b.path("build/html5ever/debug")); + } + mod.linkSystemLibrary("litefetch_html5ever", .{}); + + break :blk mod; + }; { // browser - // ------- - - // compile and install const exe = b.addExecutable(.{ .name = "lightpanda", .use_llvm = true, - .root_module = lightpanda_module, + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + .sanitize_c = enable_csan, + .sanitize_thread = enable_tsan, + .imports = &.{ + .{.name = "lightpanda", .module = lightpanda_module}, + }, + }), }); b.installArtifact(exe); - // run const run_cmd = b.addRunArtifact(exe); if (b.args) |args| { run_cmd.addArgs(args); } - - // step const run_step = b.step("run", "Run the app"); run_step.dependOn(&run_cmd.step); } { - // tests - // ---- - - // compile + // test const tests = b.addTest(.{ .root_module = lightpanda_module, - .use_llvm = true, .test_runner = .{ .path = b.path("src/test_runner.zig"), .mode = .simple }, }); - const run_tests = b.addRunArtifact(tests); - if (b.args) |args| { - run_tests.addArgs(args); - } - - // step - const tests_step = b.step("test", "Run unit tests"); - tests_step.dependOn(&run_tests.step); + const test_step = b.step("test", "Run unit tests"); + test_step.dependOn(&run_tests.step); } { // wpt - // ----- - const wpt_module = b.createModule(.{ - .root_source_file = b.path("src/main_wpt.zig"), - .target = target, - .optimize = optimize, - }); - try addDependencies(b, wpt_module, opts); - - // compile and install - const wpt = b.addExecutable(.{ + const exe = b.addExecutable(.{ .name = "lightpanda-wpt", .use_llvm = true, - .root_module = wpt_module, + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main_wpt.zig"), + .target = target, + .optimize = optimize, + .sanitize_c = enable_csan, + .sanitize_thread = enable_tsan, + .imports = &.{ + .{.name = "lightpanda", .module = lightpanda_module}, + }, + }), }); + b.installArtifact(exe); - // run - const wpt_cmd = b.addRunArtifact(wpt); + const run_cmd = b.addRunArtifact(exe); if (b.args) |args| { - wpt_cmd.addArgs(args); + run_cmd.addArgs(args); } - // step - const wpt_step = b.step("wpt", "WPT tests"); - wpt_step.dependOn(&wpt_cmd.step); + const run_step = b.step("wpt", "Run WPT tests"); + run_step.dependOn(&run_cmd.step); } { @@ -152,7 +158,6 @@ pub fn build(b: *Build) !void { } fn addDependencies(b: *Build, mod: *Build.Module, opts: *Build.Step.Options) !void { - try moduleNetSurf(b, mod); mod.addImport("build_config", opts.createModule()); const target = mod.resolved_target.?; @@ -397,63 +402,6 @@ fn addDependencies(b: *Build, mod: *Build.Module, opts: *Build.Step.Options) !vo } } -fn moduleNetSurf(b: *Build, mod: *Build.Module) !void { - const target = mod.resolved_target.?; - const os = target.result.os.tag; - const arch = target.result.cpu.arch; - - // iconv - const libiconv_lib_path = try std.fmt.allocPrint( - b.allocator, - "vendor/libiconv/out/{s}-{s}/lib/libiconv.a", - .{ @tagName(os), @tagName(arch) }, - ); - const libiconv_include_path = try std.fmt.allocPrint( - b.allocator, - "vendor/libiconv/out/{s}-{s}/lib/libiconv.a", - .{ @tagName(os), @tagName(arch) }, - ); - mod.addObjectFile(b.path(libiconv_lib_path)); - mod.addIncludePath(b.path(libiconv_include_path)); - - { - // mimalloc - const mimalloc = "vendor/mimalloc"; - const lib_path = try std.fmt.allocPrint( - b.allocator, - mimalloc ++ "/out/{s}-{s}/lib/libmimalloc.a", - .{ @tagName(os), @tagName(arch) }, - ); - mod.addObjectFile(b.path(lib_path)); - mod.addIncludePath(b.path(mimalloc ++ "/include")); - } - - // netsurf libs - const ns = "vendor/netsurf"; - const ns_include_path = try std.fmt.allocPrint( - b.allocator, - ns ++ "/out/{s}-{s}/include", - .{ @tagName(os), @tagName(arch) }, - ); - mod.addIncludePath(b.path(ns_include_path)); - - const libs: [4][]const u8 = .{ - "libdom", - "libhubbub", - "libparserutils", - "libwapcaplet", - }; - inline for (libs) |lib| { - const ns_lib_path = try std.fmt.allocPrint( - b.allocator, - ns ++ "/out/{s}-{s}/lib/" ++ lib ++ ".a", - .{ @tagName(os), @tagName(arch) }, - ); - mod.addObjectFile(b.path(ns_lib_path)); - mod.addIncludePath(b.path(ns ++ "/" ++ lib ++ "/src")); - } -} - fn buildZlib(b: *Build, m: *Build.Module) !void { const zlib = b.addLibrary(.{ .name = "zlib", diff --git a/flake.nix b/flake.nix index 971f0f44c..fd5fbef87 100644 --- a/flake.nix +++ b/flake.nix @@ -49,7 +49,7 @@ targetPkgs = pkgs: with pkgs; [ # Build Tools - zigpkgs."0.15.1" + zigpkgs."0.15.2" zls python3 pkg-config diff --git a/src/Scheduler.zig b/src/Scheduler.zig new file mode 100644 index 000000000..0898d19b3 --- /dev/null +++ b/src/Scheduler.zig @@ -0,0 +1,88 @@ +const std = @import("std"); +const log = @import("log.zig"); + +const timestamp = @import("datetime.zig").milliTimestamp; + +const Queue = std.PriorityQueue(Task, void, struct { + fn compare(_: void, a: Task, b: Task) std.math.Order { + return std.math.order(a.run_at, b.run_at); + } +}.compare); + +const Scheduler = @This(); + +low_priority: Queue, +high_priority: Queue, + +pub fn init(allocator: std.mem.Allocator) Scheduler { + return .{ + .low_priority = Queue.init(allocator, {}), + .high_priority = Queue.init(allocator, {}), + }; +} + +pub fn reset(self: *Scheduler) void { + self.low_priority.cap = 0; + self.low_priority.items.len = 0; + + self.high_priority.cap = 0; + self.high_priority.items.len = 0; +} + +const AddOpts = struct { + name: []const u8 = "", + low_priority: bool = false, +}; +pub fn add(self: *Scheduler, ctx: *anyopaque, cb: Callback, run_in_ms: u32, opts: AddOpts) !void { + log.debug(.scheduler, "scheduler.add", .{ .name = opts.name, .run_in_ms = run_in_ms, .low_priority = opts.low_priority }); + var queue = if (opts.low_priority) &self.low_priority else &self.high_priority; + return queue.add(.{ + .ctx = ctx, + .callback = cb, + .name = opts.name, + .run_at = timestamp(.monotonic) + run_in_ms, + }); +} + +pub fn run(self: *Scheduler) !?u64 { + _ = try self.runQueue(&self.low_priority); + return self.runQueue(&self.high_priority); +} + +fn runQueue(self: *Scheduler, queue: *Queue) !?u64 { + if (queue.count() == 0) { + return null; + } + + const now = timestamp(.monotonic); + + while (queue.peek()) |*task_| { + if (task_.run_at > now) { + return @intCast(task_.run_at - now); + } + var task = queue.remove(); + log.debug(.scheduler, "scheduler.runTask", .{ .name = task.name }); + + const repeat_in_ms = task.callback(task.ctx) catch |err| { + log.warn(.scheduler, "task.callback", .{ .name = task.name, .err = err }); + continue; + }; + + if (repeat_in_ms) |ms| { + // Task cannot be repeated immediately, and they should know that + std.debug.assert(ms != 0); + task.run_at = now + ms; + try self.low_priority.add(task); + } + } + return null; +} + +const Task = struct { + run_at: u64, + ctx: *anyopaque, + name: []const u8, + callback: Callback, +}; + +const Callback = *const fn (ctx: *anyopaque) anyerror!?u32; diff --git a/src/TestHTTPServer.zig b/src/TestHTTPServer.zig index 9867600d0..fdc51b904 100644 --- a/src/TestHTTPServer.zig +++ b/src/TestHTTPServer.zig @@ -61,6 +61,7 @@ fn handleConnection(self: *TestHTTPServer, conn: std.net.Server.Connection) !voi return err; }, }; + self.handler(&req) catch |err| { std.debug.print("test http error '{s}': {}\n", .{ req.head.target, err }); try req.respond("server error", .{ .status = .internal_server_error }); diff --git a/src/app.zig b/src/app.zig index 719dd9b72..ef94486b1 100644 --- a/src/app.zig +++ b/src/app.zig @@ -6,93 +6,87 @@ const log = @import("log.zig"); const Http = @import("http/Http.zig"); const Platform = @import("browser/js/Platform.zig"); +const Notification = @import("Notification.zig"); const Telemetry = @import("telemetry/telemetry.zig").Telemetry; -const Notification = @import("notification.zig").Notification; // Container for global state / objects that various parts of the system // might need. -pub const App = struct { - http: Http, - config: Config, - platform: Platform, - allocator: Allocator, - telemetry: Telemetry, - app_dir_path: ?[]const u8, - notification: *Notification, - - pub const RunMode = enum { - help, - fetch, - serve, - version, - }; +const App = @This(); + +http: Http, +config: Config, +platform: Platform, +telemetry: Telemetry, +allocator: Allocator, +app_dir_path: ?[]const u8, +notification: *Notification, + +pub const RunMode = enum { + help, + fetch, + serve, + version, +}; - pub const Config = struct { - run_mode: RunMode, - tls_verify_host: bool = true, - http_proxy: ?[:0]const u8 = null, - proxy_bearer_token: ?[:0]const u8 = null, - http_timeout_ms: ?u31 = null, - http_connect_timeout_ms: ?u31 = null, - http_max_host_open: ?u8 = null, - http_max_concurrent: ?u8 = null, - user_agent: [:0]const u8, - }; +pub const Config = struct { + run_mode: RunMode, + tls_verify_host: bool = true, + http_proxy: ?[:0]const u8 = null, + proxy_bearer_token: ?[:0]const u8 = null, + http_timeout_ms: ?u31 = null, + http_connect_timeout_ms: ?u31 = null, + http_max_host_open: ?u8 = null, + http_max_concurrent: ?u8 = null, + user_agent: [:0]const u8, +}; - pub fn init(allocator: Allocator, config: Config) !*App { - const app = try allocator.create(App); - errdefer allocator.destroy(app); - - const notification = try Notification.init(allocator, null); - errdefer notification.deinit(); - - var http = try Http.init(allocator, .{ - .max_host_open = config.http_max_host_open orelse 4, - .max_concurrent = config.http_max_concurrent orelse 10, - .timeout_ms = config.http_timeout_ms orelse 5000, - .connect_timeout_ms = config.http_connect_timeout_ms orelse 0, - .http_proxy = config.http_proxy, - .tls_verify_host = config.tls_verify_host, - .proxy_bearer_token = config.proxy_bearer_token, - .user_agent = config.user_agent, - }); - errdefer http.deinit(); - - const platform = try Platform.init(); - errdefer platform.deinit(); - - const app_dir_path = getAndMakeAppDir(allocator); - - app.* = .{ - .http = http, - .allocator = allocator, - .telemetry = undefined, - .platform = platform, - .app_dir_path = app_dir_path, - .notification = notification, - .config = config, - }; - - app.telemetry = try Telemetry.init(app, config.run_mode); - errdefer app.telemetry.deinit(); - - try app.telemetry.register(app.notification); - - return app; - } +pub fn init(allocator: Allocator, config: Config) !*App { + const app = try allocator.create(App); + errdefer allocator.destroy(app); - pub fn deinit(self: *App) void { - const allocator = self.allocator; - if (self.app_dir_path) |app_dir_path| { - allocator.free(app_dir_path); - } - self.telemetry.deinit(); - self.notification.deinit(); - self.http.deinit(); - self.platform.deinit(); - allocator.destroy(self); + app.config = config; + app.allocator = allocator; + + app.notification = try Notification.init(allocator, null); + errdefer app.notification.deinit(); + + app.http = try Http.init(allocator, .{ + .max_host_open = config.http_max_host_open orelse 4, + .max_concurrent = config.http_max_concurrent orelse 10, + .timeout_ms = config.http_timeout_ms orelse 5000, + .connect_timeout_ms = config.http_connect_timeout_ms orelse 0, + .http_proxy = config.http_proxy, + .tls_verify_host = config.tls_verify_host, + .proxy_bearer_token = config.proxy_bearer_token, + .user_agent = config.user_agent, + }); + errdefer app.http.deinit(); + + app.platform = try Platform.init(); + errdefer app.platform.deinit(); + + app.app_dir_path = getAndMakeAppDir(allocator); + + app.telemetry = try Telemetry.init(app, config.run_mode); + errdefer app.telemetry.deinit(); + + try app.telemetry.register(app.notification); + + return app; +} + +pub fn deinit(self: *App) void { + const allocator = self.allocator; + if (self.app_dir_path) |app_dir_path| { + allocator.free(app_dir_path); } -}; + self.telemetry.deinit(); + self.notification.deinit(); + self.http.deinit(); + self.platform.deinit(); + + allocator.destroy(self); +} fn getAndMakeAppDir(allocator: Allocator) ?[]const u8 { if (@import("builtin").is_test) { diff --git a/src/browser/DataURI.zig b/src/browser/DataURI.zig deleted file mode 100644 index 00d3792f1..000000000 --- a/src/browser/DataURI.zig +++ /dev/null @@ -1,52 +0,0 @@ -const std = @import("std"); -const Allocator = std.mem.Allocator; - -// Parses data:[][;base64], -pub fn parse(allocator: Allocator, src: []const u8) !?[]const u8 { - if (!std.mem.startsWith(u8, src, "data:")) { - return null; - } - - const uri = src[5..]; - const data_starts = std.mem.indexOfScalar(u8, uri, ',') orelse return null; - - var data = uri[data_starts + 1 ..]; - - // Extract the encoding. - const metadata = uri[0..data_starts]; - if (std.mem.endsWith(u8, metadata, ";base64")) { - const decoder = std.base64.standard.Decoder; - const decoded_size = try decoder.calcSizeForSlice(data); - - const buffer = try allocator.alloc(u8, decoded_size); - errdefer allocator.free(buffer); - - try decoder.decode(buffer, data); - data = buffer; - } - - return data; -} - -const testing = @import("../testing.zig"); -test "DataURI: parse valid" { - try test_valid("data:text/javascript; charset=utf-8;base64,Zm9v", "foo"); - try test_valid("data:text/javascript; charset=utf-8;,foo", "foo"); - try test_valid("data:,foo", "foo"); -} - -test "DataURI: parse invalid" { - try test_cannot_parse("atad:,foo"); - try test_cannot_parse("data:foo"); - try test_cannot_parse("data:"); -} - -fn test_valid(uri: []const u8, expected: []const u8) !void { - defer testing.reset(); - const data_uri = try parse(testing.arena_allocator, uri) orelse return error.TestFailed; - try testing.expectEqual(expected, data_uri); -} - -fn test_cannot_parse(uri: []const u8) !void { - try testing.expectEqual(null, parse(undefined, uri)); -} diff --git a/src/browser/EventManager.zig b/src/browser/EventManager.zig new file mode 100644 index 000000000..89cba8019 --- /dev/null +++ b/src/browser/EventManager.zig @@ -0,0 +1,297 @@ +const std = @import("std"); +const builtin = @import("builtin"); + +const log = @import("../log.zig"); +const String = @import("../string.zig").String; + +const js = @import("js/js.zig"); +const Page = @import("Page.zig"); + +const Node = @import("webapi/Node.zig"); +const Event = @import("webapi/Event.zig"); +const EventTarget = @import("webapi/EventTarget.zig"); + +const Allocator = std.mem.Allocator; + +const IS_DEBUG = builtin.mode == .Debug; + +pub const EventManager = @This(); + +page: *Page, +arena: Allocator, +listener_pool: std.heap.MemoryPool(Listener), +lookup: std.AutoHashMapUnmanaged(usize, std.DoublyLinkedList), + +pub fn init(page: *Page) EventManager { + return .{ + .page = page, + .lookup = .{}, + .arena = page.arena, + .listener_pool = std.heap.MemoryPool(Listener).init(page.arena), + }; +} + +pub const RegisterOptions = struct { + once: bool = false, + capture: bool = false, + passive: bool = false, + signal: ?*@import("webapi/AbortSignal.zig") = null, +}; +pub fn register(self: *EventManager, target: *EventTarget, typ: []const u8, function: js.Function, opts: RegisterOptions) !void { + if (comptime IS_DEBUG) { + log.debug(.event, "eventManager.register", .{ .type = typ, .capture = opts.capture, .once = opts.once }); + } + + // If a signal is provided and already aborted, don't register the listener + if (opts.signal) |signal| { + if (signal.getAborted()) { + return; + } + } + + const gop = try self.lookup.getOrPut(self.arena, @intFromPtr(target)); + if (gop.found_existing) { + // check for duplicate functions already registered + var node = gop.value_ptr.first; + while (node) |n| { + const listener: *Listener = @alignCast(@fieldParentPtr("node", n)); + if (listener.function.eql(function) and listener.capture == opts.capture) { + return; + } + node = n.next; + } + } else { + gop.value_ptr.* = .{}; + } + + const listener = try self.listener_pool.create(); + listener.* = .{ + .node = .{}, + .once = opts.once, + .capture = opts.capture, + .passive = opts.passive, + .function = .{ .value = function }, + .signal = opts.signal, + .typ = try String.init(self.arena, typ, .{}), + }; + // append the listener to the list of listeners for this target + gop.value_ptr.append(&listener.node); +} + +pub fn remove(self: *EventManager, target: *EventTarget, typ: []const u8, function: js.Function, use_capture: bool) void { + const list = self.lookup.getPtr(@intFromPtr(target)) orelse return; + if (findListener(list, typ, function, use_capture)) |listener| { + self.removeListener(list, listener); + } +} + +pub fn dispatch(self: *EventManager, target: *EventTarget, event: *Event) !void { + if (comptime IS_DEBUG) { + log.debug(.event, "eventManager.dispatch", .{ .type = event._type_string.str(), .bubbles = event._bubbles }); + } + event._target = target; + switch (target._type) { + .node => |node| try self.dispatchNode(node, event), + .xhr, .window, .abort_signal => { + const list = self.lookup.getPtr(@intFromPtr(target)) orelse return; + try self.dispatchAll(list, target, event); + }, + } +} + +// There are a lot of events that can be attached via addEventListener or as +// a property, like the XHR events, or window.onload. You might think that the +// property is just a shortcut for calling addEventListener, but they are distinct. +// An event set via property cannot be removed by removeEventListener. If you +// set both the property and add a listener, they both execute. +const DispatchWithFunctionOptions = struct { + context: []const u8, + inject_target: bool = true, +}; +pub fn dispatchWithFunction(self: *EventManager, target: *EventTarget, event: *Event, function_: ?js.Function, comptime opts: DispatchWithFunctionOptions) !void { + if (comptime IS_DEBUG) { + log.debug(.event, "dispatchWithFunction", .{ .type = event._type_string.str(), .context = opts.context, .has_function = function_ != null }); + } + + if (comptime opts.inject_target) { + event._target = target; + } + + if (function_) |func| { + event._current_target = target; + func.call(void, .{event}) catch |err| { + // a non-JS error + log.warn(.event, opts.context, .{ .err = err }); + }; + } + + const list = self.lookup.getPtr(@intFromPtr(target)) orelse return; + try self.dispatchAll(list, target, event); +} + +fn dispatchNode(self: *EventManager, target: *Node, event: *Event) !void { + if (event._bubbles == false) { + event._event_phase = .at_target; + const target_et = target.asEventTarget(); + if (self.lookup.getPtr(@intFromPtr(target_et))) |list| { + try self.dispatchPhase(list, target_et, event, null); + } + event._event_phase = .none; + return; + } + + var path_len: usize = 0; + var path_buffer: [128]*EventTarget = undefined; + + var node: ?*Node = target; + while (node) |n| : (node = n._parent) { + if (path_len >= path_buffer.len) break; + path_buffer[path_len] = n.asEventTarget(); + path_len += 1; + } + + // Even though the window isn't part of the DOM, events bubble to it + if (path_len < path_buffer.len) { + path_buffer[path_len] = self.page.window.asEventTarget(); + path_len += 1; + } + + const path = path_buffer[0..path_len]; + + // Phase 1: Capturing phase (root → target, excluding target) + event._event_phase = .capturing_phase; + var i: usize = path_len; + while (i > 1) { + i -= 1; + const current_target = path[i]; + if (self.lookup.getPtr(@intFromPtr(current_target))) |list| { + try self.dispatchPhase(list, current_target, event, true); + if (event._stop_propagation) { + event._event_phase = .none; + return; + } + } + } + + event._event_phase = .at_target; + const target_et = target.asEventTarget(); + if (self.lookup.getPtr(@intFromPtr(target_et))) |list| { + try self.dispatchPhase(list, target_et, event, null); + if (event._stop_propagation) { + event._event_phase = .none; + return; + } + } + + event._event_phase = .bubbling_phase; + for (path[1..]) |current_target| { + if (self.lookup.getPtr(@intFromPtr(current_target))) |list| { + try self.dispatchPhase(list, current_target, event, false); + if (event._stop_propagation) { + break; + } + } + } + + event._event_phase = .none; +} + +fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_target: *EventTarget, event: *Event, comptime capture_only: ?bool) !void { + const page = self.page; + const typ = event._type_string; + + var node = list.first; + while (node) |n| { + // do this now, in case we need to remove n (once: true or aborted signal) + node = n.next; + + const listener: *Listener = @alignCast(@fieldParentPtr("node", n)); + if (!listener.typ.eql(typ)) { + continue; + } + + // Can be null when dispatching to the target itself + if (comptime capture_only) |capture| { + if (listener.capture != capture) { + continue; + } + } + + // If the listener has an aborted signal, remove it and skip + if (listener.signal) |signal| { + if (signal.getAborted()) { + self.removeListener(list, listener); + continue; + } + } + + event._current_target = current_target; + + switch (listener.function) { + .value => |value| try value.call(void, .{event}), + .string => |string| { + const str = try page.call_arena.dupeZ(u8, string.str()); + try self.page.js.eval(str, null); + }, + } + + if (listener.once) { + self.removeListener(list, listener); + } + + if (event._stop_immediate_propagation) { + return; + } + } +} + +// Non-Node dispatching (XHR, Window without propagation) +fn dispatchAll(self: *EventManager, list: *std.DoublyLinkedList, current_target: *EventTarget, event: *Event) !void { + return self.dispatchPhase(list, current_target, event, null); +} + +fn removeListener(self: *EventManager, list: *std.DoublyLinkedList, listener: *Listener) void { + list.remove(&listener.node); + self.listener_pool.destroy(listener); +} + +fn findListener(list: *const std.DoublyLinkedList, typ: []const u8, function: js.Function, capture: bool) ?*Listener { + var node = list.first; + while (node) |n| { + node = n.next; + const listener: *Listener = @alignCast(@fieldParentPtr("node", n)); + if (!listener.function.eql(function)) { + continue; + } + if (listener.capture != capture) { + continue; + } + if (!listener.typ.eqlSlice(typ)) { + continue; + } + return listener; + } + return null; +} + +const Listener = struct { + typ: String, + once: bool, + capture: bool, + passive: bool, + function: Function, + signal: ?*@import("webapi/AbortSignal.zig") = null, + node: std.DoublyLinkedList.Node, +}; + +const Function = union(enum) { + value: js.Function, + string: String, + + fn eql(self: Function, func: js.Function) bool { + return switch (self) { + .string => false, + .value => |v| return v.id == func.id, + }; + } +}; diff --git a/src/browser/Factory.zig b/src/browser/Factory.zig new file mode 100644 index 000000000..bd04da757 --- /dev/null +++ b/src/browser/Factory.zig @@ -0,0 +1,367 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const reflect = @import("reflect.zig"); +const IS_DEBUG = builtin.mode == .Debug; + +const log = @import("../log.zig"); +const String = @import("../string.zig").String; + +const Page = @import("Page.zig"); +const Node = @import("webapi/Node.zig"); +const Event = @import("webapi/Event.zig"); +const Element = @import("webapi/Element.zig"); +const EventTarget = @import("webapi/EventTarget.zig"); +const XMLHttpRequestEventTarget = @import("webapi/net/XMLHttpRequestEventTarget.zig"); + +const MemoryPoolAligned = std.heap.MemoryPoolAligned; + +// 1. Generally, wrapping an ArenaAllocator within an ArenaAllocator doesn't make +// much sense. But wrapping a MemoryPool within an Arena does. Specifically, by +// doing so, we solve a major issue with Arena: freed memory can be re-used [for +// more of the same size]. +// 2. Normally, you have a MemoryPool(T) where T is a `User` or something. Then +// the MemoryPool can be used for creating users. But in reality, that memory +// created by that pool could be re-used for anything with the same size (or less) +// than a User (and a compatible alignment). So that's what we do - we have size +// (and alignment) based pools. +const Factory = @This(); +_page: *Page, +_size_1_8: MemoryPoolAligned([1]u8, .@"8"), +_size_8_8: MemoryPoolAligned([8]u8, .@"8"), +_size_16_8: MemoryPoolAligned([16]u8, .@"8"), +_size_24_8: MemoryPoolAligned([24]u8, .@"8"), +_size_32_8: MemoryPoolAligned([32]u8, .@"8"), +_size_32_16: MemoryPoolAligned([32]u8, .@"16"), +_size_40_8: MemoryPoolAligned([40]u8, .@"8"), +_size_48_16: MemoryPoolAligned([48]u8, .@"16"), +_size_56_8: MemoryPoolAligned([56]u8, .@"8"), +_size_64_16: MemoryPoolAligned([64]u8, .@"16"), +_size_72_8: MemoryPoolAligned([72]u8, .@"8"), +_size_80_16: MemoryPoolAligned([80]u8, .@"16"), +_size_88_8: MemoryPoolAligned([88]u8, .@"8"), +_size_96_16: MemoryPoolAligned([96]u8, .@"16"), +_size_104_8: MemoryPoolAligned([104]u8, .@"8"), +_size_112_8: MemoryPoolAligned([112]u8, .@"8"), +_size_120_8: MemoryPoolAligned([120]u8, .@"8"), +_size_128_8: MemoryPoolAligned([128]u8, .@"8"), +_size_144_8: MemoryPoolAligned([144]u8, .@"8"), +_size_456_8: MemoryPoolAligned([456]u8, .@"8"), +_size_520_8: MemoryPoolAligned([520]u8, .@"8"), +_size_648_8: MemoryPoolAligned([648]u8, .@"8"), + +pub fn init(page: *Page) Factory { + return .{ + ._page = page, + ._size_1_8 = MemoryPoolAligned([1]u8, .@"8").init(page.arena), + ._size_8_8 = MemoryPoolAligned([8]u8, .@"8").init(page.arena), + ._size_16_8 = MemoryPoolAligned([16]u8, .@"8").init(page.arena), + ._size_24_8 = MemoryPoolAligned([24]u8, .@"8").init(page.arena), + ._size_32_8 = MemoryPoolAligned([32]u8, .@"8").init(page.arena), + ._size_32_16 = MemoryPoolAligned([32]u8, .@"16").init(page.arena), + ._size_40_8 = MemoryPoolAligned([40]u8, .@"8").init(page.arena), + ._size_48_16 = MemoryPoolAligned([48]u8, .@"16").init(page.arena), + ._size_56_8 = MemoryPoolAligned([56]u8, .@"8").init(page.arena), + ._size_64_16 = MemoryPoolAligned([64]u8, .@"16").init(page.arena), + ._size_72_8 = MemoryPoolAligned([72]u8, .@"8").init(page.arena), + ._size_80_16 = MemoryPoolAligned([80]u8, .@"16").init(page.arena), + ._size_88_8 = MemoryPoolAligned([88]u8, .@"8").init(page.arena), + ._size_96_16 = MemoryPoolAligned([96]u8, .@"16").init(page.arena), + ._size_104_8 = MemoryPoolAligned([104]u8, .@"8").init(page.arena), + ._size_112_8 = MemoryPoolAligned([112]u8, .@"8").init(page.arena), + ._size_120_8 = MemoryPoolAligned([120]u8, .@"8").init(page.arena), + ._size_128_8 = MemoryPoolAligned([128]u8, .@"8").init(page.arena), + ._size_144_8 = MemoryPoolAligned([144]u8, .@"8").init(page.arena), + ._size_456_8 = MemoryPoolAligned([456]u8, .@"8").init(page.arena), + ._size_520_8 = MemoryPoolAligned([520]u8, .@"8").init(page.arena), + ._size_648_8 = MemoryPoolAligned([648]u8, .@"8").init(page.arena), + }; +} + +// this is a root object +pub fn eventTarget(self: *Factory, child: anytype) !*@TypeOf(child) { + const child_ptr = try self.createT(@TypeOf(child)); + child_ptr.* = child; + + const et = try self.createT(EventTarget); + child_ptr._proto = et; + et.* = .{ ._type = unionInit(EventTarget.Type, child_ptr) }; + return child_ptr; +} + +pub fn node(self: *Factory, child: anytype) !*@TypeOf(child) { + const child_ptr = try self.createT(@TypeOf(child)); + child_ptr.* = child; + child_ptr._proto = try self.eventTarget(Node{ + ._proto = undefined, + ._type = unionInit(Node.Type, child_ptr), + }); + return child_ptr; +} + +pub fn element(self: *Factory, child: anytype) !*@TypeOf(child) { + const child_ptr = try self.createT(@TypeOf(child)); + child_ptr.* = child; + child_ptr._proto = try self.node(Element{ + ._proto = undefined, + ._type = unionInit(Element.Type, child_ptr), + }); + return child_ptr; +} + +pub fn htmlElement(self: *Factory, child: anytype) !*@TypeOf(child) { + if (comptime fieldIsPointer(Element.Html.Type, @TypeOf(child))) { + const child_ptr = try self.createT(@TypeOf(child)); + child_ptr.* = child; + child_ptr._proto = try self.element(Element.Html{ + ._proto = undefined, + ._type = unionInit(Element.Html.Type, child_ptr), + }); + return child_ptr; + } + + // Our union type fields are usually pointers. But, at the leaf, they + // can be struct (if all they contain is the `_proto` field, then we might + // as well store it directly in the struct). + + const html = try self.element(Element.Html{ + ._proto = undefined, + ._type = unionInit(Element.Html.Type, child), + }); + const field_name = comptime unionFieldName(Element.Html.Type, @TypeOf(child)); + var child_ptr = &@field(html._type, field_name); + child_ptr._proto = html; + return child_ptr; +} + +pub fn svgElement(self: *Factory, tag_name: []const u8, child: anytype) !*@TypeOf(child) { + if (@TypeOf(child) == Element.Svg) { + return self.element(child); + } + + // will never allocate, can't fail + const tag_name_str = String.init(undefined, tag_name, .{}) catch unreachable; + + if (comptime fieldIsPointer(Element.Svg.Type, @TypeOf(child))) { + const child_ptr = try self.createT(@TypeOf(child)); + child_ptr.* = child; + child_ptr._proto = try self.element(Element.Svg{ + ._proto = undefined, + ._tag_name = tag_name_str, + ._type = unionInit(Element.Svg.Type, child_ptr), + }); + return child_ptr; + } + + // Our union type fields are usually pointers. But, at the leaf, they + // can be struct (if all they contain is the `_proto` field, then we might + // as well store it directly in the struct). + const svg = try self.element(Element.Svg{ + ._proto = undefined, + ._tag_name = tag_name_str, + ._type = unionInit(Element.Svg.Type, child), + }); + const field_name = comptime unionFieldName(Element.Svg.Type, @TypeOf(child)); + var child_ptr = &@field(svg._type, field_name); + child_ptr._proto = svg; + return child_ptr; +} + +// this is a root object +pub fn event(self: *Factory, typ: []const u8, child: anytype) !*@TypeOf(child) { + const child_ptr = try self.createT(@TypeOf(child)); + child_ptr.* = child; + + const e = try self.createT(Event); + child_ptr._proto = e; + e.* = .{ + ._type = unionInit(Event.Type, child_ptr), + ._type_string = try String.init(self._page.arena, typ, .{}), + }; + return child_ptr; +} + +pub fn xhrEventTarget(self: *Factory, child: anytype) !*@TypeOf(child) { + const et = try self.eventTarget(XMLHttpRequestEventTarget{ + ._proto = undefined, + ._type = unionInit(XMLHttpRequestEventTarget.Type, child), + }); + const field_name = comptime unionFieldName(XMLHttpRequestEventTarget.Type, @TypeOf(child)); + var child_ptr = &@field(et._type, field_name); + child_ptr._proto = et; + return child_ptr; +} + +pub fn create(self: *Factory, value: anytype) !*@TypeOf(value) { + const ptr = try self.createT(@TypeOf(value)); + ptr.* = value; + return ptr; +} + +pub fn createT(self: *Factory, comptime T: type) !*T { + const SO = @sizeOf(T); + if (comptime SO == 1) return @ptrCast(try self._size_1_8.create()); + if (comptime SO == 8) return @ptrCast(try self._size_8_8.create()); + if (comptime SO == 16) return @ptrCast(try self._size_16_8.create()); + if (comptime SO == 24) return @ptrCast(try self._size_24_8.create()); + if (comptime SO == 32) { + if (comptime @alignOf(T) == 8) return @ptrCast(try self._size_32_8.create()); + if (comptime @alignOf(T) == 16) return @ptrCast(try self._size_32_16.create()); + } + if (comptime SO == 40) return @ptrCast(try self._size_40_8.create()); + if (comptime SO == 48) return @ptrCast(try self._size_48_16.create()); + if (comptime SO == 56) return @ptrCast(try self._size_56_8.create()); + if (comptime SO == 64) return @ptrCast(try self._size_64_16.create()); + if (comptime SO == 72) return @ptrCast(try self._size_72_8.create()); + if (comptime SO == 80) return @ptrCast(try self._size_80_16.create()); + if (comptime SO == 88) return @ptrCast(try self._size_88_8.create()); + if (comptime SO == 96) return @ptrCast(try self._size_96_16.create()); + if (comptime SO == 104) return @ptrCast(try self._size_104_8.create()); + if (comptime SO == 112) return @ptrCast(try self._size_112_8.create()); + if (comptime SO == 120) return @ptrCast(try self._size_120_8.create()); + if (comptime SO == 128) return @ptrCast(try self._size_128_8.create()); + if (comptime SO == 144) return @ptrCast(try self._size_144_8.create()); + if (comptime SO == 456) return @ptrCast(try self._size_456_8.create()); + if (comptime SO == 520) return @ptrCast(try self._size_520_8.create()); + if (comptime SO == 648) return @ptrCast(try self._size_648_8.create()); + @compileError(std.fmt.comptimePrint("No pool configured for @sizeOf({d}), @alignOf({d}): ({s})", .{ SO, @alignOf(T), @typeName(T) })); +} + +pub fn destroy(self: *Factory, value: anytype) void { + const S = reflect.Struct(@TypeOf(value)); + if (comptime IS_DEBUG) { + // We should always destroy from the leaf down. + if (@hasField(S, "_type") and @typeInfo(@TypeOf(value._type)) == .@"union") { + // A Event{._type == .generic} (or any other similar types) + // _should_ be destoyed directly. The _type = .generic is a pseudo + // child + if (S != Event or value._type != .generic) { + log.fatal(.bug, "factory.destroy.event", .{ .type = @typeName(S) }); + unreachable; + } + } + } + + self.destroyChain(value, true); +} + +fn destroyChain(self: *Factory, value: anytype, comptime first: bool) void { + const S = reflect.Struct(@TypeOf(value)); + + // This is initially called from a deinit. We don't want to call that + // same deinit. So when this is the first time destroyChain is called + // we don't call deinit (because we're in that deinit) + if (!comptime first) { + // But if it isn't the first time + if (@hasDecl(S, "deinit")) { + // And it has a deinit, we'll call it + switch (@typeInfo(@TypeOf(S.deinit)).@"fn".params.len) { + 1 => value.deinit(), + 2 => value.deinit(self._page), + else => @compileLog(@typeName(S) ++ " has an invalid deinit function"), + } + } + } + + if (@hasField(S, "_proto")) { + self.destroyChain(value._proto, false); + } else if (@hasDecl(S, "JsApi")) { + // Doesn't have a _proto, but has a JsApi. + if (self._page.js.removeTaggedMapping(@intFromPtr(value))) |tagged| { + self._size_24_8.destroy(@ptrCast(tagged)); + } + } + + // Leaf types are allowed by be placed directly within their _proto + // (which makes sense when the @sizeOf(Leaf) == 8). These don't need to + // be (cannot be) freed. But we'll still free the chain. + if (comptime wasAllocated(S)) { + switch (@sizeOf(S)) { + 1 => self._size_1_8.destroy(@ptrCast(@alignCast(value))), + 8 => self._size_8_8.destroy(@ptrCast(@alignCast(value))), + 16 => self._size_16_8.destroy(@ptrCast(value)), + 24 => self._size_24_8.destroy(@ptrCast(value)), + 32 => { + if (comptime @alignOf(S) == 8) { + self._size_32_8.destroy(@ptrCast(value)); + } else if (comptime @alignOf(S) == 16) { + self._size_32_16.destroy(@ptrCast(value)); + } + }, + 40 => self._size_40_8.destroy(@ptrCast(value)), + 48 => self._size_48_16.destroy(@ptrCast(@alignCast(value))), + 56 => self._size_56_8.destroy(@ptrCast(value)), + 64 => self._size_64_16.destroy(@ptrCast(@alignCast(value))), + 72 => self._size_72_8.destroy(@ptrCast(@alignCast(value))), + 80 => self._size_80_16.destroy(@ptrCast(@alignCast(value))), + 88 => self._size_88_8.destroy(@ptrCast(@alignCast(value))), + 96 => self._size_96_16.destroy(@ptrCast(@alignCast(value))), + 104 => self._size_104_8.destroy(@ptrCast(value)), + 112 => self._size_112_8.destroy(@ptrCast(value)), + 120 => self._size_120_8.destroy(@ptrCast(value)), + 128 => self._size_128_8.destroy(@ptrCast(value)), + 144 => self._size_144_8.destroy(@ptrCast(value)), + 456 => self._size_456_8.destroy(@ptrCast(value)), + 520 => self._size_520_8.destroy(@ptrCast(value)), + 648 => self._size_648_8.destroy(@ptrCast(value)), + else => |SO| @compileError(std.fmt.comptimePrint("Don't know what I'm being asked to destroy @sizeOf({d}), @alignOf({d}): ({s})", .{ SO, @alignOf(S), @typeName(S) })), + } + } +} + +fn wasAllocated(comptime S: type) bool { + // Whether it's heap allocate or not, we should have a pointer. + // (If it isn't heap allocated, it'll be a pointer from the proto's type + // e.g. &html._type.title) + if (!@hasField(S, "_proto")) { + // a root is always on the heap. + return true; + } + + // the _proto type + const P = reflect.Struct(std.meta.fieldInfo(S, ._proto).type); + + // the _proto._type type (the parent's _type union) + const U = std.meta.fieldInfo(P, ._type).type; + inline for (@typeInfo(U).@"union".fields) |field| { + if (field.type == S) { + // One of the types in the proto's _type union is this non-pointer + // structure, so it isn't heap allocted. + return false; + } + } + return true; +} + +fn unionInit(comptime T: type, value: anytype) T { + const V = @TypeOf(value); + const field_name = comptime unionFieldName(T, V); + return @unionInit(T, field_name, value); +} + +// There can be friction between comptime and runtime. Comptime has to +// account for all possible types, even if some runtime flow makes certain +// cases impossible. At runtime, we always call `unionFieldName` with the +// correct struct or pointer type. But at comptime time, `unionFieldName` +// is called with both variants (S and *S). So we use reflect.Struct(). +// This only works because we never have a union with a field S and another +// field *S. +fn unionFieldName(comptime T: type, comptime V: type) []const u8 { + inline for (@typeInfo(T).@"union".fields) |field| { + if (reflect.Struct(field.type) == reflect.Struct(V)) { + return field.name; + } + } + @compileError(@typeName(V) ++ " is not a valid type for " ++ @typeName(T) ++ ".type"); +} + +fn fieldIsPointer(comptime T: type, comptime V: type) bool { + inline for (@typeInfo(T).@"union".fields) |field| { + if (field.type == V) { + return false; + } + if (field.type == *V) { + return true; + } + } + @compileError(@typeName(V) ++ " is not a valid type for " ++ @typeName(T) ++ ".type"); +} diff --git a/src/browser/Mime.zig b/src/browser/Mime.zig new file mode 100644 index 000000000..27fe35a85 --- /dev/null +++ b/src/browser/Mime.zig @@ -0,0 +1,518 @@ +// Copyright (C) 2023-2025 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +const std = @import("std"); + +const Mime = @This(); +content_type: ContentType, +params: []const u8 = "", +// IANA defines max. charset value length as 40. +// We keep 41 for null-termination since HTML parser expects in this format. +charset: [41]u8 = default_charset, + +/// String "UTF-8" continued by null characters. +pub const default_charset = .{ 'U', 'T', 'F', '-', '8' } ++ .{0} ** 36; + +/// Mime with unknown Content-Type, empty params and empty charset. +pub const unknown = Mime{ .content_type = .{ .unknown = {} } }; + +pub const ContentTypeEnum = enum { + text_xml, + text_html, + text_javascript, + text_plain, + text_css, + application_json, + unknown, + other, +}; + +pub const ContentType = union(ContentTypeEnum) { + text_xml: void, + text_html: void, + text_javascript: void, + text_plain: void, + text_css: void, + application_json: void, + unknown: void, + other: struct { type: []const u8, sub_type: []const u8 }, +}; + +/// Returns the null-terminated charset value. +pub fn charsetString(mime: *const Mime) [:0]const u8 { + return @ptrCast(&mime.charset); +} + +/// Removes quotes of value if quotes are given. +/// +/// Currently we don't validate the charset. +/// See section 2.3 Naming Requirements: +/// https://datatracker.ietf.org/doc/rfc2978/ +fn parseCharset(value: []const u8) error{ CharsetTooBig, Invalid }![]const u8 { + // Cannot be larger than 40. + // https://datatracker.ietf.org/doc/rfc2978/ + if (value.len > 40) return error.CharsetTooBig; + + // If the first char is a quote, look for a pair. + if (value[0] == '"') { + if (value.len < 3 or value[value.len - 1] != '"') { + return error.Invalid; + } + + return value[1 .. value.len - 1]; + } + + // No quotes. + return value; +} + +pub fn parse(input: []u8) !Mime { + if (input.len > 255) { + return error.TooBig; + } + + // Zig's trim API is broken. The return type is always `[]const u8`, + // even if the input type is `[]u8`. @constCast is safe here. + var normalized = @constCast(std.mem.trim(u8, input, &std.ascii.whitespace)); + _ = std.ascii.lowerString(normalized, normalized); + + const content_type, const type_len = try parseContentType(normalized); + if (type_len >= normalized.len) { + return .{ .content_type = content_type }; + } + + const params = trimLeft(normalized[type_len..]); + + var charset: [41]u8 = undefined; + + var it = std.mem.splitScalar(u8, params, ';'); + while (it.next()) |attr| { + const i = std.mem.indexOfScalarPos(u8, attr, 0, '=') orelse return error.Invalid; + const name = trimLeft(attr[0..i]); + + const value = trimRight(attr[i + 1 ..]); + if (value.len == 0) { + return error.Invalid; + } + + const attribute_name = std.meta.stringToEnum(enum { + charset, + }, name) orelse continue; + + switch (attribute_name) { + .charset => { + if (value.len == 0) { + break; + } + + const attribute_value = try parseCharset(value); + @memcpy(charset[0..attribute_value.len], attribute_value); + // Null-terminate right after attribute value. + charset[attribute_value.len] = 0; + }, + } + } + + return .{ + .params = params, + .charset = charset, + .content_type = content_type, + }; +} + +pub fn sniff(body: []const u8) ?Mime { + // 0x0C is form feed + const content = std.mem.trimLeft(u8, body, &.{ ' ', '\t', '\n', '\r', 0x0C }); + if (content.len == 0) { + return null; + } + + if (content[0] != '<') { + if (std.mem.startsWith(u8, content, &.{ 0xEF, 0xBB, 0xBF })) { + // UTF-8 BOM + return .{ .content_type = .{ .text_plain = {} } }; + } + if (std.mem.startsWith(u8, content, &.{ 0xFE, 0xFF })) { + // UTF-16 big-endian BOM + return .{ .content_type = .{ .text_plain = {} } }; + } + if (std.mem.startsWith(u8, content, &.{ 0xFF, 0xFE })) { + // UTF-16 little-endian BOM + return .{ .content_type = .{ .text_plain = {} } }; + } + return null; + } + + // The longest prefix we have is " known_prefix.len) { + const next = prefix[known_prefix.len]; + // a "tag-terminating-byte" + if (next == ' ' or next == '>') { + return .{ .content_type = kp.@"1" }; + } + } + } + + return null; +} + +pub fn isHTML(self: *const Mime) bool { + return self.content_type == .text_html; +} + +// we expect value to be lowercase +fn parseContentType(value: []const u8) !struct { ContentType, usize } { + const end = std.mem.indexOfScalarPos(u8, value, 0, ';') orelse value.len; + const type_name = trimRight(value[0..end]); + const attribute_start = end + 1; + + if (std.meta.stringToEnum(enum { + @"text/xml", + @"text/html", + @"text/css", + @"text/plain", + + @"text/javascript", + @"application/javascript", + @"application/x-javascript", + + @"application/json", + }, type_name)) |known_type| { + const ct: ContentType = switch (known_type) { + .@"text/xml" => .{ .text_xml = {} }, + .@"text/html" => .{ .text_html = {} }, + .@"text/javascript", .@"application/javascript", .@"application/x-javascript" => .{ .text_javascript = {} }, + .@"text/plain" => .{ .text_plain = {} }, + .@"text/css" => .{ .text_css = {} }, + .@"application/json" => .{ .application_json = {} }, + }; + return .{ ct, attribute_start }; + } + + const separator = std.mem.indexOfScalarPos(u8, type_name, 0, '/') orelse return error.Invalid; + + const main_type = value[0..separator]; + const sub_type = trimRight(value[separator + 1 .. end]); + + if (main_type.len == 0 or validType(main_type) == false) { + return error.Invalid; + } + if (sub_type.len == 0 or validType(sub_type) == false) { + return error.Invalid; + } + + return .{ .{ .other = .{ + .type = main_type, + .sub_type = sub_type, + } }, attribute_start }; +} + +const T_SPECIAL = blk: { + var v = [_]bool{false} ** 256; + for ("()<>@,;:\\\"/[]?=") |b| { + v[b] = true; + } + break :blk v; +}; + +const VALID_CODEPOINTS = blk: { + var v: [256]bool = undefined; + for (0..256) |i| { + v[i] = std.ascii.isAlphanumeric(i); + } + for ("!#$%&\\*+-.^'_`|~") |b| { + v[b] = true; + } + break :blk v; +}; + +fn validType(value: []const u8) bool { + for (value) |b| { + if (VALID_CODEPOINTS[b] == false) { + return false; + } + } + return true; +} + +fn trimLeft(s: []const u8) []const u8 { + return std.mem.trimLeft(u8, s, &std.ascii.whitespace); +} + +fn trimRight(s: []const u8) []const u8 { + return std.mem.trimRight(u8, s, &std.ascii.whitespace); +} + +const testing = @import("../testing.zig"); +test "Mime: invalid" { + defer testing.reset(); + + const invalids = [_][]const u8{ + "", + "text", + "text /html", + "text/ html", + "text / html", + "text/html other", + "text/html; x", + "text/html; x=", + "text/html; x= ", + "text/html; = ", + "text/html;=", + "text/html; charset=\"\"", + "text/html; charset=\"", + "text/html; charset=\"\\", + }; + + for (invalids) |invalid| { + const mutable_input = try testing.arena_allocator.dupe(u8, invalid); + try testing.expectError(error.Invalid, Mime.parse(mutable_input)); + } +} + +test "Mime: parse common" { + defer testing.reset(); + + try expect(.{ .content_type = .{ .text_xml = {} } }, "text/xml"); + try expect(.{ .content_type = .{ .text_html = {} } }, "text/html"); + try expect(.{ .content_type = .{ .text_plain = {} } }, "text/plain"); + + try expect(.{ .content_type = .{ .text_xml = {} } }, "text/xml;"); + try expect(.{ .content_type = .{ .text_html = {} } }, "text/html;"); + try expect(.{ .content_type = .{ .text_plain = {} } }, "text/plain;"); + + try expect(.{ .content_type = .{ .text_xml = {} } }, " \ttext/xml"); + try expect(.{ .content_type = .{ .text_html = {} } }, "text/html "); + try expect(.{ .content_type = .{ .text_plain = {} } }, "text/plain \t\t"); + + try expect(.{ .content_type = .{ .text_xml = {} } }, "TEXT/xml"); + try expect(.{ .content_type = .{ .text_html = {} } }, "text/Html"); + try expect(.{ .content_type = .{ .text_plain = {} } }, "TEXT/PLAIN"); + + try expect(.{ .content_type = .{ .text_xml = {} } }, " TeXT/xml"); + try expect(.{ .content_type = .{ .text_html = {} } }, "teXt/HtML ;"); + try expect(.{ .content_type = .{ .text_plain = {} } }, "tExT/PlAiN;"); + + try expect(.{ .content_type = .{ .text_javascript = {} } }, "text/javascript"); + try expect(.{ .content_type = .{ .text_javascript = {} } }, "Application/JavaScript"); + try expect(.{ .content_type = .{ .text_javascript = {} } }, "application/x-javascript"); + + try expect(.{ .content_type = .{ .application_json = {} } }, "application/json"); + try expect(.{ .content_type = .{ .text_css = {} } }, "text/css"); +} + +test "Mime: parse uncommon" { + defer testing.reset(); + + const text_csv = Expectation{ + .content_type = .{ .other = .{ .type = "text", .sub_type = "csv" } }, + }; + try expect(text_csv, "text/csv"); + try expect(text_csv, "text/csv;"); + try expect(text_csv, " text/csv\t "); + try expect(text_csv, " text/csv\t ;"); + + try expect( + .{ .content_type = .{ .other = .{ .type = "text", .sub_type = "csv" } } }, + "Text/CSV", + ); +} + +test "Mime: parse charset" { + defer testing.reset(); + + try expect(.{ + .content_type = .{ .text_xml = {} }, + .charset = "utf-8", + .params = "charset=utf-8", + }, "text/xml; charset=utf-8"); + + try expect(.{ + .content_type = .{ .text_xml = {} }, + .charset = "utf-8", + .params = "charset=\"utf-8\"", + }, "text/xml;charset=\"UTF-8\""); + + try expect(.{ + .content_type = .{ .text_html = {} }, + .charset = "iso-8859-1", + .params = "charset=\"iso-8859-1\"", + }, "text/html; charset=\"iso-8859-1\""); + + try expect(.{ + .content_type = .{ .text_html = {} }, + .charset = "iso-8859-1", + .params = "charset=\"iso-8859-1\"", + }, "text/html; charset=\"ISO-8859-1\""); + + try expect(.{ + .content_type = .{ .text_xml = {} }, + .charset = "custom-non-standard-charset-value", + .params = "charset=\"custom-non-standard-charset-value\"", + }, "text/xml;charset=\"custom-non-standard-charset-value\""); +} + +test "Mime: isHTML" { + defer testing.reset(); + + const assert = struct { + fn assert(expected: bool, input: []const u8) !void { + const mutable_input = try testing.arena_allocator.dupe(u8, input); + var mime = try Mime.parse(mutable_input); + try testing.expectEqual(expected, mime.isHTML()); + } + }.assert; + try assert(true, "text/html"); + try assert(true, "text/html;"); + try assert(true, "text/html; charset=utf-8"); + try assert(false, "text/htm"); // htm not html + try assert(false, "text/plain"); + try assert(false, "over/9000"); +} + +test "Mime: sniff" { + try testing.expectEqual(null, Mime.sniff("")); + try testing.expectEqual(null, Mime.sniff("")); + try testing.expectEqual(null, Mime.sniff("\n ")); + try testing.expectEqual(null, Mime.sniff("\n \t ")); + + const expectHTML = struct { + fn expect(input: []const u8) !void { + try testing.expectEqual(.text_html, std.meta.activeTag(Mime.sniff(input).?.content_type)); + } + }.expect; + + try expectHTML(" even more stufff"); + + try expectHTML(""); + + try expectHTML(" - - - - diff --git a/src/tests/window/window.html b/src/tests/window/window.html deleted file mode 100644 index cbe67f5f4..000000000 --- a/src/tests/window/window.html +++ /dev/null @@ -1,151 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/tests/xhr/file.html b/src/tests/xhr/file.html deleted file mode 100644 index 622846028..000000000 --- a/src/tests/xhr/file.html +++ /dev/null @@ -1,6 +0,0 @@ - - - diff --git a/src/tests/xhr/form_data.html b/src/tests/xhr/form_data.html deleted file mode 100644 index 94bf8a272..000000000 --- a/src/tests/xhr/form_data.html +++ /dev/null @@ -1,130 +0,0 @@ - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- diff --git a/src/tests/xhr/progress_event.html b/src/tests/xhr/progress_event.html deleted file mode 100644 index 4b7f5df4a..000000000 --- a/src/tests/xhr/progress_event.html +++ /dev/null @@ -1,17 +0,0 @@ - - - diff --git a/src/tests/xhr/xhr.html b/src/tests/xhr/xhr.html deleted file mode 100644 index 13ab6216e..000000000 --- a/src/tests/xhr/xhr.html +++ /dev/null @@ -1,110 +0,0 @@ - - - - - - - - - - - diff --git a/src/tests/xmlserializer.html b/src/tests/xmlserializer.html deleted file mode 100644 index 0d3d46284..000000000 --- a/src/tests/xmlserializer.html +++ /dev/null @@ -1,8 +0,0 @@ - - -

And

- diff --git a/src/url.zig b/src/url.zig deleted file mode 100644 index acfac2560..000000000 --- a/src/url.zig +++ /dev/null @@ -1,555 +0,0 @@ -const std = @import("std"); - -const Uri = std.Uri; -const Allocator = std.mem.Allocator; -const WebApiURL = @import("browser/url/url.zig").URL; - -pub const stitch = URL.stitch; - -pub const URL = struct { - uri: Uri, - raw: []const u8, - - pub const empty = URL{ .uri = .{ .scheme = "" }, .raw = "" }; - pub const about_blank = URL{ .uri = .{ .scheme = "" }, .raw = "about:blank" }; - - // We assume str will last as long as the URL - // In some cases, this is safe to do, because we know the URL is short lived. - // In most cases though, we assume the caller will just dupe the string URL - // into an arena - pub fn parse(str: []const u8, default_scheme: ?[]const u8) !URL { - var uri = Uri.parse(str) catch try Uri.parseAfterScheme(default_scheme orelse "https", str); - - // special case, url scheme is about, like about:blank. - // Use an empty string as host. - if (std.mem.eql(u8, uri.scheme, "about")) { - uri.host = .{ .percent_encoded = "" }; - } - - if (uri.host == null) { - return error.MissingHost; - } - - std.debug.assert(uri.host.? == .percent_encoded); - - return .{ - .uri = uri, - .raw = str, - }; - } - - pub fn fromURI(arena: Allocator, uri: *const Uri) !URL { - // This is embarrassing. - var buf: std.ArrayListUnmanaged(u8) = .{}; - try uri.writeToStream(.{ - .scheme = true, - .authentication = true, - .authority = true, - .path = true, - .query = true, - .fragment = true, - }, buf.writer(arena)); - - return parse(buf.items, null); - } - - // Above, in `parse`, we error if a host doesn't exist - // In other words, we can't have a URL with a null host. - pub fn host(self: *const URL) []const u8 { - return self.uri.host.?.percent_encoded; - } - - pub fn port(self: *const URL) ?u16 { - return self.uri.port; - } - - pub fn scheme(self: *const URL) []const u8 { - return self.uri.scheme; - } - - pub fn origin(self: *const URL, writer: *std.Io.Writer) !void { - return self.uri.writeToStream(writer, .{ .scheme = true, .authority = true }); - } - - pub fn format(self: *const URL, writer: *std.Io.Writer) !void { - return writer.writeAll(self.raw); - } - - pub fn toWebApi(self: *const URL, allocator: Allocator) !WebApiURL { - return WebApiURL.init(allocator, self.uri); - } - - /// Properly stitches two URL fragments together. - /// - /// For URLs with a path, it will replace the last entry with the src. - /// For URLs without a path, it will add src as the path. - pub fn stitch( - allocator: Allocator, - path: []const u8, - base: []const u8, - comptime opts: StitchOpts, - ) !StitchReturn(opts) { - if (base.len == 0 or isCompleteHTTPUrl(path)) { - return simpleStitch(allocator, path, opts); - } - - if (path.len == 0) { - return simpleStitch(allocator, base, opts); - } - - if (std.mem.startsWith(u8, path, "//")) { - // network-path reference - const index = std.mem.indexOfScalar(u8, base, ':') orelse { - return simpleStitch(allocator, path, opts); - }; - - const protocol = base[0..index]; - if (comptime opts.null_terminated) { - return std.fmt.allocPrintSentinel(allocator, "{s}:{s}", .{ protocol, path }, 0); - } - return std.fmt.allocPrint(allocator, "{s}:{s}", .{ protocol, path }); - } - - // Quick hack because domains have to be at least 3 characters. - // Given https://a.b this will point to 'a' - // Given http://a.b this will point '.' - // Either way, we just care about this value to find the start of the path - const protocol_end: usize = if (isCompleteHTTPUrl(base)) 8 else 0; - - var root = base; - if (std.mem.indexOfScalar(u8, base[protocol_end..], '/')) |pos| { - root = base[0 .. pos + protocol_end]; - } - - if (path[0] == '/') { - if (comptime opts.null_terminated) { - return std.fmt.allocPrintSentinel(allocator, "{s}{s}", .{ root, path }, 0); - } - return std.fmt.allocPrint(allocator, "{s}{s}", .{ root, path }); - } - - var old_path = std.mem.trimStart(u8, base[root.len..], "/"); - if (std.mem.lastIndexOfScalar(u8, old_path, '/')) |pos| { - old_path = old_path[0..pos]; - } else { - old_path = ""; - } - - // We preallocate all of the space possibly needed. - // This is the root, old_path, new path, 3 slashes and perhaps a null terminated slot. - var out = try allocator.alloc(u8, root.len + old_path.len + path.len + 3 + if (comptime opts.null_terminated) 1 else 0); - var end: usize = 0; - @memmove(out[0..root.len], root); - end += root.len; - out[root.len] = '/'; - end += 1; - // If we don't have an old path, do nothing here. - if (old_path.len > 0) { - @memmove(out[end .. end + old_path.len], old_path); - end += old_path.len; - out[end] = '/'; - end += 1; - } - @memmove(out[end .. end + path.len], path); - end += path.len; - - var read: usize = root.len; - var write: usize = root.len; - - // Strip out ./ and ../. This is done in-place, because doing so can - // only ever make `out` smaller. After this, `out` cannot be freed by - // an allocator, which is ok, because we expect allocator to be an arena. - while (read < end) { - if (std.mem.startsWith(u8, out[read..], "./")) { - read += 2; - continue; - } - - if (std.mem.startsWith(u8, out[read..], "../")) { - if (write > root.len + 1) { - const search_range = out[root.len .. write - 1]; - if (std.mem.lastIndexOfScalar(u8, search_range, '/')) |pos| { - write = root.len + pos + 1; - } else { - write = root.len + 1; - } - } - - read += 3; - continue; - } - - out[write] = out[read]; - write += 1; - read += 1; - } - - if (comptime opts.null_terminated) { - // we always have an extra space - out[write] = 0; - return out[0..write :0]; - } - - return out[0..write]; - } - - pub fn concatQueryString(arena: Allocator, url: []const u8, query_string: []const u8) ![]const u8 { - std.debug.assert(url.len != 0); - - if (query_string.len == 0) { - return url; - } - - var buf: std.ArrayListUnmanaged(u8) = .empty; - - // the most space well need is the url + ('?' or '&') + the query_string - try buf.ensureTotalCapacity(arena, url.len + 1 + query_string.len); - buf.appendSliceAssumeCapacity(url); - - if (std.mem.indexOfScalar(u8, url, '?')) |index| { - const last_index = url.len - 1; - if (index != last_index and url[last_index] != '&') { - buf.appendAssumeCapacity('&'); - } - } else { - buf.appendAssumeCapacity('?'); - } - buf.appendSliceAssumeCapacity(query_string); - return buf.items; - } -}; - -const StitchOpts = struct { - alloc: AllocWhen = .always, - null_terminated: bool = false, - - const AllocWhen = enum { - always, - if_needed, - }; -}; - -fn StitchReturn(comptime opts: StitchOpts) type { - return if (opts.null_terminated) [:0]const u8 else []const u8; -} - -fn simpleStitch(allocator: Allocator, url: []const u8, comptime opts: StitchOpts) !StitchReturn(opts) { - if (comptime opts.null_terminated) { - return allocator.dupeZ(u8, url); - } - - if (comptime opts.alloc == .always) { - return allocator.dupe(u8, url); - } - - return url; -} - -fn isCompleteHTTPUrl(url: []const u8) bool { - if (url.len < 8) { - return false; - } - - if (!std.ascii.startsWithIgnoreCase(url, "http")) { - return false; - } - - var pos: usize = 4; - if (url[4] == 's' or url[4] == 'S') { - pos = 5; - } - return std.mem.startsWith(u8, url[pos..], "://"); -} - -const testing = @import("testing.zig"); -test "URL: isCompleteHTTPUrl" { - try testing.expectEqual(true, isCompleteHTTPUrl("http://lightpanda.io/about")); - try testing.expectEqual(true, isCompleteHTTPUrl("HttP://lightpanda.io/about")); - try testing.expectEqual(true, isCompleteHTTPUrl("httpS://lightpanda.io/about")); - try testing.expectEqual(true, isCompleteHTTPUrl("HTTPs://lightpanda.io/about")); - - try testing.expectEqual(false, isCompleteHTTPUrl("/lightpanda.io")); - try testing.expectEqual(false, isCompleteHTTPUrl("../../about")); - try testing.expectEqual(false, isCompleteHTTPUrl("about")); - try testing.expectEqual(false, isCompleteHTTPUrl("//lightpanda.io")); - try testing.expectEqual(false, isCompleteHTTPUrl("//lightpanda.io/about")); -} - -test "URL: stitch" { - defer testing.reset(); - - const Case = struct { - base: []const u8, - path: []const u8, - expected: []const u8, - }; - - const cases = [_]Case{ - .{ - .base = "https://lightpanda.io/xyz/abc/123", - .path = "something1.js", - .expected = "https://lightpanda.io/xyz/abc/something1.js", - }, - .{ - .base = "https://lightpanda.io/xyz/abc/123", - .path = "/something2.js", - .expected = "https://lightpanda.io/something2.js", - }, - .{ - .base = "https://lightpanda.io/", - .path = "something3.js", - .expected = "https://lightpanda.io/something3.js", - }, - .{ - .base = "https://lightpanda.io/", - .path = "/something4.js", - .expected = "https://lightpanda.io/something4.js", - }, - .{ - .base = "https://lightpanda.io", - .path = "something5.js", - .expected = "https://lightpanda.io/something5.js", - }, - .{ - .base = "https://lightpanda.io", - .path = "abc/something6.js", - .expected = "https://lightpanda.io/abc/something6.js", - }, - .{ - .base = "https://lightpanda.io/nested", - .path = "abc/something7.js", - .expected = "https://lightpanda.io/abc/something7.js", - }, - .{ - .base = "https://lightpanda.io/nested/", - .path = "abc/something8.js", - .expected = "https://lightpanda.io/nested/abc/something8.js", - }, - .{ - .base = "https://lightpanda.io/nested/", - .path = "/abc/something9.js", - .expected = "https://lightpanda.io/abc/something9.js", - }, - .{ - .base = "https://lightpanda.io/nested/", - .path = "http://www.github.com/lightpanda-io/", - .expected = "http://www.github.com/lightpanda-io/", - }, - .{ - .base = "https://lightpanda.io/nested/", - .path = "", - .expected = "https://lightpanda.io/nested/", - }, - .{ - .base = "https://lightpanda.io/abc/aaa", - .path = "./hello/./world", - .expected = "https://lightpanda.io/abc/hello/world", - }, - .{ - .base = "https://lightpanda.io/abc/aaa/", - .path = "../hello", - .expected = "https://lightpanda.io/abc/hello", - }, - .{ - .base = "https://lightpanda.io/abc/aaa", - .path = "../hello", - .expected = "https://lightpanda.io/hello", - }, - .{ - .base = "https://lightpanda.io/abc/aaa/", - .path = "./.././.././hello", - .expected = "https://lightpanda.io/hello", - }, - .{ - .base = "some/page", - .path = "hello", - .expected = "some/hello", - }, - .{ - .base = "some/page/", - .path = "hello", - .expected = "some/page/hello", - }, - .{ - .base = "some/page/other", - .path = ".././hello", - .expected = "some/hello", - }, - .{ - .path = "//static.lightpanda.io/hello.js", - .base = "https://lightpanda.io/about/", - .expected = "https://static.lightpanda.io/hello.js", - }, - }; - - for (cases) |case| { - const result = try stitch(testing.arena_allocator, case.path, case.base, .{}); - try testing.expectString(case.expected, result); - } -} - -test "URL: stitch regression (#1093)" { - defer testing.reset(); - - const Case = struct { - base: []const u8, - path: []const u8, - expected: []const u8, - }; - - const cases = [_]Case{ - .{ - .base = "https://alas.aws.amazon.com/alas2.html", - .path = "../static/bootstrap.min.css", - .expected = "https://alas.aws.amazon.com/static/bootstrap.min.css", - }, - }; - - for (cases) |case| { - const result = try stitch(testing.arena_allocator, case.path, case.base, .{}); - try testing.expectString(case.expected, result); - } -} - -test "URL: stitch null terminated" { - defer testing.reset(); - - const Case = struct { - base: []const u8, - path: []const u8, - expected: []const u8, - }; - - const cases = [_]Case{ - .{ - .base = "https://lightpanda.io/xyz/abc/123", - .path = "something1.js", - .expected = "https://lightpanda.io/xyz/abc/something1.js", - }, - .{ - .base = "https://lightpanda.io/xyz/abc/123", - .path = "/something2.js", - .expected = "https://lightpanda.io/something2.js", - }, - .{ - .base = "https://lightpanda.io/", - .path = "something3.js", - .expected = "https://lightpanda.io/something3.js", - }, - .{ - .base = "https://lightpanda.io/", - .path = "/something4.js", - .expected = "https://lightpanda.io/something4.js", - }, - .{ - .base = "https://lightpanda.io", - .path = "something5.js", - .expected = "https://lightpanda.io/something5.js", - }, - .{ - .base = "https://lightpanda.io", - .path = "abc/something6.js", - .expected = "https://lightpanda.io/abc/something6.js", - }, - .{ - .base = "https://lightpanda.io/nested", - .path = "abc/something7.js", - .expected = "https://lightpanda.io/abc/something7.js", - }, - .{ - .base = "https://lightpanda.io/nested/", - .path = "abc/something8.js", - .expected = "https://lightpanda.io/nested/abc/something8.js", - }, - .{ - .base = "https://lightpanda.io/nested/", - .path = "/abc/something9.js", - .expected = "https://lightpanda.io/abc/something9.js", - }, - .{ - .base = "https://lightpanda.io/nested/", - .path = "http://www.github.com/lightpanda-io/", - .expected = "http://www.github.com/lightpanda-io/", - }, - .{ - .base = "https://lightpanda.io/nested/", - .path = "", - .expected = "https://lightpanda.io/nested/", - }, - .{ - .base = "https://lightpanda.io/abc/aaa", - .path = "./hello/./world", - .expected = "https://lightpanda.io/abc/hello/world", - }, - .{ - .base = "https://lightpanda.io/abc/aaa/", - .path = "../hello", - .expected = "https://lightpanda.io/abc/hello", - }, - .{ - .base = "https://lightpanda.io/abc/aaa", - .path = "../hello", - .expected = "https://lightpanda.io/hello", - }, - .{ - .base = "https://lightpanda.io/abc/aaa/", - .path = "./.././.././hello", - .expected = "https://lightpanda.io/hello", - }, - .{ - .base = "some/page", - .path = "hello", - .expected = "some/hello", - }, - .{ - .base = "some/page/", - .path = "hello", - .expected = "some/page/hello", - }, - .{ - .base = "some/page/other", - .path = ".././hello", - .expected = "some/hello", - }, - .{ - .path = "//static.lightpanda.io/hello.js", - .base = "https://lightpanda.io/about/", - .expected = "https://static.lightpanda.io/hello.js", - }, - }; - - for (cases) |case| { - const result = try stitch(testing.arena_allocator, case.path, case.base, .{ .null_terminated = true }); - try testing.expectString(case.expected, result); - } -} - -test "URL: concatQueryString" { - defer testing.reset(); - const arena = testing.arena_allocator; - - { - const url = try URL.concatQueryString(arena, "https://www.lightpanda.io/", ""); - try testing.expectEqual("https://www.lightpanda.io/", url); - } - - { - const url = try URL.concatQueryString(arena, "https://www.lightpanda.io/index?", ""); - try testing.expectEqual("https://www.lightpanda.io/index?", url); - } - - { - const url = try URL.concatQueryString(arena, "https://www.lightpanda.io/index?", "a=b"); - try testing.expectEqual("https://www.lightpanda.io/index?a=b", url); - } - - { - const url = try URL.concatQueryString(arena, "https://www.lightpanda.io/index?1=2", "a=b"); - try testing.expectEqual("https://www.lightpanda.io/index?1=2&a=b", url); - } - - { - const url = try URL.concatQueryString(arena, "https://www.lightpanda.io/index?1=2&", "a=b"); - try testing.expectEqual("https://www.lightpanda.io/index?1=2&a=b", url); - } -} diff --git a/vendor/mimalloc b/vendor/mimalloc deleted file mode 160000 index 8f7d1e9a4..000000000 --- a/vendor/mimalloc +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 8f7d1e9a41bb0182166aac6a8d4d8b00f60ed032 diff --git a/vendor/netsurf/libdom b/vendor/netsurf/libdom deleted file mode 160000 index c7f2d3cd2..000000000 --- a/vendor/netsurf/libdom +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c7f2d3cd27d6dc853d8f4cc29ac51ef47944c233 diff --git a/vendor/netsurf/libhubbub b/vendor/netsurf/libhubbub deleted file mode 160000 index 1624ba625..000000000 --- a/vendor/netsurf/libhubbub +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 1624ba625047eebdaaefd0c5aa161a91e6e2e641 diff --git a/vendor/netsurf/libparserutils b/vendor/netsurf/libparserutils deleted file mode 160000 index 094dc22e2..000000000 --- a/vendor/netsurf/libparserutils +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 094dc22e2b3c21e8d12f2275fd7bf09bc4da3f3e diff --git a/vendor/netsurf/libwapcaplet b/vendor/netsurf/libwapcaplet deleted file mode 160000 index 74f1e0117..000000000 --- a/vendor/netsurf/libwapcaplet +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 74f1e0117310b5392da484a71346cf09f78e8216 diff --git a/vendor/netsurf/share/netsurf-buildsystem b/vendor/netsurf/share/netsurf-buildsystem deleted file mode 160000 index b4ba781fe..000000000 --- a/vendor/netsurf/share/netsurf-buildsystem +++ /dev/null @@ -1 +0,0 @@ -Subproject commit b4ba781fe22f356d7c53b1674dff91323af61458 From cdd31353c52bd2da4fb72bebfad6f51dc6bb5154 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Tue, 28 Oct 2025 11:24:29 +0800 Subject: [PATCH 02/16] get fetch campire working --- src/browser/dump.zig | 8 ++- src/browser/js/Context.zig | 2 - src/browser/js/Env.zig | 1 - src/browser/js/bridge.zig | 1 - src/browser/js/js.zig | 4 +- src/browser/page.zig | 12 +++- .../tests/document/query_selector.html | 2 +- src/browser/webapi/Document.zig | 2 +- src/browser/webapi/Element.zig | 13 ++-- src/browser/webapi/element/Attribute.zig | 39 ++++++++++- src/browser/webapi/net/Fetch.zig | 66 +++++++++++++++++-- src/browser/webapi/net/Response.zig | 2 +- src/lightpanda.zig | 13 ++-- src/log.zig | 2 +- src/main.zig | 21 +++--- 15 files changed, 142 insertions(+), 46 deletions(-) diff --git a/src/browser/dump.zig b/src/browser/dump.zig index 494c4d772..22460ee56 100644 --- a/src/browser/dump.zig +++ b/src/browser/dump.zig @@ -2,10 +2,14 @@ const std = @import("std"); const Node = @import("webapi/Node.zig"); pub const Opts = struct { + // @ZIGDOM (none of these do anything) + with_base: bool = false, strip_mode: StripMode = .{}, - const StripMode = struct { - // @ZIGDOM + pub const StripMode = struct { + js: bool = false, + ui: bool = false, + css: bool = false, }; }; diff --git a/src/browser/js/Context.zig b/src/browser/js/Context.zig index 0b7f258cb..c325df9d3 100644 --- a/src/browser/js/Context.zig +++ b/src/browser/js/Context.zig @@ -387,7 +387,6 @@ pub fn throw(self: *Context, err: []const u8) js.Exception { pub fn zigValueToJs(self: *Context, value: anytype, comptime opts: Caller.CallOpts) !v8.Value { const isolate = self.isolate; - // Check if it's a "simple" type. This is extracted so that it can be // reused by other parts of the code. "simple" types only require an // isolate to create (specifically, they don't our templates array) @@ -595,7 +594,6 @@ pub fn mapZigInstanceToJs(self: *Context, js_obj_: ?v8.Object, value: anytype) ! }; const JsApi = bridge.Struct(ptr.child).JsApi; - // The TAO contains the pointer to our Zig instance as // well as any meta data we'll need to use it later. // See the TaggedAnyOpaque struct for more details. diff --git a/src/browser/js/Env.zig b/src/browser/js/Env.zig index 046d5b401..386775e0f 100644 --- a/src/browser/js/Env.zig +++ b/src/browser/js/Env.zig @@ -311,7 +311,6 @@ fn generateConstructor(comptime JsApi: type, isolate: v8.Isolate) v8.FunctionTem return template; } - // ZIGDOM (HTMLAllCollection I think) // fn generateUndetectable(comptime Struct: type, template: v8.ObjectTemplate) void { // const has_js_call_as_function = @hasDecl(Struct, "jsCallAsFunction"); diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index b0732de1f..0e2d5c80a 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -401,7 +401,6 @@ pub const SubType = enum { webassemblymemory, }; - pub const JsApis = flattenTypes(&.{ @import("../webapi/AbortController.zig"), @import("../webapi/AbortSignal.zig"), diff --git a/src/browser/js/js.zig b/src/browser/js/js.zig index 444c0c571..f0d45e97e 100644 --- a/src/browser/js/js.zig +++ b/src/browser/js/js.zig @@ -106,7 +106,7 @@ pub const PersistentPromiseResolver = struct { pub fn resolve(self: PersistentPromiseResolver, value: anytype) !void { const context = self.context; - const js_value = try context.zigValueToJs(value); + const js_value = try context.zigValueToJs(value, .{}); // resolver.resolve will return null if the promise isn't pending const ok = self.resolver.castToPromiseResolver().resolve(context.v8_context, js_value) orelse return; @@ -117,7 +117,7 @@ pub const PersistentPromiseResolver = struct { pub fn reject(self: PersistentPromiseResolver, value: anytype) !void { const context = self.context; - const js_value = try context.zigValueToJs(value); + const js_value = try context.zigValueToJs(value, .{}); // resolver.reject will return null if the promise isn't pending const ok = self.resolver.castToPromiseResolver().reject(context.v8_context, js_value) orelse return; diff --git a/src/browser/page.zig b/src/browser/page.zig index 634f6eb77..1851bdc2f 100644 --- a/src/browser/page.zig +++ b/src/browser/page.zig @@ -58,6 +58,10 @@ _parse_mode: enum { document, fragment }, // even thoug we'll create very few (if any) actual *Attributes. _attribute_lookup: std.AutoHashMapUnmanaged(usize, *Element.Attribute), +// Same as _atlribute_lookup, but instead of individual attributes, this is for +// the return of elements.attributes. +_attribute_named_node_map_lookup: std.AutoHashMapUnmanaged(usize, *Element.Attribute.NamedNodeMap), + _script_manager: ScriptManager, _polyfill_loader: polyfill.Loader = .{}, @@ -119,6 +123,7 @@ pub fn deinit(self: *Page) void { log.debug(.page, "page.deinit", .{ .url = self.url }); } self.js.deinit(); + self._script_manager.deinit(); } fn reset(self: *Page, comptime initializing: bool) !void { @@ -144,6 +149,7 @@ fn reset(self: *Page, comptime initializing: bool) !void { self._parse_state = .pre; self._load_state = .parsing; self._attribute_lookup = .empty; + self._attribute_named_node_map_lookup = .empty; self._event_manager = EventManager.init(self); self._script_manager = ScriptManager.init(self); @@ -165,7 +171,7 @@ fn registerBackgroundTasks(self: *Page) !void { const Browser = @import("Browser.zig"); try self.scheduler.add(self._session.browser, struct { - fn runMicrotasks(ctx: *anyopaque) ?u32 { + fn runMicrotasks(ctx: *anyopaque) !?u32 { const b: *Browser = @ptrCast(@alignCast(ctx)); b.runMicrotasks(); return 5; @@ -173,7 +179,7 @@ fn registerBackgroundTasks(self: *Page) !void { }.runMicrotasks, 5, .{ .name = "page.microtasks" }); try self.scheduler.add(self._session.browser, struct { - fn runMessageLoop(ctx: *anyopaque) ?u32 { + fn runMessageLoop(ctx: *anyopaque) !?u32 { const b: *Browser = @ptrCast(@alignCast(ctx)); b.runMessageLoop(); return 100; @@ -992,7 +998,7 @@ fn populateElementAttributes(self: *Page, element: *Element, list: anytype) !voi if (@TypeOf(list) == ?*Element.Attribute.List) { // from cloneNode - var existing = list orelse return ; + var existing = list orelse return; var attributes = try self.arena.create(Element.Attribute.List); attributes.* = .{}; diff --git a/src/browser/tests/document/query_selector.html b/src/browser/tests/document/query_selector.html index 2399b3ea5..265273079 100644 --- a/src/browser/tests/document/query_selector.html +++ b/src/browser/tests/document/query_selector.html @@ -57,7 +57,7 @@
Heading 6
const firstScript = document.querySelector('script'); testing.expectEqual('SCRIPT', firstScript.tagName); - testing.expectEqual(null, document.querySelector('select')); + testing.expectEqual(null, document.querySelector('article')); testing.expectEqual(null, document.querySelector('another')); } diff --git a/src/browser/webapi/Document.zig b/src/browser/webapi/Document.zig index 753ecf669..e7dd31ec1 100644 --- a/src/browser/webapi/Document.zig +++ b/src/browser/webapi/Document.zig @@ -170,7 +170,7 @@ pub fn createTreeWalker(_: *const Document, root: *Node, what_to_show: ?u32, fil return DOMTreeWalker.init(root, show, filter, page); } - // @ZIGDOM what_to_show tristate (null vs undefined vs value) +// @ZIGDOM what_to_show tristate (null vs undefined vs value) pub fn createNodeIterator(_: *const Document, root: *Node, what_to_show: ?u32, filter: ?DOMNodeIterator.FilterOpts, page: *Page) !*DOMNodeIterator { const show = what_to_show orelse NodeFilter.SHOW_ALL; return DOMNodeIterator.init(root, show, filter, page); diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index 2e2e36b67..68dcb6c89 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -118,7 +118,7 @@ pub fn getTagNameLower(self: *const Element) []const u8 { .script => "script", .select => "select", .style => "style", - .text_area => "textara", + .text_area => "textarea", .title => "title", .ul => "ul", .unknown => |e| e._tag_name.str(), @@ -311,9 +311,14 @@ pub fn getAttributeNames(self: *const Element, page: *Page) ![][]const u8 { return attributes.getNames(page); } -pub fn getAttributeNamedNodeMap(self: *Element) Attribute.NamedNodeMap { - const attributes = self._attributes orelse return .{}; - return .{ ._list = attributes.*, ._element = self }; +pub fn getAttributeNamedNodeMap(self: *Element, page: *Page) !*Attribute.NamedNodeMap { + const gop = try page._attribute_named_node_map_lookup.getOrPut(page.arena, @intFromPtr(self)); + if (!gop.found_existing) { + const attributes = try self.getOrCreateAttributeList(page); + const named_node_map = try page._factory.create(Attribute.NamedNodeMap{ ._list = attributes, ._element = self }); + gop.value_ptr.* = named_node_map; + } + return gop.value_ptr.*; } pub fn getStyle(self: *Element, page: *Page) !*CSSStyleProperties { diff --git a/src/browser/webapi/element/Attribute.zig b/src/browser/webapi/element/Attribute.zig index 0e619ca8e..f3fcbe04d 100644 --- a/src/browser/webapi/element/Attribute.zig +++ b/src/browser/webapi/element/Attribute.zig @@ -326,7 +326,7 @@ fn needsLowerCasing(name: []const u8) bool { } pub const NamedNodeMap = struct { - _list: List = .{}, + _list: *List, // Whenever the NamedNodeMap creates an Attribute, it needs to provide the // "ownerElement". @@ -418,6 +418,12 @@ pub const InnerIterator = struct { fn formatAttribute(name: []const u8, value: []const u8, writer: *std.Io.Writer) !void { try writer.writeAll(name); + + // Boolean attributes with empty values are serialized without a value + if (value.len == 0 and boolean_attributes_lookup.has(name)) { + return; + } + try writer.writeByte('='); if (value.len == 0) { return writer.writeAll("\"\""); @@ -433,6 +439,37 @@ fn formatAttribute(name: []const u8, value: []const u8, writer: *std.Io.Writer) return writer.writeByte('"'); } +const boolean_attributes = [_][]const u8{ + "checked", + "disabled", + "required", + "readonly", + "multiple", + "selected", + "autofocus", + "autoplay", + "controls", + "loop", + "muted", + "hidden", + "async", + "defer", + "novalidate", + "formnovalidate", + "ismap", + "reversed", + "default", + "open", +}; + +const boolean_attributes_lookup = std.StaticStringMap(void).initComptime(blk: { + var entries: [boolean_attributes.len]struct { []const u8, void } = undefined; + for (boolean_attributes, 0..) |attr, i| { + entries[i] = .{ attr, {} }; + } + break :blk entries; +}); + fn writeEscapedAttributeValue(value: []const u8, first_offset: usize, writer: *std.Io.Writer) !void { // Write everything before the first special character try writer.writeAll(value[0..first_offset]); diff --git a/src/browser/webapi/net/Fetch.zig b/src/browser/webapi/net/Fetch.zig index f838ad34c..0d4853f98 100644 --- a/src/browser/webapi/net/Fetch.zig +++ b/src/browser/webapi/net/Fetch.zig @@ -1,5 +1,8 @@ const std = @import("std"); +const log = @import("../../../log.zig"); +const Http = @import("../../../http/Http.zig"); + const js = @import("../../js/js.zig"); const Page = @import("../../Page.zig"); @@ -8,15 +11,64 @@ const Response = @import("Response.zig"); const Allocator = std.mem.Allocator; -_arena: Allocator, -_promise: js.Promise, -_has_response: bool, +const Fetch = @This(); + +_page: *Page, +_response: std.ArrayList(u8), +_resolver: js.PersistentPromiseResolver, pub const Input = Request.Input; +// @ZIGDOM just enough to get campire demo working pub fn init(input: Input, page: *Page) !js.Promise { - // @ZIGDOM - _ = input; - _ = page; - return undefined; + const request = try Request.init(input, page); + + const fetch = try page.arena.create(Fetch); + fetch.* = .{ + ._page = page, + ._response = .empty, + ._resolver = try page.js.createPromiseResolver(.page), + }; + + const http_client = page._session.browser.http_client; + const headers = try http_client.newHeaders(); + + try http_client.request(.{ + .ctx = fetch, + .url = request._url, + .method = .GET, + .headers = headers, + .cookie_jar = &page._session.cookie_jar, + .resource_type = .fetch, + .header_callback = httpHeaderDoneCallback, + .data_callback = httpDataCallback, + .done_callback = httpDoneCallback, + .error_callback = httpErrorCallback, + }); + return fetch._resolver.promise(); +} + +fn httpHeaderDoneCallback(transfer: *Http.Transfer) !void { + const self: *Fetch = @ptrCast(@alignCast(transfer.ctx)); + _ = self; +} + +fn httpDataCallback(transfer: *Http.Transfer, data: []const u8) !void { + const self: *Fetch = @ptrCast(@alignCast(transfer.ctx)); + try self._response.appendSlice(self._page.arena, data); +} + +fn httpDoneCallback(ctx: *anyopaque) !void { + const self: *Fetch = @ptrCast(@alignCast(ctx)); + + const page = self._page; + const res = try Response.initFromFetch(page.arena, self._response.items, page); + return self._resolver.resolve(res); +} + +fn httpErrorCallback(ctx: *anyopaque, err: anyerror) void { + const self: *Fetch = @ptrCast(@alignCast(ctx)); + self._resolver.reject(@errorName(err)) catch |inner| { + log.err(.bug, "failed to reject", .{ .source = "fetch", .err = inner, .reject = err }); + }; } diff --git a/src/browser/webapi/net/Response.zig b/src/browser/webapi/net/Response.zig index a2fe44f2e..e7f3168db 100644 --- a/src/browser/webapi/net/Response.zig +++ b/src/browser/webapi/net/Response.zig @@ -35,7 +35,7 @@ pub fn getJson(self: *Response, page: *Page) !js.Promise { ) catch |err| { return page.js.rejectPromise(.{@errorName(err)}); }; - return page.js.resolvePromise(.{value}); + return page.js.resolvePromise(value); } pub const JsApi = struct { diff --git a/src/lightpanda.zig b/src/lightpanda.zig index a2ad306fc..54e425735 100644 --- a/src/lightpanda.zig +++ b/src/lightpanda.zig @@ -8,8 +8,8 @@ const Allocator = std.mem.Allocator; pub const FetchOpts = struct { wait_ms: u32 = 5000, - dump_opts: dump.Opts, - dump_file: ?std.fs.File = null, + dump: dump.Opts, + writer: ?*std.Io.Writer = null, }; pub fn fetch(app: *App, url: [:0]const u8, opts: FetchOpts) !void { const Browser = @import("browser/Browser.zig"); @@ -40,12 +40,9 @@ pub fn fetch(app: *App, url: [:0]const u8, opts: FetchOpts) !void { _ = try page.navigate(url, .{}); _ = session.fetchWait(opts.wait_ms); - const file = opts.dump_file orelse return; - - var buf: [4096]u8 = undefined; - var writer = file.writer(&buf); - try dump.deep(page.document.asNode(), opts.dump_opts, &writer.interface); - try writer.interface.flush(); + const writer = opts.writer orelse return; + try dump.deep(page.document.asNode(), opts.dump, writer); + try writer.flush(); } test { diff --git a/src/log.zig b/src/log.zig index 03547ba45..d0f02bf9d 100644 --- a/src/log.zig +++ b/src/log.zig @@ -352,7 +352,7 @@ fn elapsed() struct { time: f64, unit: []const u8 } { } const datetime = @import("datetime.zig"); -fn timestamp(mode: datetime.TimestampMode) u64 { +fn timestamp(comptime mode: datetime.TimestampMode) u64 { if (comptime @import("builtin").is_test) { return 1739795092929; } diff --git a/src/main.zig b/src/main.zig index b1a6cb5e4..6c90196d2 100644 --- a/src/main.zig +++ b/src/main.zig @@ -38,19 +38,17 @@ pub fn main() !void { if (gpa.detectLeaks()) std.posix.exit(1); }; - var global_allocator = lp.GlobalAllocator.init(allocator); - // arena for main-specific allocations - var main_arena = std.heap.ArenaAllocator.init(global_allocator.allocator()); + var main_arena = std.heap.ArenaAllocator.init(allocator); defer main_arena.deinit(); - run(&global_allocator, main_arena.allocator()) catch |err| { + run(allocator, main_arena.allocator()) catch |err| { log.fatal(.app, "exit", .{ .err = err }); std.posix.exit(1); }; } -fn run(allocator: *lp.GlobalAllocator, main_arena: Allocator) !void { +fn run(allocator: Allocator, main_arena: Allocator) !void { const args = try parseArgs(main_arena); switch (args.mode) { @@ -102,7 +100,6 @@ fn run(allocator: *lp.GlobalAllocator, main_arena: Allocator) !void { switch (args.mode) { .serve => { - log.fatal(.app, "serve not not supported in the zigdom branch yet\n", .{}); return; // @ZIGDOM-CDP // .serve => |opts| { @@ -131,13 +128,15 @@ fn run(allocator: *lp.GlobalAllocator, main_arena: Allocator) !void { var fetch_opts = lp.FetchOpts{ .wait_ms = 5000, .dump = .{ - .with_base = opts.with_base, + .with_base = opts.withbase, .strip_mode = opts.strip_mode, }, }; + var stdout = std.fs.File.stdout(); + var writer = stdout.writer(&.{}); if (opts.dump) { - fetch_opts.dump_file = std.fs.File.stdout(); + fetch_opts.writer = &writer.interface; } lp.fetch(app, url, fetch_opts) catch |err| { @@ -245,7 +244,7 @@ const Command = struct { }; const Fetch = struct { - url: []const u8, + url: [:0]const u8, dump: bool = false, common: Common, withbase: bool = false, @@ -513,7 +512,7 @@ fn parseFetchArgs( ) !Command.Fetch { var dump: bool = false; var withbase: bool = false; - var url: ?[]const u8 = null; + var url: ?[:0]const u8 = null; var common: Command.Common = .{}; var strip_mode: lp.dump.Opts.StripMode = .{}; @@ -576,7 +575,7 @@ fn parseFetchArgs( log.fatal(.app, "duplicate fetch url", .{ .help = "only 1 URL can be specified" }); return error.TooManyURLs; } - url = try allocator.dupe(u8, opt); + url = try allocator.dupeZ(u8, opt); } if (url == null) { From d3973172e8dbb3a7fffeaaa8c5c63ef5e4f3712c Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Tue, 28 Oct 2025 18:56:03 +0800 Subject: [PATCH 03/16] re-enable minimum viable CDP server --- src/browser/URL.zig | 129 +++ src/browser/js/bridge.zig | 1 + src/browser/session.zig | 2 +- src/browser/webapi/MutationObserver.zig | 23 + src/browser/webapi/Node.zig | 3 + src/browser/webapi/TreeWalker.zig | 20 +- src/browser/webapi/URL.zig | 83 +- src/browser/webapi/storage/storage.zig | 1 + src/cdp/Node.zig | 1137 ++++++++++---------- src/cdp/cdp.zig | 72 +- src/cdp/domains/dom.zig | 1283 ++++++++++++----------- src/cdp/domains/fetch.zig | 2 +- src/cdp/domains/input.zig | 2 +- src/cdp/domains/log.zig | 2 +- src/cdp/domains/network.zig | 65 +- src/cdp/domains/page.zig | 13 +- src/cdp/domains/storage.zig | 6 +- src/cdp/domains/target.zig | 5 +- src/cdp/testing.zig | 1 - src/http/Client.zig | 4 +- src/http/Http.zig | 2 +- src/lightpanda.zig | 2 + src/main.zig | 39 +- src/server.zig | 20 +- src/telemetry/lightpanda.zig | 2 +- 25 files changed, 1516 insertions(+), 1403 deletions(-) create mode 100644 src/browser/webapi/MutationObserver.zig diff --git a/src/browser/URL.zig b/src/browser/URL.zig index a2062d507..da0319497 100644 --- a/src/browser/URL.zig +++ b/src/browser/URL.zig @@ -122,6 +122,135 @@ pub fn isCompleteHTTPUrl(url: []const u8) bool { std.ascii.startsWithIgnoreCase(url, "ftp://"); } +pub fn getUsername(raw: [:0]const u8) []const u8 { + const user_info = getUserInfo(raw) orelse return ""; + const pos = std.mem.indexOfScalarPos(u8, user_info, 0, ':') orelse return user_info; + return user_info[0..pos]; +} + +pub fn getPassword(raw: [:0]const u8) []const u8 { + const user_info = getUserInfo(raw) orelse return ""; + const pos = std.mem.indexOfScalarPos(u8, user_info, 0, ':') orelse return ""; + return user_info[pos + 1 ..]; +} + +pub fn getPathname(raw: [:0]const u8) []const u8 { + const protocol_end = std.mem.indexOf(u8, raw, "://") orelse 0; + const path_start = std.mem.indexOfScalarPos(u8, raw, if (protocol_end > 0) protocol_end + 3 else 0, '/') orelse raw.len; + + const query_or_hash_start = std.mem.indexOfAnyPos(u8, raw, path_start, "?#") orelse raw.len; + + if (path_start >= query_or_hash_start) { + if (std.mem.indexOf(u8, raw, "://") != null) return "/"; + return ""; + } + + return raw[path_start..query_or_hash_start]; +} + +pub fn getProtocol(raw: [:0]const u8) []const u8 { + const pos = std.mem.indexOfScalarPos(u8, raw, 0, ':') orelse return ""; + return raw[0 .. pos + 1]; +} + +pub fn getHostname(raw: [:0]const u8) []const u8 { + const host = getHost(raw); + const pos = std.mem.lastIndexOfScalar(u8, host, ':') orelse return host; + return host[0..pos]; +} + +pub fn getPort(raw: [:0]const u8) []const u8 { + const host = getHost(raw); + const pos = std.mem.lastIndexOfScalar(u8, host, ':') orelse return ""; + + if (pos + 1 >= host.len) { + return ""; + } + + for (host[pos + 1 ..]) |c| { + if (c < '0' or c > '9') { + return ""; + } + } + + return host[pos + 1 ..]; +} + +pub fn getSearch(raw: [:0]const u8) []const u8 { + const pos = std.mem.indexOfScalarPos(u8, raw, 0, '?') orelse return ""; + const query_part = raw[pos..]; + + if (std.mem.indexOfScalarPos(u8, query_part, 0, '#')) |fragment_start| { + return query_part[0..fragment_start]; + } + + return query_part; +} + +pub fn getHash(raw: [:0]const u8) []const u8 { + const start = std.mem.indexOfScalarPos(u8, raw, 0, '#') orelse return ""; + return raw[start..]; +} + +pub fn getOrigin(allocator: Allocator, raw: [:0]const u8) !?[]const u8 { + const port = getPort(raw); + const protocol = getProtocol(raw); + const hostname = getHostname(raw); + + const p = std.meta.stringToEnum(KnownProtocol, getProtocol(raw)) orelse return null; + + const include_port = blk: { + if (port.len == 0) { + break :blk false; + } + if (p == .@"https:" and std.mem.eql(u8, port, "443")) { + break :blk false; + } + if (p == .@"http:" and std.mem.eql(u8, port, "80")) { + break :blk false; + } + break :blk true; + }; + + if (include_port) { + return try std.fmt.allocPrint(allocator, "{s}//{s}:{s}", .{ protocol, hostname, port }); + } + return try std.fmt.allocPrint(allocator, "{s}//{s}", .{ protocol, hostname }); +} + +fn getUserInfo(raw: [:0]const u8) ?[]const u8 { + const scheme_end = std.mem.indexOf(u8, raw, "://") orelse return null; + const authority_start = scheme_end + 3; + + const pos = std.mem.indexOfScalar(u8, raw[authority_start..], '@') orelse return null; + const path_start = std.mem.indexOfScalarPos(u8, raw, authority_start, '/') orelse raw.len; + + const full_pos = authority_start + pos; + if (full_pos < path_start) { + return raw[authority_start..full_pos]; + } + + return null; +} + +fn getHost(raw: [:0]const u8) []const u8 { + const scheme_end = std.mem.indexOf(u8, raw, "://") orelse return ""; + + var authority_start = scheme_end + 3; + if (std.mem.indexOf(u8, raw[authority_start..], "@")) |pos| { + authority_start += pos + 1; + } + + const authority = raw[authority_start..]; + const path_start = std.mem.indexOfAny(u8, authority, "/?#") orelse return authority; + return authority[0..path_start]; +} + +const KnownProtocol = enum { + @"http:", + @"https:", +}; + const testing = @import("../testing.zig"); test "URL: isCompleteHTTPUrl" { try testing.expectEqual(true, isCompleteHTTPUrl("http://example.com/about")); diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index 0e2d5c80a..ae2790eab 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -467,4 +467,5 @@ pub const JsApis = flattenTypes(&.{ @import("../webapi/storage/storage.zig"), @import("../webapi/URL.zig"), @import("../webapi/Window.zig"), + @import("../webapi/MutationObserver.zig"), }); diff --git a/src/browser/session.zig b/src/browser/session.zig index 41fd795a6..0f90a82a3 100644 --- a/src/browser/session.zig +++ b/src/browser/session.zig @@ -141,7 +141,7 @@ pub fn wait(self: *Session, wait_ms: u32) WaitResult { return .done; }; - if (self.page) |*page| { + if (self.page) |page| { return page.wait(wait_ms); } return .no_page; diff --git a/src/browser/webapi/MutationObserver.zig b/src/browser/webapi/MutationObserver.zig new file mode 100644 index 000000000..73001ee44 --- /dev/null +++ b/src/browser/webapi/MutationObserver.zig @@ -0,0 +1,23 @@ +const js = @import("../js/js.zig"); + +// @ZIGDOM (haha, bet you wish you hadn't opened this file) +// puppeteer's startup script creates a MutationObserver, even if it doesn't use +// it in simple scripts. This not-even-a-skeleton is required for puppeteer/cdp.js +// to run +const MutationObserver = @This(); + +pub fn init() MutationObserver { + return .{}; +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(MutationObserver); + + pub const Meta = struct { + pub const name = "MutationObserver"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_index: u16 = 0; + }; + + pub const constructor = bridge.constructor(MutationObserver.init, .{}); +}; diff --git a/src/browser/webapi/Node.zig b/src/browser/webapi/Node.zig index e6f03d980..88a827fab 100644 --- a/src/browser/webapi/Node.zig +++ b/src/browser/webapi/Node.zig @@ -340,6 +340,9 @@ pub fn setNodeValue(self: *const Node, value: ?[]const u8, page: *Page) !void { } pub fn format(self: *Node, writer: *std.Io.Writer) !void { + // // If you need extra debugging: + // return @import("../dump.zig").deep(self, .{}, writer); + return switch (self._type) { .cdata => |cd| cd.format(writer), .element => |el| writer.print("{f}", .{el}), diff --git a/src/browser/webapi/TreeWalker.zig b/src/browser/webapi/TreeWalker.zig index c2b1f39e0..cee99ff14 100644 --- a/src/browser/webapi/TreeWalker.zig +++ b/src/browser/webapi/TreeWalker.zig @@ -39,14 +39,22 @@ pub fn TreeWalker(comptime mode: Mode) type { self._next = children.first(); } else if (node._child_link.next) |n| { self._next = Node.linkToNode(n); - } else if (node._parent) |n| { - if (n == self._root) { - self._next = null; + } else { + // No children, no next sibling - walk up until we find a next sibling or hit root + var current = node._parent; + while (current) |parent| { + if (parent == self._root) { + self._next = null; + break; + } + if (parent._child_link.next) |next_sibling| { + self._next = Node.linkToNode(next_sibling); + break; + } + current = parent._parent; } else { - self._next = Node.linkToNodeOrNull(n._child_link.next); + self._next = null; } - } else { - self._next = null; } return node; } diff --git a/src/browser/webapi/URL.zig b/src/browser/webapi/URL.zig index b81c8cb2e..d7bf0d7db 100644 --- a/src/browser/webapi/URL.zig +++ b/src/browser/webapi/URL.zig @@ -1,6 +1,7 @@ const std = @import("std"); const js = @import("../js/js.zig"); +const U = @import("../URL.zig"); const Page = @import("../Page.zig"); const URLSearchParams = @import("net/URLSearchParams.zig"); @@ -42,106 +43,42 @@ pub fn init(url: [:0]const u8, base_: ?[:0]const u8, page: *Page) !*URL { } pub fn getUsername(self: *const URL) []const u8 { - const user_info = self.getUserInfo() orelse return ""; - const pos = std.mem.indexOfScalarPos(u8, user_info, 0, ':') orelse return user_info; - return user_info[0..pos]; + return U.getUsername(self._raw); } pub fn getPassword(self: *const URL) []const u8 { - const user_info = self.getUserInfo() orelse return ""; - const pos = std.mem.indexOfScalarPos(u8, user_info, 0, ':') orelse return ""; - return user_info[pos + 1 ..]; + return U.getPassword(self._raw); } pub fn getPathname(self: *const URL) []const u8 { - const raw = self._raw; - const protocol_end = std.mem.indexOf(u8, raw, "://") orelse 0; - const path_start = std.mem.indexOfScalarPos(u8, raw, if (protocol_end > 0) protocol_end + 3 else 0, '/') orelse raw.len; - - const query_or_hash_start = std.mem.indexOfAnyPos(u8, raw, path_start, "?#") orelse raw.len; - - if (path_start >= query_or_hash_start) { - if (std.mem.indexOf(u8, raw, "://") != null) return "/"; - return ""; - } - - return raw[path_start..query_or_hash_start]; + return U.getPathname(self._raw); } pub fn getProtocol(self: *const URL) []const u8 { - const raw = self._raw; - const pos = std.mem.indexOfScalarPos(u8, raw, 0, ':') orelse return ""; - return raw[0 .. pos + 1]; + return U.getProtocol(self._raw); } pub fn getHostname(self: *const URL) []const u8 { - const host = self.getHost(); - const pos = std.mem.lastIndexOfScalar(u8, host, ':') orelse return host; - return host[0..pos]; + return U.getHostname(self._raw); } pub fn getPort(self: *const URL) []const u8 { - const host = self.getHost(); - const pos = std.mem.lastIndexOfScalar(u8, host, ':') orelse return ""; - - if (pos + 1 >= host.len) { - return ""; - } - - for (host[pos + 1 ..]) |c| { - if (c < '0' or c > '9') { - return ""; - } - } - - return host[pos + 1 ..]; + return U.getPort(self._raw); } pub fn getOrigin(self: *const URL, page: *const Page) ![]const u8 { - const port = self.getPort(); - const protocol = self.getProtocol(); - const hostname = self.getHostname(); - - const p = std.meta.stringToEnum(KnownProtocol, self.getProtocol()) orelse { + return (try U.getOrigin(page.call_arena, self._raw)) orelse { // yes, a null string, that's what the spec wants return "null"; }; - - const include_port = blk: { - if (port.len == 0) { - break :blk false; - } - if (p == .@"https:" and std.mem.eql(u8, port, "443")) { - break :blk false; - } - if (p == .@"http:" and std.mem.eql(u8, port, "80")) { - break :blk false; - } - break :blk true; - }; - - if (include_port) { - return std.fmt.allocPrint(page.call_arena, "{s}//{s}:{s}", .{ protocol, hostname, port }); - } - return std.fmt.allocPrint(page.call_arena, "{s}//{s}", .{ protocol, hostname }); } pub fn getSearch(self: *const URL) []const u8 { - const raw = self._raw; - const pos = std.mem.indexOfScalarPos(u8, raw, 0, '?') orelse return ""; - const query_part = raw[pos..]; - - if (std.mem.indexOfScalarPos(u8, query_part, 0, '#')) |fragment_start| { - return query_part[0..fragment_start]; - } - - return query_part; + return U.getSearch(self._raw); } pub fn getHash(self: *const URL) []const u8 { - const raw = self._raw; - const start = std.mem.indexOfScalarPos(u8, raw, 0, '#') orelse return ""; - return raw[start..]; + return U.getHash(self._raw); } pub fn getSearchParams(self: *URL, page: *Page) !*URLSearchParams { diff --git a/src/browser/webapi/storage/storage.zig b/src/browser/webapi/storage/storage.zig index 13bbc72f2..8813c0928 100644 --- a/src/browser/webapi/storage/storage.zig +++ b/src/browser/webapi/storage/storage.zig @@ -9,6 +9,7 @@ pub fn registerTypes() []const type { } pub const Jar = @import("cookie.zig").Jar; +pub const Cookie =@import("cookie.zig").Cookie; pub const Shed = struct { _origins: std.StringHashMapUnmanaged(*Bucket) = .empty, diff --git a/src/cdp/Node.zig b/src/cdp/Node.zig index be18206c4..c51093128 100644 --- a/src/cdp/Node.zig +++ b/src/cdp/Node.zig @@ -16,571 +16,572 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -const std = @import("std"); -const Allocator = std.mem.Allocator; - -const log = @import("../log.zig"); -const parser = @import("../browser/netsurf.zig"); - -pub const Id = u32; - -const Node = @This(); - -id: Id, -_node: *parser.Node, -set_child_nodes_event: bool, - -// Whenever we send a node to the client, we register it here for future lookup. -// We maintain a node -> id and id -> node lookup. -pub const Registry = struct { - node_id: u32, - allocator: Allocator, - arena: std.heap.ArenaAllocator, - node_pool: std.heap.MemoryPool(Node), - lookup_by_id: std.AutoHashMapUnmanaged(Id, *Node), - lookup_by_node: std.HashMapUnmanaged(*parser.Node, *Node, NodeContext, std.hash_map.default_max_load_percentage), - - pub fn init(allocator: Allocator) Registry { - return .{ - .node_id = 1, - .lookup_by_id = .{}, - .lookup_by_node = .{}, - .allocator = allocator, - .arena = std.heap.ArenaAllocator.init(allocator), - .node_pool = std.heap.MemoryPool(Node).init(allocator), - }; - } - - pub fn deinit(self: *Registry) void { - const allocator = self.allocator; - self.lookup_by_id.deinit(allocator); - self.lookup_by_node.deinit(allocator); - self.node_pool.deinit(); - self.arena.deinit(); - } - - pub fn reset(self: *Registry) void { - self.lookup_by_id.clearRetainingCapacity(); - self.lookup_by_node.clearRetainingCapacity(); - _ = self.arena.reset(.{ .retain_with_limit = 1024 }); - _ = self.node_pool.reset(.{ .retain_with_limit = 1024 }); - } - - pub fn register(self: *Registry, n: *parser.Node) !*Node { - const node_lookup_gop = try self.lookup_by_node.getOrPut(self.allocator, n); - if (node_lookup_gop.found_existing) { - return node_lookup_gop.value_ptr.*; - } - - // on error, we're probably going to abort the entire browser context - // but, just in case, let's try to keep things tidy. - errdefer _ = self.lookup_by_node.remove(n); - - const node = try self.node_pool.create(); - errdefer self.node_pool.destroy(node); - - const id = self.node_id; - self.node_id = id + 1; - - node.* = .{ - ._node = n, - .id = id, - .set_child_nodes_event = false, - }; - - node_lookup_gop.value_ptr.* = node; - try self.lookup_by_id.putNoClobber(self.allocator, id, node); - return node; - } -}; - -const NodeContext = struct { - pub fn hash(_: NodeContext, n: *parser.Node) u64 { - return std.hash.Wyhash.hash(0, std.mem.asBytes(&@intFromPtr(n))); - } - - pub fn eql(_: NodeContext, a: *parser.Node, b: *parser.Node) bool { - return @intFromPtr(a) == @intFromPtr(b); - } -}; - -// Searches are a 3 step process: -// 1 - Dom.performSearch -// 2 - Dom.getSearchResults -// 3 - Dom.discardSearchResults -// -// For a given browser context, we can have multiple active searches. I.e. -// performSearch could be called multiple times without getSearchResults or -// discardSearchResults being called. We keep these active searches in the -// browser context's node_search_list, which is a SearchList. Since we don't -// expect many active searches (mostly just 1), a list is fine to scan through. -pub const Search = struct { - name: []const u8, - node_ids: []const Id, - - pub const List = struct { - registry: *Registry, - search_id: u16 = 0, - arena: std.heap.ArenaAllocator, - searches: std.ArrayListUnmanaged(Search) = .{}, - - pub fn init(allocator: Allocator, registry: *Registry) List { - return .{ - .registry = registry, - .arena = std.heap.ArenaAllocator.init(allocator), - }; - } - - pub fn deinit(self: *List) void { - self.arena.deinit(); - } - - pub fn reset(self: *List) void { - self.search_id = 0; - self.searches = .{}; - _ = self.arena.reset(.{ .retain_with_limit = 4096 }); - } - - pub fn create(self: *List, nodes: []const *parser.Node) !Search { - const id = self.search_id; - defer self.search_id = id +% 1; - - const arena = self.arena.allocator(); - - const name = switch (id) { - 0 => "0", - 1 => "1", - 2 => "2", - 3 => "3", - 4 => "4", - 5 => "5", - 6 => "6", - 7 => "7", - 8 => "8", - 9 => "9", - else => try std.fmt.allocPrint(arena, "{d}", .{id}), - }; - - var registry = self.registry; - const node_ids = try arena.alloc(Id, nodes.len); - for (nodes, node_ids) |node, *node_id| { - node_id.* = (try registry.register(node)).id; - } - - const search = Search{ - .name = name, - .node_ids = node_ids, - }; - try self.searches.append(arena, search); - return search; - } - - pub fn remove(self: *List, name: []const u8) void { - for (self.searches.items, 0..) |search, i| { - if (std.mem.eql(u8, name, search.name)) { - _ = self.searches.swapRemove(i); - return; - } - } - } - - pub fn get(self: *const List, name: []const u8) ?Search { - for (self.searches.items) |search| { - if (std.mem.eql(u8, name, search.name)) { - return search; - } - } - return null; - } - }; -}; - -// Need a custom writer, because we can't just serialize the node as-is. -// Sometimes we want to serializ the node without chidren, sometimes with just -// its direct children, and sometimes the entire tree. -// (For now, we only support direct children) - -pub const Writer = struct { - depth: i32, - exclude_root: bool, - root: *const Node, - registry: *Registry, - - pub const Opts = struct { - depth: i32 = 0, - exclude_root: bool = false, - }; - - pub fn jsonStringify(self: *const Writer, w: anytype) error{WriteFailed}!void { - if (self.exclude_root) { - _ = self.writeChildren(self.root, 1, w) catch |err| { - log.err(.cdp, "node writeChildren", .{ .err = err }); - return error.WriteFailed; - }; - } else { - self.toJSON(self.root, 0, w) catch |err| { - // The only error our jsonStringify method can return is - // @TypeOf(w).Error. In other words, our code can't return its own - // error, we can only return a writer error. Kinda sucks. - log.err(.cdp, "node toJSON stringify", .{ .err = err }); - return error.WriteFailed; - }; - } - } - - fn toJSON(self: *const Writer, node: *const Node, depth: usize, w: anytype) !void { - try w.beginObject(); - try self.writeCommon(node, false, w); - - try w.objectField("children"); - const child_count = try self.writeChildren(node, depth, w); - try w.objectField("childNodeCount"); - try w.write(child_count); - - try w.endObject(); - } - - fn writeChildren(self: *const Writer, node: *const Node, depth: usize, w: anytype) anyerror!usize { - var registry = self.registry; - const child_nodes = try parser.nodeGetChildNodes(node._node); - const child_count = parser.nodeListLength(child_nodes); - const full_child = self.depth < 0 or self.depth < depth; - - var i: usize = 0; - try w.beginArray(); - for (0..child_count) |_| { - const child = (parser.nodeListItem(child_nodes, @intCast(i))) orelse break; - const child_node = try registry.register(child); - if (full_child) { - try self.toJSON(child_node, depth + 1, w); - } else { - try w.beginObject(); - try self.writeCommon(child_node, true, w); - try w.endObject(); - } - - i += 1; - } - try w.endArray(); - - return i; - } - - fn writeCommon(self: *const Writer, node: *const Node, include_child_count: bool, w: anytype) !void { - try w.objectField("nodeId"); - try w.write(node.id); - - try w.objectField("backendNodeId"); - try w.write(node.id); - - const n = node._node; - - if (parser.nodeParentNode(n)) |p| { - const parent_node = try self.registry.register(p); - try w.objectField("parentId"); - try w.write(parent_node.id); - } - - const _map = try parser.nodeGetAttributes(n); - if (_map) |map| { - const attr_count = try parser.namedNodeMapGetLength(map); - try w.objectField("attributes"); - try w.beginArray(); - for (0..attr_count) |i| { - const attr = try parser.namedNodeMapItem(map, @intCast(i)) orelse continue; - try w.write(try parser.attributeGetName(attr)); - try w.write(try parser.attributeGetValue(attr) orelse continue); - } - try w.endArray(); - } - - try w.objectField("nodeType"); - try w.write(@intFromEnum(parser.nodeType(n))); - - try w.objectField("nodeName"); - try w.write(try parser.nodeName(n)); - - try w.objectField("localName"); - try w.write(try parser.nodeLocalName(n)); - - try w.objectField("nodeValue"); - try w.write((parser.nodeValue(n)) orelse ""); - - if (include_child_count) { - try w.objectField("childNodeCount"); - const child_nodes = try parser.nodeGetChildNodes(n); - try w.write(parser.nodeListLength(child_nodes)); - } - - try w.objectField("documentURL"); - try w.write(null); - - try w.objectField("baseURL"); - try w.write(null); - - try w.objectField("xmlVersion"); - try w.write(""); - - try w.objectField("compatibilityMode"); - try w.write("NoQuirksMode"); - - try w.objectField("isScrollable"); - try w.write(false); - } -}; - -const testing = @import("testing.zig"); -test "cdp Node: Registry register" { - parser.init(); - defer parser.deinit(); - - var registry = Registry.init(testing.allocator); - defer registry.deinit(); - - try testing.expectEqual(0, registry.lookup_by_id.count()); - try testing.expectEqual(0, registry.lookup_by_node.count()); - - var doc = try testing.Document.init("link1

other

"); - defer doc.deinit(); - - { - const n = (try doc.querySelector("#a1")).?; - const node = try registry.register(n); - const n1b = registry.lookup_by_id.get(1).?; - const n1c = registry.lookup_by_node.get(node._node).?; - try testing.expectEqual(node, n1b); - try testing.expectEqual(node, n1c); - - try testing.expectEqual(1, node.id); - try testing.expectEqual(n, node._node); - } - - { - const n = (try doc.querySelector("p")).?; - const node = try registry.register(n); - const n1b = registry.lookup_by_id.get(2).?; - const n1c = registry.lookup_by_node.get(node._node).?; - try testing.expectEqual(node, n1b); - try testing.expectEqual(node, n1c); - - try testing.expectEqual(2, node.id); - try testing.expectEqual(n, node._node); - } -} - -test "cdp Node: search list" { - parser.init(); - defer parser.deinit(); - - var registry = Registry.init(testing.allocator); - defer registry.deinit(); - - var search_list = Search.List.init(testing.allocator, ®istry); - defer search_list.deinit(); - - { - // empty search list, noops - search_list.remove("0"); - try testing.expectEqual(null, search_list.get("0")); - } - - { - // empty nodes - const s1 = try search_list.create(&.{}); - try testing.expectEqual("0", s1.name); - try testing.expectEqual(0, s1.node_ids.len); - - const s2 = search_list.get("0").?; - try testing.expectEqual("0", s2.name); - try testing.expectEqual(0, s2.node_ids.len); - - search_list.remove("0"); - try testing.expectEqual(null, search_list.get("0")); - } - - { - var doc = try testing.Document.init(""); - defer doc.deinit(); - - const s1 = try search_list.create(try doc.querySelectorAll("a")); - try testing.expectEqual("1", s1.name); - try testing.expectEqualSlices(u32, &.{ 1, 2 }, s1.node_ids); - - try testing.expectEqual(2, registry.lookup_by_id.count()); - try testing.expectEqual(2, registry.lookup_by_node.count()); - - const s2 = try search_list.create(try doc.querySelectorAll("#a1")); - try testing.expectEqual("2", s2.name); - try testing.expectEqualSlices(u32, &.{1}, s2.node_ids); - - const s3 = try search_list.create(try doc.querySelectorAll("#a2")); - try testing.expectEqual("3", s3.name); - try testing.expectEqualSlices(u32, &.{2}, s3.node_ids); - - try testing.expectEqual(2, registry.lookup_by_id.count()); - try testing.expectEqual(2, registry.lookup_by_node.count()); - } -} - -test "cdp Node: Writer" { - parser.init(); - defer parser.deinit(); - - var registry = Registry.init(testing.allocator); - defer registry.deinit(); - - var doc = try testing.Document.init("
"); - defer doc.deinit(); - - { - const node = try registry.register(doc.asNode()); - const json = try std.json.Stringify.valueAlloc(testing.allocator, Writer{ - .root = node, - .depth = 0, - .exclude_root = false, - .registry = ®istry, - }, .{}); - defer testing.allocator.free(json); - - try testing.expectJson(.{ - .nodeId = 1, - .backendNodeId = 1, - .nodeType = 9, - .nodeName = "#document", - .localName = "", - .nodeValue = "", - .documentURL = null, - .baseURL = null, - .xmlVersion = "", - .isScrollable = false, - .compatibilityMode = "NoQuirksMode", - .childNodeCount = 1, - .children = &.{.{ - .nodeId = 2, - .backendNodeId = 2, - .nodeType = 1, - .nodeName = "HTML", - .localName = "html", - .nodeValue = "", - .childNodeCount = 2, - .documentURL = null, - .baseURL = null, - .xmlVersion = "", - .compatibilityMode = "NoQuirksMode", - .isScrollable = false, - }}, - }, json); - } - - { - const node = registry.lookup_by_id.get(2).?; - const json = try std.json.Stringify.valueAlloc(testing.allocator, Writer{ - .root = node, - .depth = 1, - .exclude_root = false, - .registry = ®istry, - }, .{}); - defer testing.allocator.free(json); - - try testing.expectJson(.{ - .nodeId = 2, - .backendNodeId = 2, - .nodeType = 1, - .nodeName = "HTML", - .localName = "html", - .nodeValue = "", - .childNodeCount = 2, - .documentURL = null, - .baseURL = null, - .xmlVersion = "", - .compatibilityMode = "NoQuirksMode", - .isScrollable = false, - .children = &.{ .{ - .nodeId = 3, - .backendNodeId = 3, - .nodeType = 1, - .nodeName = "HEAD", - .localName = "head", - .nodeValue = "", - .childNodeCount = 0, - .documentURL = null, - .baseURL = null, - .xmlVersion = "", - .compatibilityMode = "NoQuirksMode", - .isScrollable = false, - .parentId = 2, - }, .{ - .nodeId = 4, - .backendNodeId = 4, - .nodeType = 1, - .nodeName = "BODY", - .localName = "body", - .nodeValue = "", - .childNodeCount = 2, - .documentURL = null, - .baseURL = null, - .xmlVersion = "", - .compatibilityMode = "NoQuirksMode", - .isScrollable = false, - .parentId = 2, - } }, - }, json); - } - - { - const node = registry.lookup_by_id.get(2).?; - const json = try std.json.Stringify.valueAlloc(testing.allocator, Writer{ - .root = node, - .depth = -1, - .exclude_root = true, - .registry = ®istry, - }, .{}); - defer testing.allocator.free(json); - - try testing.expectJson(&.{ .{ - .nodeId = 3, - .backendNodeId = 3, - .nodeType = 1, - .nodeName = "HEAD", - .localName = "head", - .nodeValue = "", - .childNodeCount = 0, - .documentURL = null, - .baseURL = null, - .xmlVersion = "", - .compatibilityMode = "NoQuirksMode", - .isScrollable = false, - .parentId = 2, - }, .{ - .nodeId = 4, - .backendNodeId = 4, - .nodeType = 1, - .nodeName = "BODY", - .localName = "body", - .nodeValue = "", - .childNodeCount = 2, - .documentURL = null, - .baseURL = null, - .xmlVersion = "", - .compatibilityMode = "NoQuirksMode", - .isScrollable = false, - .children = &.{ .{ - .nodeId = 5, - .localName = "a", - .childNodeCount = 0, - .parentId = 4, - }, .{ - .nodeId = 6, - .localName = "div", - .childNodeCount = 1, - .parentId = 4, - .children = &.{.{ - .nodeId = 7, - .localName = "a", - .childNodeCount = 0, - .parentId = 6, - }}, - } }, - } }, json); - } -} +// @ZIGDOM +// const std = @import("std"); +// const Allocator = std.mem.Allocator; + +// const log = @import("../log.zig"); +// const parser = @import("../browser/netsurf.zig"); + +// pub const Id = u32; + +// const Node = @This(); + +// id: Id, +// _node: *parser.Node, +// set_child_nodes_event: bool, + +// // Whenever we send a node to the client, we register it here for future lookup. +// // We maintain a node -> id and id -> node lookup. +// pub const Registry = struct { +// node_id: u32, +// allocator: Allocator, +// arena: std.heap.ArenaAllocator, +// node_pool: std.heap.MemoryPool(Node), +// lookup_by_id: std.AutoHashMapUnmanaged(Id, *Node), +// lookup_by_node: std.HashMapUnmanaged(*parser.Node, *Node, NodeContext, std.hash_map.default_max_load_percentage), + +// pub fn init(allocator: Allocator) Registry { +// return .{ +// .node_id = 1, +// .lookup_by_id = .{}, +// .lookup_by_node = .{}, +// .allocator = allocator, +// .arena = std.heap.ArenaAllocator.init(allocator), +// .node_pool = std.heap.MemoryPool(Node).init(allocator), +// }; +// } + +// pub fn deinit(self: *Registry) void { +// const allocator = self.allocator; +// self.lookup_by_id.deinit(allocator); +// self.lookup_by_node.deinit(allocator); +// self.node_pool.deinit(); +// self.arena.deinit(); +// } + +// pub fn reset(self: *Registry) void { +// self.lookup_by_id.clearRetainingCapacity(); +// self.lookup_by_node.clearRetainingCapacity(); +// _ = self.arena.reset(.{ .retain_with_limit = 1024 }); +// _ = self.node_pool.reset(.{ .retain_with_limit = 1024 }); +// } + +// pub fn register(self: *Registry, n: *parser.Node) !*Node { +// const node_lookup_gop = try self.lookup_by_node.getOrPut(self.allocator, n); +// if (node_lookup_gop.found_existing) { +// return node_lookup_gop.value_ptr.*; +// } + +// // on error, we're probably going to abort the entire browser context +// // but, just in case, let's try to keep things tidy. +// errdefer _ = self.lookup_by_node.remove(n); + +// const node = try self.node_pool.create(); +// errdefer self.node_pool.destroy(node); + +// const id = self.node_id; +// self.node_id = id + 1; + +// node.* = .{ +// ._node = n, +// .id = id, +// .set_child_nodes_event = false, +// }; + +// node_lookup_gop.value_ptr.* = node; +// try self.lookup_by_id.putNoClobber(self.allocator, id, node); +// return node; +// } +// }; + +// const NodeContext = struct { +// pub fn hash(_: NodeContext, n: *parser.Node) u64 { +// return std.hash.Wyhash.hash(0, std.mem.asBytes(&@intFromPtr(n))); +// } + +// pub fn eql(_: NodeContext, a: *parser.Node, b: *parser.Node) bool { +// return @intFromPtr(a) == @intFromPtr(b); +// } +// }; + +// // Searches are a 3 step process: +// // 1 - Dom.performSearch +// // 2 - Dom.getSearchResults +// // 3 - Dom.discardSearchResults +// // +// // For a given browser context, we can have multiple active searches. I.e. +// // performSearch could be called multiple times without getSearchResults or +// // discardSearchResults being called. We keep these active searches in the +// // browser context's node_search_list, which is a SearchList. Since we don't +// // expect many active searches (mostly just 1), a list is fine to scan through. +// pub const Search = struct { +// name: []const u8, +// node_ids: []const Id, + +// pub const List = struct { +// registry: *Registry, +// search_id: u16 = 0, +// arena: std.heap.ArenaAllocator, +// searches: std.ArrayListUnmanaged(Search) = .{}, + +// pub fn init(allocator: Allocator, registry: *Registry) List { +// return .{ +// .registry = registry, +// .arena = std.heap.ArenaAllocator.init(allocator), +// }; +// } + +// pub fn deinit(self: *List) void { +// self.arena.deinit(); +// } + +// pub fn reset(self: *List) void { +// self.search_id = 0; +// self.searches = .{}; +// _ = self.arena.reset(.{ .retain_with_limit = 4096 }); +// } + +// pub fn create(self: *List, nodes: []const *parser.Node) !Search { +// const id = self.search_id; +// defer self.search_id = id +% 1; + +// const arena = self.arena.allocator(); + +// const name = switch (id) { +// 0 => "0", +// 1 => "1", +// 2 => "2", +// 3 => "3", +// 4 => "4", +// 5 => "5", +// 6 => "6", +// 7 => "7", +// 8 => "8", +// 9 => "9", +// else => try std.fmt.allocPrint(arena, "{d}", .{id}), +// }; + +// var registry = self.registry; +// const node_ids = try arena.alloc(Id, nodes.len); +// for (nodes, node_ids) |node, *node_id| { +// node_id.* = (try registry.register(node)).id; +// } + +// const search = Search{ +// .name = name, +// .node_ids = node_ids, +// }; +// try self.searches.append(arena, search); +// return search; +// } + +// pub fn remove(self: *List, name: []const u8) void { +// for (self.searches.items, 0..) |search, i| { +// if (std.mem.eql(u8, name, search.name)) { +// _ = self.searches.swapRemove(i); +// return; +// } +// } +// } + +// pub fn get(self: *const List, name: []const u8) ?Search { +// for (self.searches.items) |search| { +// if (std.mem.eql(u8, name, search.name)) { +// return search; +// } +// } +// return null; +// } +// }; +// }; + +// // Need a custom writer, because we can't just serialize the node as-is. +// // Sometimes we want to serializ the node without chidren, sometimes with just +// // its direct children, and sometimes the entire tree. +// // (For now, we only support direct children) + +// pub const Writer = struct { +// depth: i32, +// exclude_root: bool, +// root: *const Node, +// registry: *Registry, + +// pub const Opts = struct { +// depth: i32 = 0, +// exclude_root: bool = false, +// }; + +// pub fn jsonStringify(self: *const Writer, w: anytype) error{WriteFailed}!void { +// if (self.exclude_root) { +// _ = self.writeChildren(self.root, 1, w) catch |err| { +// log.err(.cdp, "node writeChildren", .{ .err = err }); +// return error.WriteFailed; +// }; +// } else { +// self.toJSON(self.root, 0, w) catch |err| { +// // The only error our jsonStringify method can return is +// // @TypeOf(w).Error. In other words, our code can't return its own +// // error, we can only return a writer error. Kinda sucks. +// log.err(.cdp, "node toJSON stringify", .{ .err = err }); +// return error.WriteFailed; +// }; +// } +// } + +// fn toJSON(self: *const Writer, node: *const Node, depth: usize, w: anytype) !void { +// try w.beginObject(); +// try self.writeCommon(node, false, w); + +// try w.objectField("children"); +// const child_count = try self.writeChildren(node, depth, w); +// try w.objectField("childNodeCount"); +// try w.write(child_count); + +// try w.endObject(); +// } + +// fn writeChildren(self: *const Writer, node: *const Node, depth: usize, w: anytype) anyerror!usize { +// var registry = self.registry; +// const child_nodes = try parser.nodeGetChildNodes(node._node); +// const child_count = parser.nodeListLength(child_nodes); +// const full_child = self.depth < 0 or self.depth < depth; + +// var i: usize = 0; +// try w.beginArray(); +// for (0..child_count) |_| { +// const child = (parser.nodeListItem(child_nodes, @intCast(i))) orelse break; +// const child_node = try registry.register(child); +// if (full_child) { +// try self.toJSON(child_node, depth + 1, w); +// } else { +// try w.beginObject(); +// try self.writeCommon(child_node, true, w); +// try w.endObject(); +// } + +// i += 1; +// } +// try w.endArray(); + +// return i; +// } + +// fn writeCommon(self: *const Writer, node: *const Node, include_child_count: bool, w: anytype) !void { +// try w.objectField("nodeId"); +// try w.write(node.id); + +// try w.objectField("backendNodeId"); +// try w.write(node.id); + +// const n = node._node; + +// if (parser.nodeParentNode(n)) |p| { +// const parent_node = try self.registry.register(p); +// try w.objectField("parentId"); +// try w.write(parent_node.id); +// } + +// const _map = try parser.nodeGetAttributes(n); +// if (_map) |map| { +// const attr_count = try parser.namedNodeMapGetLength(map); +// try w.objectField("attributes"); +// try w.beginArray(); +// for (0..attr_count) |i| { +// const attr = try parser.namedNodeMapItem(map, @intCast(i)) orelse continue; +// try w.write(try parser.attributeGetName(attr)); +// try w.write(try parser.attributeGetValue(attr) orelse continue); +// } +// try w.endArray(); +// } + +// try w.objectField("nodeType"); +// try w.write(@intFromEnum(parser.nodeType(n))); + +// try w.objectField("nodeName"); +// try w.write(try parser.nodeName(n)); + +// try w.objectField("localName"); +// try w.write(try parser.nodeLocalName(n)); + +// try w.objectField("nodeValue"); +// try w.write((parser.nodeValue(n)) orelse ""); + +// if (include_child_count) { +// try w.objectField("childNodeCount"); +// const child_nodes = try parser.nodeGetChildNodes(n); +// try w.write(parser.nodeListLength(child_nodes)); +// } + +// try w.objectField("documentURL"); +// try w.write(null); + +// try w.objectField("baseURL"); +// try w.write(null); + +// try w.objectField("xmlVersion"); +// try w.write(""); + +// try w.objectField("compatibilityMode"); +// try w.write("NoQuirksMode"); + +// try w.objectField("isScrollable"); +// try w.write(false); +// } +// }; + +// const testing = @import("testing.zig"); +// test "cdp Node: Registry register" { +// parser.init(); +// defer parser.deinit(); + +// var registry = Registry.init(testing.allocator); +// defer registry.deinit(); + +// try testing.expectEqual(0, registry.lookup_by_id.count()); +// try testing.expectEqual(0, registry.lookup_by_node.count()); + +// var doc = try testing.Document.init("link1

other

"); +// defer doc.deinit(); + +// { +// const n = (try doc.querySelector("#a1")).?; +// const node = try registry.register(n); +// const n1b = registry.lookup_by_id.get(1).?; +// const n1c = registry.lookup_by_node.get(node._node).?; +// try testing.expectEqual(node, n1b); +// try testing.expectEqual(node, n1c); + +// try testing.expectEqual(1, node.id); +// try testing.expectEqual(n, node._node); +// } + +// { +// const n = (try doc.querySelector("p")).?; +// const node = try registry.register(n); +// const n1b = registry.lookup_by_id.get(2).?; +// const n1c = registry.lookup_by_node.get(node._node).?; +// try testing.expectEqual(node, n1b); +// try testing.expectEqual(node, n1c); + +// try testing.expectEqual(2, node.id); +// try testing.expectEqual(n, node._node); +// } +// } + +// test "cdp Node: search list" { +// parser.init(); +// defer parser.deinit(); + +// var registry = Registry.init(testing.allocator); +// defer registry.deinit(); + +// var search_list = Search.List.init(testing.allocator, ®istry); +// defer search_list.deinit(); + +// { +// // empty search list, noops +// search_list.remove("0"); +// try testing.expectEqual(null, search_list.get("0")); +// } + +// { +// // empty nodes +// const s1 = try search_list.create(&.{}); +// try testing.expectEqual("0", s1.name); +// try testing.expectEqual(0, s1.node_ids.len); + +// const s2 = search_list.get("0").?; +// try testing.expectEqual("0", s2.name); +// try testing.expectEqual(0, s2.node_ids.len); + +// search_list.remove("0"); +// try testing.expectEqual(null, search_list.get("0")); +// } + +// { +// var doc = try testing.Document.init(""); +// defer doc.deinit(); + +// const s1 = try search_list.create(try doc.querySelectorAll("a")); +// try testing.expectEqual("1", s1.name); +// try testing.expectEqualSlices(u32, &.{ 1, 2 }, s1.node_ids); + +// try testing.expectEqual(2, registry.lookup_by_id.count()); +// try testing.expectEqual(2, registry.lookup_by_node.count()); + +// const s2 = try search_list.create(try doc.querySelectorAll("#a1")); +// try testing.expectEqual("2", s2.name); +// try testing.expectEqualSlices(u32, &.{1}, s2.node_ids); + +// const s3 = try search_list.create(try doc.querySelectorAll("#a2")); +// try testing.expectEqual("3", s3.name); +// try testing.expectEqualSlices(u32, &.{2}, s3.node_ids); + +// try testing.expectEqual(2, registry.lookup_by_id.count()); +// try testing.expectEqual(2, registry.lookup_by_node.count()); +// } +// } + +// test "cdp Node: Writer" { +// parser.init(); +// defer parser.deinit(); + +// var registry = Registry.init(testing.allocator); +// defer registry.deinit(); + +// var doc = try testing.Document.init("
"); +// defer doc.deinit(); + +// { +// const node = try registry.register(doc.asNode()); +// const json = try std.json.Stringify.valueAlloc(testing.allocator, Writer{ +// .root = node, +// .depth = 0, +// .exclude_root = false, +// .registry = ®istry, +// }, .{}); +// defer testing.allocator.free(json); + +// try testing.expectJson(.{ +// .nodeId = 1, +// .backendNodeId = 1, +// .nodeType = 9, +// .nodeName = "#document", +// .localName = "", +// .nodeValue = "", +// .documentURL = null, +// .baseURL = null, +// .xmlVersion = "", +// .isScrollable = false, +// .compatibilityMode = "NoQuirksMode", +// .childNodeCount = 1, +// .children = &.{.{ +// .nodeId = 2, +// .backendNodeId = 2, +// .nodeType = 1, +// .nodeName = "HTML", +// .localName = "html", +// .nodeValue = "", +// .childNodeCount = 2, +// .documentURL = null, +// .baseURL = null, +// .xmlVersion = "", +// .compatibilityMode = "NoQuirksMode", +// .isScrollable = false, +// }}, +// }, json); +// } + +// { +// const node = registry.lookup_by_id.get(2).?; +// const json = try std.json.Stringify.valueAlloc(testing.allocator, Writer{ +// .root = node, +// .depth = 1, +// .exclude_root = false, +// .registry = ®istry, +// }, .{}); +// defer testing.allocator.free(json); + +// try testing.expectJson(.{ +// .nodeId = 2, +// .backendNodeId = 2, +// .nodeType = 1, +// .nodeName = "HTML", +// .localName = "html", +// .nodeValue = "", +// .childNodeCount = 2, +// .documentURL = null, +// .baseURL = null, +// .xmlVersion = "", +// .compatibilityMode = "NoQuirksMode", +// .isScrollable = false, +// .children = &.{ .{ +// .nodeId = 3, +// .backendNodeId = 3, +// .nodeType = 1, +// .nodeName = "HEAD", +// .localName = "head", +// .nodeValue = "", +// .childNodeCount = 0, +// .documentURL = null, +// .baseURL = null, +// .xmlVersion = "", +// .compatibilityMode = "NoQuirksMode", +// .isScrollable = false, +// .parentId = 2, +// }, .{ +// .nodeId = 4, +// .backendNodeId = 4, +// .nodeType = 1, +// .nodeName = "BODY", +// .localName = "body", +// .nodeValue = "", +// .childNodeCount = 2, +// .documentURL = null, +// .baseURL = null, +// .xmlVersion = "", +// .compatibilityMode = "NoQuirksMode", +// .isScrollable = false, +// .parentId = 2, +// } }, +// }, json); +// } + +// { +// const node = registry.lookup_by_id.get(2).?; +// const json = try std.json.Stringify.valueAlloc(testing.allocator, Writer{ +// .root = node, +// .depth = -1, +// .exclude_root = true, +// .registry = ®istry, +// }, .{}); +// defer testing.allocator.free(json); + +// try testing.expectJson(&.{ .{ +// .nodeId = 3, +// .backendNodeId = 3, +// .nodeType = 1, +// .nodeName = "HEAD", +// .localName = "head", +// .nodeValue = "", +// .childNodeCount = 0, +// .documentURL = null, +// .baseURL = null, +// .xmlVersion = "", +// .compatibilityMode = "NoQuirksMode", +// .isScrollable = false, +// .parentId = 2, +// }, .{ +// .nodeId = 4, +// .backendNodeId = 4, +// .nodeType = 1, +// .nodeName = "BODY", +// .localName = "body", +// .nodeValue = "", +// .childNodeCount = 2, +// .documentURL = null, +// .baseURL = null, +// .xmlVersion = "", +// .compatibilityMode = "NoQuirksMode", +// .isScrollable = false, +// .children = &.{ .{ +// .nodeId = 5, +// .localName = "a", +// .childNodeCount = 0, +// .parentId = 4, +// }, .{ +// .nodeId = 6, +// .localName = "div", +// .childNodeCount = 1, +// .parentId = 4, +// .children = &.{.{ +// .nodeId = 7, +// .localName = "a", +// .childNodeCount = 0, +// .parentId = 6, +// }}, +// } }, +// } }, json); +// } +// } diff --git a/src/cdp/cdp.zig b/src/cdp/cdp.zig index 7b6590e8c..73c5e514b 100644 --- a/src/cdp/cdp.zig +++ b/src/cdp/cdp.zig @@ -24,12 +24,12 @@ const log = @import("../log.zig"); const js = @import("../browser/js/js.zig"); const polyfill = @import("../browser/polyfill/polyfill.zig"); -const App = @import("../app.zig").App; -const Browser = @import("../browser/browser.zig").Browser; -const Session = @import("../browser/session.zig").Session; -const Page = @import("../browser/page.zig").Page; +const App = @import("../App.zig"); +const Browser = @import("../browser/Browser.zig"); +const Session = @import("../browser/Session.zig"); +const Page = @import("../browser/Page.zig"); const Incrementing = @import("../id.zig").Incrementing; -const Notification = @import("../notification.zig").Notification; +const Notification = @import("../Notification.zig"); const LogInterceptor = @import("domains/log.zig").LogInterceptor; const InterceptState = @import("domains/fetch.zig").InterceptState; @@ -37,7 +37,7 @@ pub const URL_BASE = "chrome://newtab/"; pub const LOADER_ID = "LOADERID24DD2FD56CF1EF33C965C79C"; pub const CDP = CDPT(struct { - const Client = *@import("../server.zig").Client; + const Client = *@import("../Server.zig").Client; }); const SessionIdGen = Incrementing(u32, "SID"); @@ -117,7 +117,7 @@ pub fn CDPT(comptime TypeProvider: type) type { // timeouts (or http events) which are ready to be processed. pub fn hasPage() bool {} - pub fn pageWait(self: *Self, ms: i32) Session.WaitResult { + pub fn pageWait(self: *Self, ms: u32) Session.WaitResult { const session = &(self.browser.session orelse return .no_page); return session.wait(ms); } @@ -203,7 +203,8 @@ pub fn CDPT(comptime TypeProvider: type) type { }, 5 => switch (@as(u40, @bitCast(domain[0..5].*))) { asUint(u40, "Fetch") => return @import("domains/fetch.zig").processMessage(command), - asUint(u40, "Input") => return @import("domains/input.zig").processMessage(command), + // @ZIGDOM + // asUint(u40, "Input") => return @import("domains/input.zig").processMessage(command), else => {}, }, 6 => switch (@as(u48, @bitCast(domain[0..6].*))) { @@ -286,7 +287,8 @@ pub fn CDPT(comptime TypeProvider: type) type { } pub fn BrowserContext(comptime CDP_T: type) type { - const Node = @import("Node.zig"); + // @ZIGMOD + // const Node = @import("Node.zig"); return struct { id: []const u8, @@ -326,8 +328,9 @@ pub fn BrowserContext(comptime CDP_T: type) type { security_origin: []const u8, page_life_cycle_events: bool, secure_context_type: []const u8, - node_registry: Node.Registry, - node_search_list: Node.Search.List, + // @ZIGDOM + // node_registry: Node.Registry, + // node_search_list: Node.Search.List, inspector: js.Inspector, isolated_worlds: std.ArrayListUnmanaged(IsolatedWorld), @@ -360,8 +363,9 @@ pub fn BrowserContext(comptime CDP_T: type) type { const inspector = try cdp.browser.env.newInspector(arena, self); - var registry = Node.Registry.init(allocator); - errdefer registry.deinit(); + // @ZIGDOM + // var registry = Node.Registry.init(allocator); + // errdefer registry.deinit(); self.* = .{ .id = id, @@ -374,8 +378,9 @@ pub fn BrowserContext(comptime CDP_T: type) type { .secure_context_type = "Secure", // TODO = enum .loader_id = LOADER_ID, .page_life_cycle_events = false, // TODO; Target based value - .node_registry = registry, - .node_search_list = undefined, + // @ZIGDOM + // .node_registry = registry, + // .node_search_list = undefined, .isolated_worlds = .empty, .inspector = inspector, .notification_arena = cdp.notification_arena.allocator(), @@ -383,7 +388,8 @@ pub fn BrowserContext(comptime CDP_T: type) type { .captured_responses = .empty, .log_interceptor = LogInterceptor(Self).init(allocator, self), }; - self.node_search_list = Node.Search.List.init(allocator, &self.node_registry); + // ZIGDOM + // self.node_search_list = Node.Search.List.init(allocator, &self.node_registry); errdefer self.deinit(); try cdp.browser.notification.register(.page_remove, self, onPageRemove); @@ -418,8 +424,9 @@ pub fn BrowserContext(comptime CDP_T: type) type { world.deinit(); } self.isolated_worlds.clearRetainingCapacity(); - self.node_registry.deinit(); - self.node_search_list.deinit(); + // @ZIGDOM + // self.node_registry.deinit(); + // self.node_search_list.deinit(); self.cdp.browser.notification.unregisterAll(self); if (self.http_proxy_changed) { @@ -433,8 +440,10 @@ pub fn BrowserContext(comptime CDP_T: type) type { } pub fn reset(self: *Self) void { - self.node_registry.reset(); - self.node_search_list.reset(); + // @ZIGDOM + _ = self; + // self.node_registry.reset(); + // self.node_search_list.reset(); } pub fn createIsolatedWorld(self: *Self, world_name: []const u8, grant_universal_access: bool) !*IsolatedWorld { @@ -453,19 +462,20 @@ pub fn BrowserContext(comptime CDP_T: type) type { return world; } - pub fn nodeWriter(self: *Self, root: *const Node, opts: Node.Writer.Opts) Node.Writer { - return .{ - .root = root, - .depth = opts.depth, - .exclude_root = opts.exclude_root, - .registry = &self.node_registry, - }; - } + // @ZIGDOM + // pub fn nodeWriter(self: *Self, root: *const Node, opts: Node.Writer.Opts) Node.Writer { + // return .{ + // .root = root, + // .depth = opts.depth, + // .exclude_root = opts.exclude_root, + // .registry = &self.node_registry, + // }; + // } - pub fn getURL(self: *const Self) ?[]const u8 { + pub fn getURL(self: *const Self) ?[:0]const u8 { const page = self.session.currentPage() orelse return null; - const raw_url = page.url.raw; - return if (raw_url.len == 0) null else raw_url; + const url = page.url; + return if (url.len == 0) null else url; } pub fn networkEnable(self: *Self) !void { diff --git a/src/cdp/domains/dom.zig b/src/cdp/domains/dom.zig index 0f0ff8f8b..e99fd6b65 100644 --- a/src/cdp/domains/dom.zig +++ b/src/cdp/domains/dom.zig @@ -19,655 +19,656 @@ const std = @import("std"); const log = @import("../../log.zig"); const Allocator = std.mem.Allocator; -const Node = @import("../Node.zig"); -const css = @import("../../browser/dom/css.zig"); -const parser = @import("../../browser/netsurf.zig"); -const dom_node = @import("../../browser/dom/node.zig"); -const Element = @import("../../browser/dom/element.zig").Element; +// const css = @import("../../browser/dom/css.zig"); +// const parser = @import("../../browser/netsurf.zig"); +// const dom_node = @import("../../browser/dom/node.zig"); pub fn processMessage(cmd: anytype) !void { const action = std.meta.stringToEnum(enum { enable, - getDocument, - performSearch, - getSearchResults, - discardSearchResults, - querySelector, - querySelectorAll, - resolveNode, - describeNode, - scrollIntoViewIfNeeded, - getContentQuads, - getBoxModel, - requestChildNodes, - getFrameOwner, + // ZIGDOM + // getDocument, + // performSearch, + // getSearchResults, + // discardSearchResults, + // querySelector, + // querySelectorAll, + // resolveNode, + // describeNode, + // scrollIntoViewIfNeeded, + // getContentQuads, + // getBoxModel, + // requestChildNodes, + // getFrameOwner, }, cmd.input.action) orelse return error.UnknownMethod; switch (action) { .enable => return cmd.sendResult(null, .{}), - .getDocument => return getDocument(cmd), - .performSearch => return performSearch(cmd), - .getSearchResults => return getSearchResults(cmd), - .discardSearchResults => return discardSearchResults(cmd), - .querySelector => return querySelector(cmd), - .querySelectorAll => return querySelectorAll(cmd), - .resolveNode => return resolveNode(cmd), - .describeNode => return describeNode(cmd), - .scrollIntoViewIfNeeded => return scrollIntoViewIfNeeded(cmd), - .getContentQuads => return getContentQuads(cmd), - .getBoxModel => return getBoxModel(cmd), - .requestChildNodes => return requestChildNodes(cmd), - .getFrameOwner => return getFrameOwner(cmd), + // @ZIGDOM + // .getDocument => return getDocument(cmd), + // .performSearch => return performSearch(cmd), + // .getSearchResults => return getSearchResults(cmd), + // .discardSearchResults => return discardSearchResults(cmd), + // .querySelector => return querySelector(cmd), + // .querySelectorAll => return querySelectorAll(cmd), + // .resolveNode => return resolveNode(cmd), + // .describeNode => return describeNode(cmd), + // .scrollIntoViewIfNeeded => return scrollIntoViewIfNeeded(cmd), + // .getContentQuads => return getContentQuads(cmd), + // .getBoxModel => return getBoxModel(cmd), + // .requestChildNodes => return requestChildNodes(cmd), + // .getFrameOwner => return getFrameOwner(cmd), } } -// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-getDocument -fn getDocument(cmd: anytype) !void { - const Params = struct { - // CDP documentation implies that 0 isn't valid, but it _does_ work in Chrome - depth: i32 = 3, - pierce: bool = false, - }; - const params = try cmd.params(Params) orelse Params{}; - - if (params.pierce) { - log.warn(.cdp, "not implemented", .{ .feature = "DOM.getDocument: Not implemented pierce parameter" }); - } - - const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; - const page = bc.session.currentPage() orelse return error.PageNotLoaded; - const doc = parser.documentHTMLToDocument(page.window.document); - - const node = try bc.node_registry.register(parser.documentToNode(doc)); - return cmd.sendResult(.{ .root = bc.nodeWriter(node, .{ .depth = params.depth }) }, .{}); -} - -// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-performSearch -fn performSearch(cmd: anytype) !void { - const params = (try cmd.params(struct { - query: []const u8, - includeUserAgentShadowDOM: ?bool = null, - })) orelse return error.InvalidParams; - - const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; - const page = bc.session.currentPage() orelse return error.PageNotLoaded; - const doc = parser.documentHTMLToDocument(page.window.document); - - const allocator = cmd.cdp.allocator; - var list = try css.querySelectorAll(allocator, parser.documentToNode(doc), params.query); - defer list.deinit(allocator); - - const search = try bc.node_search_list.create(list.nodes.items); - - // dispatch setChildNodesEvents to inform the client of the subpart of node - // tree covering the results. - try dispatchSetChildNodes(cmd, list.nodes.items); - - return cmd.sendResult(.{ - .searchId = search.name, - .resultCount = @as(u32, @intCast(search.node_ids.len)), - }, .{}); -} - -// dispatchSetChildNodes send the setChildNodes event for the whole DOM tree -// hierarchy of each nodes. -// We dispatch event in the reverse order: from the top level to the direct parents. -// We should dispatch a node only if it has never been sent. -fn dispatchSetChildNodes(cmd: anytype, nodes: []*parser.Node) !void { - const arena = cmd.arena; - const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; - const session_id = bc.session_id orelse return error.SessionIdNotLoaded; - - var parents: std.ArrayListUnmanaged(*Node) = .{}; - for (nodes) |_n| { - var n = _n; - while (true) { - const p = parser.nodeParentNode(n) orelse break; - - // Register the node. - const node = try bc.node_registry.register(p); - if (node.set_child_nodes_event) break; - try parents.append(arena, node); - n = p; - } - } - - const plen = parents.items.len; - if (plen == 0) return; - - var i: usize = plen; - // We're going to iterate in reverse order from how we added them. - // This ensures that we're emitting the tree of nodes top-down. - while (i > 0) { - i -= 1; - const node = parents.items[i]; - // Although our above loop won't add an already-sent node to `parents` - // this can still be true because two nodes can share the same parent node - // so we might have just sent the node a previous iteration of this loop - if (node.set_child_nodes_event) continue; - - node.set_child_nodes_event = true; - - // If the node has no parent, it's the root node. - // We don't dispatch event for it because we assume the root node is - // dispatched via the DOM.getDocument command. - const p = parser.nodeParentNode(node._node) orelse { - continue; - }; - - // Retrieve the parent from the registry. - const parent_node = try bc.node_registry.register(p); - - try cmd.sendEvent("DOM.setChildNodes", .{ - .parentId = parent_node.id, - .nodes = .{bc.nodeWriter(node, .{})}, - }, .{ - .session_id = session_id, - }); - } -} - -// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-discardSearchResults -fn discardSearchResults(cmd: anytype) !void { - const params = (try cmd.params(struct { - searchId: []const u8, - })) orelse return error.InvalidParams; - - const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; - - bc.node_search_list.remove(params.searchId); - return cmd.sendResult(null, .{}); -} - -// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-getSearchResults -fn getSearchResults(cmd: anytype) !void { - const params = (try cmd.params(struct { - searchId: []const u8, - fromIndex: u32, - toIndex: u32, - })) orelse return error.InvalidParams; - - if (params.fromIndex >= params.toIndex) { - return error.BadIndices; - } - - const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; - - const search = bc.node_search_list.get(params.searchId) orelse { - return error.SearchResultNotFound; - }; - - const node_ids = search.node_ids; - - if (params.fromIndex >= node_ids.len) return error.BadFromIndex; - if (params.toIndex > node_ids.len) return error.BadToIndex; - - return cmd.sendResult(.{ .nodeIds = node_ids[params.fromIndex..params.toIndex] }, .{}); -} - -fn querySelector(cmd: anytype) !void { - const params = (try cmd.params(struct { - nodeId: Node.Id, - selector: []const u8, - })) orelse return error.InvalidParams; - - const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; - - const node = bc.node_registry.lookup_by_id.get(params.nodeId) orelse { - return cmd.sendError(-32000, "Could not find node with given id", .{}); - }; - - const selected_node = try css.querySelector( - cmd.arena, - node._node, - params.selector, - ) orelse return error.NodeNotFoundForGivenId; - - const registered_node = try bc.node_registry.register(selected_node); - - // Dispatch setChildNodesEvents to inform the client of the subpart of node tree covering the results. - var array = [1]*parser.Node{selected_node}; - try dispatchSetChildNodes(cmd, array[0..]); - - return cmd.sendResult(.{ - .nodeId = registered_node.id, - }, .{}); -} - -fn querySelectorAll(cmd: anytype) !void { - const params = (try cmd.params(struct { - nodeId: Node.Id, - selector: []const u8, - })) orelse return error.InvalidParams; - - const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; - - const node = bc.node_registry.lookup_by_id.get(params.nodeId) orelse { - return cmd.sendError(-32000, "Could not find node with given id", .{}); - }; - - const arena = cmd.arena; - const selected_nodes = try css.querySelectorAll(arena, node._node, params.selector); - const nodes = selected_nodes.nodes.items; - - const node_ids = try arena.alloc(Node.Id, nodes.len); - for (nodes, node_ids) |selected_node, *node_id| { - node_id.* = (try bc.node_registry.register(selected_node)).id; - } - - // Dispatch setChildNodesEvents to inform the client of the subpart of node tree covering the results. - try dispatchSetChildNodes(cmd, nodes); - - return cmd.sendResult(.{ - .nodeIds = node_ids, - }, .{}); -} - -fn resolveNode(cmd: anytype) !void { - const params = (try cmd.params(struct { - nodeId: ?Node.Id = null, - backendNodeId: ?u32 = null, - objectGroup: ?[]const u8 = null, - executionContextId: ?u32 = null, - })) orelse return error.InvalidParams; - - const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; - const page = bc.session.currentPage() orelse return error.PageNotLoaded; - - var js_context = page.js; - if (params.executionContextId) |context_id| { - if (js_context.v8_context.debugContextId() != context_id) { - for (bc.isolated_worlds.items) |*isolated_world| { - js_context = &(isolated_world.executor.context orelse return error.ContextNotFound); - if (js_context.v8_context.debugContextId() == context_id) { - break; - } - } else return error.ContextNotFound; - } - } - - const input_node_id = params.nodeId orelse params.backendNodeId orelse return error.InvalidParam; - const node = bc.node_registry.lookup_by_id.get(input_node_id) orelse return error.UnknownNode; - - // node._node is a *parser.Node we need this to be able to find its most derived type e.g. Node -> Element -> HTMLElement - // So we use the Node.Union when retrieve the value from the environment - const remote_object = try bc.inspector.getRemoteObject( - js_context, - params.objectGroup orelse "", - try dom_node.Node.toInterface(node._node), - ); - defer remote_object.deinit(); - - const arena = cmd.arena; - return cmd.sendResult(.{ .object = .{ - .type = try remote_object.getType(arena), - .subtype = try remote_object.getSubtype(arena), - .className = try remote_object.getClassName(arena), - .description = try remote_object.getDescription(arena), - .objectId = try remote_object.getObjectId(arena), - } }, .{}); -} - -fn describeNode(cmd: anytype) !void { - const params = (try cmd.params(struct { - nodeId: ?Node.Id = null, - backendNodeId: ?Node.Id = null, - objectId: ?[]const u8 = null, - depth: i32 = 1, - pierce: bool = false, - })) orelse return error.InvalidParams; - - if (params.pierce) { - log.warn(.cdp, "not implemented", .{ .feature = "DOM.describeNode: Not implemented pierce parameter" }); - } - const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; - - const node = try getNode(cmd.arena, bc, params.nodeId, params.backendNodeId, params.objectId); - - return cmd.sendResult(.{ .node = bc.nodeWriter(node, .{ .depth = params.depth }) }, .{}); -} - -// An array of quad vertices, x immediately followed by y for each point, points clock-wise. -// Note Y points downward -// We are assuming the start/endpoint is not repeated. -const Quad = [8]f64; - -const BoxModel = struct { - content: Quad, - padding: Quad, - border: Quad, - margin: Quad, - width: i32, - height: i32, - // shapeOutside: ?ShapeOutsideInfo, -}; - -fn rectToQuad(rect: Element.DOMRect) Quad { - return Quad{ - rect.x, - rect.y, - rect.x + rect.width, - rect.y, - rect.x + rect.width, - rect.y + rect.height, - rect.x, - rect.y + rect.height, - }; -} - -fn scrollIntoViewIfNeeded(cmd: anytype) !void { - const params = (try cmd.params(struct { - nodeId: ?Node.Id = null, - backendNodeId: ?u32 = null, - objectId: ?[]const u8 = null, - rect: ?Element.DOMRect = null, - })) orelse return error.InvalidParams; - // Only 1 of nodeId, backendNodeId, objectId may be set, but chrome just takes the first non-null - - // We retrieve the node to at least check if it exists and is valid. - const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; - const node = try getNode(cmd.arena, bc, params.nodeId, params.backendNodeId, params.objectId); - - const node_type = parser.nodeType(node._node); - switch (node_type) { - .element => {}, - .document => {}, - .text => {}, - else => return error.NodeDoesNotHaveGeometry, - } - - return cmd.sendResult(null, .{}); -} - -fn getNode(arena: Allocator, browser_context: anytype, node_id: ?Node.Id, backend_node_id: ?Node.Id, object_id: ?[]const u8) !*Node { - const input_node_id = node_id orelse backend_node_id; - if (input_node_id) |input_node_id_| { - return browser_context.node_registry.lookup_by_id.get(input_node_id_) orelse return error.NodeNotFound; - } - if (object_id) |object_id_| { - // Retrieve the object from which ever context it is in. - const parser_node = try browser_context.inspector.getNodePtr(arena, object_id_); - return try browser_context.node_registry.register(@ptrCast(@alignCast(parser_node))); - } - return error.MissingParams; -} - -// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-getContentQuads -// Related to: https://drafts.csswg.org/cssom-view/#the-geometryutils-interface -fn getContentQuads(cmd: anytype) !void { - const params = (try cmd.params(struct { - nodeId: ?Node.Id = null, - backendNodeId: ?Node.Id = null, - objectId: ?[]const u8 = null, - })) orelse return error.InvalidParams; - - const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; - const page = bc.session.currentPage() orelse return error.PageNotLoaded; - - const node = try getNode(cmd.arena, bc, params.nodeId, params.backendNodeId, params.objectId); - - // TODO likely if the following CSS properties are set the quads should be empty - // visibility: hidden - // display: none - - if (parser.nodeType(node._node) != .element) return error.NodeIsNotAnElement; - // TODO implement for document or text - // Most likely document would require some hierachgy in the renderer. It is left unimplemented till we have a good example. - // Text may be tricky, multiple quads in case of multiple lines? empty quads of text = ""? - // Elements like SVGElement may have multiple quads. - - const element = parser.nodeToElement(node._node); - const rect = try Element._getBoundingClientRect(element, page); - const quad = rectToQuad(rect); - - return cmd.sendResult(.{ .quads = &.{quad} }, .{}); -} - -fn getBoxModel(cmd: anytype) !void { - const params = (try cmd.params(struct { - nodeId: ?Node.Id = null, - backendNodeId: ?u32 = null, - objectId: ?[]const u8 = null, - })) orelse return error.InvalidParams; - - const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; - const page = bc.session.currentPage() orelse return error.PageNotLoaded; - - const node = try getNode(cmd.arena, bc, params.nodeId, params.backendNodeId, params.objectId); - - // TODO implement for document or text - if (parser.nodeType(node._node) != .element) return error.NodeIsNotAnElement; - const element = parser.nodeToElement(node._node); - - const rect = try Element._getBoundingClientRect(element, page); - const quad = rectToQuad(rect); - - return cmd.sendResult(.{ .model = BoxModel{ - .content = quad, - .padding = quad, - .border = quad, - .margin = quad, - .width = @intFromFloat(rect.width), - .height = @intFromFloat(rect.height), - } }, .{}); -} - -fn requestChildNodes(cmd: anytype) !void { - const params = (try cmd.params(struct { - nodeId: Node.Id, - depth: i32 = 1, - pierce: bool = false, - })) orelse return error.InvalidParams; - - if (params.depth == 0) return error.InvalidParams; - const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; - const session_id = bc.session_id orelse return error.SessionIdNotLoaded; - const node = bc.node_registry.lookup_by_id.get(params.nodeId) orelse { - return error.InvalidNode; - }; - - try cmd.sendEvent("DOM.setChildNodes", .{ - .parentId = node.id, - .nodes = bc.nodeWriter(node, .{ .depth = params.depth, .exclude_root = true }), - }, .{ - .session_id = session_id, - }); - - return cmd.sendResult(null, .{}); -} - -fn getFrameOwner(cmd: anytype) !void { - const params = (try cmd.params(struct { - frameId: []const u8, - })) orelse return error.InvalidParams; - - const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; - const target_id = bc.target_id orelse return error.TargetNotLoaded; - if (std.mem.eql(u8, target_id, params.frameId) == false) { - return cmd.sendError(-32000, "Frame with the given id does not belong to the target.", .{}); - } - - const page = bc.session.currentPage() orelse return error.PageNotLoaded; - const doc = parser.documentHTMLToDocument(page.window.document); - - const node = try bc.node_registry.register(parser.documentToNode(doc)); - return cmd.sendResult(.{ .nodeId = node.id, .backendNodeId = node.id }, .{}); -} - -const testing = @import("../testing.zig"); - -test "cdp.dom: getSearchResults unknown search id" { - var ctx = testing.context(); - defer ctx.deinit(); - - try testing.expectError(error.BrowserContextNotLoaded, ctx.processMessage(.{ - .id = 8, - .method = "DOM.getSearchResults", - .params = .{ .searchId = "Nope", .fromIndex = 0, .toIndex = 10 }, - })); -} - -test "cdp.dom: search flow" { - var ctx = testing.context(); - defer ctx.deinit(); - - _ = try ctx.loadBrowserContext(.{ .id = "BID-A", .html = "

1

2

" }); - - try ctx.processMessage(.{ - .id = 12, - .method = "DOM.performSearch", - .params = .{ .query = "p" }, - }); - try ctx.expectSentResult(.{ .searchId = "0", .resultCount = 2 }, .{ .id = 12 }); - - { - // getSearchResults - try ctx.processMessage(.{ - .id = 13, - .method = "DOM.getSearchResults", - .params = .{ .searchId = "0", .fromIndex = 0, .toIndex = 2 }, - }); - try ctx.expectSentResult(.{ .nodeIds = &.{ 1, 2 } }, .{ .id = 13 }); - - // different fromIndex - try ctx.processMessage(.{ - .id = 14, - .method = "DOM.getSearchResults", - .params = .{ .searchId = "0", .fromIndex = 1, .toIndex = 2 }, - }); - try ctx.expectSentResult(.{ .nodeIds = &.{2} }, .{ .id = 14 }); - - // different toIndex - try ctx.processMessage(.{ - .id = 15, - .method = "DOM.getSearchResults", - .params = .{ .searchId = "0", .fromIndex = 0, .toIndex = 1 }, - }); - try ctx.expectSentResult(.{ .nodeIds = &.{1} }, .{ .id = 15 }); - } - - try ctx.processMessage(.{ - .id = 16, - .method = "DOM.discardSearchResults", - .params = .{ .searchId = "0" }, - }); - try ctx.expectSentResult(null, .{ .id = 16 }); - - // make sure the delete actually did something - try testing.expectError(error.SearchResultNotFound, ctx.processMessage(.{ - .id = 17, - .method = "DOM.getSearchResults", - .params = .{ .searchId = "0", .fromIndex = 0, .toIndex = 1 }, - })); -} - -test "cdp.dom: querySelector unknown search id" { - var ctx = testing.context(); - defer ctx.deinit(); - - _ = try ctx.loadBrowserContext(.{ .id = "BID-A", .html = "

1

2

" }); - - try ctx.processMessage(.{ - .id = 9, - .method = "DOM.querySelector", - .params = .{ .nodeId = 99, .selector = "" }, - }); - try ctx.expectSentError(-32000, "Could not find node with given id", .{}); - - try ctx.processMessage(.{ - .id = 9, - .method = "DOM.querySelectorAll", - .params = .{ .nodeId = 99, .selector = "" }, - }); - try ctx.expectSentError(-32000, "Could not find node with given id", .{}); -} - -test "cdp.dom: querySelector Node not found" { - var ctx = testing.context(); - defer ctx.deinit(); - - _ = try ctx.loadBrowserContext(.{ .id = "BID-A", .html = "

1

2

" }); - - try ctx.processMessage(.{ // Hacky way to make sure nodeId 1 exists in the registry - .id = 3, - .method = "DOM.performSearch", - .params = .{ .query = "p" }, - }); - try ctx.expectSentResult(.{ .searchId = "0", .resultCount = 2 }, .{ .id = 3 }); - - try testing.expectError(error.NodeNotFoundForGivenId, ctx.processMessage(.{ - .id = 4, - .method = "DOM.querySelector", - .params = .{ .nodeId = 1, .selector = "a" }, - })); - - try ctx.processMessage(.{ - .id = 5, - .method = "DOM.querySelectorAll", - .params = .{ .nodeId = 1, .selector = "a" }, - }); - try ctx.expectSentResult(.{ .nodeIds = &[_]u32{} }, .{ .id = 5 }); -} - -test "cdp.dom: querySelector Nodes found" { - var ctx = testing.context(); - defer ctx.deinit(); - - _ = try ctx.loadBrowserContext(.{ .id = "BID-A", .html = "

2

" }); - - try ctx.processMessage(.{ // Hacky way to make sure nodeId 1 exists in the registry - .id = 3, - .method = "DOM.performSearch", - .params = .{ .query = "div" }, - }); - try ctx.expectSentResult(.{ .searchId = "0", .resultCount = 1 }, .{ .id = 3 }); - - try ctx.processMessage(.{ - .id = 4, - .method = "DOM.querySelector", - .params = .{ .nodeId = 1, .selector = "p" }, - }); - try ctx.expectSentEvent("DOM.setChildNodes", null, .{}); - try ctx.expectSentResult(.{ .nodeId = 6 }, .{ .id = 4 }); - - try ctx.processMessage(.{ - .id = 5, - .method = "DOM.querySelectorAll", - .params = .{ .nodeId = 1, .selector = "p" }, - }); - try ctx.expectSentEvent("DOM.setChildNodes", null, .{}); - try ctx.expectSentResult(.{ .nodeIds = &.{6} }, .{ .id = 5 }); -} - -test "cdp.dom: getBoxModel" { - var ctx = testing.context(); - defer ctx.deinit(); - - _ = try ctx.loadBrowserContext(.{ .id = "BID-A", .html = "

2

" }); - - try ctx.processMessage(.{ // Hacky way to make sure nodeId 1 exists in the registry - .id = 3, - .method = "DOM.getDocument", - }); - - try ctx.processMessage(.{ - .id = 4, - .method = "DOM.querySelector", - .params = .{ .nodeId = 1, .selector = "p" }, - }); - try ctx.expectSentResult(.{ .nodeId = 3 }, .{ .id = 4 }); - - try ctx.processMessage(.{ - .id = 5, - .method = "DOM.getBoxModel", - .params = .{ .nodeId = 6 }, - }); - try ctx.expectSentResult(.{ .model = BoxModel{ - .content = Quad{ 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0 }, - .padding = Quad{ 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0 }, - .border = Quad{ 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0 }, - .margin = Quad{ 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0 }, - .width = 1, - .height = 1, - } }, .{ .id = 5 }); -} +// ZIGDOM +// // https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-getDocument +// fn getDocument(cmd: anytype) !void { +// const Params = struct { +// // CDP documentation implies that 0 isn't valid, but it _does_ work in Chrome +// depth: i32 = 3, +// pierce: bool = false, +// }; +// const params = try cmd.params(Params) orelse Params{}; + +// if (params.pierce) { +// log.warn(.cdp, "not implemented", .{ .feature = "DOM.getDocument: Not implemented pierce parameter" }); +// } + +// const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; +// const page = bc.session.currentPage() orelse return error.PageNotLoaded; +// const doc = parser.documentHTMLToDocument(page.window.document); + +// const node = try bc.node_registry.register(parser.documentToNode(doc)); +// return cmd.sendResult(.{ .root = bc.nodeWriter(node, .{ .depth = params.depth }) }, .{}); +// } + +// // https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-performSearch +// fn performSearch(cmd: anytype) !void { +// const params = (try cmd.params(struct { +// query: []const u8, +// includeUserAgentShadowDOM: ?bool = null, +// })) orelse return error.InvalidParams; + +// const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; +// const page = bc.session.currentPage() orelse return error.PageNotLoaded; +// const doc = parser.documentHTMLToDocument(page.window.document); + +// const allocator = cmd.cdp.allocator; +// var list = try css.querySelectorAll(allocator, parser.documentToNode(doc), params.query); +// defer list.deinit(allocator); + +// const search = try bc.node_search_list.create(list.nodes.items); + +// // dispatch setChildNodesEvents to inform the client of the subpart of node +// // tree covering the results. +// try dispatchSetChildNodes(cmd, list.nodes.items); + +// return cmd.sendResult(.{ +// .searchId = search.name, +// .resultCount = @as(u32, @intCast(search.node_ids.len)), +// }, .{}); +// } + +// // dispatchSetChildNodes send the setChildNodes event for the whole DOM tree +// // hierarchy of each nodes. +// // We dispatch event in the reverse order: from the top level to the direct parents. +// // We should dispatch a node only if it has never been sent. +// fn dispatchSetChildNodes(cmd: anytype, nodes: []*parser.Node) !void { +// const arena = cmd.arena; +// const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; +// const session_id = bc.session_id orelse return error.SessionIdNotLoaded; + +// var parents: std.ArrayListUnmanaged(*Node) = .{}; +// for (nodes) |_n| { +// var n = _n; +// while (true) { +// const p = parser.nodeParentNode(n) orelse break; + +// // Register the node. +// const node = try bc.node_registry.register(p); +// if (node.set_child_nodes_event) break; +// try parents.append(arena, node); +// n = p; +// } +// } + +// const plen = parents.items.len; +// if (plen == 0) return; + +// var i: usize = plen; +// // We're going to iterate in reverse order from how we added them. +// // This ensures that we're emitting the tree of nodes top-down. +// while (i > 0) { +// i -= 1; +// const node = parents.items[i]; +// // Although our above loop won't add an already-sent node to `parents` +// // this can still be true because two nodes can share the same parent node +// // so we might have just sent the node a previous iteration of this loop +// if (node.set_child_nodes_event) continue; + +// node.set_child_nodes_event = true; + +// // If the node has no parent, it's the root node. +// // We don't dispatch event for it because we assume the root node is +// // dispatched via the DOM.getDocument command. +// const p = parser.nodeParentNode(node._node) orelse { +// continue; +// }; + +// // Retrieve the parent from the registry. +// const parent_node = try bc.node_registry.register(p); + +// try cmd.sendEvent("DOM.setChildNodes", .{ +// .parentId = parent_node.id, +// .nodes = .{bc.nodeWriter(node, .{})}, +// }, .{ +// .session_id = session_id, +// }); +// } +// } + +// // https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-discardSearchResults +// fn discardSearchResults(cmd: anytype) !void { +// const params = (try cmd.params(struct { +// searchId: []const u8, +// })) orelse return error.InvalidParams; + +// const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; + +// bc.node_search_list.remove(params.searchId); +// return cmd.sendResult(null, .{}); +// } + +// // https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-getSearchResults +// fn getSearchResults(cmd: anytype) !void { +// const params = (try cmd.params(struct { +// searchId: []const u8, +// fromIndex: u32, +// toIndex: u32, +// })) orelse return error.InvalidParams; + +// if (params.fromIndex >= params.toIndex) { +// return error.BadIndices; +// } + +// const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; + +// const search = bc.node_search_list.get(params.searchId) orelse { +// return error.SearchResultNotFound; +// }; + +// const node_ids = search.node_ids; + +// if (params.fromIndex >= node_ids.len) return error.BadFromIndex; +// if (params.toIndex > node_ids.len) return error.BadToIndex; + +// return cmd.sendResult(.{ .nodeIds = node_ids[params.fromIndex..params.toIndex] }, .{}); +// } + +// fn querySelector(cmd: anytype) !void { +// const params = (try cmd.params(struct { +// nodeId: Node.Id, +// selector: []const u8, +// })) orelse return error.InvalidParams; + +// const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; + +// const node = bc.node_registry.lookup_by_id.get(params.nodeId) orelse { +// return cmd.sendError(-32000, "Could not find node with given id", .{}); +// }; + +// const selected_node = try css.querySelector( +// cmd.arena, +// node._node, +// params.selector, +// ) orelse return error.NodeNotFoundForGivenId; + +// const registered_node = try bc.node_registry.register(selected_node); + +// // Dispatch setChildNodesEvents to inform the client of the subpart of node tree covering the results. +// var array = [1]*parser.Node{selected_node}; +// try dispatchSetChildNodes(cmd, array[0..]); + +// return cmd.sendResult(.{ +// .nodeId = registered_node.id, +// }, .{}); +// } + +// fn querySelectorAll(cmd: anytype) !void { +// const params = (try cmd.params(struct { +// nodeId: Node.Id, +// selector: []const u8, +// })) orelse return error.InvalidParams; + +// const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; + +// const node = bc.node_registry.lookup_by_id.get(params.nodeId) orelse { +// return cmd.sendError(-32000, "Could not find node with given id", .{}); +// }; + +// const arena = cmd.arena; +// const selected_nodes = try css.querySelectorAll(arena, node._node, params.selector); +// const nodes = selected_nodes.nodes.items; + +// const node_ids = try arena.alloc(Node.Id, nodes.len); +// for (nodes, node_ids) |selected_node, *node_id| { +// node_id.* = (try bc.node_registry.register(selected_node)).id; +// } + +// // Dispatch setChildNodesEvents to inform the client of the subpart of node tree covering the results. +// try dispatchSetChildNodes(cmd, nodes); + +// return cmd.sendResult(.{ +// .nodeIds = node_ids, +// }, .{}); +// } + +// fn resolveNode(cmd: anytype) !void { +// const params = (try cmd.params(struct { +// nodeId: ?Node.Id = null, +// backendNodeId: ?u32 = null, +// objectGroup: ?[]const u8 = null, +// executionContextId: ?u32 = null, +// })) orelse return error.InvalidParams; + +// const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; +// const page = bc.session.currentPage() orelse return error.PageNotLoaded; + +// var js_context = page.js; +// if (params.executionContextId) |context_id| { +// if (js_context.v8_context.debugContextId() != context_id) { +// for (bc.isolated_worlds.items) |*isolated_world| { +// js_context = &(isolated_world.executor.context orelse return error.ContextNotFound); +// if (js_context.v8_context.debugContextId() == context_id) { +// break; +// } +// } else return error.ContextNotFound; +// } +// } + +// const input_node_id = params.nodeId orelse params.backendNodeId orelse return error.InvalidParam; +// const node = bc.node_registry.lookup_by_id.get(input_node_id) orelse return error.UnknownNode; + +// // node._node is a *parser.Node we need this to be able to find its most derived type e.g. Node -> Element -> HTMLElement +// // So we use the Node.Union when retrieve the value from the environment +// const remote_object = try bc.inspector.getRemoteObject( +// js_context, +// params.objectGroup orelse "", +// try dom_node.Node.toInterface(node._node), +// ); +// defer remote_object.deinit(); + +// const arena = cmd.arena; +// return cmd.sendResult(.{ .object = .{ +// .type = try remote_object.getType(arena), +// .subtype = try remote_object.getSubtype(arena), +// .className = try remote_object.getClassName(arena), +// .description = try remote_object.getDescription(arena), +// .objectId = try remote_object.getObjectId(arena), +// } }, .{}); +// } + +// fn describeNode(cmd: anytype) !void { +// const params = (try cmd.params(struct { +// nodeId: ?Node.Id = null, +// backendNodeId: ?Node.Id = null, +// objectId: ?[]const u8 = null, +// depth: i32 = 1, +// pierce: bool = false, +// })) orelse return error.InvalidParams; + +// if (params.pierce) { +// log.warn(.cdp, "not implemented", .{ .feature = "DOM.describeNode: Not implemented pierce parameter" }); +// } +// const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; + +// const node = try getNode(cmd.arena, bc, params.nodeId, params.backendNodeId, params.objectId); + +// return cmd.sendResult(.{ .node = bc.nodeWriter(node, .{ .depth = params.depth }) }, .{}); +// } + +// // An array of quad vertices, x immediately followed by y for each point, points clock-wise. +// // Note Y points downward +// // We are assuming the start/endpoint is not repeated. +// const Quad = [8]f64; + +// const BoxModel = struct { +// content: Quad, +// padding: Quad, +// border: Quad, +// margin: Quad, +// width: i32, +// height: i32, +// // shapeOutside: ?ShapeOutsideInfo, +// }; + +// fn rectToQuad(rect: Element.DOMRect) Quad { +// return Quad{ +// rect.x, +// rect.y, +// rect.x + rect.width, +// rect.y, +// rect.x + rect.width, +// rect.y + rect.height, +// rect.x, +// rect.y + rect.height, +// }; +// } + +// fn scrollIntoViewIfNeeded(cmd: anytype) !void { +// const params = (try cmd.params(struct { +// nodeId: ?Node.Id = null, +// backendNodeId: ?u32 = null, +// objectId: ?[]const u8 = null, +// rect: ?Element.DOMRect = null, +// })) orelse return error.InvalidParams; +// // Only 1 of nodeId, backendNodeId, objectId may be set, but chrome just takes the first non-null + +// // We retrieve the node to at least check if it exists and is valid. +// const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; +// const node = try getNode(cmd.arena, bc, params.nodeId, params.backendNodeId, params.objectId); + +// const node_type = parser.nodeType(node._node); +// switch (node_type) { +// .element => {}, +// .document => {}, +// .text => {}, +// else => return error.NodeDoesNotHaveGeometry, +// } + +// return cmd.sendResult(null, .{}); +// } + +// fn getNode(arena: Allocator, browser_context: anytype, node_id: ?Node.Id, backend_node_id: ?Node.Id, object_id: ?[]const u8) !*Node { +// const input_node_id = node_id orelse backend_node_id; +// if (input_node_id) |input_node_id_| { +// return browser_context.node_registry.lookup_by_id.get(input_node_id_) orelse return error.NodeNotFound; +// } +// if (object_id) |object_id_| { +// // Retrieve the object from which ever context it is in. +// const parser_node = try browser_context.inspector.getNodePtr(arena, object_id_); +// return try browser_context.node_registry.register(@ptrCast(@alignCast(parser_node))); +// } +// return error.MissingParams; +// } + +// // https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-getContentQuads +// // Related to: https://drafts.csswg.org/cssom-view/#the-geometryutils-interface +// fn getContentQuads(cmd: anytype) !void { +// const params = (try cmd.params(struct { +// nodeId: ?Node.Id = null, +// backendNodeId: ?Node.Id = null, +// objectId: ?[]const u8 = null, +// })) orelse return error.InvalidParams; + +// const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; +// const page = bc.session.currentPage() orelse return error.PageNotLoaded; + +// const node = try getNode(cmd.arena, bc, params.nodeId, params.backendNodeId, params.objectId); + +// // TODO likely if the following CSS properties are set the quads should be empty +// // visibility: hidden +// // display: none + +// if (parser.nodeType(node._node) != .element) return error.NodeIsNotAnElement; +// // TODO implement for document or text +// // Most likely document would require some hierachgy in the renderer. It is left unimplemented till we have a good example. +// // Text may be tricky, multiple quads in case of multiple lines? empty quads of text = ""? +// // Elements like SVGElement may have multiple quads. + +// const element = parser.nodeToElement(node._node); +// const rect = try Element._getBoundingClientRect(element, page); +// const quad = rectToQuad(rect); + +// return cmd.sendResult(.{ .quads = &.{quad} }, .{}); +// } + +// fn getBoxModel(cmd: anytype) !void { +// const params = (try cmd.params(struct { +// nodeId: ?Node.Id = null, +// backendNodeId: ?u32 = null, +// objectId: ?[]const u8 = null, +// })) orelse return error.InvalidParams; + +// const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; +// const page = bc.session.currentPage() orelse return error.PageNotLoaded; + +// const node = try getNode(cmd.arena, bc, params.nodeId, params.backendNodeId, params.objectId); + +// // TODO implement for document or text +// if (parser.nodeType(node._node) != .element) return error.NodeIsNotAnElement; +// const element = parser.nodeToElement(node._node); + +// const rect = try Element._getBoundingClientRect(element, page); +// const quad = rectToQuad(rect); + +// return cmd.sendResult(.{ .model = BoxModel{ +// .content = quad, +// .padding = quad, +// .border = quad, +// .margin = quad, +// .width = @intFromFloat(rect.width), +// .height = @intFromFloat(rect.height), +// } }, .{}); +// } + +// fn requestChildNodes(cmd: anytype) !void { +// const params = (try cmd.params(struct { +// nodeId: Node.Id, +// depth: i32 = 1, +// pierce: bool = false, +// })) orelse return error.InvalidParams; + +// if (params.depth == 0) return error.InvalidParams; +// const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; +// const session_id = bc.session_id orelse return error.SessionIdNotLoaded; +// const node = bc.node_registry.lookup_by_id.get(params.nodeId) orelse { +// return error.InvalidNode; +// }; + +// try cmd.sendEvent("DOM.setChildNodes", .{ +// .parentId = node.id, +// .nodes = bc.nodeWriter(node, .{ .depth = params.depth, .exclude_root = true }), +// }, .{ +// .session_id = session_id, +// }); + +// return cmd.sendResult(null, .{}); +// } + +// fn getFrameOwner(cmd: anytype) !void { +// const params = (try cmd.params(struct { +// frameId: []const u8, +// })) orelse return error.InvalidParams; + +// const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; +// const target_id = bc.target_id orelse return error.TargetNotLoaded; +// if (std.mem.eql(u8, target_id, params.frameId) == false) { +// return cmd.sendError(-32000, "Frame with the given id does not belong to the target.", .{}); +// } + +// const page = bc.session.currentPage() orelse return error.PageNotLoaded; +// const doc = parser.documentHTMLToDocument(page.window.document); + +// const node = try bc.node_registry.register(parser.documentToNode(doc)); +// return cmd.sendResult(.{ .nodeId = node.id, .backendNodeId = node.id }, .{}); +// } + +// const testing = @import("../testing.zig"); + +// test "cdp.dom: getSearchResults unknown search id" { +// var ctx = testing.context(); +// defer ctx.deinit(); + +// try testing.expectError(error.BrowserContextNotLoaded, ctx.processMessage(.{ +// .id = 8, +// .method = "DOM.getSearchResults", +// .params = .{ .searchId = "Nope", .fromIndex = 0, .toIndex = 10 }, +// })); +// } + +// test "cdp.dom: search flow" { +// var ctx = testing.context(); +// defer ctx.deinit(); + +// _ = try ctx.loadBrowserContext(.{ .id = "BID-A", .html = "

1

2

" }); + +// try ctx.processMessage(.{ +// .id = 12, +// .method = "DOM.performSearch", +// .params = .{ .query = "p" }, +// }); +// try ctx.expectSentResult(.{ .searchId = "0", .resultCount = 2 }, .{ .id = 12 }); + +// { +// // getSearchResults +// try ctx.processMessage(.{ +// .id = 13, +// .method = "DOM.getSearchResults", +// .params = .{ .searchId = "0", .fromIndex = 0, .toIndex = 2 }, +// }); +// try ctx.expectSentResult(.{ .nodeIds = &.{ 1, 2 } }, .{ .id = 13 }); + +// // different fromIndex +// try ctx.processMessage(.{ +// .id = 14, +// .method = "DOM.getSearchResults", +// .params = .{ .searchId = "0", .fromIndex = 1, .toIndex = 2 }, +// }); +// try ctx.expectSentResult(.{ .nodeIds = &.{2} }, .{ .id = 14 }); + +// // different toIndex +// try ctx.processMessage(.{ +// .id = 15, +// .method = "DOM.getSearchResults", +// .params = .{ .searchId = "0", .fromIndex = 0, .toIndex = 1 }, +// }); +// try ctx.expectSentResult(.{ .nodeIds = &.{1} }, .{ .id = 15 }); +// } + +// try ctx.processMessage(.{ +// .id = 16, +// .method = "DOM.discardSearchResults", +// .params = .{ .searchId = "0" }, +// }); +// try ctx.expectSentResult(null, .{ .id = 16 }); + +// // make sure the delete actually did something +// try testing.expectError(error.SearchResultNotFound, ctx.processMessage(.{ +// .id = 17, +// .method = "DOM.getSearchResults", +// .params = .{ .searchId = "0", .fromIndex = 0, .toIndex = 1 }, +// })); +// } + +// test "cdp.dom: querySelector unknown search id" { +// var ctx = testing.context(); +// defer ctx.deinit(); + +// _ = try ctx.loadBrowserContext(.{ .id = "BID-A", .html = "

1

2

" }); + +// try ctx.processMessage(.{ +// .id = 9, +// .method = "DOM.querySelector", +// .params = .{ .nodeId = 99, .selector = "" }, +// }); +// try ctx.expectSentError(-32000, "Could not find node with given id", .{}); + +// try ctx.processMessage(.{ +// .id = 9, +// .method = "DOM.querySelectorAll", +// .params = .{ .nodeId = 99, .selector = "" }, +// }); +// try ctx.expectSentError(-32000, "Could not find node with given id", .{}); +// } + +// test "cdp.dom: querySelector Node not found" { +// var ctx = testing.context(); +// defer ctx.deinit(); + +// _ = try ctx.loadBrowserContext(.{ .id = "BID-A", .html = "

1

2

" }); + +// try ctx.processMessage(.{ // Hacky way to make sure nodeId 1 exists in the registry +// .id = 3, +// .method = "DOM.performSearch", +// .params = .{ .query = "p" }, +// }); +// try ctx.expectSentResult(.{ .searchId = "0", .resultCount = 2 }, .{ .id = 3 }); + +// try testing.expectError(error.NodeNotFoundForGivenId, ctx.processMessage(.{ +// .id = 4, +// .method = "DOM.querySelector", +// .params = .{ .nodeId = 1, .selector = "a" }, +// })); + +// try ctx.processMessage(.{ +// .id = 5, +// .method = "DOM.querySelectorAll", +// .params = .{ .nodeId = 1, .selector = "a" }, +// }); +// try ctx.expectSentResult(.{ .nodeIds = &[_]u32{} }, .{ .id = 5 }); +// } + +// test "cdp.dom: querySelector Nodes found" { +// var ctx = testing.context(); +// defer ctx.deinit(); + +// _ = try ctx.loadBrowserContext(.{ .id = "BID-A", .html = "

2

" }); + +// try ctx.processMessage(.{ // Hacky way to make sure nodeId 1 exists in the registry +// .id = 3, +// .method = "DOM.performSearch", +// .params = .{ .query = "div" }, +// }); +// try ctx.expectSentResult(.{ .searchId = "0", .resultCount = 1 }, .{ .id = 3 }); + +// try ctx.processMessage(.{ +// .id = 4, +// .method = "DOM.querySelector", +// .params = .{ .nodeId = 1, .selector = "p" }, +// }); +// try ctx.expectSentEvent("DOM.setChildNodes", null, .{}); +// try ctx.expectSentResult(.{ .nodeId = 6 }, .{ .id = 4 }); + +// try ctx.processMessage(.{ +// .id = 5, +// .method = "DOM.querySelectorAll", +// .params = .{ .nodeId = 1, .selector = "p" }, +// }); +// try ctx.expectSentEvent("DOM.setChildNodes", null, .{}); +// try ctx.expectSentResult(.{ .nodeIds = &.{6} }, .{ .id = 5 }); +// } + +// test "cdp.dom: getBoxModel" { +// var ctx = testing.context(); +// defer ctx.deinit(); + +// _ = try ctx.loadBrowserContext(.{ .id = "BID-A", .html = "

2

" }); + +// try ctx.processMessage(.{ // Hacky way to make sure nodeId 1 exists in the registry +// .id = 3, +// .method = "DOM.getDocument", +// }); + +// try ctx.processMessage(.{ +// .id = 4, +// .method = "DOM.querySelector", +// .params = .{ .nodeId = 1, .selector = "p" }, +// }); +// try ctx.expectSentResult(.{ .nodeId = 3 }, .{ .id = 4 }); + +// try ctx.processMessage(.{ +// .id = 5, +// .method = "DOM.getBoxModel", +// .params = .{ .nodeId = 6 }, +// }); +// try ctx.expectSentResult(.{ .model = BoxModel{ +// .content = Quad{ 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0 }, +// .padding = Quad{ 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0 }, +// .border = Quad{ 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0 }, +// .margin = Quad{ 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0 }, +// .width = 1, +// .height = 1, +// } }, .{ .id = 5 }); +// } diff --git a/src/cdp/domains/fetch.zig b/src/cdp/domains/fetch.zig index f6fb302b9..ef11e15de 100644 --- a/src/cdp/domains/fetch.zig +++ b/src/cdp/domains/fetch.zig @@ -23,7 +23,7 @@ const log = @import("../../log.zig"); const network = @import("network.zig"); const Http = @import("../../http/Http.zig"); -const Notification = @import("../../notification.zig").Notification; +const Notification = @import("../../Notification.zig"); pub fn processMessage(cmd: anytype) !void { const action = std.meta.stringToEnum(enum { diff --git a/src/cdp/domains/input.zig b/src/cdp/domains/input.zig index d81fb1c8c..b4f2990a0 100644 --- a/src/cdp/domains/input.zig +++ b/src/cdp/domains/input.zig @@ -17,7 +17,7 @@ // along with this program. If not, see . const std = @import("std"); -const Page = @import("../../browser/page.zig").Page; +const Page = @import("../../browser/Page.zig"); pub fn processMessage(cmd: anytype) !void { const action = std.meta.stringToEnum(enum { diff --git a/src/cdp/domains/log.zig b/src/cdp/domains/log.zig index 368a79545..07d3c6d65 100644 --- a/src/cdp/domains/log.zig +++ b/src/cdp/domains/log.zig @@ -101,7 +101,7 @@ pub fn LogInterceptor(comptime BC: type) type { .fatal => "error", }, .text = self.allocating.written(), - .timestamp = @import("../../datetime.zig").milliTimestamp(), + .timestamp = @import("../../datetime.zig").milliTimestamp(.monotonic), }, }, .{ .session_id = self.bc.session_id, diff --git a/src/cdp/domains/network.zig b/src/cdp/domains/network.zig index 0d7014d0e..c41d19887 100644 --- a/src/cdp/domains/network.zig +++ b/src/cdp/domains/network.zig @@ -21,7 +21,7 @@ const Allocator = std.mem.Allocator; const CdpStorage = @import("storage.zig"); const Transfer = @import("../../http/Client.zig").Transfer; -const Notification = @import("../../notification.zig").Notification; +const Notification = @import("../../Notification.zig"); pub fn processMessage(cmd: anytype) !void { const action = std.meta.stringToEnum(enum { @@ -87,7 +87,7 @@ fn setExtraHTTPHeaders(cmd: anytype) !void { return cmd.sendResult(null, .{}); } -const Cookie = @import("../../browser/storage/storage.zig").Cookie; +const Cookie = @import("../../browser/webapi/storage/storage.zig").Cookie; // Only matches the cookie on provided parameters fn cookieMatches(cookie: *const Cookie, name: []const u8, domain: ?[]const u8, path: ?[]const u8) bool { @@ -173,7 +173,7 @@ fn getCookies(cmd: anytype) !void { const params = (try cmd.params(GetCookiesParam)) orelse GetCookiesParam{}; // If not specified, use the URLs of the page and all of its subframes. TODO subframes - const page_url = if (bc.session.page) |*page| page.url.raw else null; // @speed: avoid repasing the URL + const page_url = if (bc.session.page) |page| page.url else null; const param_urls = params.urls orelse &[_][]const u8{page_url orelse return error.InvalidParams}; var urls = try std.ArrayListUnmanaged(CdpStorage.PreparedUri).initCapacity(cmd.arena, param_urls.len); @@ -247,7 +247,7 @@ pub fn httpRequestStart(arena: Allocator, bc: anytype, msg: *const Notification. .requestId = try std.fmt.allocPrint(arena, "REQ-{d}", .{transfer.id}), .frameId = target_id, .loaderId = bc.loader_id, - .documentUrl = DocumentUrlWriter.init(&page.url.uri), + .documentUrl = page.url, .request = TransferAsRequestWriter.init(transfer), .initiator = .{ .type = "other" }, }, .{ .session_id = session_id }); @@ -416,34 +416,35 @@ const TransferAsResponseWriter = struct { } }; -const DocumentUrlWriter = struct { - uri: *std.Uri, - - fn init(uri: *std.Uri) DocumentUrlWriter { - return .{ - .uri = uri, - }; - } - - pub fn jsonStringify(self: *const DocumentUrlWriter, jws: anytype) !void { - self._jsonStringify(jws) catch return error.WriteFailed; - } - fn _jsonStringify(self: *const DocumentUrlWriter, jws: anytype) !void { - const writer = jws.writer; - - try jws.beginWriteRaw(); - try writer.writeByte('\"'); - try self.uri.writeToStream(writer, .{ - .scheme = true, - .authentication = true, - .authority = true, - .path = true, - .query = true, - }); - try writer.writeByte('\"'); - jws.endWriteRaw(); - } -}; +// @ZIGDOM - do we still need this? just send the full URL? +// const DocumentUrlWriter = struct { +// uri: *std.Uri, + +// fn init(uri: *std.Uri) DocumentUrlWriter { +// return .{ +// .uri = uri, +// }; +// } + +// pub fn jsonStringify(self: *const DocumentUrlWriter, jws: anytype) !void { +// self._jsonStringify(jws) catch return error.WriteFailed; +// } +// fn _jsonStringify(self: *const DocumentUrlWriter, jws: anytype) !void { +// const writer = jws.writer; + +// try jws.beginWriteRaw(); +// try writer.writeByte('\"'); +// try self.uri.writeToStream(writer, .{ +// .scheme = true, +// .authentication = true, +// .authority = true, +// .path = true, +// .query = true, +// }); +// try writer.writeByte('\"'); +// jws.endWriteRaw(); +// } +// }; fn idFromRequestId(request_id: []const u8) !u64 { if (!std.mem.startsWith(u8, request_id, "REQ-")) { diff --git a/src/cdp/domains/page.zig b/src/cdp/domains/page.zig index 1f6b720aa..7107d6866 100644 --- a/src/cdp/domains/page.zig +++ b/src/cdp/domains/page.zig @@ -17,8 +17,8 @@ // along with this program. If not, see . const std = @import("std"); -const Page = @import("../../browser/page.zig").Page; -const Notification = @import("../../notification.zig").Notification; +const Page = @import("../../browser/Page.zig"); +const Notification = @import("../../Notification.zig"); const Allocator = std.mem.Allocator; @@ -134,7 +134,7 @@ fn createIsolatedWorld(cmd: anytype) !void { fn navigate(cmd: anytype) !void { const params = (try cmd.params(struct { - url: []const u8, + url: [:0]const u8, // referrer: ?[]const u8 = null, // transitionType: ?[]const u8 = null, // TODO: enum // frameId: ?[]const u8 = null, @@ -253,7 +253,8 @@ pub fn pageNavigate(arena: Allocator, bc: anytype, event: *const Notification.Pa bc.inspector.contextCreated( page.js, "", - try page.origin(arena), + "", // @ZIGDOM + // try page.origin(arena), aux_data, true, ); @@ -360,7 +361,7 @@ pub fn pageNetworkAlmostIdle(bc: anytype, event: *const Notification.PageNetwork return sendPageLifecycle(bc, "networkAlmostIdle", event.timestamp); } -fn sendPageLifecycle(bc: anytype, name: []const u8, timestamp: u32) !void { +fn sendPageLifecycle(bc: anytype, name: []const u8, timestamp: u64) !void { // detachTarget could be called, in which case, we still have a page doing // things, but no session. const session_id = bc.session_id orelse return; @@ -379,7 +380,7 @@ const LifecycleEvent = struct { frameId: []const u8, loaderId: ?[]const u8, name: []const u8, - timestamp: u32, + timestamp: u64, }; const testing = @import("../testing.zig"); diff --git a/src/cdp/domains/storage.zig b/src/cdp/domains/storage.zig index 662d079f0..83547502a 100644 --- a/src/cdp/domains/storage.zig +++ b/src/cdp/domains/storage.zig @@ -19,9 +19,9 @@ const std = @import("std"); const log = @import("../../log.zig"); -const Cookie = @import("../../browser/storage/storage.zig").Cookie; -const CookieJar = @import("../../browser/storage/storage.zig").CookieJar; -pub const PreparedUri = @import("../../browser/storage/cookie.zig").PreparedUri; +const Cookie = @import("../../browser/webapi/storage/storage.zig").Cookie; +const CookieJar = @import("../../browser/webapi/storage/storage.zig").Jar; +pub const PreparedUri = @import("../../browser/webapi/storage/cookie.zig").PreparedUri; pub fn processMessage(cmd: anytype) !void { const action = std.meta.stringToEnum(enum { diff --git a/src/cdp/domains/target.zig b/src/cdp/domains/target.zig index 26f4cfbe3..3ea78b718 100644 --- a/src/cdp/domains/target.zig +++ b/src/cdp/domains/target.zig @@ -143,13 +143,14 @@ fn createTarget(cmd: anytype) !void { bc.target_id = target_id; - var page = try bc.session.createPage(); + const page = try bc.session.createPage(); { const aux_data = try std.fmt.allocPrint(cmd.arena, "{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\"}}", .{target_id}); bc.inspector.contextCreated( page.js, "", - try page.origin(cmd.arena), + "", // @ZIGDOM + // try page.origin(arena), aux_data, true, ); diff --git a/src/cdp/testing.zig b/src/cdp/testing.zig index 0c052d12a..7c086f6f2 100644 --- a/src/cdp/testing.zig +++ b/src/cdp/testing.zig @@ -24,7 +24,6 @@ const ArenaAllocator = std.heap.ArenaAllocator; const Testing = @This(); const main = @import("cdp.zig"); -const parser = @import("../browser/netsurf.zig"); const base = @import("../testing.zig"); pub const allocator = base.allocator; diff --git a/src/http/Client.zig b/src/http/Client.zig index fe0a5a1f7..65f310667 100644 --- a/src/http/Client.zig +++ b/src/http/Client.zig @@ -176,7 +176,7 @@ pub fn abort(self: *Client) void { } } -pub fn tick(self: *Client, timeout_ms: i32) !PerformStatus { +pub fn tick(self: *Client, timeout_ms: u32) !PerformStatus { while (true) { if (self.handles.hasAvailable() == false) { break; @@ -188,7 +188,7 @@ pub fn tick(self: *Client, timeout_ms: i32) !PerformStatus { const handle = self.handles.getFreeHandle().?; try self.makeRequest(handle, transfer); } - return self.perform(timeout_ms); + return self.perform(@intCast(timeout_ms)); } pub fn request(self: *Client, req: Request) !void { diff --git a/src/http/Http.zig b/src/http/Http.zig index 17b481d09..e5be87ee2 100644 --- a/src/http/Http.zig +++ b/src/http/Http.zig @@ -83,7 +83,7 @@ pub fn deinit(self: *Http) void { self.arena.deinit(); } -pub fn poll(self: *Http, timeout_ms: i32) Client.PerformStatus { +pub fn poll(self: *Http, timeout_ms: u32) Client.PerformStatus { return self.client.tick(timeout_ms) catch |err| { log.err(.app, "http poll", .{ .err = err }); return .normal; diff --git a/src/lightpanda.zig b/src/lightpanda.zig index 54e425735..f037ce3e0 100644 --- a/src/lightpanda.zig +++ b/src/lightpanda.zig @@ -1,5 +1,7 @@ const std = @import("std"); pub const App = @import("App.zig"); +pub const Server = @import("Server.zig"); + pub const log = @import("log.zig"); pub const dump = @import("browser/dump.zig"); pub const build_config = @import("build_config"); diff --git a/src/main.zig b/src/main.zig index 6c90196d2..1da7af4bc 100644 --- a/src/main.zig +++ b/src/main.zig @@ -99,27 +99,24 @@ fn run(allocator: Allocator, main_arena: Allocator) !void { app.telemetry.record(.{ .run = {} }); switch (args.mode) { - .serve => { - return; - // @ZIGDOM-CDP - // .serve => |opts| { - // log.debug(.app, "startup", .{ .mode = "serve" }); - // const address = std.net.Address.parseIp4(opts.host, opts.port) catch |err| { - // log.fatal(.app, "invalid server address", .{ .err = err, .host = opts.host, .port = opts.port }); - // return args.printUsageAndExit(false); - // }; - - // // _server is global to handle graceful shutdown. - // _server = try lp.Server.init(app, address); - // const server = &_server.?; - // defer server.deinit(); - - // // max timeout of 1 week. - // const timeout = if (opts.timeout > 604_800) 604_800_000 else @as(i32, opts.timeout) * 1000; - // server.run(address, timeout) catch |err| { - // log.fatal(.app, "server run error", .{ .err = err }); - // return err; - // }; + .serve => |opts| { + log.debug(.app, "startup", .{ .mode = "serve" }); + const address = std.net.Address.parseIp4(opts.host, opts.port) catch |err| { + log.fatal(.app, "invalid server address", .{ .err = err, .host = opts.host, .port = opts.port }); + return args.printUsageAndExit(false); + }; + + // _server is global to handle graceful shutdown. + _server = try lp.Server.init(app, address); + const server = &_server.?; + defer server.deinit(); + + // max timeout of 1 week. + const timeout = if (opts.timeout > 604_800) 604_800_000 else @as(u32, opts.timeout) * 1000; + server.run(address, timeout) catch |err| { + log.fatal(.app, "server run error", .{ .err = err }); + return err; + }; }, .fetch => |opts| { const url = opts.url; diff --git a/src/server.zig b/src/server.zig index afb55e434..4d42f0010 100644 --- a/src/server.zig +++ b/src/server.zig @@ -26,7 +26,7 @@ const Allocator = std.mem.Allocator; const ArenaAllocator = std.heap.ArenaAllocator; const log = @import("log.zig"); -const App = @import("app.zig").App; +const App = @import("App.zig"); const CDP = @import("cdp/cdp.zig").CDP; const MAX_HTTP_REQUEST_SIZE = 4096; @@ -69,7 +69,7 @@ pub fn deinit(self: *Server) void { self.allocator.free(self.json_version_response); } -pub fn run(self: *Server, address: net.Address, timeout_ms: i32) !void { +pub fn run(self: *Server, address: net.Address, timeout_ms: u32) !void { const flags = posix.SOCK.STREAM | posix.SOCK.CLOEXEC; const listener = try posix.socket(address.any.family, flags, posix.IPPROTO.TCP); self.listener = listener; @@ -112,7 +112,7 @@ pub fn run(self: *Server, address: net.Address, timeout_ms: i32) !void { } } -fn readLoop(self: *Server, socket: posix.socket_t, timeout_ms: i32) !void { +fn readLoop(self: *Server, socket: posix.socket_t, timeout_ms: u32) !void { // This shouldn't be necessary, but the Client is HUGE (> 512KB) because // it has a large read buffer. I don't know why, but v8 crashes if this // is on the stack (and I assume it's related to its size). @@ -143,7 +143,7 @@ fn readLoop(self: *Server, socket: posix.socket_t, timeout_ms: i32) !void { } var cdp = &client.mode.cdp; - var last_message = timestamp(); + var last_message = timestamp(.monotonic); var ms_remaining = timeout_ms; while (true) { switch (cdp.pageWait(ms_remaining)) { @@ -151,7 +151,7 @@ fn readLoop(self: *Server, socket: posix.socket_t, timeout_ms: i32) !void { if (try client.readSocket() == false) { return; } - last_message = timestamp(); + last_message = timestamp(.monotonic); ms_remaining = timeout_ms; }, .no_page => { @@ -162,16 +162,16 @@ fn readLoop(self: *Server, socket: posix.socket_t, timeout_ms: i32) !void { if (try client.readSocket() == false) { return; } - last_message = timestamp(); + last_message = timestamp(.monotonic); ms_remaining = timeout_ms; }, .done => { - const elapsed = timestamp() - last_message; + const elapsed = timestamp(.monotonic) - last_message; if (elapsed > ms_remaining) { log.info(.app, "CDP timeout", .{}); return; } - ms_remaining -= @as(i32, @intCast(elapsed)); + ms_remaining -= @intCast(elapsed); }, } } @@ -928,9 +928,7 @@ fn buildJSONVersionResponse( return try std.fmt.allocPrint(allocator, response_format, .{ body_len, address }); } -fn timestamp() u32 { - return @import("datetime.zig").timestamp(); -} +pub const timestamp = @import("datetime.zig").timestamp; // In-place string lowercase fn toLower(str: []u8) []u8 { diff --git a/src/telemetry/lightpanda.zig b/src/telemetry/lightpanda.zig index 621f4742e..cd87bf8ea 100644 --- a/src/telemetry/lightpanda.zig +++ b/src/telemetry/lightpanda.zig @@ -6,7 +6,7 @@ const Thread = std.Thread; const Allocator = std.mem.Allocator; const log = @import("../log.zig"); -const App = @import("../app.zig").App; +const App = @import("../App.zig"); const Http = @import("../http/Http.zig"); const telemetry = @import("telemetry.zig"); From 59bbfc4e06ca582abee90fdf5ac203d3c1661b93 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Tue, 28 Oct 2025 19:07:58 +0800 Subject: [PATCH 04/16] fix casing --- src/{app.zig => App.zig} | 0 src/{notification.zig => Notification.zig} | 0 src/{server.zig => Server.zig} | 0 src/browser/{browser.zig => Browser.zig} | 0 src/browser/{page.zig => Page.zig} | 0 src/browser/{session.zig => Session.zig} | 0 6 files changed, 0 insertions(+), 0 deletions(-) rename src/{app.zig => App.zig} (100%) rename src/{notification.zig => Notification.zig} (100%) rename src/{server.zig => Server.zig} (100%) rename src/browser/{browser.zig => Browser.zig} (100%) rename src/browser/{page.zig => Page.zig} (100%) rename src/browser/{session.zig => Session.zig} (100%) diff --git a/src/app.zig b/src/App.zig similarity index 100% rename from src/app.zig rename to src/App.zig diff --git a/src/notification.zig b/src/Notification.zig similarity index 100% rename from src/notification.zig rename to src/Notification.zig diff --git a/src/server.zig b/src/Server.zig similarity index 100% rename from src/server.zig rename to src/Server.zig diff --git a/src/browser/browser.zig b/src/browser/Browser.zig similarity index 100% rename from src/browser/browser.zig rename to src/browser/Browser.zig diff --git a/src/browser/page.zig b/src/browser/Page.zig similarity index 100% rename from src/browser/page.zig rename to src/browser/Page.zig diff --git a/src/browser/session.zig b/src/browser/Session.zig similarity index 100% rename from src/browser/session.zig rename to src/browser/Session.zig From 1a04ebce35830d474a5a75053ee9cf63a81f6042 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Tue, 28 Oct 2025 19:12:47 +0800 Subject: [PATCH 05/16] fix Node.contains --- src/browser/tests/node/child_nodes.html | 2 ++ src/browser/webapi/Node.zig | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/src/browser/tests/node/child_nodes.html b/src/browser/tests/node/child_nodes.html index 3ecc3150c..7534eff44 100644 --- a/src/browser/tests/node/child_nodes.html +++ b/src/browser/tests/node/child_nodes.html @@ -77,6 +77,8 @@ , it needs to block the caller + // until it's evaluated + var client = self.client; + while (true) { + if (pending_script.complete) { + return pending_script.script.eval(page); + } + _ = try client.tick(200); + } } // Resolve a module specifier to an valid URL. @@ -394,6 +413,7 @@ pub fn getAsyncModule(self: *ScriptManager, url: [:0]const u8, cb: AsyncModule.C .error_callback = AsyncModule.errorCallback, }); } + pub fn pageIsLoaded(self: *ScriptManager) void { std.debug.assert(self.static_scripts_done == false); self.static_scripts_done = true; @@ -415,15 +435,6 @@ fn evaluate(self: *ScriptManager) void { self.is_evaluating = true; defer self.is_evaluating = false; - while (self.scripts.first) |n| { - var pending_script: *PendingScript = @fieldParentPtr("node", n); - if (pending_script.complete == false) { - return; - } - defer pending_script.deinit(); - pending_script.script.eval(page); - } - if (self.static_scripts_done == false) { // We can only execute deferred scripts if // 1 - all the normal scripts are done @@ -460,7 +471,6 @@ fn evaluate(self: *ScriptManager) void { pub fn isDone(self: *const ScriptManager) bool { return self.asyncs.first == null and // there are no more async scripts self.static_scripts_done and // and we've finished parsing the HTML to queue all - self.scripts.first == null and // and there are no more --> +
diff --git a/src/browser/tests/net/url_search_params.html b/src/browser/tests/net/url_search_params.html index 689e9e683..12b98f26e 100644 --- a/src/browser/tests/net/url_search_params.html +++ b/src/browser/tests/net/url_search_params.html @@ -20,8 +20,8 @@ - + --> diff --git a/src/browser/webapi/EventTarget.zig b/src/browser/webapi/EventTarget.zig index d808e70f9..453790180 100644 --- a/src/browser/webapi/EventTarget.zig +++ b/src/browser/webapi/EventTarget.zig @@ -56,6 +56,15 @@ pub fn removeEventListener(self: *EventTarget, typ: []const u8, callback: js.Fun return page._event_manager.remove(self, typ, callback, use_capture); } +pub fn format(self: *EventTarget, writer: *std.Io.Writer) !void { + return switch (self._type) { + .node => |n| n.format(writer), + .window => writer.writeAll(""), + .xhr => writer.writeAll(""), + .abort_signal => writer.writeAll(""), + }; +} + pub const JsApi = struct { pub const bridge = js.Bridge(EventTarget); diff --git a/src/browser/webapi/storage/storage.zig b/src/browser/webapi/storage/storage.zig index 8813c0928..00e06bf33 100644 --- a/src/browser/webapi/storage/storage.zig +++ b/src/browser/webapi/storage/storage.zig @@ -9,7 +9,7 @@ pub fn registerTypes() []const type { } pub const Jar = @import("cookie.zig").Jar; -pub const Cookie =@import("cookie.zig").Cookie; +pub const Cookie = @import("cookie.zig").Cookie; pub const Shed = struct { _origins: std.StringHashMapUnmanaged(*Bucket) = .empty, diff --git a/src/cdp/testing.zig b/src/cdp/testing.zig index 7c086f6f2..3912b842f 100644 --- a/src/cdp/testing.zig +++ b/src/cdp/testing.zig @@ -117,11 +117,12 @@ const TestContext = struct { bc.session_id = sid; } - if (opts.html) |html| { - if (bc.session_id == null) bc.session_id = "SID-X"; - const page = try bc.session.createPage(); - page.window.document = (try Document.init(html)).doc; - } + // @ZIGDOM + // if (opts.html) |html| { + // if (bc.session_id == null) bc.session_id = "SID-X"; + // const page = try bc.session.createPage(); + // page.window._document = (try Document.init(html)).doc; + // } return bc; } diff --git a/src/testing.zig b/src/testing.zig index 7526180eb..a4805f985 100644 --- a/src/testing.zig +++ b/src/testing.zig @@ -422,9 +422,8 @@ test { const log = @import("log.zig"); const TestHTTPServer = @import("TestHTTPServer.zig"); -// @ZIGDOM-CDP -// const Server = @import("Server.zig"); -// var test_cdp_server: ?Server = null; +const Server = @import("Server.zig"); +var test_cdp_server: ?Server = null; var test_http_server: ?TestHTTPServer = null; test "tests:beforeAll" { @@ -446,12 +445,10 @@ test "tests:beforeAll" { var wg: std.Thread.WaitGroup = .{}; wg.startMany(2); - // @ZIGDOM-CDP - // { - // const thread = try std.Thread.spawn(.{}, serveCDP, .{&wg}); - // thread.detach(); - // } - wg.finish(); // @ZIGDOM-CDP REMOVE + { + const thread = try std.Thread.spawn(.{}, serveCDP, .{&wg}); + thread.detach(); + } test_http_server = TestHTTPServer.init(testHTTPHandler); { @@ -465,10 +462,9 @@ test "tests:beforeAll" { } test "tests:afterAll" { - // @ZIGDOM-CDP - // if (test_cdp_server) |*server| { - // server.deinit(); - // } + if (test_cdp_server) |*server| { + server.deinit(); + } if (test_http_server) |*server| { server.deinit(); } @@ -477,20 +473,19 @@ test "tests:afterAll" { test_app.deinit(); } -// @ZIGDOM-CDP -// fn serveCDP(wg: *std.Thread.WaitGroup) !void { -// const address = try std.net.Address.parseIp("127.0.0.1", 9583); -// test_cdp_server = try Server.init(test_app, address); +fn serveCDP(wg: *std.Thread.WaitGroup) !void { + const address = try std.net.Address.parseIp("127.0.0.1", 9583); + test_cdp_server = try Server.init(test_app, address); -// var server = try Server.init(test_app, address); -// defer server.deinit(); -// wg.finish(); + var server = try Server.init(test_app, address); + defer server.deinit(); + wg.finish(); -// test_cdp_server.?.run(address, 5) catch |err| { -// std.debug.print("CDP server error: {}", .{err}); -// return err; -// }; -// } + test_cdp_server.?.run(address, 5) catch |err| { + std.debug.print("CDP server error: {}", .{err}); + return err; + }; +} fn testHTTPHandler(req: *std.http.Server.Request) !void { const path = req.head.target; From 5ae1190ddd411365fb51478ed948a1a9909b979b Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Wed, 29 Oct 2025 22:23:05 +0800 Subject: [PATCH 07/16] HTMLDocument --- src/browser/Factory.zig | 11 ++ src/browser/Page.zig | 2 +- src/browser/js/bridge.zig | 1 + src/browser/tests/page/meta.html | 30 ++++- src/browser/tests/page/module.html | 8 +- src/browser/webapi/Document.zig | 119 +++++------------ src/browser/webapi/Element.zig | 6 +- src/browser/webapi/HTMLDocument.zig | 131 +++++++++++++++++++ src/browser/webapi/Node.zig | 3 + src/browser/webapi/collections/node_live.zig | 2 +- 10 files changed, 216 insertions(+), 97 deletions(-) create mode 100644 src/browser/webapi/HTMLDocument.zig diff --git a/src/browser/Factory.zig b/src/browser/Factory.zig index bd04da757..b1b41f9de 100644 --- a/src/browser/Factory.zig +++ b/src/browser/Factory.zig @@ -10,6 +10,7 @@ const Page = @import("Page.zig"); const Node = @import("webapi/Node.zig"); const Event = @import("webapi/Event.zig"); const Element = @import("webapi/Element.zig"); +const Document = @import("webapi/Document.zig"); const EventTarget = @import("webapi/EventTarget.zig"); const XMLHttpRequestEventTarget = @import("webapi/net/XMLHttpRequestEventTarget.zig"); @@ -98,6 +99,16 @@ pub fn node(self: *Factory, child: anytype) !*@TypeOf(child) { return child_ptr; } +pub fn document(self: *Factory, child: anytype) !*@TypeOf(child) { + const child_ptr = try self.createT(@TypeOf(child)); + child_ptr.* = child; + child_ptr._proto = try self.node(Document{ + ._proto = undefined, + ._type = unionInit(Document.Type, child_ptr), + }); + return child_ptr; +} + pub fn element(self: *Factory, child: anytype) !*@TypeOf(child) { const child_ptr = try self.createT(@TypeOf(child)); child_ptr.* = child; diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 1851bdc2f..b44dfb3c5 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -136,7 +136,7 @@ fn reset(self: *Page, comptime initializing: bool) !void { self.version = 0; self.url = "about/blank"; - self.document = try self._factory.node(Document{ ._proto = undefined }); + self.document = (try self._factory.document(Node.Document.HTMLDocument{ ._proto = undefined })).asDocument(); const storage_bucket = try self._factory.create(storage.Bucket{}); self.window = try self._factory.eventTarget(Window{ diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index 1e9e9739d..6928a4951 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -417,6 +417,7 @@ pub const JsApis = flattenTypes(&.{ @import("../webapi/css/CSSStyleDeclaration.zig"), @import("../webapi/css/CSSStyleProperties.zig"), @import("../webapi/Document.zig"), + @import("../webapi/HTMLDocument.zig"), @import("../webapi/DocumentFragment.zig"), @import("../webapi/DOMException.zig"), @import("../webapi/DOMTreeWalker.zig"), diff --git a/src/browser/tests/page/meta.html b/src/browser/tests/page/meta.html index fe2d32691..bf310c416 100644 --- a/src/browser/tests/page/meta.html +++ b/src/browser/tests/page/meta.html @@ -1,7 +1,8 @@ diff --git a/src/browser/tests/page/module.html b/src/browser/tests/page/module.html index 4a431b1fe..f3dae6d1b 100644 --- a/src/browser/tests/page/module.html +++ b/src/browser/tests/page/module.html @@ -1,7 +1,7 @@ - + --> - + -->