From b047cb6dc166fca24325947e50a10be0c072f865 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Sat, 18 Oct 2025 07:49:15 +0800 Subject: [PATCH 01/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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 @@ - + --> - + --> +

Direct child paragraph

@@ -51,15 +51,15 @@ { const container = $('#desc-container'); - // testing.expectEqual('Nested paragraph', container.querySelector('div p').textContent); - // testing.expectEqual('Nested paragraph', container.querySelector('div.nested p').textContent); + testing.expectEqual('Direct child paragraph', container.querySelector('div p').textContent); + testing.expectEqual('Nested paragraph', container.querySelector('div.nested p').textContent); testing.expectEqual('Deeply nested paragraph', container.querySelector('div span p').textContent); - // testing.expectEqual('Nested paragraph', container.querySelector('.nested .text').textContent); - // testing.expectEqual(null, container.querySelector('article div p')); + testing.expectEqual('Nested paragraph', container.querySelector('.nested .text').textContent); + testing.expectEqual(null, container.querySelector('article div p')); - // const outerDiv = $('#outer-div'); - // testing.expectEqual('deep-span', outerDiv.querySelector('div div span').id); - // testing.expectEqual('deep-span', outerDiv.querySelector('.level1 span').id); - // testing.expectEqual('deep-span', outerDiv.querySelector('.level1 .level2 span').id); + const outerDiv = $('#outer-div'); + testing.expectEqual('deep-span', outerDiv.querySelector('div div span').id); + testing.expectEqual('deep-span', outerDiv.querySelector('.level1 span').id); + testing.expectEqual('deep-span', outerDiv.querySelector('.level1 .level2 span').id); } diff --git a/src/browser/tests/page/module.html b/src/browser/tests/page/module.html index f3dae6d1b..1dd797944 100644 --- a/src/browser/tests/page/module.html +++ b/src/browser/tests/page/module.html @@ -1,7 +1,7 @@ - + - - - diff --git a/src/browser/tests/testing.js b/src/browser/tests/testing.js index 89c3d16be..d5ac8b971 100644 --- a/src/browser/tests/testing.js +++ b/src/browser/tests/testing.js @@ -2,6 +2,7 @@ let failed = false; let observed_ids = {}; let eventuallies = []; + let async_capture = null; let current_script_id = null; function expectTrue(actual) { @@ -12,14 +13,17 @@ expectEqual(false, actual); } - function expectEqual(expected, actual) { + function expectEqual(expected, actual, opts) { if (_equal(expected, actual)) { - _registerObservation('ok'); + _registerObservation('ok', opts); return; } failed = true; - _registerObservation('fail'); + _registerObservation('fail', opts); let err = `expected: ${_displayValue(expected)}, got: ${_displayValue(actual)}\n script_id: ${_currentScriptId()}`; + if (async_capture) { + err += `\n stack: ${async_capture.stack}`; + } console.error(err); throw new Error('expectEqual failed'); } @@ -57,7 +61,14 @@ callback: cb, script_id: script_id, }); + } + async function async(cb) { + const script_id = document.currentScript.id; + const stack = new Error().stack; + async_capture = {script_id: script_id, stack: stack}; + await cb(); + async_capture = null; } function assertOk() { @@ -92,6 +103,7 @@ window.testing = { fail: fail, + async: async, assertOk: assertOk, expectTrue: expectTrue, expectFalse: expectFalse, @@ -125,7 +137,6 @@ return false; } - if (expected instanceof Node) { if (!(actual instanceof Node)) { return false; @@ -145,8 +156,8 @@ return true; } - function _registerObservation(status) { - const script_id = _currentScriptId(); + function _registerObservation(status, opts) { + script_id = opts?.script_id || _currentScriptId(); if (!script_id) { return; } @@ -161,7 +172,12 @@ return current_script_id; } + if (async_capture) { + return async_capture.script_id; + } + const current_script = document.currentScript; + if (!current_script) { return null; } diff --git a/src/browser/tests/window/location.html b/src/browser/tests/window/location.html index f5ce7ffb4..01a4049db 100644 --- a/src/browser/tests/window/location.html +++ b/src/browser/tests/window/location.html @@ -2,6 +2,6 @@ diff --git a/src/browser/tests/window/report_error.html b/src/browser/tests/window/report_error.html index 6796d46f8..c2d66125a 100644 --- a/src/browser/tests/window/report_error.html +++ b/src/browser/tests/window/report_error.html @@ -35,9 +35,6 @@ window.reportError(err); testing.expectEqual(true, evt.message.includes('Detailed error')); - testing.expectEqual('script.js', evt.filename); - testing.expectEqual(100, evt.lineno); - testing.expectEqual(25, evt.colno); testing.expectEqual(err, evt.error); } diff --git a/src/browser/webapi/Window.zig b/src/browser/webapi/Window.zig index d5261fede..0ea5fc06c 100644 --- a/src/browser/webapi/Window.zig +++ b/src/browser/webapi/Window.zig @@ -147,6 +147,29 @@ pub fn cancelAnimationFrame(self: *Window, id: u32) void { sc.removed = true; } +pub fn reportError(self: *Window, err: js.Object, page: *Page) !void { + const error_event = try ErrorEvent.init("error", .{ + .@"error" = err, + .message = err.toString() catch "Unknown error", + .bubbles = false, + .cancelable = true, + }, page); + + const event = error_event.asEvent(); + try page._event_manager.dispatch(self.asEventTarget(), event); + + if (comptime builtin.is_test == false) { + if (!event._prevent_default) { + log.warn(.js, "window.reportError", .{ + .message = error_event._message, + .filename = error_event._filename, + .line_number = error_event._line_number, + .column_number = error_event._column_number, + }); + } + } +} + pub fn matchMedia(_: *const Window, query: []const u8, page: *Page) !*MediaQueryList { return page._factory.eventTarget(MediaQueryList{ ._proto = undefined, @@ -290,6 +313,7 @@ pub const JsApi = struct { pub const matchMedia = bridge.function(Window.matchMedia, .{}); pub const btoa = bridge.function(Window.btoa, .{}); pub const atob = bridge.function(Window.atob, .{}); + pub const reportError = bridge.function(Window.reportError, .{}); }; const testing = @import("../../testing.zig"); diff --git a/src/browser/webapi/element/html/Body.zig b/src/browser/webapi/element/html/Body.zig index 9822502fb..585e74d63 100644 --- a/src/browser/webapi/element/html/Body.zig +++ b/src/browser/webapi/element/html/Body.zig @@ -30,11 +30,11 @@ pub const JsApi = struct { pub const Build = struct { pub fn complete(node: *Node, page: *Page) !void { - _ = node; - _ = page; - // @ZIGDOM - // const el = node.as(Element); - // const on_load = el.getAttributeSafe("onload") orelse return; - // page.window._on_load = page.js.stringToFunction(on_load); + const el = node.as(Element); + const on_load = el.getAttributeSafe("onload") orelse return; + page.window._on_load = page.js.stringToFunction(on_load) catch |err| blk: { + log.err(.js, "body.onload", .{.err = err, .str = on_load}); + break :blk null; + }; } }; diff --git a/src/browser/webapi/element/html/Script.zig b/src/browser/webapi/element/html/Script.zig index e60811069..df224ae25 100644 --- a/src/browser/webapi/element/html/Script.zig +++ b/src/browser/webapi/element/html/Script.zig @@ -1,3 +1,4 @@ +const log = @import("../../../../log.zig"); const js = @import("../../../js/js.zig"); const Page = @import("../../../Page.zig"); @@ -77,15 +78,19 @@ pub const Build = struct { const element = self.asElement(); self._src = element.getAttributeSafe("src") orelse ""; - // @ZIGDOM - _ = page; - // if (element.getAttributeSafe("onload")) |on_load| { - // self._on_load = page.js.stringToFunction(on_load); - // } - - // if (element.getAttributeSafe("onerror")) |on_error| { - // self._on_error = page.js.stringToFunction(on_error); - // } + if (element.getAttributeSafe("onload")) |on_load| { + self._on_load = page.js.stringToFunction(on_load) catch |err| blk: { + log.err(.js, "script.onload", .{.err = err, .str = on_load}); + break :blk null; + }; + } + + if (element.getAttributeSafe("onerror")) |on_error| { + self._on_error = page.js.stringToFunction(on_error) catch |err| blk: { + log.err(.js, "script.onerror", .{.err = err, .str = on_error}); + break :blk null; + }; + } } }; diff --git a/src/browser/webapi/selector/List.zig b/src/browser/webapi/selector/List.zig index 0283178db..0bedc46ca 100644 --- a/src/browser/webapi/selector/List.zig +++ b/src/browser/webapi/selector/List.zig @@ -22,32 +22,26 @@ pub const EntryIterator = GenericIterator(Iterator, null); pub const KeyIterator = GenericIterator(Iterator, "0"); pub const ValueIterator = GenericIterator(Iterator, "1"); -pub fn init(arena: Allocator, root: *Node, selector: Selector.Selector, page: *Page) !*List { - var list = try page._factory.create(List{ - ._arena = arena, - ._nodes = &.{}, - }); - +pub fn collect( + allocator: std.mem.Allocator, + root: *Node, + selector: Selector.Selector, + nodes: *std.AutoArrayHashMapUnmanaged(*Node, void), + page: *Page, +) !void { if (optimizeSelector(root, &selector, page)) |result| { - var nodes: std.ArrayListUnmanaged(*Node) = .empty; - var tw = TreeWalker.init(result.root, .{}); - const optimized_selector = result.selector; if (result.exclude_root) { _ = tw.next(); } - // When exclude_root is true, pass root as boundary so it can match but we won't search beyond it - // When exclude_root is false, pass null so there's no boundary (root already matched, searching descendants) + const boundary = if (result.exclude_root) result.root else null; while (tw.next()) |node| { - if (matches(node, optimized_selector, boundary)) { - try nodes.append(arena, node); + if (matches(node, result.selector, boundary)) { + try nodes.put(allocator, node, {}); } } - list._nodes = nodes.items; } - - return list; } // used internally to find the first match @@ -135,7 +129,7 @@ fn optimizeSelector(root: *Node, selector: *const Selector.Selector, page: *Page .first = selector.first, .segments = selector.segments, }, - .exclude_root = false, + .exclude_root = true, }; } @@ -238,7 +232,7 @@ fn findIdSelector(selector: *const Selector.Selector) ?IdAnchor { return null; } -fn matches(node: *Node, selector: Selector.Selector, root: ?*Node) bool { +pub fn matches(node: *Node, selector: Selector.Selector, root: ?*Node) bool { const el = node.is(Node.Element) orelse return false; if (selector.segments.len == 0) { @@ -333,8 +327,9 @@ fn matchChild(node: *Node, compound: Selector.Compound, root: ?*Node) ?*Node { const parent = node._parent orelse return null; // Don't match beyond the root boundary + // If there's a boundary, check if parent is outside (an ancestor of) the boundary if (root) |boundary| { - if (parent == boundary) { + if (!boundary.contains(parent)) { return null; } } diff --git a/src/browser/webapi/selector/Parser.zig b/src/browser/webapi/selector/Parser.zig index 7fccc812d..0e88df0df 100644 --- a/src/browser/webapi/selector/Parser.zig +++ b/src/browser/webapi/selector/Parser.zig @@ -1,5 +1,7 @@ const std = @import("std"); +const Allocator = std.mem.Allocator; + const Page = @import("../../Page.zig"); const Node = @import("../Node.zig"); @@ -8,7 +10,6 @@ const Part = Selector.Part; const Combinator = Selector.Combinator; const Segment = Selector.Segment; const Attribute = @import("../element/Attribute.zig"); -const Allocator = std.mem.Allocator; const Parser = @This(); @@ -26,10 +27,56 @@ const ParseError = error{ InvalidTagSelector, InvalidSelector, }; + +pub fn parseList(arena: Allocator, input: []const u8, page: *Page) ParseError![]const Selector.Selector { + var selectors: std.ArrayList(Selector.Selector) = .empty; + + var remaining = input; + while (true) { + const trimmed = std.mem.trimLeft(u8, remaining, &std.ascii.whitespace); + if (trimmed.len == 0) break; + + var comma_pos: usize = trimmed.len; + var depth: usize = 0; + for (trimmed, 0..) |c, i| { + switch (c) { + '(' => depth += 1, + ')' => { + if (depth > 0) depth -= 1; + }, + ',' => { + if (depth == 0) { + comma_pos = i; + break; + } + }, + else => {}, + } + } + + const selector_input = std.mem.trimRight(u8, trimmed[0..comma_pos], &std.ascii.whitespace); + + if (selector_input.len > 0) { + const selector = try parse(arena, selector_input, page); + try selectors.append(arena, selector); + } + + if (comma_pos >= trimmed.len) break; + remaining = trimmed[comma_pos + 1 ..]; + } + + if (selectors.items.len == 0) { + return error.InvalidSelector; + } + + return selectors.items; +} + pub fn parse(arena: Allocator, input: []const u8, page: *Page) ParseError!Selector.Selector { var parser = Parser{ .input = input }; - var segments: std.ArrayListUnmanaged(Segment) = .empty; - var current_compound: std.ArrayListUnmanaged(Part) = .empty; + var segments: std.ArrayList(Segment) = .empty; + var current_compound: std.ArrayList(Part) = .empty; + // Parse the first compound (no combinator before it) while (parser.skipSpaces()) { @@ -302,7 +349,7 @@ fn pseudoClass(self: *Parser, arena: Allocator, page: *Page) !Selector.PseudoCla if (std.mem.eql(u8, name, "not")) { // CSS Level 4: :not() can contain a full selector list (comma-separated selectors) // e.g., :not(div, .class, #id > span) - var selectors: std.ArrayListUnmanaged(Selector.Selector) = .empty; + var selectors: std.ArrayList(Selector.Selector) = .empty; _ = self.skipSpaces(); diff --git a/src/browser/webapi/selector/Selector.zig b/src/browser/webapi/selector/Selector.zig index 7e4da77c9..8839b1b6f 100644 --- a/src/browser/webapi/selector/Selector.zig +++ b/src/browser/webapi/selector/Selector.zig @@ -10,23 +10,28 @@ pub fn querySelector(root: *Node, input: []const u8, page: *Page) !?*Node.Elemen return error.SyntaxError; } - const selector = try Parser.parse(page.call_arena, input, page); - - // Fast path: single compound with only an ID selector - if (selector.segments.len == 0 and selector.first.parts.len == 1) { - const first = selector.first.parts[0]; - if (first == .id) { - const el = page.document._elements_by_id.get(first.id) orelse return null; - // Check if the element is within the root subtree - if (root.contains(el.asNode())) { - return el; + const arena = page.call_arena; + const selectors = try Parser.parseList(arena, input, page); + + for (selectors) |selector| { + // Fast path: single compound with only an ID selector + if (selector.segments.len == 0 and selector.first.parts.len == 1) { + const first = selector.first.parts[0]; + if (first == .id) { + const el = page.document._elements_by_id.get(first.id) orelse continue; + // Check if the element is within the root subtree + if (root.contains(el.asNode())) { + return el; + } + continue; } - return null; } - } - if (List.initOne(root, selector, page)) |node| { - return node.is(Node.Element); + if (List.initOne(root, selector, page)) |node| { + if (node.is(Node.Element)) |el| { + return el; + } + } } return null; } @@ -37,8 +42,33 @@ pub fn querySelectorAll(root: *Node, input: []const u8, page: *Page) !*List { } const arena = page.arena; - const selector = try Parser.parse(arena, input, page); - return List.init(arena, root, selector, page); + var nodes: std.AutoArrayHashMapUnmanaged(*Node, void) = .empty; + + const selectors = try Parser.parseList(arena, input, page); + for (selectors) |selector| { + try List.collect(arena, root, selector, &nodes, page); + } + + return page._factory.create(List{ + ._arena = arena, + ._nodes = nodes.keys(), + }); +} + +pub fn matches(el: *Node.Element, input: []const u8, page: *Page) !bool { + if (input.len == 0) { + return error.SyntaxError; + } + + const arena = page.call_arena; + const selectors = try Parser.parseList(arena, input, page); + + for (selectors) |selector| { + if (List.matches(el.asNode(), selector, null)) { + return true; + } + } + return false; } pub fn classAttributeContains(class_attr: []const u8, class_name: []const u8) bool { diff --git a/src/log.zig b/src/log.zig index d0f02bf9d..f791e9f7b 100644 --- a/src/log.zig +++ b/src/log.zig @@ -131,7 +131,7 @@ pub fn log(comptime scope: Scope, level: Level, comptime msg: []const u8, data: var writer = stderr.writer(&buf); logTo(scope, level, msg, data, &writer.interface) catch |log_err| { - std.debug.print("$time={d} $level=fatal $scope={s} $msg=\"log err\" err={s} log_msg=\"{s}\"", .{ timestamp(.clock), @errorName(log_err), @tagName(scope), msg }); + std.debug.print("$time={d} $level=fatal $scope={s} $msg=\"log err\" err={s} log_msg=\"{s}\"\n", .{ timestamp(.clock), @errorName(log_err), @tagName(scope), msg }); }; } @@ -147,7 +147,6 @@ fn logTo(comptime scope: Scope, level: Level, comptime msg: []const u8, data: an } } } - switch (opts.format) { .logfmt => try logLogfmt(scope, level, msg, data, out), .pretty => try logPretty(scope, level, msg, data, out), From 32bad5f8bb63a4ee92c046ddf5935294df89ebec Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 13 Nov 2025 20:09:38 +0800 Subject: [PATCH 18/30] Element.matches, Element.hasAttributes and DOMStringMap (Element.dataset) --- src/browser/Page.zig | 4 + src/browser/ScriptManager.zig | 1 - src/browser/js/Caller.zig | 49 +++++- src/browser/js/Env.zig | 14 +- src/browser/js/bridge.zig | 52 ++++-- src/browser/tests/element/attributes.html | 19 +++ src/browser/tests/element/dataset.html | 150 ++++++++++++++++++ src/browser/tests/element/matches.html | 76 +++++++++ src/browser/webapi/Document.zig | 4 +- src/browser/webapi/Element.zig | 24 +++ src/browser/webapi/KeyValueList.zig | 2 +- src/browser/webapi/Node.zig | 4 +- src/browser/webapi/Window.zig | 2 +- .../webapi/collections/HTMLAllCollection.zig | 2 +- .../webapi/collections/HTMLCollection.zig | 2 +- src/browser/webapi/css/CSSStyleProperties.zig | 2 +- src/browser/webapi/css/MediaQueryList.zig | 2 +- src/browser/webapi/element/Attribute.zig | 2 +- src/browser/webapi/element/DOMStringMap.zig | 87 ++++++++++ src/browser/webapi/element/html/Body.zig | 2 +- src/browser/webapi/element/html/Script.zig | 4 +- src/browser/webapi/selector/Parser.zig | 1 - 22 files changed, 467 insertions(+), 38 deletions(-) create mode 100644 src/browser/tests/element/dataset.html create mode 100644 src/browser/tests/element/matches.html create mode 100644 src/browser/webapi/element/DOMStringMap.zig diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 6f0afe853..fa1c9fdd8 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -63,6 +63,9 @@ _attribute_lookup: std.AutoHashMapUnmanaged(usize, *Element.Attribute), // the return of elements.attributes. _attribute_named_node_map_lookup: std.AutoHashMapUnmanaged(usize, *Element.Attribute.NamedNodeMap), +// element.dataset -> DOMStringMap +_element_datasets: std.AutoHashMapUnmanaged(*Element, *Element.DOMStringMap), + _script_manager: ScriptManager, _polyfill_loader: polyfill.Loader = .{}, @@ -152,6 +155,7 @@ fn reset(self: *Page, comptime initializing: bool) !void { self._load_state = .parsing; self._attribute_lookup = .empty; self._attribute_named_node_map_lookup = .empty; + self._element_datasets = .empty; self._event_manager = EventManager.init(self); self._script_manager = ScriptManager.init(self); diff --git a/src/browser/ScriptManager.zig b/src/browser/ScriptManager.zig index 4a394f651..29e9f6839 100644 --- a/src/browser/ScriptManager.zig +++ b/src/browser/ScriptManager.zig @@ -707,7 +707,6 @@ const Script = struct { .cacheable = cacheable, }); - // Handle importmap special case here: the content is a JSON containing // imports. if (self.kind == .importmap) { diff --git a/src/browser/js/Caller.zig b/src/browser/js/Caller.zig index aea3a1af3..0b1b5e4a2 100644 --- a/src/browser/js/Caller.zig +++ b/src/browser/js/Caller.zig @@ -157,7 +157,7 @@ pub fn _getIndex(self: *Caller, comptime T: type, func: anytype, idx: u32, info: @field(args, "0") = try Context.typeTaggedAnyOpaque(*T, info.getThis()); @field(args, "1") = idx; const ret = @call(.auto, func, args); - return self.handleIndexedReturn(T, F, ret, info, opts); + return self.handleIndexedReturn(T, F, true, ret, info, opts); } pub fn getNamedIndex(self: *Caller, comptime T: type, func: anytype, name: v8.Name, info: v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 { @@ -173,10 +173,49 @@ pub fn _getNamedIndex(self: *Caller, comptime T: type, func: anytype, name: v8.N @field(args, "0") = try Context.typeTaggedAnyOpaque(*T, info.getThis()); @field(args, "1") = try self.nameToString(name); const ret = @call(.auto, func, args); - return self.handleIndexedReturn(T, F, ret, info, opts); + return self.handleIndexedReturn(T, F, true, ret, info, opts); } -fn handleIndexedReturn(self: *Caller, comptime T: type, comptime F: type, ret: anytype, info: v8.PropertyCallbackInfo, comptime opts: CallOpts) !u8 { +pub fn setNamedIndex(self: *Caller, comptime T: type, func: anytype, name: v8.Name, js_value: v8.Value, info: v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 { + return self._setNamedIndex(T, func, name, js_value, info, opts) catch |err| { + self.handleError(T, @TypeOf(func), err, info, opts); + return v8.Intercepted.No; + }; +} + +pub fn _setNamedIndex(self: *Caller, comptime T: type, func: anytype, name: v8.Name, js_value: v8.Value, info: v8.PropertyCallbackInfo, comptime opts: CallOpts) !u8 { + const F = @TypeOf(func); + var args: ParameterTypes(F) = undefined; + @field(args, "0") = try Context.typeTaggedAnyOpaque(*T, info.getThis()); + @field(args, "1") = try self.nameToString(name); + @field(args, "2") = try self.context.jsValueToZig(@TypeOf(@field(args, "2")), js_value); + if (@typeInfo(F).@"fn".params.len == 4) { + @field(args, "3") = self.context.page; + } + const ret = @call(.auto, func, args); + return self.handleIndexedReturn(T, F, false, ret, info, opts); +} + +pub fn deleteNamedIndex(self: *Caller, comptime T: type, func: anytype, name: v8.Name, info: v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 { + return self._deleteNamedIndex(T, func, name, info, opts) catch |err| { + self.handleError(T, @TypeOf(func), err, info, opts); + return v8.Intercepted.No; + }; +} + +pub fn _deleteNamedIndex(self: *Caller, comptime T: type, func: anytype, name: v8.Name, info: v8.PropertyCallbackInfo, comptime opts: CallOpts) !u8 { + const F = @TypeOf(func); + var args: ParameterTypes(F) = undefined; + @field(args, "0") = try Context.typeTaggedAnyOpaque(*T, info.getThis()); + @field(args, "1") = try self.nameToString(name); + if (@typeInfo(F).@"fn".params.len == 3) { + @field(args, "2") = self.context.page; + } + const ret = @call(.auto, func, args); + return self.handleIndexedReturn(T, F, false, ret, info, opts); +} + +fn handleIndexedReturn(self: *Caller, comptime T: type, comptime F: type, comptime getter: bool, ret: anytype, info: v8.PropertyCallbackInfo, comptime opts: CallOpts) !u8 { // need to unwrap this error immediately for when opts.null_as_undefined == true // and we need to compare it to null; const non_error_ret = switch (@typeInfo(@TypeOf(ret))) { @@ -197,7 +236,9 @@ fn handleIndexedReturn(self: *Caller, comptime T: type, comptime F: type, ret: a else => ret, }; - info.getReturnValue().set(try self.context.zigValueToJs(non_error_ret, opts)); + if (comptime getter) { + info.getReturnValue().set(try self.context.zigValueToJs(non_error_ret, opts)); + } return v8.Intercepted.Yes; } diff --git a/src/browser/js/Env.zig b/src/browser/js/Env.zig index fa4595e32..71bed313a 100644 --- a/src/browser/js/Env.zig +++ b/src/browser/js/Env.zig @@ -253,13 +253,12 @@ pub fn attachClass(comptime JsApi: type, isolate: v8.Isolate, template: v8.Funct }; template_proto.setIndexedProperty(configuration, null); }, - bridge.NamedIndexed => { - const configuration = v8.NamedPropertyHandlerConfiguration{ - .getter = value.getter, - .flags = v8.PropertyHandlerFlags.OnlyInterceptStrings | v8.PropertyHandlerFlags.NonMasking, - }; - template_proto.setNamedProperty(configuration, null); - }, + bridge.NamedIndexed => template.getInstanceTemplate().setNamedProperty(.{ + .getter = value.getter, + .setter = value.setter, + .deleter = value.deleter, + .flags = v8.PropertyHandlerFlags.OnlyInterceptStrings | v8.PropertyHandlerFlags.NonMasking, + }, null), bridge.Iterator => { // Same as a function, but with a specific name const function_template = v8.FunctionTemplate.initCallback(isolate, value.func); @@ -326,7 +325,6 @@ fn generateConstructor(comptime JsApi: type, isolate: v8.Isolate) v8.FunctionTem // if (has_js_call_as_function) { - // if (@hasDecl(Struct, "htmldda") and Struct.htmldda) { // if (!has_js_call_as_function) { // @compileError(@typeName(Struct) ++ ": htmldda required jsCallAsFunction to be defined. This is a hard-coded requirement in V8, because mark_as_undetectable only exists for HTMLAllCollection which is also callable."); diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index 4a313b6b3..fe1d4ec12 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -45,8 +45,8 @@ pub fn Builder(comptime T: type) type { return Indexed.init(T, getter_func, opts); } - pub fn namedIndexed(comptime getter_func: anytype, comptime opts: NamedIndexed.Opts) NamedIndexed { - return NamedIndexed.init(T, getter_func, opts); + pub fn namedIndexed(comptime getter_func: anytype, setter_func: anytype, deleter_func: anytype, comptime opts: NamedIndexed.Opts) NamedIndexed { + return NamedIndexed.init(T, getter_func, setter_func, deleter_func, opts); } pub fn iterator(comptime func: anytype, comptime opts: Iterator.Opts) Iterator { @@ -221,14 +221,16 @@ pub const Indexed = struct { pub const NamedIndexed = struct { getter: *const fn (c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8, + setter: ?*const fn (c_name: ?*const v8.C_Name, c_value: ?*const v8.C_Value, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 = null, + deleter: ?*const fn (c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 = null, const Opts = struct { as_typed_array: bool = false, null_as_undefined: bool = false, }; - fn init(comptime T: type, comptime getter: anytype, comptime opts: Opts) NamedIndexed { - return .{ .getter = struct { + fn init(comptime T: type, comptime getter: anytype, setter: anytype, deleter: anytype, comptime opts: Opts) NamedIndexed { + const getter_fn = struct { fn wrap(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 { const info = v8.PropertyCallbackInfo.initFromV8(raw_info); var caller = Caller.init(info); @@ -238,7 +240,39 @@ pub const NamedIndexed = struct { .null_as_undefined = opts.null_as_undefined, }); } - }.wrap }; + }.wrap; + + const setter_fn = if (@typeInfo(@TypeOf(setter)) == .null) null else struct { + fn wrap(c_name: ?*const v8.C_Name, c_value: ?*const v8.C_Value, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 { + const info = v8.PropertyCallbackInfo.initFromV8(raw_info); + var caller = Caller.init(info); + defer caller.deinit(); + + return caller.setNamedIndex(T, setter, .{ .handle = c_name.? }, .{ .handle = c_value.? }, info, .{ + .as_typed_array = opts.as_typed_array, + .null_as_undefined = opts.null_as_undefined, + }); + } + }.wrap; + + const deleter_fn = if (@typeInfo(@TypeOf(deleter)) == .null) null else struct { + fn wrap(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 { + const info = v8.PropertyCallbackInfo.initFromV8(raw_info); + var caller = Caller.init(info); + defer caller.deinit(); + + return caller.deleteNamedIndex(T, deleter, .{ .handle = c_name.? }, info, .{ + .as_typed_array = opts.as_typed_array, + .null_as_undefined = opts.null_as_undefined, + }); + } + }.wrap; + + return .{ + .getter = getter_fn, + .setter = setter_fn, + .deleter = deleter_fn, + }; } }; @@ -269,7 +303,6 @@ pub const Iterator = struct { } }; - pub const Callable = struct { func: *const fn (?*const v8.C_FunctionCallbackInfo) callconv(.c) void, @@ -278,7 +311,7 @@ pub const Callable = struct { }; fn init(comptime T: type, comptime func: anytype, comptime opts: Opts) Callable { - return .{.func = struct { + return .{ .func = struct { fn wrap(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void { const info = v8.FunctionCallbackInfo.initFromV8(raw_info); var caller = Caller.init(info); @@ -286,8 +319,8 @@ pub const Callable = struct { caller.method(T, func, info, .{ .null_as_undefined = opts.null_as_undefined, }); - }}.wrap - }; + } + }.wrap }; } }; @@ -457,6 +490,7 @@ pub const JsApis = flattenTypes(&.{ @import("../webapi/DOMNodeIterator.zig"), @import("../webapi/NodeFilter.zig"), @import("../webapi/Element.zig"), + @import("../webapi/element/DOMStringMap.zig"), @import("../webapi/element/Attribute.zig"), @import("../webapi/element/Html.zig"), @import("../webapi/element/html/IFrame.zig"), diff --git a/src/browser/tests/element/attributes.html b/src/browser/tests/element/attributes.html index 4f557676a..d4d416f6a 100644 --- a/src/browser/tests/element/attributes.html +++ b/src/browser/tests/element/attributes.html @@ -83,3 +83,22 @@ assertAttributes([{name: 'id', value: 'attr1'}, {name: 'class', value: 'sHow'}]); + + diff --git a/src/browser/tests/element/dataset.html b/src/browser/tests/element/dataset.html new file mode 100644 index 000000000..c9178c743 --- /dev/null +++ b/src/browser/tests/element/dataset.html @@ -0,0 +1,150 @@ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/browser/tests/element/matches.html b/src/browser/tests/element/matches.html new file mode 100644 index 000000000..324453cbd --- /dev/null +++ b/src/browser/tests/element/matches.html @@ -0,0 +1,76 @@ + + + +
+

Paragraph 1

+
+

Paragraph 2

+ +

Paragraph 3

+
+
+
+ + + + + + + + diff --git a/src/browser/webapi/Document.zig b/src/browser/webapi/Document.zig index 70a40767d..bbdd267c2 100644 --- a/src/browser/webapi/Document.zig +++ b/src/browser/webapi/Document.zig @@ -195,11 +195,11 @@ pub const JsApi = struct { pub const querySelectorAll = bridge.function(Document.querySelectorAll, .{ .dom_exception = true }); pub const getElementsByTagName = bridge.function(Document.getElementsByTagName, .{}); pub const getElementsByClassName = bridge.function(Document.getElementsByClassName, .{}); - pub const defaultView = bridge.accessor(struct{ + pub const defaultView = bridge.accessor(struct { fn defaultView(_: *const Document, page: *Page) *@import("Window.zig") { return page.window; } - }.defaultView, null, .{.cache = "defaultView"}); + }.defaultView, null, .{ .cache = "defaultView" }); }; const testing = @import("../../testing.zig"); diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index e00890769..0ca657579 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -12,6 +12,7 @@ const collections = @import("collections.zig"); const Selector = @import("selector/Selector.zig"); pub const Attribute = @import("element/Attribute.zig"); const CSSStyleProperties = @import("css/CSSStyleProperties.zig"); +pub const DOMStringMap = @import("element/DOMStringMap.zig"); pub const Svg = @import("element/Svg.zig"); pub const Html = @import("element/Html.zig"); @@ -247,6 +248,12 @@ pub fn getAttribute(self: *const Element, name: []const u8, page: *Page) !?[]con return attributes.get(name, page); } +pub fn hasAttribute(self: *const Element, name: []const u8, page: *Page) !bool { + const attributes = self._attributes orelse return false; + const value = try attributes.get(name, page); + return value != null; +} + pub fn getAttributeNode(self: *Element, name: []const u8, page: *Page) !?*Attribute { const attributes = self._attributes orelse return null; return attributes.getAttribute(name, self, page); @@ -342,6 +349,16 @@ pub fn getClassList(self: *Element, page: *Page) !*collections.DOMTokenList { }; } +pub fn getDataset(self: *Element, page: *Page) !*DOMStringMap { + const gop = try page._element_datasets.getOrPut(page.arena, self); + if (!gop.found_existing) { + gop.value_ptr.* = try page._factory.create(DOMStringMap{ + ._element = self, + }); + } + return gop.value_ptr.*; +} + pub fn replaceChildren(self: *Element, nodes: []const Node.NodeOrText, page: *Page) !void { page.domChanged(); var parent = self.asNode(); @@ -438,6 +455,10 @@ pub fn getChildElementCount(self: *Element) usize { return count; } +pub fn matches(self: *Element, selector: []const u8, page: *Page) !bool { + return Selector.matches(self, selector, page); +} + pub fn querySelector(self: *Element, selector: []const u8, page: *Page) !?*Element { return Selector.querySelector(self.asNode(), selector, page); } @@ -658,8 +679,10 @@ pub const JsApi = struct { pub const id = bridge.accessor(Element.getId, Element.setId, .{}); pub const className = bridge.accessor(Element.getClassName, Element.setClassName, .{}); pub const classList = bridge.accessor(Element.getClassList, null, .{}); + pub const dataset = bridge.accessor(Element.getDataset, null, .{}); pub const style = bridge.accessor(Element.getStyle, null, .{}); pub const attributes = bridge.accessor(Element.getAttributeNamedNodeMap, null, .{}); + pub const hasAttribute = bridge.function(Element.hasAttribute, .{}); pub const getAttribute = bridge.function(Element.getAttribute, .{}); pub const getAttributeNode = bridge.function(Element.getAttributeNode, .{}); pub const setAttribute = bridge.function(Element.setAttribute, .{}); @@ -676,6 +699,7 @@ pub const JsApi = struct { pub const nextElementSibling = bridge.accessor(Element.nextElementSibling, null, .{}); pub const previousElementSibling = bridge.accessor(Element.previousElementSibling, null, .{}); pub const childElementCount = bridge.accessor(Element.getChildElementCount, null, .{}); + pub const matches = bridge.function(Element.matches, .{ .dom_exception = true }); pub const querySelector = bridge.function(Element.querySelector, .{ .dom_exception = true }); pub const querySelectorAll = bridge.function(Element.querySelectorAll, .{ .dom_exception = true }); pub const getElementsByTagName = bridge.function(Element.getElementsByTagName, .{}); diff --git a/src/browser/webapi/KeyValueList.zig b/src/browser/webapi/KeyValueList.zig index 9f0cca7c0..3b105bd8f 100644 --- a/src/browser/webapi/KeyValueList.zig +++ b/src/browser/webapi/KeyValueList.zig @@ -45,7 +45,7 @@ pub fn get(self: *const KeyValueList, name: []const u8) ?[]const u8 { return null; } -pub fn getAll(self: *const KeyValueList, name: []const u8, page: *Page) ![]const []const u8 { +pub fn getAll(self: *const KeyValueList, name: []const u8, page: *Page) ![]const []const u8 { const arena = page.call_arena; var arr: std.ArrayList([]const u8) = .empty; for (self._entries.items) |*entry| { diff --git a/src/browser/webapi/Node.zig b/src/browser/webapi/Node.zig index cdec332ab..7af7c4fad 100644 --- a/src/browser/webapi/Node.zig +++ b/src/browser/webapi/Node.zig @@ -614,9 +614,7 @@ pub const JsApi = struct { pub const textContent = bridge.accessor(_textContext, Node.setTextContent, .{}); fn _textContext(self: *Node, page: *const Page) !?[]const u8 { - // can't call node.getTextContent directly, because - // 1 - document should return null, not empty - // 2 - cdata and attributes can return value directly, avoiding the copy + // cdata and attributes can return value directly, avoiding the copy switch (self._type) { .element => |el| { var buf = std.Io.Writer.Allocating.init(page.call_arena); diff --git a/src/browser/webapi/Window.zig b/src/browser/webapi/Window.zig index 0ea5fc06c..605b9fdc7 100644 --- a/src/browser/webapi/Window.zig +++ b/src/browser/webapi/Window.zig @@ -149,7 +149,7 @@ pub fn cancelAnimationFrame(self: *Window, id: u32) void { pub fn reportError(self: *Window, err: js.Object, page: *Page) !void { const error_event = try ErrorEvent.init("error", .{ - .@"error" = err, + .@"error" = err, .message = err.toString() catch "Unknown error", .bubbles = false, .cancelable = true, diff --git a/src/browser/webapi/collections/HTMLAllCollection.zig b/src/browser/webapi/collections/HTMLAllCollection.zig index 9e883ad5a..60aba31cf 100644 --- a/src/browser/webapi/collections/HTMLAllCollection.zig +++ b/src/browser/webapi/collections/HTMLAllCollection.zig @@ -152,7 +152,7 @@ pub const JsApi = struct { pub const length = bridge.accessor(HTMLAllCollection.length, null, .{}); pub const @"[int]" = bridge.indexed(HTMLAllCollection.getAtIndex, .{ .null_as_undefined = true }); - pub const @"[str]" = bridge.namedIndexed(HTMLAllCollection.getByName, .{ .null_as_undefined = true }); + pub const @"[str]" = bridge.namedIndexed(HTMLAllCollection.getByName, null, null, .{ .null_as_undefined = true }); pub const item = bridge.function(_item, .{}); fn _item(self: *HTMLAllCollection, index: i32, page: *Page) ?*Element { diff --git a/src/browser/webapi/collections/HTMLCollection.zig b/src/browser/webapi/collections/HTMLCollection.zig index 134ac5cc7..34d2f0713 100644 --- a/src/browser/webapi/collections/HTMLCollection.zig +++ b/src/browser/webapi/collections/HTMLCollection.zig @@ -83,7 +83,7 @@ pub const JsApi = struct { pub const length = bridge.accessor(HTMLCollection.length, null, .{}); pub const @"[int]" = bridge.indexed(HTMLCollection.getAtIndex, .{ .null_as_undefined = true }); - pub const @"[str]" = bridge.namedIndexed(HTMLCollection.getByName, .{ .null_as_undefined = true }); + pub const @"[str]" = bridge.namedIndexed(HTMLCollection.getByName, null, null, .{ .null_as_undefined = true }); pub const item = bridge.function(_item, .{}); fn _item(self: *HTMLCollection, index: i32, page: *Page) ?*Element { diff --git a/src/browser/webapi/css/CSSStyleProperties.zig b/src/browser/webapi/css/CSSStyleProperties.zig index d0b4a6087..2eeefff64 100644 --- a/src/browser/webapi/css/CSSStyleProperties.zig +++ b/src/browser/webapi/css/CSSStyleProperties.zig @@ -120,7 +120,7 @@ pub const JsApi = struct { pub var class_id: bridge.ClassId = undefined; }; - pub const @"[]" = bridge.namedIndexed(_getPropertyIndexed, .{}); + pub const @"[]" = bridge.namedIndexed(_getPropertyIndexed, null, null, .{}); const method_names = std.StaticStringMap(void).initComptime(.{ .{ "getPropertyValue", {} }, diff --git a/src/browser/webapi/css/MediaQueryList.zig b/src/browser/webapi/css/MediaQueryList.zig index 25f813b34..f67e754c7 100644 --- a/src/browser/webapi/css/MediaQueryList.zig +++ b/src/browser/webapi/css/MediaQueryList.zig @@ -31,7 +31,7 @@ pub const JsApi = struct { pub const Meta = struct { pub const name = "MediaQueryList"; pub const prototype_chain = bridge.prototypeChain(); - pub var class_id: bridge.ClassId = undefined; + pub var class_id: bridge.ClassId = undefined; }; pub const media = bridge.accessor(MediaQueryList.getMedia, null, .{}); diff --git a/src/browser/webapi/element/Attribute.zig b/src/browser/webapi/element/Attribute.zig index dce7655ca..85d6bf6d5 100644 --- a/src/browser/webapi/element/Attribute.zig +++ b/src/browser/webapi/element/Attribute.zig @@ -386,7 +386,7 @@ pub const NamedNodeMap = struct { pub const length = bridge.accessor(NamedNodeMap.length, null, .{}); pub const @"[int]" = bridge.indexed(NamedNodeMap.getAtIndex, .{ .null_as_undefined = true }); - pub const @"[str]" = bridge.namedIndexed(NamedNodeMap.getByName, .{ .null_as_undefined = true }); + pub const @"[str]" = bridge.namedIndexed(NamedNodeMap.getByName, null, null, .{ .null_as_undefined = true }); pub const getNamedItem = bridge.function(NamedNodeMap.getByName, .{}); pub const item = bridge.function(_item, .{}); fn _item(self: *const NamedNodeMap, index: i32, page: *Page) !?*Attribute { diff --git a/src/browser/webapi/element/DOMStringMap.zig b/src/browser/webapi/element/DOMStringMap.zig new file mode 100644 index 000000000..b2aa8babf --- /dev/null +++ b/src/browser/webapi/element/DOMStringMap.zig @@ -0,0 +1,87 @@ +const std = @import("std"); +const js = @import("../../js/js.zig"); + +const Element = @import("../Element.zig"); +const Page = @import("../../Page.zig"); + +const Allocator = std.mem.Allocator; + +const DOMStringMap = @This(); + +_element: *Element, + +fn _getProperty(self: *DOMStringMap, name: []const u8, page: *Page) !?[]const u8 { + const attr_name = try camelToKebab(page.call_arena, name); + return try self._element.getAttribute(attr_name, page); +} + +fn _setProperty(self: *DOMStringMap, name: []const u8, value: []const u8, page: *Page) !void { + const attr_name = try camelToKebab(page.call_arena, name); + return self._element.setAttributeSafe(attr_name, value, page); +} + +fn _deleteProperty(self: *DOMStringMap, name: []const u8, page: *Page) !void { + const attr_name = try camelToKebab(page.call_arena, name); + try self._element.removeAttribute(attr_name, page); +} + +// fooBar -> foo-bar +fn camelToKebab(arena: Allocator, camel: []const u8) ![]const u8 { + var result: std.ArrayList(u8) = .empty; + try result.ensureTotalCapacity(arena, 5 + camel.len * 2); + result.appendSliceAssumeCapacity("data-"); + + for (camel, 0..) |c, i| { + if (std.ascii.isUpper(c)) { + if (i > 0) { + result.appendAssumeCapacity('-'); + } + result.appendAssumeCapacity(std.ascii.toLower(c)); + } else { + result.appendAssumeCapacity(c); + } + } + + return result.items; +} + +// data-foo-bar -> fooBar +fn kebabToCamel(arena: Allocator, kebab: []const u8) !?[]const u8 { + if (!std.mem.startsWith(u8, kebab, "data-")) { + return null; + } + + const data_part = kebab[5..]; // Skip "data-" + if (data_part.len == 0) { + return null; + } + + var result: std.ArrayList(u8) = .empty; + try result.ensureTotalCapacity(arena, data_part.len); + + var capitalize_next = false; + for (data_part) |c| { + if (c == '-') { + capitalize_next = true; + } else if (capitalize_next) { + result.appendAssumeCapacity(std.ascii.toUpper(c)); + capitalize_next = false; + } else { + result.appendAssumeCapacity(c); + } + } + + return result.items; +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(DOMStringMap); + + pub const Meta = struct { + pub const name = "DOMStringMap"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; + + pub const @"[]" = bridge.namedIndexed(_getProperty, _setProperty, _deleteProperty, .{.null_as_undefined = true}); +}; diff --git a/src/browser/webapi/element/html/Body.zig b/src/browser/webapi/element/html/Body.zig index 585e74d63..cf04a9397 100644 --- a/src/browser/webapi/element/html/Body.zig +++ b/src/browser/webapi/element/html/Body.zig @@ -33,7 +33,7 @@ pub const Build = struct { const el = node.as(Element); const on_load = el.getAttributeSafe("onload") orelse return; page.window._on_load = page.js.stringToFunction(on_load) catch |err| blk: { - log.err(.js, "body.onload", .{.err = err, .str = on_load}); + log.err(.js, "body.onload", .{ .err = err, .str = on_load }); break :blk null; }; } diff --git a/src/browser/webapi/element/html/Script.zig b/src/browser/webapi/element/html/Script.zig index df224ae25..6bf306d3c 100644 --- a/src/browser/webapi/element/html/Script.zig +++ b/src/browser/webapi/element/html/Script.zig @@ -80,14 +80,14 @@ pub const Build = struct { if (element.getAttributeSafe("onload")) |on_load| { self._on_load = page.js.stringToFunction(on_load) catch |err| blk: { - log.err(.js, "script.onload", .{.err = err, .str = on_load}); + log.err(.js, "script.onload", .{ .err = err, .str = on_load }); break :blk null; }; } if (element.getAttributeSafe("onerror")) |on_error| { self._on_error = page.js.stringToFunction(on_error) catch |err| blk: { - log.err(.js, "script.onerror", .{.err = err, .str = on_error}); + log.err(.js, "script.onerror", .{ .err = err, .str = on_error }); break :blk null; }; } diff --git a/src/browser/webapi/selector/Parser.zig b/src/browser/webapi/selector/Parser.zig index 0e88df0df..335f22456 100644 --- a/src/browser/webapi/selector/Parser.zig +++ b/src/browser/webapi/selector/Parser.zig @@ -77,7 +77,6 @@ pub fn parse(arena: Allocator, input: []const u8, page: *Page) ParseError!Select var segments: std.ArrayList(Segment) = .empty; var current_compound: std.ArrayList(Part) = .empty; - // Parse the first compound (no combinator before it) while (parser.skipSpaces()) { if (parser.peek() == 0) break; From c5a1d8a8bdb398e196ef30927d089dffe0b1107e Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 13 Nov 2025 20:18:34 +0800 Subject: [PATCH 19/30] Element.checkVisibility and Element.checkVisibility --- src/browser/js/bridge.zig | 1 + src/browser/webapi/DOMRect.zig | 64 +++++++++ src/browser/webapi/Element.zig | 124 ++++++++++++++++++ src/browser/webapi/css.zig | 14 ++ .../webapi/css/CSSStyleDeclaration.zig | 4 +- src/browser/webapi/css/CSSStyleProperties.zig | 2 +- 6 files changed, 206 insertions(+), 3 deletions(-) create mode 100644 src/browser/webapi/DOMRect.zig create mode 100644 src/browser/webapi/css.zig diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index fe1d4ec12..4fb65b705 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -488,6 +488,7 @@ pub const JsApis = flattenTypes(&.{ @import("../webapi/DOMImplementation.zig"), @import("../webapi/DOMTreeWalker.zig"), @import("../webapi/DOMNodeIterator.zig"), + @import("../webapi/DOMRect.zig"), @import("../webapi/NodeFilter.zig"), @import("../webapi/Element.zig"), @import("../webapi/element/DOMStringMap.zig"), diff --git a/src/browser/webapi/DOMRect.zig b/src/browser/webapi/DOMRect.zig new file mode 100644 index 000000000..6309a20ea --- /dev/null +++ b/src/browser/webapi/DOMRect.zig @@ -0,0 +1,64 @@ +const DOMRect = @This(); + +const js = @import("../js/js.zig"); +const Page = @import("../Page.zig"); + +_x: f64, +_y: f64, +_width: f64, +_height: f64, +_top: f64, +_right: f64, +_bottom: f64, +_left: f64, + +pub fn getX(self: *DOMRect) f64 { + return self._x; +} + +pub fn getY(self: *DOMRect) f64 { + return self._y; +} + +pub fn getWidth(self: *DOMRect) f64 { + return self._width; +} + +pub fn getHeight(self: *DOMRect) f64 { + return self._height; +} + +pub fn getTop(self: *DOMRect) f64 { + return self._top; +} + +pub fn getRight(self: *DOMRect) f64 { + return self._right; +} + +pub fn getBottom(self: *DOMRect) f64 { + return self._bottom; +} + +pub fn getLeft(self: *DOMRect) f64 { + return self._left; +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(DOMRect); + + pub const Meta = struct { + pub const name = "DOMRect"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; + + pub const x = bridge.accessor(DOMRect.getX, null, .{}); + pub const y = bridge.accessor(DOMRect.getY, null, .{}); + pub const width = bridge.accessor(DOMRect.getWidth, null, .{}); + pub const height = bridge.accessor(DOMRect.getHeight, null, .{}); + pub const top = bridge.accessor(DOMRect.getTop, null, .{}); + pub const right = bridge.accessor(DOMRect.getRight, null, .{}); + pub const bottom = bridge.accessor(DOMRect.getBottom, null, .{}); + pub const left = bridge.accessor(DOMRect.getLeft, null, .{}); +}; diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index 0ca657579..7849e3e37 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -13,6 +13,8 @@ const Selector = @import("selector/Selector.zig"); pub const Attribute = @import("element/Attribute.zig"); const CSSStyleProperties = @import("css/CSSStyleProperties.zig"); pub const DOMStringMap = @import("element/DOMStringMap.zig"); +const DOMRect = @import("DOMRect.zig"); +const css = @import("css.zig"); pub const Svg = @import("element/Svg.zig"); pub const Html = @import("element/Html.zig"); @@ -467,6 +469,126 @@ pub fn querySelectorAll(self: *Element, input: []const u8, page: *Page) !*Select return Selector.querySelectorAll(self.asNode(), input, page); } +pub fn parentElement(self: *Element) ?*Element { + return self._proto.parentElement(); +} + +pub fn checkVisibility(self: *Element, page: *Page) !bool { + var current: ?*Element = self; + + while (current) |el| { + const style = try el.getStyle(page); + const display = style.asCSSStyleDeclaration().getPropertyValue("display", page); + if (std.mem.eql(u8, display, "none")) { + return false; + } + current = el.parentElement(); + } + + return true; +} + +pub fn getBoundingClientRect(self: *Element, page: *Page) !*DOMRect { + const is_visible = try self.checkVisibility(page); + if (!is_visible) { + return page._factory.create(DOMRect{ + ._x = 0.0, + ._y = 0.0, + ._width = 0.0, + ._height = 0.0, + ._top = 0.0, + ._right = 0.0, + ._bottom = 0.0, + ._left = 0.0, + }); + } + + const y = calculateDocumentPosition(self.asNode()); + + var width: f64 = 1.0; + var height: f64 = 1.0; + + const style = try self.getStyle(page); + const decl = style.asCSSStyleDeclaration(); + width = css.parseDimension(decl.getPropertyValue("width", page)) orelse 1.0; + height = css.parseDimension(decl.getPropertyValue("height", page)) orelse 1.0; + + if (width == 1.0 or height == 1.0) { + const tag = self.getTag(); + if (tag == .img or tag == .iframe) { + if (self.getAttributeSafe("width")) |w| { + width = std.fmt.parseFloat(f64, w) catch width; + } + if (self.getAttributeSafe("height")) |h| { + height = std.fmt.parseFloat(f64, h) catch height; + } + } + } + + const x: f64 = 0.0; + const top = y; + const left = x; + const right = x + width; + const bottom = y + height; + + return page._factory.create(DOMRect{ + ._x = x, + ._y = y, + ._width = width, + ._height = height, + ._top = top, + ._right = right, + ._bottom = bottom, + ._left = left, + }); +} + +// Calculates a pseudo-position in the document using an efficient heuristic. +// +// Instead of walking the entire DOM tree (which would be O(total_nodes)), this +// function walks UP the tree counting previous siblings at each level. Each level +// uses exponential weighting (1000x per depth level) to preserve document order. +// +// This gives O(depth * avg_siblings) complexity while maintaining relative positioning +// that's useful for scraping and understanding element flow in the document. +// +// Example: +// → position 0 +//
→ position 0 (0 siblings at level 1) +// → position 0 (0 siblings at level 2) +// → position 1 (1 sibling at level 2) +//
+//
→ position 1000 (1 sibling at level 1, weighted by 1000) +//

→ position 1000 (0 siblings at level 2, parent has 1000) +//
+// +// +// Trade-offs: +// - Much faster than full tree-walking for deep/large DOMs +// - Positions reflect document order and parent-child relationships +// - Not pixel-accurate, but sufficient for 1x1 layout heuristics +fn calculateDocumentPosition(node: *Node) f64 { + var position: f64 = 0.0; + var multiplier: f64 = 1.0; + var current = node; + + while (current.parentNode()) |parent| { + var count: f64 = 0.0; + var sibling = parent.firstChild(); + while (sibling) |s| { + if (s == current) break; + count += 1.0; + sibling = s.nextSibling(); + } + + position += count * multiplier; + multiplier *= 1000.0; + current = parent; + } + + return position; +} + const GetElementsByTagNameResult = union(enum) { tag: collections.NodeLive(.tag), tag_name: collections.NodeLive(.tag_name), @@ -702,6 +824,8 @@ pub const JsApi = struct { pub const matches = bridge.function(Element.matches, .{ .dom_exception = true }); pub const querySelector = bridge.function(Element.querySelector, .{ .dom_exception = true }); pub const querySelectorAll = bridge.function(Element.querySelectorAll, .{ .dom_exception = true }); + pub const checkVisibility = bridge.function(Element.checkVisibility, .{}); + pub const getBoundingClientRect = bridge.function(Element.getBoundingClientRect, .{}); pub const getElementsByTagName = bridge.function(Element.getElementsByTagName, .{}); pub const getElementsByClassName = bridge.function(Element.getElementsByClassName, .{}); pub const children = bridge.accessor(Element.getChildren, null, .{}); diff --git a/src/browser/webapi/css.zig b/src/browser/webapi/css.zig new file mode 100644 index 000000000..ea4b1e908 --- /dev/null +++ b/src/browser/webapi/css.zig @@ -0,0 +1,14 @@ +const std = @import("std"); + +pub fn parseDimension(value: []const u8) ?f64 { + if (value.len == 0) { + return null; + } + + var num_str = value; + if (std.mem.endsWith(u8, value, "px")) { + num_str = value[0 .. value.len - 2]; + } + + return std.fmt.parseFloat(f64, num_str) catch null; +} diff --git a/src/browser/webapi/css/CSSStyleDeclaration.zig b/src/browser/webapi/css/CSSStyleDeclaration.zig index 1b8c8424a..36569866d 100644 --- a/src/browser/webapi/css/CSSStyleDeclaration.zig +++ b/src/browser/webapi/css/CSSStyleDeclaration.zig @@ -57,13 +57,13 @@ pub fn item(self: *const CSSStyleDeclaration, index: u32) []const u8 { return ""; } -pub fn getPropertyValue(self: *const CSSStyleDeclaration, property_name: []const u8, page: *Page) ![]const u8 { +pub fn getPropertyValue(self: *const CSSStyleDeclaration, property_name: []const u8, page: *Page) []const u8 { const normalized = normalizePropertyName(property_name, &page.buf); const prop = self.findProperty(normalized) orelse return ""; return prop._value.str(); } -pub fn getPropertyPriority(self: *const CSSStyleDeclaration, property_name: []const u8, page: *Page) ![]const u8 { +pub fn getPropertyPriority(self: *const CSSStyleDeclaration, property_name: []const u8, page: *Page) []const u8 { const normalized = normalizePropertyName(property_name, &page.buf); const prop = self.findProperty(normalized) orelse return ""; return if (prop._important) "important" else ""; diff --git a/src/browser/webapi/css/CSSStyleProperties.zig b/src/browser/webapi/css/CSSStyleProperties.zig index 2eeefff64..1de71ea4e 100644 --- a/src/browser/webapi/css/CSSStyleProperties.zig +++ b/src/browser/webapi/css/CSSStyleProperties.zig @@ -154,7 +154,7 @@ pub const JsApi = struct { } } - const value = try self._proto.getPropertyValue(dash_case, page); + const value = self._proto.getPropertyValue(dash_case, page); // Property accessors have special handling for empty values: // - Known CSS properties return '' when not set From 7a5cade51029b72a3e5c3a2ee2f1d35ef7257f5c Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 13 Nov 2025 20:30:02 +0800 Subject: [PATCH 20/30] remove 16 bytes from Element --- src/browser/Page.zig | 12 +++++++--- src/browser/webapi/Element.zig | 26 +++++++++++---------- src/browser/webapi/element/DOMStringMap.zig | 2 +- 3 files changed, 24 insertions(+), 16 deletions(-) diff --git a/src/browser/Page.zig b/src/browser/Page.zig index fa1c9fdd8..47d5f1238 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -63,8 +63,11 @@ _attribute_lookup: std.AutoHashMapUnmanaged(usize, *Element.Attribute), // the return of elements.attributes. _attribute_named_node_map_lookup: std.AutoHashMapUnmanaged(usize, *Element.Attribute.NamedNodeMap), -// element.dataset -> DOMStringMap -_element_datasets: std.AutoHashMapUnmanaged(*Element, *Element.DOMStringMap), +// Lazily-created style, classList, and dataset objects. Only stored for elements +// that actually access these features via JavaScript, saving 24 bytes per element. +_element_styles: Element.StyleLookup = .{}, +_element_datasets: Element.DatasetLookup = .{}, +_element_class_lists: Element.ClassListLookup = .{}, _script_manager: ScriptManager, @@ -155,7 +158,6 @@ fn reset(self: *Page, comptime initializing: bool) !void { self._load_state = .parsing; self._attribute_lookup = .empty; self._attribute_named_node_map_lookup = .empty; - self._element_datasets = .empty; self._event_manager = EventManager.init(self); self._script_manager = ScriptManager.init(self); @@ -164,6 +166,10 @@ fn reset(self: *Page, comptime initializing: bool) !void { self.js = try self._session.executor.createContext(self, true, JS.GlobalMissingCallback.init(&self._polyfill_loader)); errdefer self.js.deinit(); + self._element_styles = .{}; + self._element_datasets = .{}; + self._element_class_lists = .{}; + try polyfill.preload(self.arena, self.js); try self.registerBackgroundTasks(); } diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index 7849e3e37..c29ad4c77 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -21,6 +21,10 @@ pub const Html = @import("element/Html.zig"); const Element = @This(); +pub const DatasetLookup = std.AutoHashMapUnmanaged(*Element, *DOMStringMap); +pub const StyleLookup = std.AutoHashMapUnmanaged(*Element, *CSSStyleProperties); +pub const ClassListLookup = std.AutoHashMapUnmanaged(*Element, *collections.DOMTokenList); + pub const Namespace = enum(u8) { html, svg, @@ -41,8 +45,6 @@ _type: Type, _proto: *Node, _namespace: Namespace = .html, _attributes: ?*Attribute.List = null, -_style: ?*CSSStyleProperties = null, -_class_list: ?*collections.DOMTokenList = null, pub const Type = union(enum) { html: *Html, @@ -333,22 +335,22 @@ pub fn getAttributeNamedNodeMap(self: *Element, page: *Page) !*Attribute.NamedNo } pub fn getStyle(self: *Element, page: *Page) !*CSSStyleProperties { - return self._style orelse blk: { - const s = try CSSStyleProperties.init(self, page); - self._style = s; - break :blk s; - }; + const gop = try page._element_styles.getOrPut(page.arena, self); + if (!gop.found_existing) { + gop.value_ptr.* = try CSSStyleProperties.init(self, page); + } + return gop.value_ptr.*; } pub fn getClassList(self: *Element, page: *Page) !*collections.DOMTokenList { - return self._class_list orelse blk: { - const cl = try page._factory.create(collections.DOMTokenList{ + const gop = try page._element_class_lists.getOrPut(page.arena, self); + if (!gop.found_existing) { + gop.value_ptr.* = try page._factory.create(collections.DOMTokenList{ ._element = self, ._attribute_name = "class", }); - self._class_list = cl; - break :blk cl; - }; + } + return gop.value_ptr.*; } pub fn getDataset(self: *Element, page: *Page) !*DOMStringMap { diff --git a/src/browser/webapi/element/DOMStringMap.zig b/src/browser/webapi/element/DOMStringMap.zig index b2aa8babf..4fd029552 100644 --- a/src/browser/webapi/element/DOMStringMap.zig +++ b/src/browser/webapi/element/DOMStringMap.zig @@ -83,5 +83,5 @@ pub const JsApi = struct { pub var class_id: bridge.ClassId = undefined; }; - pub const @"[]" = bridge.namedIndexed(_getProperty, _setProperty, _deleteProperty, .{.null_as_undefined = true}); + pub const @"[]" = bridge.namedIndexed(_getProperty, _setProperty, _deleteProperty, .{ .null_as_undefined = true }); }; From 6cf01631adda909c105e059c955ce6d750ffe8e1 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 13 Nov 2025 20:37:00 +0800 Subject: [PATCH 21/30] Document.activeElement, focus and blur --- src/browser/tests/document/focus.html | 81 +++++++++++++++++++++++++++ src/browser/webapi/Document.zig | 18 ++++++ src/browser/webapi/Element.zig | 32 +++++++++++ 3 files changed, 131 insertions(+) create mode 100644 src/browser/tests/document/focus.html diff --git a/src/browser/tests/document/focus.html b/src/browser/tests/document/focus.html new file mode 100644 index 000000000..5b7b7c078 --- /dev/null +++ b/src/browser/tests/document/focus.html @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + diff --git a/src/browser/webapi/Document.zig b/src/browser/webapi/Document.zig index bbdd267c2..5d58d3aba 100644 --- a/src/browser/webapi/Document.zig +++ b/src/browser/webapi/Document.zig @@ -24,6 +24,7 @@ _location: ?*Location = null, _ready_state: ReadyState = .loading, _current_script: ?*Element.Html.Script = null, _elements_by_id: std.StringHashMapUnmanaged(*Element) = .empty, +_active_element: ?*Element = null, pub const Type = union(enum) { generic, @@ -155,6 +156,22 @@ pub fn getReadyState(self: *const Document) []const u8 { return @tagName(self._ready_state); } +pub fn getActiveElement(self: *Document) ?*Element { + if (self._active_element) |el| { + return el; + } + + // Default to body if it exists + if (self.is(HTMLDocument)) |html_doc| { + if (html_doc.getBody()) |body| { + return body.asElement(); + } + } + + // Fallback to document element + return self.getDocumentElement(); +} + const ReadyState = enum { loading, interactive, @@ -182,6 +199,7 @@ pub const JsApi = struct { pub const documentElement = bridge.accessor(Document.getDocumentElement, null, .{}); pub const readyState = bridge.accessor(Document.getReadyState, null, .{}); pub const implementation = bridge.accessor(Document.getImplementation, null, .{}); + pub const activeElement = bridge.accessor(Document.getActiveElement, null, .{}); pub const createElement = bridge.function(Document.createElement, .{}); pub const createElementNS = bridge.function(Document.createElementNS, .{}); diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index c29ad4c77..1c55e6f34 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -82,6 +82,10 @@ pub fn asNode(self: *Element) *Node { return self._proto; } +pub fn asEventTarget(self: *Element) *@import("EventTarget.zig") { + return self._proto.asEventTarget(); +} + pub fn asConstNode(self: *const Element) *const Node { return self._proto; } @@ -390,6 +394,32 @@ pub fn remove(self: *Element, page: *Page) void { page.removeNode(parent, node, .{ .will_be_reconnected = false }); } +pub fn focus(self: *Element, page: *Page) !void { + const Event = @import("Event.zig"); + + if (page.document._active_element) |old| { + if (old == self) return; + + const blur_event = try Event.init("blur", null, page); + try page._event_manager.dispatch(old.asEventTarget(), blur_event); + } + + page.document._active_element = self; + + const focus_event = try Event.init("focus", null, page); + try page._event_manager.dispatch(self.asEventTarget(), focus_event); +} + +pub fn blur(self: *Element, page: *Page) !void { + if (page.document._active_element != self) return; + + page.document._active_element = null; + + const Event = @import("Event.zig"); + const blur_event = try Event.init("blur", null, page); + try page._event_manager.dispatch(self.asEventTarget(), blur_event); +} + pub fn getChildren(self: *Element, page: *Page) !collections.NodeLive(.child_elements) { return collections.NodeLive(.child_elements).init(null, self.asNode(), {}, page); } @@ -831,6 +861,8 @@ pub const JsApi = struct { pub const getElementsByTagName = bridge.function(Element.getElementsByTagName, .{}); pub const getElementsByClassName = bridge.function(Element.getElementsByClassName, .{}); pub const children = bridge.accessor(Element.getChildren, null, .{}); + pub const focus = bridge.function(Element.focus, .{}); + pub const blur = bridge.function(Element.blur, .{}); }; pub const Build = struct { From 6742646e89127cda58d42a2f99cfec8fa755e1c4 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 13 Nov 2025 20:57:17 +0800 Subject: [PATCH 22/30] DOMParser --- src/browser/js/bridge.zig | 1 + src/browser/tests/domparser.html | 121 +++++++++++++++++++++++++++++++ src/browser/webapi/DOMParser.zig | 57 +++++++++++++++ src/browser/webapi/Document.zig | 7 +- 4 files changed, 184 insertions(+), 2 deletions(-) create mode 100644 src/browser/tests/domparser.html create mode 100644 src/browser/webapi/DOMParser.zig diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index 4fb65b705..08bb93ca6 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -489,6 +489,7 @@ pub const JsApis = flattenTypes(&.{ @import("../webapi/DOMTreeWalker.zig"), @import("../webapi/DOMNodeIterator.zig"), @import("../webapi/DOMRect.zig"), + @import("../webapi/DOMParser.zig"), @import("../webapi/NodeFilter.zig"), @import("../webapi/Element.zig"), @import("../webapi/element/DOMStringMap.zig"), diff --git a/src/browser/tests/domparser.html b/src/browser/tests/domparser.html new file mode 100644 index 000000000..390f7bfe7 --- /dev/null +++ b/src/browser/tests/domparser.html @@ -0,0 +1,121 @@ + + + + + + + + diff --git a/src/browser/webapi/DOMParser.zig b/src/browser/webapi/DOMParser.zig new file mode 100644 index 000000000..df87f915d --- /dev/null +++ b/src/browser/webapi/DOMParser.zig @@ -0,0 +1,57 @@ +const std = @import("std"); + +const js = @import("../js/js.zig"); +const Page = @import("../Page.zig"); +const Document = @import("Document.zig"); +const HTMLDocument = @import("HTMLDocument.zig"); + +const DOMParser = @This(); +// @ZIGDOM support empty structs +_: u8 = 0, + +pub fn init() DOMParser { + return .{}; +} + +pub fn parseFromString(self: *const DOMParser, html: []const u8, mime_type: []const u8, page: *Page) !*HTMLDocument { + _ = self; + + // For now, only support text/html + if (!std.mem.eql(u8, mime_type, "text/html")) { + return error.NotSupported; + } + + // Create a new HTMLDocument + const doc = try page._factory.document(HTMLDocument{ + ._proto = undefined, + }); + + // Parse HTML into the document + const Parser = @import("../parser/Parser.zig"); + var parser = Parser.init(page.arena, doc.asNode(), page); + parser.parse(html); + + if (parser.err) |pe| { + return pe.err; + } + + return doc; +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(DOMParser); + + pub const Meta = struct { + pub const name = "DOMParser"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; + + pub const constructor = bridge.constructor(DOMParser.init, .{}); + pub const parseFromString = bridge.function(DOMParser.parseFromString, .{}); +}; + +const testing = @import("../../testing.zig"); +test "WebApi: DOMParser" { + try testing.htmlRunner("domparser.html", .{}); +} diff --git a/src/browser/webapi/Document.zig b/src/browser/webapi/Document.zig index 5d58d3aba..767acf324 100644 --- a/src/browser/webapi/Document.zig +++ b/src/browser/webapi/Document.zig @@ -122,8 +122,11 @@ pub fn querySelectorAll(self: *Document, input: []const u8, page: *Page) !*Selec return Selector.querySelectorAll(self.asNode(), input, page); } -pub fn className(_: *const Document) []const u8 { - return "[object Document]"; +pub fn className(self: *const Document) []const u8 { + return switch (self._type) { + .generic => "[object Document]", + .html => "[object HTMLDocument]", + }; } pub fn getImplementation(_: *const Document) DOMImplementation { From 1164da5e7aa4fee56e36aa9b884b026b55872567 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Fri, 14 Nov 2025 10:46:20 +0800 Subject: [PATCH 23/30] copyright notices --- src/App.zig | 18 ++++++++++++++ src/Notification.zig | 18 ++++++++++++++ src/Scheduler.zig | 18 ++++++++++++++ src/Server.zig | 2 +- src/TestHTTPServer.zig | 18 ++++++++++++++ src/browser/EventManager.zig | 18 ++++++++++++++ src/browser/Factory.zig | 18 ++++++++++++++ src/browser/Page.zig | 18 ++++++++++++++ src/browser/Scheduler.zig | 18 ++++++++++++++ src/browser/URL.zig | 18 ++++++++++++++ src/browser/dump.zig | 18 ++++++++++++++ src/browser/js/bridge.zig | 1 - src/browser/parser/Parser.zig | 19 +++++++++++++++ src/browser/parser/html5ever.zig | 19 +++++++++++++++ src/browser/reflect.zig | 18 ++++++++++++++ src/browser/webapi/AbortController.zig | 18 ++++++++++++++ src/browser/webapi/AbortSignal.zig | 18 ++++++++++++++ src/browser/webapi/CData.zig | 18 ++++++++++++++ src/browser/webapi/Console.zig | 18 ++++++++++++++ src/browser/webapi/Crypto.zig | 18 ++++++++++++++ src/browser/webapi/DOMException.zig | 18 ++++++++++++++ src/browser/webapi/DOMImplementation.zig | 18 ++++++++++++++ src/browser/webapi/DOMNodeIterator.zig | 18 ++++++++++++++ src/browser/webapi/DOMParser.zig | 18 ++++++++++++++ src/browser/webapi/DOMRect.zig | 18 ++++++++++++++ src/browser/webapi/DOMTreeWalker.zig | 18 ++++++++++++++ src/browser/webapi/Document.zig | 18 ++++++++++++++ src/browser/webapi/DocumentFragment.zig | 18 ++++++++++++++ src/browser/webapi/DocumentType.zig | 18 ++++++++++++++ src/browser/webapi/Element.zig | 18 ++++++++++++++ src/browser/webapi/Event.zig | 18 ++++++++++++++ src/browser/webapi/EventTarget.zig | 18 ++++++++++++++ src/browser/webapi/HTMLDocument.zig | 18 ++++++++++++++ src/browser/webapi/History.zig | 18 ++++++++++++++ src/browser/webapi/KeyValueList.zig | 18 ++++++++++++++ src/browser/webapi/Location.zig | 18 ++++++++++++++ src/browser/webapi/MutationObserver.zig | 18 ++++++++++++++ src/browser/webapi/Navigator.zig | 18 ++++++++++++++ src/browser/webapi/Node.zig | 18 ++++++++++++++ src/browser/webapi/NodeFilter.zig | 18 ++++++++++++++ src/browser/webapi/TreeWalker.zig | 18 ++++++++++++++ src/browser/webapi/URL.zig | 18 ++++++++++++++ src/browser/webapi/Window.zig | 24 ++++++++++++++----- src/browser/webapi/cdata/Comment.zig | 18 ++++++++++++++ src/browser/webapi/cdata/Text.zig | 18 ++++++++++++++ src/browser/webapi/collections.zig | 18 ++++++++++++++ src/browser/webapi/collections/ChildNodes.zig | 18 ++++++++++++++ .../webapi/collections/DOMTokenList.zig | 18 ++++++++++++++ .../webapi/collections/HTMLAllCollection.zig | 18 ++++++++++++++ .../webapi/collections/HTMLCollection.zig | 18 ++++++++++++++ src/browser/webapi/collections/NodeList.zig | 18 ++++++++++++++ src/browser/webapi/collections/iterator.zig | 18 ++++++++++++++ src/browser/webapi/collections/node_live.zig | 18 ++++++++++++++ src/browser/webapi/css.zig | 18 ++++++++++++++ .../webapi/css/CSSStyleDeclaration.zig | 18 ++++++++++++++ src/browser/webapi/css/CSSStyleProperties.zig | 18 ++++++++++++++ src/browser/webapi/css/MediaQueryList.zig | 18 ++++++++++++++ src/browser/webapi/element/Attribute.zig | 18 ++++++++++++++ src/browser/webapi/element/DOMStringMap.zig | 18 ++++++++++++++ src/browser/webapi/element/Html.zig | 18 ++++++++++++++ src/browser/webapi/element/Svg.zig | 18 ++++++++++++++ src/browser/webapi/element/html/Anchor.zig | 18 ++++++++++++++ src/browser/webapi/element/html/Body.zig | 18 ++++++++++++++ src/browser/webapi/element/html/Button.zig | 18 ++++++++++++++ src/browser/webapi/element/html/Custom.zig | 18 ++++++++++++++ src/browser/webapi/element/html/Div.zig | 18 ++++++++++++++ src/browser/webapi/element/html/Form.zig | 18 ++++++++++++++ src/browser/webapi/element/html/Generic.zig | 18 ++++++++++++++ src/browser/webapi/element/html/HR.zig | 18 ++++++++++++++ src/browser/webapi/element/html/Head.zig | 18 ++++++++++++++ src/browser/webapi/element/html/Heading.zig | 18 ++++++++++++++ src/browser/webapi/element/html/Html.zig | 18 ++++++++++++++ src/browser/webapi/element/html/IFrame.zig | 18 ++++++++++++++ src/browser/webapi/element/html/Image.zig | 18 ++++++++++++++ src/browser/webapi/element/html/Input.zig | 18 ++++++++++++++ src/browser/webapi/element/html/LI.zig | 18 ++++++++++++++ src/browser/webapi/element/html/Link.zig | 18 ++++++++++++++ src/browser/webapi/element/html/Meta.zig | 18 ++++++++++++++ src/browser/webapi/element/html/OL.zig | 18 ++++++++++++++ src/browser/webapi/element/html/Option.zig | 18 ++++++++++++++ src/browser/webapi/element/html/Paragraph.zig | 18 ++++++++++++++ src/browser/webapi/element/html/Script.zig | 18 ++++++++++++++ src/browser/webapi/element/html/Select.zig | 18 ++++++++++++++ src/browser/webapi/element/html/Style.zig | 18 ++++++++++++++ src/browser/webapi/element/html/TextArea.zig | 18 ++++++++++++++ src/browser/webapi/element/html/UL.zig | 18 ++++++++++++++ src/browser/webapi/element/html/Unknown.zig | 18 ++++++++++++++ src/browser/webapi/element/svg/Generic.zig | 18 ++++++++++++++ src/browser/webapi/element/svg/Rect.zig | 18 ++++++++++++++ src/browser/webapi/encoding/TextDecoder.zig | 18 ++++++++++++++ src/browser/webapi/encoding/TextEncoder.zig | 18 ++++++++++++++ src/browser/webapi/event/ErrorEvent.zig | 18 ++++++++++++++ src/browser/webapi/event/ProgressEvent.zig | 18 ++++++++++++++ src/browser/webapi/intl/Intl.zig | 20 ---------------- src/browser/webapi/net/Fetch.zig | 18 ++++++++++++++ src/browser/webapi/net/FormData.zig | 18 ++++++++++++++ src/browser/webapi/net/Request.zig | 18 ++++++++++++++ src/browser/webapi/net/Response.zig | 18 ++++++++++++++ src/browser/webapi/net/URLSearchParams.zig | 18 ++++++++++++++ src/browser/webapi/net/XMLHttpRequest.zig | 18 ++++++++++++++ .../webapi/net/XMLHttpRequestEventTarget.zig | 18 ++++++++++++++ src/browser/webapi/selector/List.zig | 18 ++++++++++++++ src/browser/webapi/selector/Parser.zig | 18 ++++++++++++++ src/browser/webapi/selector/Selector.zig | 18 ++++++++++++++ src/browser/webapi/storage/cookie.zig | 18 ++++++++++++++ src/browser/webapi/storage/storage.zig | 18 ++++++++++++++ src/datetime.zig | 18 ++++++++++++++ src/html5ever/lib.rs | 18 ++++++++++++++ src/html5ever/sink.rs | 18 ++++++++++++++ src/html5ever/types.rs | 18 ++++++++++++++ src/id.zig | 18 ++++++++++++++ src/lightpanda.zig | 18 ++++++++++++++ src/log.zig | 2 +- src/main.zig | 2 +- src/main_wpt.zig | 2 +- src/string.zig | 18 ++++++++++++++ src/test_runner.zig | 18 ++++++++++++++ src/testing.zig | 2 +- 118 files changed, 2005 insertions(+), 32 deletions(-) delete mode 100644 src/browser/webapi/intl/Intl.zig diff --git a/src/App.zig b/src/App.zig index ef94486b1..24d015c01 100644 --- a/src/App.zig +++ b/src/App.zig @@ -1,3 +1,21 @@ +// 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 Allocator = std.mem.Allocator; diff --git a/src/Notification.zig b/src/Notification.zig index 89646cff0..f535abd92 100644 --- a/src/Notification.zig +++ b/src/Notification.zig @@ -1,3 +1,21 @@ +// 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 log = @import("log.zig"); diff --git a/src/Scheduler.zig b/src/Scheduler.zig index 0898d19b3..6ba8b6e18 100644 --- a/src/Scheduler.zig +++ b/src/Scheduler.zig @@ -1,3 +1,21 @@ +// 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 log = @import("log.zig"); diff --git a/src/Server.zig b/src/Server.zig index 4d42f0010..481b2cb32 100644 --- a/src/Server.zig +++ b/src/Server.zig @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2024 Lightpanda (Selecy SAS) +// Copyright (C) 2023-2025 Lightpanda (Selecy SAS) // // Francis Bouvier // Pierre Tachoire diff --git a/src/TestHTTPServer.zig b/src/TestHTTPServer.zig index fdc51b904..fdf4e1247 100644 --- a/src/TestHTTPServer.zig +++ b/src/TestHTTPServer.zig @@ -1,3 +1,21 @@ +// 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 TestHTTPServer = @This(); diff --git a/src/browser/EventManager.zig b/src/browser/EventManager.zig index 5efa111be..aa5f023ad 100644 --- a/src/browser/EventManager.zig +++ b/src/browser/EventManager.zig @@ -1,3 +1,21 @@ +// 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 builtin = @import("builtin"); diff --git a/src/browser/Factory.zig b/src/browser/Factory.zig index 7d75fb402..56d3eb985 100644 --- a/src/browser/Factory.zig +++ b/src/browser/Factory.zig @@ -1,3 +1,21 @@ +// 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 builtin = @import("builtin"); const reflect = @import("reflect.zig"); diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 47d5f1238..0169e9d52 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -1,3 +1,21 @@ +// 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 JS = @import("js/js.zig"); const builtin = @import("builtin"); diff --git a/src/browser/Scheduler.zig b/src/browser/Scheduler.zig index 4b4fa71b0..6ad048877 100644 --- a/src/browser/Scheduler.zig +++ b/src/browser/Scheduler.zig @@ -1,3 +1,21 @@ +// 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 builtin = @import("builtin"); diff --git a/src/browser/URL.zig b/src/browser/URL.zig index d1b4d609c..cd56bbd83 100644 --- a/src/browser/URL.zig +++ b/src/browser/URL.zig @@ -1,3 +1,21 @@ +// 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 Allocator = std.mem.Allocator; diff --git a/src/browser/dump.zig b/src/browser/dump.zig index 620ef470c..8efc8da49 100644 --- a/src/browser/dump.zig +++ b/src/browser/dump.zig @@ -1,3 +1,21 @@ +// 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 Node = @import("webapi/Node.zig"); diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index 08bb93ca6..7bf3cbc63 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -480,7 +480,6 @@ pub const JsApis = flattenTypes(&.{ @import("../webapi/Document.zig"), @import("../webapi/HTMLDocument.zig"), @import("../webapi/History.zig"), - @import("../webapi/intl/Intl.zig"), @import("../webapi/KeyValueList.zig"), @import("../webapi/DocumentFragment.zig"), @import("../webapi/DocumentType.zig"), diff --git a/src/browser/parser/Parser.zig b/src/browser/parser/Parser.zig index a4db0aeb8..f4c6232fd 100644 --- a/src/browser/parser/Parser.zig +++ b/src/browser/parser/Parser.zig @@ -1,3 +1,22 @@ +// 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 h5e = @import("html5ever.zig"); diff --git a/src/browser/parser/html5ever.zig b/src/browser/parser/html5ever.zig index 1245f9f54..ea3e7668b 100644 --- a/src/browser/parser/html5ever.zig +++ b/src/browser/parser/html5ever.zig @@ -1,3 +1,22 @@ +// 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 ParsedNode = @import("Parser.zig").ParsedNode; pub extern "c" fn html5ever_parse_document( diff --git a/src/browser/reflect.zig b/src/browser/reflect.zig index 66f096213..ad0c54be3 100644 --- a/src/browser/reflect.zig +++ b/src/browser/reflect.zig @@ -1,3 +1,21 @@ +// 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"); // Gets the Parent of child. diff --git a/src/browser/webapi/AbortController.zig b/src/browser/webapi/AbortController.zig index cd1325d40..13718b97f 100644 --- a/src/browser/webapi/AbortController.zig +++ b/src/browser/webapi/AbortController.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../js/js.zig"); diff --git a/src/browser/webapi/AbortSignal.zig b/src/browser/webapi/AbortSignal.zig index 4974a2aa2..40ac9e895 100644 --- a/src/browser/webapi/AbortSignal.zig +++ b/src/browser/webapi/AbortSignal.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../js/js.zig"); diff --git a/src/browser/webapi/CData.zig b/src/browser/webapi/CData.zig index 78bef052b..a0b569e8a 100644 --- a/src/browser/webapi/CData.zig +++ b/src/browser/webapi/CData.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../js/js.zig"); diff --git a/src/browser/webapi/Console.zig b/src/browser/webapi/Console.zig index 43d603b77..e3e856ab9 100644 --- a/src/browser/webapi/Console.zig +++ b/src/browser/webapi/Console.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../js/js.zig"); diff --git a/src/browser/webapi/Crypto.zig b/src/browser/webapi/Crypto.zig index 6c9e980d8..1b1e6f0fb 100644 --- a/src/browser/webapi/Crypto.zig +++ b/src/browser/webapi/Crypto.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../js/js.zig"); diff --git a/src/browser/webapi/DOMException.zig b/src/browser/webapi/DOMException.zig index 61ceac208..07c7137f1 100644 --- a/src/browser/webapi/DOMException.zig +++ b/src/browser/webapi/DOMException.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../js/js.zig"); const Page = @import("../Page.zig"); diff --git a/src/browser/webapi/DOMImplementation.zig b/src/browser/webapi/DOMImplementation.zig index 0b7aba793..e2a863571 100644 --- a/src/browser/webapi/DOMImplementation.zig +++ b/src/browser/webapi/DOMImplementation.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../js/js.zig"); diff --git a/src/browser/webapi/DOMNodeIterator.zig b/src/browser/webapi/DOMNodeIterator.zig index 762f7bd73..3314416e5 100644 --- a/src/browser/webapi/DOMNodeIterator.zig +++ b/src/browser/webapi/DOMNodeIterator.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../js/js.zig"); const Page = @import("../Page.zig"); diff --git a/src/browser/webapi/DOMParser.zig b/src/browser/webapi/DOMParser.zig index df87f915d..358312955 100644 --- a/src/browser/webapi/DOMParser.zig +++ b/src/browser/webapi/DOMParser.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../js/js.zig"); diff --git a/src/browser/webapi/DOMRect.zig b/src/browser/webapi/DOMRect.zig index 6309a20ea..4b3e36723 100644 --- a/src/browser/webapi/DOMRect.zig +++ b/src/browser/webapi/DOMRect.zig @@ -1,3 +1,21 @@ +// 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 DOMRect = @This(); const js = @import("../js/js.zig"); diff --git a/src/browser/webapi/DOMTreeWalker.zig b/src/browser/webapi/DOMTreeWalker.zig index dd709c513..88ca271a1 100644 --- a/src/browser/webapi/DOMTreeWalker.zig +++ b/src/browser/webapi/DOMTreeWalker.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../js/js.zig"); const Page = @import("../Page.zig"); diff --git a/src/browser/webapi/Document.zig b/src/browser/webapi/Document.zig index 767acf324..4f04f22f3 100644 --- a/src/browser/webapi/Document.zig +++ b/src/browser/webapi/Document.zig @@ -1,3 +1,21 @@ +// 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 String = @import("../../string.zig").String; diff --git a/src/browser/webapi/DocumentFragment.zig b/src/browser/webapi/DocumentFragment.zig index 38d15b538..9813d922f 100644 --- a/src/browser/webapi/DocumentFragment.zig +++ b/src/browser/webapi/DocumentFragment.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../js/js.zig"); diff --git a/src/browser/webapi/DocumentType.zig b/src/browser/webapi/DocumentType.zig index c6ff06342..aab8052eb 100644 --- a/src/browser/webapi/DocumentType.zig +++ b/src/browser/webapi/DocumentType.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../js/js.zig"); diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index 1c55e6f34..9f0fdd5f5 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -1,3 +1,21 @@ +// 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 log = @import("../../log.zig"); diff --git a/src/browser/webapi/Event.zig b/src/browser/webapi/Event.zig index e4b3ff6f2..9884ff855 100644 --- a/src/browser/webapi/Event.zig +++ b/src/browser/webapi/Event.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../js/js.zig"); diff --git a/src/browser/webapi/EventTarget.zig b/src/browser/webapi/EventTarget.zig index a313bc731..23ecdf985 100644 --- a/src/browser/webapi/EventTarget.zig +++ b/src/browser/webapi/EventTarget.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../js/js.zig"); diff --git a/src/browser/webapi/HTMLDocument.zig b/src/browser/webapi/HTMLDocument.zig index ed7e5b31f..5e22ecff1 100644 --- a/src/browser/webapi/HTMLDocument.zig +++ b/src/browser/webapi/HTMLDocument.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../js/js.zig"); diff --git a/src/browser/webapi/History.zig b/src/browser/webapi/History.zig index ada62226d..3bc568662 100644 --- a/src/browser/webapi/History.zig +++ b/src/browser/webapi/History.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../js/js.zig"); diff --git a/src/browser/webapi/KeyValueList.zig b/src/browser/webapi/KeyValueList.zig index 3b105bd8f..c9eb70c8d 100644 --- a/src/browser/webapi/KeyValueList.zig +++ b/src/browser/webapi/KeyValueList.zig @@ -1,3 +1,21 @@ +// 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 String = @import("../../string.zig").String; diff --git a/src/browser/webapi/Location.zig b/src/browser/webapi/Location.zig index 25a4cab30..e7191a138 100644 --- a/src/browser/webapi/Location.zig +++ b/src/browser/webapi/Location.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../js/js.zig"); const URL = @import("URL.zig"); diff --git a/src/browser/webapi/MutationObserver.zig b/src/browser/webapi/MutationObserver.zig index a169f7194..e33f3223a 100644 --- a/src/browser/webapi/MutationObserver.zig +++ b/src/browser/webapi/MutationObserver.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../js/js.zig"); // @ZIGDOM (haha, bet you wish you hadn't opened this file) diff --git a/src/browser/webapi/Navigator.zig b/src/browser/webapi/Navigator.zig index 26f7f609b..981fc2e1c 100644 --- a/src/browser/webapi/Navigator.zig +++ b/src/browser/webapi/Navigator.zig @@ -1,3 +1,21 @@ +// 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 builtin = @import("builtin"); const js = @import("../js/js.zig"); diff --git a/src/browser/webapi/Node.zig b/src/browser/webapi/Node.zig index 7af7c4fad..d162ae81f 100644 --- a/src/browser/webapi/Node.zig +++ b/src/browser/webapi/Node.zig @@ -1,3 +1,21 @@ +// 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 log = @import("../../log.zig"); diff --git a/src/browser/webapi/NodeFilter.zig b/src/browser/webapi/NodeFilter.zig index 911e82dc2..c9fab4155 100644 --- a/src/browser/webapi/NodeFilter.zig +++ b/src/browser/webapi/NodeFilter.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../js/js.zig"); const Page = @import("../Page.zig"); diff --git a/src/browser/webapi/TreeWalker.zig b/src/browser/webapi/TreeWalker.zig index cee99ff14..b6df32fd1 100644 --- a/src/browser/webapi/TreeWalker.zig +++ b/src/browser/webapi/TreeWalker.zig @@ -1,3 +1,21 @@ +// 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 Node = @import("Node.zig"); const Element = @import("Element.zig"); diff --git a/src/browser/webapi/URL.zig b/src/browser/webapi/URL.zig index 74eb3200f..15beb6c09 100644 --- a/src/browser/webapi/URL.zig +++ b/src/browser/webapi/URL.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../js/js.zig"); diff --git a/src/browser/webapi/Window.zig b/src/browser/webapi/Window.zig index 605b9fdc7..b3ac4414a 100644 --- a/src/browser/webapi/Window.zig +++ b/src/browser/webapi/Window.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../js/js.zig"); const builtin = @import("builtin"); @@ -6,7 +24,6 @@ const log = @import("../../log.zig"); const Page = @import("../Page.zig"); const Console = @import("Console.zig"); const History = @import("History.zig"); -const Intl = @import("intl/Intl.zig"); const Navigator = @import("Navigator.zig"); const Document = @import("Document.zig"); const Location = @import("Location.zig"); @@ -53,10 +70,6 @@ pub fn getNavigator(_: *const Window) Navigator { return .{}; } -pub fn getIntl(_: *const Window) Intl { - return .{}; -} - pub fn getLocalStorage(self: *const Window) *storage.Lookup { return &self._storage_bucket.local; } @@ -294,7 +307,6 @@ pub const JsApi = struct { pub const parent = bridge.accessor(Window.getWindow, null, .{ .cache = "parent" }); pub const console = bridge.accessor(Window.getConsole, null, .{ .cache = "console" }); pub const navigator = bridge.accessor(Window.getNavigator, null, .{ .cache = "navigator" }); - pub const Intl = bridge.accessor(Window.getIntl, null, .{ .cache = "Intl" }); pub const localStorage = bridge.accessor(Window.getLocalStorage, null, .{ .cache = "localStorage" }); pub const sessionStorage = bridge.accessor(Window.getSessionStorage, null, .{ .cache = "sessionStorage" }); pub const document = bridge.accessor(Window.getDocument, null, .{ .cache = "document" }); diff --git a/src/browser/webapi/cdata/Comment.zig b/src/browser/webapi/cdata/Comment.zig index 39f84b192..f91faf895 100644 --- a/src/browser/webapi/cdata/Comment.zig +++ b/src/browser/webapi/cdata/Comment.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../../js/js.zig"); const CData = @import("../CData.zig"); diff --git a/src/browser/webapi/cdata/Text.zig b/src/browser/webapi/cdata/Text.zig index 83815f79d..ad440348c 100644 --- a/src/browser/webapi/cdata/Text.zig +++ b/src/browser/webapi/cdata/Text.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../../js/js.zig"); const CData = @import("../CData.zig"); diff --git a/src/browser/webapi/collections.zig b/src/browser/webapi/collections.zig index cb6b2daad..0e091cbda 100644 --- a/src/browser/webapi/collections.zig +++ b/src/browser/webapi/collections.zig @@ -1,3 +1,21 @@ +// 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 . + pub const NodeLive = @import("collections/node_live.zig").NodeLive; pub const ChildNodes = @import("collections/ChildNodes.zig"); pub const DOMTokenList = @import("collections/DOMTokenList.zig"); diff --git a/src/browser/webapi/collections/ChildNodes.zig b/src/browser/webapi/collections/ChildNodes.zig index f224b4374..1008d7e9d 100644 --- a/src/browser/webapi/collections/ChildNodes.zig +++ b/src/browser/webapi/collections/ChildNodes.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../../js/js.zig"); diff --git a/src/browser/webapi/collections/DOMTokenList.zig b/src/browser/webapi/collections/DOMTokenList.zig index a7c5525ab..67ba027f6 100644 --- a/src/browser/webapi/collections/DOMTokenList.zig +++ b/src/browser/webapi/collections/DOMTokenList.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../../js/js.zig"); diff --git a/src/browser/webapi/collections/HTMLAllCollection.zig b/src/browser/webapi/collections/HTMLAllCollection.zig index 60aba31cf..f781986d3 100644 --- a/src/browser/webapi/collections/HTMLAllCollection.zig +++ b/src/browser/webapi/collections/HTMLAllCollection.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../../js/js.zig"); diff --git a/src/browser/webapi/collections/HTMLCollection.zig b/src/browser/webapi/collections/HTMLCollection.zig index 34d2f0713..e3f42a904 100644 --- a/src/browser/webapi/collections/HTMLCollection.zig +++ b/src/browser/webapi/collections/HTMLCollection.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../../js/js.zig"); diff --git a/src/browser/webapi/collections/NodeList.zig b/src/browser/webapi/collections/NodeList.zig index 5b672380b..b49a29b6b 100644 --- a/src/browser/webapi/collections/NodeList.zig +++ b/src/browser/webapi/collections/NodeList.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../../js/js.zig"); diff --git a/src/browser/webapi/collections/iterator.zig b/src/browser/webapi/collections/iterator.zig index ee7583f9d..2c16ed85b 100644 --- a/src/browser/webapi/collections/iterator.zig +++ b/src/browser/webapi/collections/iterator.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../../js/js.zig"); const Page = @import("../../Page.zig"); diff --git a/src/browser/webapi/collections/node_live.zig b/src/browser/webapi/collections/node_live.zig index 9eef667d5..45eea51c9 100644 --- a/src/browser/webapi/collections/node_live.zig +++ b/src/browser/webapi/collections/node_live.zig @@ -1,3 +1,21 @@ +// 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 builtin = @import("builtin"); diff --git a/src/browser/webapi/css.zig b/src/browser/webapi/css.zig index ea4b1e908..f285e8d2d 100644 --- a/src/browser/webapi/css.zig +++ b/src/browser/webapi/css.zig @@ -1,3 +1,21 @@ +// 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"); pub fn parseDimension(value: []const u8) ?f64 { diff --git a/src/browser/webapi/css/CSSStyleDeclaration.zig b/src/browser/webapi/css/CSSStyleDeclaration.zig index 36569866d..887a8098d 100644 --- a/src/browser/webapi/css/CSSStyleDeclaration.zig +++ b/src/browser/webapi/css/CSSStyleDeclaration.zig @@ -1,3 +1,21 @@ +// 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 log = @import("../../../log.zig"); const String = @import("../../../string.zig").String; diff --git a/src/browser/webapi/css/CSSStyleProperties.zig b/src/browser/webapi/css/CSSStyleProperties.zig index 1de71ea4e..f595838e1 100644 --- a/src/browser/webapi/css/CSSStyleProperties.zig +++ b/src/browser/webapi/css/CSSStyleProperties.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../../js/js.zig"); diff --git a/src/browser/webapi/css/MediaQueryList.zig b/src/browser/webapi/css/MediaQueryList.zig index f67e754c7..4e0da9710 100644 --- a/src/browser/webapi/css/MediaQueryList.zig +++ b/src/browser/webapi/css/MediaQueryList.zig @@ -1,3 +1,21 @@ +// 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 . + // zlint-disable unused-decls const std = @import("std"); const js = @import("../../js/js.zig"); diff --git a/src/browser/webapi/element/Attribute.zig b/src/browser/webapi/element/Attribute.zig index 85d6bf6d5..66357754e 100644 --- a/src/browser/webapi/element/Attribute.zig +++ b/src/browser/webapi/element/Attribute.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../../js/js.zig"); diff --git a/src/browser/webapi/element/DOMStringMap.zig b/src/browser/webapi/element/DOMStringMap.zig index 4fd029552..518f996a8 100644 --- a/src/browser/webapi/element/DOMStringMap.zig +++ b/src/browser/webapi/element/DOMStringMap.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../../js/js.zig"); diff --git a/src/browser/webapi/element/Html.zig b/src/browser/webapi/element/Html.zig index c16b539d6..c418f1609 100644 --- a/src/browser/webapi/element/Html.zig +++ b/src/browser/webapi/element/Html.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../../js/js.zig"); const reflect = @import("../../reflect.zig"); diff --git a/src/browser/webapi/element/Svg.zig b/src/browser/webapi/element/Svg.zig index 561e2867a..71a7cab9c 100644 --- a/src/browser/webapi/element/Svg.zig +++ b/src/browser/webapi/element/Svg.zig @@ -1,3 +1,21 @@ +// 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 String = @import("../../../string.zig").String; const js = @import("../../js/js.zig"); diff --git a/src/browser/webapi/element/html/Anchor.zig b/src/browser/webapi/element/html/Anchor.zig index d45d519bb..5a0f6b60d 100644 --- a/src/browser/webapi/element/html/Anchor.zig +++ b/src/browser/webapi/element/html/Anchor.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../../../js/js.zig"); const Page = @import("../../../Page.zig"); diff --git a/src/browser/webapi/element/html/Body.zig b/src/browser/webapi/element/html/Body.zig index cf04a9397..5be6d4fef 100644 --- a/src/browser/webapi/element/html/Body.zig +++ b/src/browser/webapi/element/html/Body.zig @@ -1,3 +1,21 @@ +// 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 log = @import("../../../../log.zig"); const js = @import("../../../js/js.zig"); diff --git a/src/browser/webapi/element/html/Button.zig b/src/browser/webapi/element/html/Button.zig index b3d44ddea..2e1a40165 100644 --- a/src/browser/webapi/element/html/Button.zig +++ b/src/browser/webapi/element/html/Button.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../../../js/js.zig"); const Page = @import("../../../Page.zig"); diff --git a/src/browser/webapi/element/html/Custom.zig b/src/browser/webapi/element/html/Custom.zig index 6bfbfec4b..686389249 100644 --- a/src/browser/webapi/element/html/Custom.zig +++ b/src/browser/webapi/element/html/Custom.zig @@ -1,3 +1,21 @@ +// 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 String = @import("../../../../string.zig").String; const js = @import("../../../js/js.zig"); diff --git a/src/browser/webapi/element/html/Div.zig b/src/browser/webapi/element/html/Div.zig index 4789bf166..0fe21d950 100644 --- a/src/browser/webapi/element/html/Div.zig +++ b/src/browser/webapi/element/html/Div.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../../../js/js.zig"); const Node = @import("../../Node.zig"); const Element = @import("../../Element.zig"); diff --git a/src/browser/webapi/element/html/Form.zig b/src/browser/webapi/element/html/Form.zig index f9e098034..66f23ddec 100644 --- a/src/browser/webapi/element/html/Form.zig +++ b/src/browser/webapi/element/html/Form.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../../../js/js.zig"); const Page = @import("../../../Page.zig"); diff --git a/src/browser/webapi/element/html/Generic.zig b/src/browser/webapi/element/html/Generic.zig index a567a938f..15e7d1b13 100644 --- a/src/browser/webapi/element/html/Generic.zig +++ b/src/browser/webapi/element/html/Generic.zig @@ -1,3 +1,21 @@ +// 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 String = @import("../../../../string.zig").String; const js = @import("../../../js/js.zig"); diff --git a/src/browser/webapi/element/html/HR.zig b/src/browser/webapi/element/html/HR.zig index 262bc8016..231e0b1ae 100644 --- a/src/browser/webapi/element/html/HR.zig +++ b/src/browser/webapi/element/html/HR.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../../../js/js.zig"); const Node = @import("../../Node.zig"); const Element = @import("../../Element.zig"); diff --git a/src/browser/webapi/element/html/Head.zig b/src/browser/webapi/element/html/Head.zig index cd4afb4da..5bb081630 100644 --- a/src/browser/webapi/element/html/Head.zig +++ b/src/browser/webapi/element/html/Head.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../../../js/js.zig"); const Node = @import("../../Node.zig"); const Element = @import("../../Element.zig"); diff --git a/src/browser/webapi/element/html/Heading.zig b/src/browser/webapi/element/html/Heading.zig index 2a185ecbe..a700bdf02 100644 --- a/src/browser/webapi/element/html/Heading.zig +++ b/src/browser/webapi/element/html/Heading.zig @@ -1,3 +1,21 @@ +// 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 String = @import("../../../../string.zig").String; const js = @import("../../../js/js.zig"); diff --git a/src/browser/webapi/element/html/Html.zig b/src/browser/webapi/element/html/Html.zig index 12b69b821..94fa2c333 100644 --- a/src/browser/webapi/element/html/Html.zig +++ b/src/browser/webapi/element/html/Html.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../../../js/js.zig"); const Node = @import("../../Node.zig"); const Element = @import("../../Element.zig"); diff --git a/src/browser/webapi/element/html/IFrame.zig b/src/browser/webapi/element/html/IFrame.zig index a92676662..b08300638 100644 --- a/src/browser/webapi/element/html/IFrame.zig +++ b/src/browser/webapi/element/html/IFrame.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../../../js/js.zig"); const Node = @import("../../Node.zig"); const Element = @import("../../Element.zig"); diff --git a/src/browser/webapi/element/html/Image.zig b/src/browser/webapi/element/html/Image.zig index 0d1ac1e4a..2cbd2634d 100644 --- a/src/browser/webapi/element/html/Image.zig +++ b/src/browser/webapi/element/html/Image.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../../../js/js.zig"); const Node = @import("../../Node.zig"); const Element = @import("../../Element.zig"); diff --git a/src/browser/webapi/element/html/Input.zig b/src/browser/webapi/element/html/Input.zig index 72d8b0e10..d805ba6fe 100644 --- a/src/browser/webapi/element/html/Input.zig +++ b/src/browser/webapi/element/html/Input.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../../../js/js.zig"); const Page = @import("../../../Page.zig"); diff --git a/src/browser/webapi/element/html/LI.zig b/src/browser/webapi/element/html/LI.zig index cf816d8b9..e02130208 100644 --- a/src/browser/webapi/element/html/LI.zig +++ b/src/browser/webapi/element/html/LI.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../../../js/js.zig"); const Node = @import("../../Node.zig"); const Element = @import("../../Element.zig"); diff --git a/src/browser/webapi/element/html/Link.zig b/src/browser/webapi/element/html/Link.zig index a108a8e07..3fbfdaa06 100644 --- a/src/browser/webapi/element/html/Link.zig +++ b/src/browser/webapi/element/html/Link.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../../../js/js.zig"); const Node = @import("../../Node.zig"); const Element = @import("../../Element.zig"); diff --git a/src/browser/webapi/element/html/Meta.zig b/src/browser/webapi/element/html/Meta.zig index d9ed67469..900d49328 100644 --- a/src/browser/webapi/element/html/Meta.zig +++ b/src/browser/webapi/element/html/Meta.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../../../js/js.zig"); const Node = @import("../../Node.zig"); const Element = @import("../../Element.zig"); diff --git a/src/browser/webapi/element/html/OL.zig b/src/browser/webapi/element/html/OL.zig index a19ebda11..844205d1f 100644 --- a/src/browser/webapi/element/html/OL.zig +++ b/src/browser/webapi/element/html/OL.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../../../js/js.zig"); const Node = @import("../../Node.zig"); const Element = @import("../../Element.zig"); diff --git a/src/browser/webapi/element/html/Option.zig b/src/browser/webapi/element/html/Option.zig index 311a00b8b..5123e088e 100644 --- a/src/browser/webapi/element/html/Option.zig +++ b/src/browser/webapi/element/html/Option.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../../../js/js.zig"); const Page = @import("../../../Page.zig"); diff --git a/src/browser/webapi/element/html/Paragraph.zig b/src/browser/webapi/element/html/Paragraph.zig index bf4b13dea..0822703a6 100644 --- a/src/browser/webapi/element/html/Paragraph.zig +++ b/src/browser/webapi/element/html/Paragraph.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../../../js/js.zig"); const Node = @import("../../Node.zig"); const Element = @import("../../Element.zig"); diff --git a/src/browser/webapi/element/html/Script.zig b/src/browser/webapi/element/html/Script.zig index 6bf306d3c..1e548c4e3 100644 --- a/src/browser/webapi/element/html/Script.zig +++ b/src/browser/webapi/element/html/Script.zig @@ -1,3 +1,21 @@ +// 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 log = @import("../../../../log.zig"); const js = @import("../../../js/js.zig"); const Page = @import("../../../Page.zig"); diff --git a/src/browser/webapi/element/html/Select.zig b/src/browser/webapi/element/html/Select.zig index 23bf540b7..c521c3f42 100644 --- a/src/browser/webapi/element/html/Select.zig +++ b/src/browser/webapi/element/html/Select.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../../../js/js.zig"); const Page = @import("../../../Page.zig"); diff --git a/src/browser/webapi/element/html/Style.zig b/src/browser/webapi/element/html/Style.zig index efb7eaeeb..d774e93e9 100644 --- a/src/browser/webapi/element/html/Style.zig +++ b/src/browser/webapi/element/html/Style.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../../../js/js.zig"); const Node = @import("../../Node.zig"); const Element = @import("../../Element.zig"); diff --git a/src/browser/webapi/element/html/TextArea.zig b/src/browser/webapi/element/html/TextArea.zig index fa6732aed..dcb282f05 100644 --- a/src/browser/webapi/element/html/TextArea.zig +++ b/src/browser/webapi/element/html/TextArea.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../../../js/js.zig"); const Page = @import("../../../Page.zig"); diff --git a/src/browser/webapi/element/html/UL.zig b/src/browser/webapi/element/html/UL.zig index d4f5ac1a0..14bd69a20 100644 --- a/src/browser/webapi/element/html/UL.zig +++ b/src/browser/webapi/element/html/UL.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../../../js/js.zig"); const Node = @import("../../Node.zig"); const Element = @import("../../Element.zig"); diff --git a/src/browser/webapi/element/html/Unknown.zig b/src/browser/webapi/element/html/Unknown.zig index 23e375852..0ea8f9473 100644 --- a/src/browser/webapi/element/html/Unknown.zig +++ b/src/browser/webapi/element/html/Unknown.zig @@ -1,3 +1,21 @@ +// 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 String = @import("../../../../string.zig").String; const js = @import("../../../js/js.zig"); diff --git a/src/browser/webapi/element/svg/Generic.zig b/src/browser/webapi/element/svg/Generic.zig index f5b3a2605..368370e56 100644 --- a/src/browser/webapi/element/svg/Generic.zig +++ b/src/browser/webapi/element/svg/Generic.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../../../js/js.zig"); const Node = @import("../../Node.zig"); const Element = @import("../../Element.zig"); diff --git a/src/browser/webapi/element/svg/Rect.zig b/src/browser/webapi/element/svg/Rect.zig index 7af604eed..0b79cc386 100644 --- a/src/browser/webapi/element/svg/Rect.zig +++ b/src/browser/webapi/element/svg/Rect.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../../../js/js.zig"); const Node = @import("../../Node.zig"); const Element = @import("../../Element.zig"); diff --git a/src/browser/webapi/encoding/TextDecoder.zig b/src/browser/webapi/encoding/TextDecoder.zig index 547319b5d..3148868b7 100644 --- a/src/browser/webapi/encoding/TextDecoder.zig +++ b/src/browser/webapi/encoding/TextDecoder.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../../js/js.zig"); diff --git a/src/browser/webapi/encoding/TextEncoder.zig b/src/browser/webapi/encoding/TextEncoder.zig index a1648c458..c7066d5ec 100644 --- a/src/browser/webapi/encoding/TextEncoder.zig +++ b/src/browser/webapi/encoding/TextEncoder.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../../js/js.zig"); diff --git a/src/browser/webapi/event/ErrorEvent.zig b/src/browser/webapi/event/ErrorEvent.zig index 896679245..9c7f15700 100644 --- a/src/browser/webapi/event/ErrorEvent.zig +++ b/src/browser/webapi/event/ErrorEvent.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../../js/js.zig"); diff --git a/src/browser/webapi/event/ProgressEvent.zig b/src/browser/webapi/event/ProgressEvent.zig index 9406b2ebf..6e824a787 100644 --- a/src/browser/webapi/event/ProgressEvent.zig +++ b/src/browser/webapi/event/ProgressEvent.zig @@ -1,3 +1,21 @@ +// 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 Page = @import("../../Page.zig"); const Event = @import("../Event.zig"); diff --git a/src/browser/webapi/intl/Intl.zig b/src/browser/webapi/intl/Intl.zig deleted file mode 100644 index 4015d478e..000000000 --- a/src/browser/webapi/intl/Intl.zig +++ /dev/null @@ -1,20 +0,0 @@ -const std = @import("std"); -const js = @import("../../js/js.zig"); - -const Intl = @This(); - -// Skeleton implementation with no actual functionality yet. -// This allows `if (Intl)` checks to pass, while property checks -// like `if (Intl.Locale)` will return undefined. -// We can add actual implementations as we encounter real-world use cases. - -pub const JsApi = struct { - pub const bridge = js.Bridge(Intl); - - pub const Meta = struct { - pub const name = "Intl"; - pub var class_id: bridge.ClassId = undefined; - pub const prototype_chain = bridge.prototypeChain(); - pub const empty_with_no_proto = true; - }; -}; diff --git a/src/browser/webapi/net/Fetch.zig b/src/browser/webapi/net/Fetch.zig index 0d4853f98..7bbc2da94 100644 --- a/src/browser/webapi/net/Fetch.zig +++ b/src/browser/webapi/net/Fetch.zig @@ -1,3 +1,21 @@ +// 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 log = @import("../../../log.zig"); diff --git a/src/browser/webapi/net/FormData.zig b/src/browser/webapi/net/FormData.zig index c44d7e824..610c88bf3 100644 --- a/src/browser/webapi/net/FormData.zig +++ b/src/browser/webapi/net/FormData.zig @@ -1,3 +1,21 @@ +// 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 log = @import("../../../log.zig"); diff --git a/src/browser/webapi/net/Request.zig b/src/browser/webapi/net/Request.zig index 5344403e8..d715c53b2 100644 --- a/src/browser/webapi/net/Request.zig +++ b/src/browser/webapi/net/Request.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../../js/js.zig"); diff --git a/src/browser/webapi/net/Response.zig b/src/browser/webapi/net/Response.zig index 549e69c17..d072f7b6c 100644 --- a/src/browser/webapi/net/Response.zig +++ b/src/browser/webapi/net/Response.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../../js/js.zig"); diff --git a/src/browser/webapi/net/URLSearchParams.zig b/src/browser/webapi/net/URLSearchParams.zig index b00f93786..64bc80863 100644 --- a/src/browser/webapi/net/URLSearchParams.zig +++ b/src/browser/webapi/net/URLSearchParams.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../../js/js.zig"); diff --git a/src/browser/webapi/net/XMLHttpRequest.zig b/src/browser/webapi/net/XMLHttpRequest.zig index 11a36f579..dfb848e66 100644 --- a/src/browser/webapi/net/XMLHttpRequest.zig +++ b/src/browser/webapi/net/XMLHttpRequest.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../../js/js.zig"); diff --git a/src/browser/webapi/net/XMLHttpRequestEventTarget.zig b/src/browser/webapi/net/XMLHttpRequestEventTarget.zig index cb1418bf1..c5568a9ae 100644 --- a/src/browser/webapi/net/XMLHttpRequestEventTarget.zig +++ b/src/browser/webapi/net/XMLHttpRequestEventTarget.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../../js/js.zig"); const Page = @import("../../Page.zig"); diff --git a/src/browser/webapi/selector/List.zig b/src/browser/webapi/selector/List.zig index 0bedc46ca..449fc70b2 100644 --- a/src/browser/webapi/selector/List.zig +++ b/src/browser/webapi/selector/List.zig @@ -1,3 +1,21 @@ +// 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 Page = @import("../../Page.zig"); diff --git a/src/browser/webapi/selector/Parser.zig b/src/browser/webapi/selector/Parser.zig index 335f22456..7d2a058fe 100644 --- a/src/browser/webapi/selector/Parser.zig +++ b/src/browser/webapi/selector/Parser.zig @@ -1,3 +1,21 @@ +// 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 Allocator = std.mem.Allocator; diff --git a/src/browser/webapi/selector/Selector.zig b/src/browser/webapi/selector/Selector.zig index 8839b1b6f..3f72c442b 100644 --- a/src/browser/webapi/selector/Selector.zig +++ b/src/browser/webapi/selector/Selector.zig @@ -1,3 +1,21 @@ +// 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 Parser = @import("Parser.zig"); diff --git a/src/browser/webapi/storage/cookie.zig b/src/browser/webapi/storage/cookie.zig index 69d17abea..25d6f51dd 100644 --- a/src/browser/webapi/storage/cookie.zig +++ b/src/browser/webapi/storage/cookie.zig @@ -1,3 +1,21 @@ +// 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 Uri = std.Uri; const Allocator = std.mem.Allocator; diff --git a/src/browser/webapi/storage/storage.zig b/src/browser/webapi/storage/storage.zig index 2e7e2609f..acaaa3fdf 100644 --- a/src/browser/webapi/storage/storage.zig +++ b/src/browser/webapi/storage/storage.zig @@ -1,3 +1,21 @@ +// 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 js = @import("../../js/js.zig"); const Page = @import("../../Page.zig"); diff --git a/src/datetime.zig b/src/datetime.zig index ec0740787..5be7d6047 100644 --- a/src/datetime.zig +++ b/src/datetime.zig @@ -1,3 +1,21 @@ +// 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 builtin = @import("builtin"); const posix = std.posix; diff --git a/src/html5ever/lib.rs b/src/html5ever/lib.rs index ee1b612b9..6128b58c6 100644 --- a/src/html5ever/lib.rs +++ b/src/html5ever/lib.rs @@ -1,3 +1,21 @@ +// 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 . + mod types; mod sink; diff --git a/src/html5ever/sink.rs b/src/html5ever/sink.rs index b468afa5f..21d3a47e4 100644 --- a/src/html5ever/sink.rs +++ b/src/html5ever/sink.rs @@ -1,3 +1,21 @@ +// 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 . + use std::ptr; use std::cell::Cell; use std::borrow::Cow; diff --git a/src/html5ever/types.rs b/src/html5ever/types.rs index a38f03a17..f87c8723b 100644 --- a/src/html5ever/types.rs +++ b/src/html5ever/types.rs @@ -1,3 +1,21 @@ +// 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 . + use std::ptr; use html5ever::{QualName, Attribute}; use std::os::raw::{c_uchar, c_void}; diff --git a/src/id.zig b/src/id.zig index 98594c0b5..8f43dbc66 100644 --- a/src/id.zig +++ b/src/id.zig @@ -1,3 +1,21 @@ +// 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"); // Generates incrementing prefixed integers, i.e. CTX-1, CTX-2, CTX-3. diff --git a/src/lightpanda.zig b/src/lightpanda.zig index f037ce3e0..57d277934 100644 --- a/src/lightpanda.zig +++ b/src/lightpanda.zig @@ -1,3 +1,21 @@ +// 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"); pub const App = @import("App.zig"); pub const Server = @import("Server.zig"); diff --git a/src/log.zig b/src/log.zig index f791e9f7b..e34329e8b 100644 --- a/src/log.zig +++ b/src/log.zig @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2024 Lightpanda (Selecy SAS) +// Copyright (C) 2023-2025 Lightpanda (Selecy SAS) // // Francis Bouvier // Pierre Tachoire diff --git a/src/main.zig b/src/main.zig index 1da7af4bc..42ad8d0f6 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2024 Lightpanda (Selecy SAS) +// Copyright (C) 2023-2025 Lightpanda (Selecy SAS) // // Francis Bouvier // Pierre Tachoire diff --git a/src/main_wpt.zig b/src/main_wpt.zig index ddda29c5e..99d7adc6b 100644 --- a/src/main_wpt.zig +++ b/src/main_wpt.zig @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2024 Lightpanda (Selecy SAS) +// Copyright (C) 2023-2025 Lightpanda (Selecy SAS) // // Francis Bouvier // Pierre Tachoire diff --git a/src/string.zig b/src/string.zig index 13ac9d884..90966d881 100644 --- a/src/string.zig +++ b/src/string.zig @@ -1,3 +1,21 @@ +// 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 js = @import("browser/js/js.zig"); const Allocator = std.mem.Allocator; diff --git a/src/test_runner.zig b/src/test_runner.zig index 2979fe0d6..c4e5d597d 100644 --- a/src/test_runner.zig +++ b/src/test_runner.zig @@ -1,3 +1,21 @@ +// 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 builtin = @import("builtin"); diff --git a/src/testing.zig b/src/testing.zig index a4805f985..77cc1f00a 100644 --- a/src/testing.zig +++ b/src/testing.zig @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2024 Lightpanda (Selecy SAS) +// Copyright (C) 2023-2025 Lightpanda (Selecy SAS) // // Francis Bouvier // Pierre Tachoire From 7ab88e9a711a36f7d0a19827f61e808d30d55634 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Fri, 14 Nov 2025 15:55:02 +0800 Subject: [PATCH 24/30] add legacy tests, optimize empty types --- build.zig | 27 ++ src/browser/js/Context.zig | 44 ++- src/browser/js/Env.zig | 4 +- src/browser/tests/domparser.html | 7 +- src/browser/tests/legacy/browser.html | 10 + src/browser/tests/legacy/crypto.html | 26 ++ src/browser/tests/legacy/css.html | 6 + .../tests/legacy/cssom/css_rule_list.html | 8 + .../legacy/cssom/css_style_declaration.html | 102 ++++++ .../tests/legacy/cssom/css_stylesheet.html | 16 + src/browser/tests/legacy/dom/animation.html | 15 + src/browser/tests/legacy/dom/attribute.html | 33 ++ .../tests/legacy/dom/character_data.html | 48 +++ src/browser/tests/legacy/dom/comment.html | 9 + src/browser/tests/legacy/dom/document.html | 190 ++++++++++ .../tests/legacy/dom/document_fragment.html | 34 ++ .../tests/legacy/dom/document_type.html | 13 + src/browser/tests/legacy/dom/dom_parser.html | 7 + src/browser/tests/legacy/dom/element.html | 341 ++++++++++++++++++ .../tests/legacy/dom/event_target.html | 116 ++++++ src/browser/tests/legacy/dom/exceptions.html | 40 ++ .../tests/legacy/dom/html_collection.html | 67 ++++ .../tests/legacy/dom/implementation.html | 14 + .../legacy/dom/intersection_observer.html | 163 +++++++++ .../tests/legacy/dom/message_channel.html | 60 +++ .../tests/legacy/dom/mutation_observer.html | 76 ++++ .../tests/legacy/dom/named_node_map.html | 19 + src/browser/tests/legacy/dom/node.html | 266 ++++++++++++++ src/browser/tests/legacy/dom/node_filter.html | 219 +++++++++++ .../tests/legacy/dom/node_iterator.html | 62 ++++ src/browser/tests/legacy/dom/node_list.html | 19 + src/browser/tests/legacy/dom/node_owner.html | 34 ++ src/browser/tests/legacy/dom/performance.html | 16 + .../legacy/dom/performance_observer.html | 5 + .../legacy/dom/processing_instruction.html | 22 ++ src/browser/tests/legacy/dom/range.html | 41 +++ src/browser/tests/legacy/dom/shadow_root.html | 49 +++ src/browser/tests/legacy/dom/text.html | 19 + src/browser/tests/legacy/dom/token_list.html | 64 ++++ .../tests/legacy/encoding/decoder.html | 60 +++ .../tests/legacy/encoding/encoder.html | 14 + .../tests/legacy/events/composition.html | 36 ++ src/browser/tests/legacy/events/custom.html | 25 ++ src/browser/tests/legacy/events/event.html | 139 +++++++ src/browser/tests/legacy/events/keyboard.html | 88 +++++ src/browser/tests/legacy/events/mouse.html | 34 ++ src/browser/tests/legacy/fetch/fetch.html | 34 ++ src/browser/tests/legacy/fetch/headers.html | 102 ++++++ src/browser/tests/legacy/fetch/request.html | 22 ++ src/browser/tests/legacy/fetch/response.html | 50 +++ src/browser/tests/legacy/file/blob.html | 125 +++++++ src/browser/tests/legacy/file/file.html | 7 + .../tests/legacy/html/abort_controller.html | 41 +++ src/browser/tests/legacy/html/canvas.html | 29 ++ src/browser/tests/legacy/html/dataset.html | 30 ++ src/browser/tests/legacy/html/document.html | 85 +++++ src/browser/tests/legacy/html/element.html | 53 +++ .../tests/legacy/html/error_event.html | 25 ++ .../tests/legacy/html/history/history.html | 37 ++ .../tests/legacy/html/history/history2.html | 26 ++ .../html/history/history_after_nav.html | 6 + src/browser/tests/legacy/html/image.html | 32 ++ src/browser/tests/legacy/html/input.html | 111 ++++++ src/browser/tests/legacy/html/link.html | 60 +++ src/browser/tests/legacy/html/location.html | 33 ++ .../legacy/html/navigation/navigation.html | 18 + .../legacy/html/navigation/navigation2.html | 8 + .../navigation_currententrychange.html | 15 + src/browser/tests/legacy/html/navigator.html | 8 + src/browser/tests/legacy/html/screen.html | 21 ++ .../legacy/html/script/dynamic_import.html | 32 ++ .../tests/legacy/html/script/import.html | 15 + .../tests/legacy/html/script/import.js | 2 + .../tests/legacy/html/script/import2.js | 2 + .../tests/legacy/html/script/importmap.html | 24 ++ .../legacy/html/script/inline_defer.html | 28 ++ .../tests/legacy/html/script/inline_defer.js | 1 + .../tests/legacy/html/script/order.html | 35 ++ src/browser/tests/legacy/html/script/order.js | 2 + .../tests/legacy/html/script/order_async.js | 3 + .../tests/legacy/html/script/order_defer.js | 2 + .../tests/legacy/html/script/script.html | 21 ++ src/browser/tests/legacy/html/select.html | 80 ++++ src/browser/tests/legacy/html/slot.html | 179 +++++++++ src/browser/tests/legacy/html/style.html | 8 + src/browser/tests/legacy/html/svg.html | 38 ++ src/browser/tests/legacy/html/template.html | 38 ++ .../tests/legacy/polyfill/webcomponents.html | 23 ++ .../tests/legacy/storage/local_storage.html | 29 ++ .../tests/legacy/streams/readable_stream.html | 134 +++++++ src/browser/tests/legacy/testing.js | 206 +++++++++++ src/browser/tests/legacy/url/url.html | 109 ++++++ .../tests/legacy/url/url_search_params.html | 94 +++++ src/browser/tests/legacy/window/frames.html | 13 + src/browser/tests/legacy/window/window.html | 167 +++++++++ src/browser/tests/legacy/xhr/form_data.html | 130 +++++++ .../tests/legacy/xhr/progress_event.html | 17 + src/browser/tests/legacy/xhr/xhr.html | 110 ++++++ src/browser/tests/legacy/xmlserializer.html | 8 + src/browser/webapi/DOMParser.zig | 3 +- src/browser/webapi/NodeFilter.zig | 1 + src/lightpanda.zig | 5 +- src/main_legacy_test.zig | 238 ++++++++++++ tests/html/bug-html-parsing-4.html | 6 - 104 files changed, 5461 insertions(+), 27 deletions(-) create mode 100644 src/browser/tests/legacy/browser.html create mode 100644 src/browser/tests/legacy/crypto.html create mode 100644 src/browser/tests/legacy/css.html create mode 100644 src/browser/tests/legacy/cssom/css_rule_list.html create mode 100644 src/browser/tests/legacy/cssom/css_style_declaration.html create mode 100644 src/browser/tests/legacy/cssom/css_stylesheet.html create mode 100644 src/browser/tests/legacy/dom/animation.html create mode 100644 src/browser/tests/legacy/dom/attribute.html create mode 100644 src/browser/tests/legacy/dom/character_data.html create mode 100644 src/browser/tests/legacy/dom/comment.html create mode 100644 src/browser/tests/legacy/dom/document.html create mode 100644 src/browser/tests/legacy/dom/document_fragment.html create mode 100644 src/browser/tests/legacy/dom/document_type.html create mode 100644 src/browser/tests/legacy/dom/dom_parser.html create mode 100644 src/browser/tests/legacy/dom/element.html create mode 100644 src/browser/tests/legacy/dom/event_target.html create mode 100644 src/browser/tests/legacy/dom/exceptions.html create mode 100644 src/browser/tests/legacy/dom/html_collection.html create mode 100644 src/browser/tests/legacy/dom/implementation.html create mode 100644 src/browser/tests/legacy/dom/intersection_observer.html create mode 100644 src/browser/tests/legacy/dom/message_channel.html create mode 100644 src/browser/tests/legacy/dom/mutation_observer.html create mode 100644 src/browser/tests/legacy/dom/named_node_map.html create mode 100644 src/browser/tests/legacy/dom/node.html create mode 100644 src/browser/tests/legacy/dom/node_filter.html create mode 100644 src/browser/tests/legacy/dom/node_iterator.html create mode 100644 src/browser/tests/legacy/dom/node_list.html create mode 100644 src/browser/tests/legacy/dom/node_owner.html create mode 100644 src/browser/tests/legacy/dom/performance.html create mode 100644 src/browser/tests/legacy/dom/performance_observer.html create mode 100644 src/browser/tests/legacy/dom/processing_instruction.html create mode 100644 src/browser/tests/legacy/dom/range.html create mode 100644 src/browser/tests/legacy/dom/shadow_root.html create mode 100644 src/browser/tests/legacy/dom/text.html create mode 100644 src/browser/tests/legacy/dom/token_list.html create mode 100644 src/browser/tests/legacy/encoding/decoder.html create mode 100644 src/browser/tests/legacy/encoding/encoder.html create mode 100644 src/browser/tests/legacy/events/composition.html create mode 100644 src/browser/tests/legacy/events/custom.html create mode 100644 src/browser/tests/legacy/events/event.html create mode 100644 src/browser/tests/legacy/events/keyboard.html create mode 100644 src/browser/tests/legacy/events/mouse.html create mode 100644 src/browser/tests/legacy/fetch/fetch.html create mode 100644 src/browser/tests/legacy/fetch/headers.html create mode 100644 src/browser/tests/legacy/fetch/request.html create mode 100644 src/browser/tests/legacy/fetch/response.html create mode 100644 src/browser/tests/legacy/file/blob.html create mode 100644 src/browser/tests/legacy/file/file.html create mode 100644 src/browser/tests/legacy/html/abort_controller.html create mode 100644 src/browser/tests/legacy/html/canvas.html create mode 100644 src/browser/tests/legacy/html/dataset.html create mode 100644 src/browser/tests/legacy/html/document.html create mode 100644 src/browser/tests/legacy/html/element.html create mode 100644 src/browser/tests/legacy/html/error_event.html create mode 100644 src/browser/tests/legacy/html/history/history.html create mode 100644 src/browser/tests/legacy/html/history/history2.html create mode 100644 src/browser/tests/legacy/html/history/history_after_nav.html create mode 100644 src/browser/tests/legacy/html/image.html create mode 100644 src/browser/tests/legacy/html/input.html create mode 100644 src/browser/tests/legacy/html/link.html create mode 100644 src/browser/tests/legacy/html/location.html create mode 100644 src/browser/tests/legacy/html/navigation/navigation.html create mode 100644 src/browser/tests/legacy/html/navigation/navigation2.html create mode 100644 src/browser/tests/legacy/html/navigation/navigation_currententrychange.html create mode 100644 src/browser/tests/legacy/html/navigator.html create mode 100644 src/browser/tests/legacy/html/screen.html create mode 100644 src/browser/tests/legacy/html/script/dynamic_import.html create mode 100644 src/browser/tests/legacy/html/script/import.html create mode 100644 src/browser/tests/legacy/html/script/import.js create mode 100644 src/browser/tests/legacy/html/script/import2.js create mode 100644 src/browser/tests/legacy/html/script/importmap.html create mode 100644 src/browser/tests/legacy/html/script/inline_defer.html create mode 100644 src/browser/tests/legacy/html/script/inline_defer.js create mode 100644 src/browser/tests/legacy/html/script/order.html create mode 100644 src/browser/tests/legacy/html/script/order.js create mode 100644 src/browser/tests/legacy/html/script/order_async.js create mode 100644 src/browser/tests/legacy/html/script/order_defer.js create mode 100644 src/browser/tests/legacy/html/script/script.html create mode 100644 src/browser/tests/legacy/html/select.html create mode 100644 src/browser/tests/legacy/html/slot.html create mode 100644 src/browser/tests/legacy/html/style.html create mode 100644 src/browser/tests/legacy/html/svg.html create mode 100644 src/browser/tests/legacy/html/template.html create mode 100644 src/browser/tests/legacy/polyfill/webcomponents.html create mode 100644 src/browser/tests/legacy/storage/local_storage.html create mode 100644 src/browser/tests/legacy/streams/readable_stream.html create mode 100644 src/browser/tests/legacy/testing.js create mode 100644 src/browser/tests/legacy/url/url.html create mode 100644 src/browser/tests/legacy/url/url_search_params.html create mode 100644 src/browser/tests/legacy/window/frames.html create mode 100644 src/browser/tests/legacy/window/window.html create mode 100644 src/browser/tests/legacy/xhr/form_data.html create mode 100644 src/browser/tests/legacy/xhr/progress_event.html create mode 100644 src/browser/tests/legacy/xhr/xhr.html create mode 100644 src/browser/tests/legacy/xmlserializer.html create mode 100644 src/main_legacy_test.zig delete mode 100644 tests/html/bug-html-parsing-4.html diff --git a/build.zig b/build.zig index d7effb26b..9f6271699 100644 --- a/build.zig +++ b/build.zig @@ -112,6 +112,33 @@ pub fn build(b: *Build) !void { test_step.dependOn(&run_tests.step); } + { + // ZIGDOM + // browser + const exe = b.addExecutable(.{ + .name = "legacy_test", + .use_llvm = true, + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main_legacy_test.zig"), + .target = target, + .optimize = optimize, + .sanitize_c = enable_csan, + .sanitize_thread = enable_tsan, + .imports = &.{ + .{.name = "lightpanda", .module = lightpanda_module}, + }, + }), + }); + b.installArtifact(exe); + + const run_cmd = b.addRunArtifact(exe); + if (b.args) |args| { + run_cmd.addArgs(args); + } + const run_step = b.step("legacy_test", "Run the app"); + run_step.dependOn(&run_cmd.step); + } + { // wpt const exe = b.addExecutable(.{ diff --git a/src/browser/js/Context.zig b/src/browser/js/Context.zig index 5b6b510bb..72ce1bef6 100644 --- a/src/browser/js/Context.zig +++ b/src/browser/js/Context.zig @@ -615,6 +615,8 @@ pub fn mapZigInstanceToJs(self: *Context, js_obj_: ?v8.Object, value: anytype) ! } const isolate = self.isolate; + const JsApi = bridge.Struct(ptr.child).JsApi; + // Sometimes we're creating a new v8.Object, like when // we're returning a value from a function. In those cases // we have to get the object template, and we can get an object @@ -626,19 +628,26 @@ pub fn mapZigInstanceToJs(self: *Context, js_obj_: ?v8.Object, value: anytype) ! const template = self.templates[resolved.class_id]; break :blk template.getInstanceTemplate().initInstance(v8_context); }; - 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. - const tao = try arena.create(TaggedAnyOpaque); - tao.* = .{ - .value = resolved.ptr, - .prototype_chain = resolved.prototype_chain.ptr, - .prototype_len = @intCast(resolved.prototype_chain.len), - .subtype = if (@hasDecl(JsApi.Meta, "subtype")) JsApi.Meta.subype else .node, - }; - js_obj.setInternalField(0, v8.External.init(isolate, tao)); + if (!@hasDecl(JsApi.Meta, "empty_with_no_proto")) { + // 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. + const tao = try arena.create(TaggedAnyOpaque); + tao.* = .{ + .value = resolved.ptr, + .prototype_chain = resolved.prototype_chain.ptr, + .prototype_len = @intCast(resolved.prototype_chain.len), + .subtype = if (@hasDecl(JsApi.Meta, "subtype")) JsApi.Meta.subype else .node, + }; + js_obj.setInternalField(0, v8.External.init(isolate, tao)); + } else { + // If the struct is empty, we don't need to do all + // the TOA stuff and setting the internal data. + // When we try to map this from JS->Zig, in + // typeTaggedAnyOpaque, we'll also know there that + // the type is empty and can create an empty instance. + } const js_persistent = PersistentObject.init(isolate, js_obj); gop.value_ptr.* = js_persistent; @@ -1504,6 +1513,15 @@ pub fn typeTaggedAnyOpaque(comptime R: type, js_obj: v8.Object) !R { } const T = ti.pointer.child; + const JsApi = bridge.Struct(T).JsApi; + + if (@hasDecl(JsApi.Meta, "empty_with_no_proto")) { + // Empty structs aren't stored as TOAs and there's no data + // stored in the JSObject's IntenrnalField. Why bother when + // we can just return an empty struct here? + return @constCast(@as(*const T, &.{})); + } + // if it isn't an empty struct, then the v8.Object should have an // InternalFieldCount > 0, since our toa pointer should be embedded // at index 0 of the internal field count. @@ -1511,7 +1529,7 @@ pub fn typeTaggedAnyOpaque(comptime R: type, js_obj: v8.Object) !R { return error.InvalidArgument; } - const type_name = @typeName(bridge.Struct(T).JsApi); + const type_name = @typeName(JsApi); if (@hasField(bridge.JsApiLookup, type_name) == false) { @compileError("unknown Zig type: " ++ @typeName(R)); } diff --git a/src/browser/js/Env.zig b/src/browser/js/Env.zig index 71bed313a..d75afd4bd 100644 --- a/src/browser/js/Env.zig +++ b/src/browser/js/Env.zig @@ -314,7 +314,9 @@ fn generateConstructor(comptime JsApi: type, isolate: v8.Isolate) v8.FunctionTem }; const template = v8.FunctionTemplate.initCallback(isolate, callback); - template.getInstanceTemplate().setInternalFieldCount(1); + if (!@hasDecl(JsApi.Meta, "empty_with_no_proto")) { + template.getInstanceTemplate().setInternalFieldCount(1); + } const class_name = v8.String.initUtf8(isolate, if (@hasDecl(JsApi.Meta, "name")) JsApi.Meta.name else @typeName(JsApi)); template.setClassName(class_name); return template; diff --git a/src/browser/tests/domparser.html b/src/browser/tests/domparser.html index 390f7bfe7..660143889 100644 --- a/src/browser/tests/domparser.html +++ b/src/browser/tests/domparser.html @@ -1,13 +1,13 @@ - + - diff --git a/src/browser/tests/legacy/browser.html b/src/browser/tests/legacy/browser.html new file mode 100644 index 000000000..1f60488bf --- /dev/null +++ b/src/browser/tests/legacy/browser.html @@ -0,0 +1,10 @@ + + + diff --git a/src/browser/tests/legacy/crypto.html b/src/browser/tests/legacy/crypto.html new file mode 100644 index 000000000..f1dc291a7 --- /dev/null +++ b/src/browser/tests/legacy/crypto.html @@ -0,0 +1,26 @@ + + + diff --git a/src/browser/tests/legacy/css.html b/src/browser/tests/legacy/css.html new file mode 100644 index 000000000..3f83e9348 --- /dev/null +++ b/src/browser/tests/legacy/css.html @@ -0,0 +1,6 @@ + + + diff --git a/src/browser/tests/legacy/cssom/css_rule_list.html b/src/browser/tests/legacy/cssom/css_rule_list.html new file mode 100644 index 000000000..577781e4f --- /dev/null +++ b/src/browser/tests/legacy/cssom/css_rule_list.html @@ -0,0 +1,8 @@ + + + diff --git a/src/browser/tests/legacy/cssom/css_style_declaration.html b/src/browser/tests/legacy/cssom/css_style_declaration.html new file mode 100644 index 000000000..ee4d3cd9e --- /dev/null +++ b/src/browser/tests/legacy/cssom/css_style_declaration.html @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + diff --git a/src/browser/tests/legacy/cssom/css_stylesheet.html b/src/browser/tests/legacy/cssom/css_stylesheet.html new file mode 100644 index 000000000..223ee2cdb --- /dev/null +++ b/src/browser/tests/legacy/cssom/css_stylesheet.html @@ -0,0 +1,16 @@ + + + diff --git a/src/browser/tests/legacy/dom/animation.html b/src/browser/tests/legacy/dom/animation.html new file mode 100644 index 000000000..27e562a0f --- /dev/null +++ b/src/browser/tests/legacy/dom/animation.html @@ -0,0 +1,15 @@ + + + + diff --git a/src/browser/tests/legacy/dom/attribute.html b/src/browser/tests/legacy/dom/attribute.html new file mode 100644 index 000000000..2e2088615 --- /dev/null +++ b/src/browser/tests/legacy/dom/attribute.html @@ -0,0 +1,33 @@ + + + +OK + + diff --git a/src/browser/tests/legacy/dom/character_data.html b/src/browser/tests/legacy/dom/character_data.html new file mode 100644 index 000000000..ff74da90c --- /dev/null +++ b/src/browser/tests/legacy/dom/character_data.html @@ -0,0 +1,48 @@ + + + +OK + + diff --git a/src/browser/tests/legacy/dom/comment.html b/src/browser/tests/legacy/dom/comment.html new file mode 100644 index 000000000..2f87846cb --- /dev/null +++ b/src/browser/tests/legacy/dom/comment.html @@ -0,0 +1,9 @@ + + + diff --git a/src/browser/tests/legacy/dom/document.html b/src/browser/tests/legacy/dom/document.html new file mode 100644 index 000000000..950daaab6 --- /dev/null +++ b/src/browser/tests/legacy/dom/document.html @@ -0,0 +1,190 @@ + + + +
+ OK +

+ +

+

And

+
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/browser/tests/legacy/dom/document_fragment.html b/src/browser/tests/legacy/dom/document_fragment.html new file mode 100644 index 000000000..ff02b3a40 --- /dev/null +++ b/src/browser/tests/legacy/dom/document_fragment.html @@ -0,0 +1,34 @@ + + + + + diff --git a/src/browser/tests/legacy/dom/document_type.html b/src/browser/tests/legacy/dom/document_type.html new file mode 100644 index 000000000..ff7cdbc82 --- /dev/null +++ b/src/browser/tests/legacy/dom/document_type.html @@ -0,0 +1,13 @@ + + + diff --git a/src/browser/tests/legacy/dom/dom_parser.html b/src/browser/tests/legacy/dom/dom_parser.html new file mode 100644 index 000000000..bf9bec8aa --- /dev/null +++ b/src/browser/tests/legacy/dom/dom_parser.html @@ -0,0 +1,7 @@ + + + diff --git a/src/browser/tests/legacy/dom/element.html b/src/browser/tests/legacy/dom/element.html new file mode 100644 index 000000000..3255b7d2f --- /dev/null +++ b/src/browser/tests/legacy/dom/element.html @@ -0,0 +1,341 @@ + + + +
+ OK +

+ +

+

And

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +

content

+
+
+ + + + diff --git a/src/browser/tests/legacy/dom/event_target.html b/src/browser/tests/legacy/dom/event_target.html new file mode 100644 index 000000000..68fb8c6b1 --- /dev/null +++ b/src/browser/tests/legacy/dom/event_target.html @@ -0,0 +1,116 @@ + + + +

+ + diff --git a/src/browser/tests/legacy/dom/exceptions.html b/src/browser/tests/legacy/dom/exceptions.html new file mode 100644 index 000000000..c6bb91f1c --- /dev/null +++ b/src/browser/tests/legacy/dom/exceptions.html @@ -0,0 +1,40 @@ + + + +
+ OK +
+ + + + diff --git a/src/browser/tests/legacy/dom/html_collection.html b/src/browser/tests/legacy/dom/html_collection.html new file mode 100644 index 000000000..22590e581 --- /dev/null +++ b/src/browser/tests/legacy/dom/html_collection.html @@ -0,0 +1,67 @@ + + +
+ OK +

+ +

+

And

+ +
+ + + + + + + + + + diff --git a/src/browser/tests/legacy/dom/implementation.html b/src/browser/tests/legacy/dom/implementation.html new file mode 100644 index 000000000..81cce8041 --- /dev/null +++ b/src/browser/tests/legacy/dom/implementation.html @@ -0,0 +1,14 @@ + + + diff --git a/src/browser/tests/legacy/dom/intersection_observer.html b/src/browser/tests/legacy/dom/intersection_observer.html new file mode 100644 index 000000000..4067edba2 --- /dev/null +++ b/src/browser/tests/legacy/dom/intersection_observer.html @@ -0,0 +1,163 @@ + + + + + + + + + + + + + + + diff --git a/src/browser/tests/legacy/dom/message_channel.html b/src/browser/tests/legacy/dom/message_channel.html new file mode 100644 index 000000000..2ab075e54 --- /dev/null +++ b/src/browser/tests/legacy/dom/message_channel.html @@ -0,0 +1,60 @@ + + + diff --git a/src/browser/tests/legacy/dom/mutation_observer.html b/src/browser/tests/legacy/dom/mutation_observer.html new file mode 100644 index 000000000..f67cb9247 --- /dev/null +++ b/src/browser/tests/legacy/dom/mutation_observer.html @@ -0,0 +1,76 @@ + +
+

And

+

And

+

And

+ + + + + diff --git a/src/browser/tests/legacy/dom/named_node_map.html b/src/browser/tests/legacy/dom/named_node_map.html new file mode 100644 index 000000000..7cdcf4b71 --- /dev/null +++ b/src/browser/tests/legacy/dom/named_node_map.html @@ -0,0 +1,19 @@ + +
+ + + diff --git a/src/browser/tests/legacy/dom/node.html b/src/browser/tests/legacy/dom/node.html new file mode 100644 index 000000000..ae9b8a3ec --- /dev/null +++ b/src/browser/tests/legacy/dom/node.html @@ -0,0 +1,266 @@ + +
+ OK +

+ +

+

And

+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +"puppeteer " +

Leto + + + Atreides

+ diff --git a/src/browser/tests/legacy/dom/node_filter.html b/src/browser/tests/legacy/dom/node_filter.html new file mode 100644 index 000000000..d5ac95f4a --- /dev/null +++ b/src/browser/tests/legacy/dom/node_filter.html @@ -0,0 +1,219 @@ + + + + +
+ +
+ + + + Text content + + + +
+ +
+ + + + + + + + + + + + + + + + diff --git a/src/browser/tests/legacy/dom/node_iterator.html b/src/browser/tests/legacy/dom/node_iterator.html new file mode 100644 index 000000000..6225dea43 --- /dev/null +++ b/src/browser/tests/legacy/dom/node_iterator.html @@ -0,0 +1,62 @@ + + + + + +
+ OK +

+ +

+

And

+ +
+ diff --git a/src/browser/tests/legacy/dom/node_list.html b/src/browser/tests/legacy/dom/node_list.html new file mode 100644 index 000000000..911b8aa84 --- /dev/null +++ b/src/browser/tests/legacy/dom/node_list.html @@ -0,0 +1,19 @@ + +
+ OK +

+ +

+

And

+ +
+ + + diff --git a/src/browser/tests/legacy/dom/node_owner.html b/src/browser/tests/legacy/dom/node_owner.html new file mode 100644 index 000000000..0aec74c53 --- /dev/null +++ b/src/browser/tests/legacy/dom/node_owner.html @@ -0,0 +1,34 @@ + +
+

+ I am the original reference node. +

+
+ + + diff --git a/src/browser/tests/legacy/dom/performance.html b/src/browser/tests/legacy/dom/performance.html new file mode 100644 index 000000000..0fbfe6fd0 --- /dev/null +++ b/src/browser/tests/legacy/dom/performance.html @@ -0,0 +1,16 @@ + + + diff --git a/src/browser/tests/legacy/dom/performance_observer.html b/src/browser/tests/legacy/dom/performance_observer.html new file mode 100644 index 000000000..303fc15f0 --- /dev/null +++ b/src/browser/tests/legacy/dom/performance_observer.html @@ -0,0 +1,5 @@ + + + diff --git a/src/browser/tests/legacy/dom/processing_instruction.html b/src/browser/tests/legacy/dom/processing_instruction.html new file mode 100644 index 000000000..67bc8fc48 --- /dev/null +++ b/src/browser/tests/legacy/dom/processing_instruction.html @@ -0,0 +1,22 @@ + + + diff --git a/src/browser/tests/legacy/dom/range.html b/src/browser/tests/legacy/dom/range.html new file mode 100644 index 000000000..a60862ca6 --- /dev/null +++ b/src/browser/tests/legacy/dom/range.html @@ -0,0 +1,41 @@ + + + +

over 9000

+ + + + + + + + diff --git a/src/browser/tests/legacy/dom/shadow_root.html b/src/browser/tests/legacy/dom/shadow_root.html new file mode 100644 index 000000000..88a302db0 --- /dev/null +++ b/src/browser/tests/legacy/dom/shadow_root.html @@ -0,0 +1,49 @@ + +
node
+ + + diff --git a/src/browser/tests/legacy/dom/text.html b/src/browser/tests/legacy/dom/text.html new file mode 100644 index 000000000..d7ceba08e --- /dev/null +++ b/src/browser/tests/legacy/dom/text.html @@ -0,0 +1,19 @@ + +OK + + + diff --git a/src/browser/tests/legacy/dom/token_list.html b/src/browser/tests/legacy/dom/token_list.html new file mode 100644 index 000000000..b04d56586 --- /dev/null +++ b/src/browser/tests/legacy/dom/token_list.html @@ -0,0 +1,64 @@ + +

+ + + diff --git a/src/browser/tests/legacy/encoding/decoder.html b/src/browser/tests/legacy/encoding/decoder.html new file mode 100644 index 000000000..8a93dc46a --- /dev/null +++ b/src/browser/tests/legacy/encoding/decoder.html @@ -0,0 +1,60 @@ + + + + + + + + + diff --git a/src/browser/tests/legacy/encoding/encoder.html b/src/browser/tests/legacy/encoding/encoder.html new file mode 100644 index 000000000..affcd5750 --- /dev/null +++ b/src/browser/tests/legacy/encoding/encoder.html @@ -0,0 +1,14 @@ + + + diff --git a/src/browser/tests/legacy/events/composition.html b/src/browser/tests/legacy/events/composition.html new file mode 100644 index 000000000..b5a6a7100 --- /dev/null +++ b/src/browser/tests/legacy/events/composition.html @@ -0,0 +1,36 @@ + + + + + + + + + diff --git a/src/browser/tests/legacy/events/custom.html b/src/browser/tests/legacy/events/custom.html new file mode 100644 index 000000000..cb6ddd2b5 --- /dev/null +++ b/src/browser/tests/legacy/events/custom.html @@ -0,0 +1,25 @@ + + + diff --git a/src/browser/tests/legacy/events/event.html b/src/browser/tests/legacy/events/event.html new file mode 100644 index 000000000..752d64baa --- /dev/null +++ b/src/browser/tests/legacy/events/event.html @@ -0,0 +1,139 @@ + + + +

+

+
+ + + + + + + + + + + + + + + + diff --git a/src/browser/tests/legacy/events/keyboard.html b/src/browser/tests/legacy/events/keyboard.html new file mode 100644 index 000000000..2b3dbefb7 --- /dev/null +++ b/src/browser/tests/legacy/events/keyboard.html @@ -0,0 +1,88 @@ + + + + + + + + + + + + diff --git a/src/browser/tests/legacy/events/mouse.html b/src/browser/tests/legacy/events/mouse.html new file mode 100644 index 000000000..4c9b3f638 --- /dev/null +++ b/src/browser/tests/legacy/events/mouse.html @@ -0,0 +1,34 @@ + + + + + + + diff --git a/src/browser/tests/legacy/fetch/fetch.html b/src/browser/tests/legacy/fetch/fetch.html new file mode 100644 index 000000000..877f887b6 --- /dev/null +++ b/src/browser/tests/legacy/fetch/fetch.html @@ -0,0 +1,34 @@ + + + + diff --git a/src/browser/tests/legacy/fetch/headers.html b/src/browser/tests/legacy/fetch/headers.html new file mode 100644 index 000000000..57d6ce2ee --- /dev/null +++ b/src/browser/tests/legacy/fetch/headers.html @@ -0,0 +1,102 @@ + + + + + + + + + diff --git a/src/browser/tests/legacy/fetch/request.html b/src/browser/tests/legacy/fetch/request.html new file mode 100644 index 000000000..7bfdfe56e --- /dev/null +++ b/src/browser/tests/legacy/fetch/request.html @@ -0,0 +1,22 @@ + + + diff --git a/src/browser/tests/legacy/fetch/response.html b/src/browser/tests/legacy/fetch/response.html new file mode 100644 index 000000000..f65a2fea9 --- /dev/null +++ b/src/browser/tests/legacy/fetch/response.html @@ -0,0 +1,50 @@ + + + + + diff --git a/src/browser/tests/legacy/file/blob.html b/src/browser/tests/legacy/file/blob.html new file mode 100644 index 000000000..343fd32be --- /dev/null +++ b/src/browser/tests/legacy/file/blob.html @@ -0,0 +1,125 @@ + + + + + + + + + + + diff --git a/src/browser/tests/legacy/file/file.html b/src/browser/tests/legacy/file/file.html new file mode 100644 index 000000000..05f23ad78 --- /dev/null +++ b/src/browser/tests/legacy/file/file.html @@ -0,0 +1,7 @@ + + + + diff --git a/src/browser/tests/legacy/html/abort_controller.html b/src/browser/tests/legacy/html/abort_controller.html new file mode 100644 index 000000000..fc5a1cdfe --- /dev/null +++ b/src/browser/tests/legacy/html/abort_controller.html @@ -0,0 +1,41 @@ + + + + + + + + diff --git a/src/browser/tests/legacy/html/canvas.html b/src/browser/tests/legacy/html/canvas.html new file mode 100644 index 000000000..ab076487c --- /dev/null +++ b/src/browser/tests/legacy/html/canvas.html @@ -0,0 +1,29 @@ + + + + + + diff --git a/src/browser/tests/legacy/html/dataset.html b/src/browser/tests/legacy/html/dataset.html new file mode 100644 index 000000000..8eff69271 --- /dev/null +++ b/src/browser/tests/legacy/html/dataset.html @@ -0,0 +1,30 @@ + + +
+ + + + diff --git a/src/browser/tests/legacy/html/document.html b/src/browser/tests/legacy/html/document.html new file mode 100644 index 000000000..cc02f7c64 --- /dev/null +++ b/src/browser/tests/legacy/html/document.html @@ -0,0 +1,85 @@ + + + +
+ + + + + + diff --git a/src/browser/tests/legacy/html/element.html b/src/browser/tests/legacy/html/element.html new file mode 100644 index 000000000..4de1f0581 --- /dev/null +++ b/src/browser/tests/legacy/html/element.html @@ -0,0 +1,53 @@ + + +
abcc
+ + + + + + + + + + + diff --git a/src/browser/tests/legacy/html/error_event.html b/src/browser/tests/legacy/html/error_event.html new file mode 100644 index 000000000..be2c56a4c --- /dev/null +++ b/src/browser/tests/legacy/html/error_event.html @@ -0,0 +1,25 @@ + + + + diff --git a/src/browser/tests/legacy/html/history/history.html b/src/browser/tests/legacy/html/history/history.html new file mode 100644 index 000000000..fbb7dd952 --- /dev/null +++ b/src/browser/tests/legacy/html/history/history.html @@ -0,0 +1,37 @@ + + + + diff --git a/src/browser/tests/legacy/html/history/history2.html b/src/browser/tests/legacy/html/history/history2.html new file mode 100644 index 000000000..83dd809a8 --- /dev/null +++ b/src/browser/tests/legacy/html/history/history2.html @@ -0,0 +1,26 @@ + + + + diff --git a/src/browser/tests/legacy/html/history/history_after_nav.html b/src/browser/tests/legacy/html/history/history_after_nav.html new file mode 100644 index 000000000..d9e4e66d1 --- /dev/null +++ b/src/browser/tests/legacy/html/history/history_after_nav.html @@ -0,0 +1,6 @@ + + + + diff --git a/src/browser/tests/legacy/html/image.html b/src/browser/tests/legacy/html/image.html new file mode 100644 index 000000000..1e3f6aff2 --- /dev/null +++ b/src/browser/tests/legacy/html/image.html @@ -0,0 +1,32 @@ + + + + diff --git a/src/browser/tests/legacy/html/input.html b/src/browser/tests/legacy/html/input.html new file mode 100644 index 000000000..4a7e991a2 --- /dev/null +++ b/src/browser/tests/legacy/html/input.html @@ -0,0 +1,111 @@ + + + +
+

+ +

+
+ + + + + + + + diff --git a/src/browser/tests/legacy/html/link.html b/src/browser/tests/legacy/html/link.html new file mode 100644 index 000000000..15da64611 --- /dev/null +++ b/src/browser/tests/legacy/html/link.html @@ -0,0 +1,60 @@ + + +OK + + diff --git a/src/browser/tests/legacy/html/location.html b/src/browser/tests/legacy/html/location.html new file mode 100644 index 000000000..a5de3ba86 --- /dev/null +++ b/src/browser/tests/legacy/html/location.html @@ -0,0 +1,33 @@ + + + + + + diff --git a/src/browser/tests/legacy/html/navigation/navigation.html b/src/browser/tests/legacy/html/navigation/navigation.html new file mode 100644 index 000000000..24efe6c75 --- /dev/null +++ b/src/browser/tests/legacy/html/navigation/navigation.html @@ -0,0 +1,18 @@ + + + + diff --git a/src/browser/tests/legacy/html/navigation/navigation2.html b/src/browser/tests/legacy/html/navigation/navigation2.html new file mode 100644 index 000000000..b16fa917d --- /dev/null +++ b/src/browser/tests/legacy/html/navigation/navigation2.html @@ -0,0 +1,8 @@ + + + + diff --git a/src/browser/tests/legacy/html/navigation/navigation_currententrychange.html b/src/browser/tests/legacy/html/navigation/navigation_currententrychange.html new file mode 100644 index 000000000..c84bcbadd --- /dev/null +++ b/src/browser/tests/legacy/html/navigation/navigation_currententrychange.html @@ -0,0 +1,15 @@ + + + + diff --git a/src/browser/tests/legacy/html/navigator.html b/src/browser/tests/legacy/html/navigator.html new file mode 100644 index 000000000..fb2b3ffe3 --- /dev/null +++ b/src/browser/tests/legacy/html/navigator.html @@ -0,0 +1,8 @@ + + + + diff --git a/src/browser/tests/legacy/html/screen.html b/src/browser/tests/legacy/html/screen.html new file mode 100644 index 000000000..82f4b71cc --- /dev/null +++ b/src/browser/tests/legacy/html/screen.html @@ -0,0 +1,21 @@ + + + + + + diff --git a/src/browser/tests/legacy/html/script/dynamic_import.html b/src/browser/tests/legacy/html/script/dynamic_import.html new file mode 100644 index 000000000..ddaa19a22 --- /dev/null +++ b/src/browser/tests/legacy/html/script/dynamic_import.html @@ -0,0 +1,32 @@ + + + + + diff --git a/src/browser/tests/legacy/html/script/import.html b/src/browser/tests/legacy/html/script/import.html new file mode 100644 index 000000000..7a4037af7 --- /dev/null +++ b/src/browser/tests/legacy/html/script/import.html @@ -0,0 +1,15 @@ + + + + + + + diff --git a/src/browser/tests/legacy/html/script/import.js b/src/browser/tests/legacy/html/script/import.js new file mode 100644 index 000000000..fb140c03f --- /dev/null +++ b/src/browser/tests/legacy/html/script/import.js @@ -0,0 +1,2 @@ +let greeting = 'hello'; +export {greeting as 'greeting'}; diff --git a/src/browser/tests/legacy/html/script/import2.js b/src/browser/tests/legacy/html/script/import2.js new file mode 100644 index 000000000..328b8943d --- /dev/null +++ b/src/browser/tests/legacy/html/script/import2.js @@ -0,0 +1,2 @@ +let greeting = 'world'; +export {greeting as 'greeting'}; diff --git a/src/browser/tests/legacy/html/script/importmap.html b/src/browser/tests/legacy/html/script/importmap.html new file mode 100644 index 000000000..973d50806 --- /dev/null +++ b/src/browser/tests/legacy/html/script/importmap.html @@ -0,0 +1,24 @@ + + + + + + + + + + diff --git a/src/browser/tests/legacy/html/script/inline_defer.html b/src/browser/tests/legacy/html/script/inline_defer.html new file mode 100644 index 000000000..ec5b44c64 --- /dev/null +++ b/src/browser/tests/legacy/html/script/inline_defer.html @@ -0,0 +1,28 @@ + + + + + + + + + + + + diff --git a/src/browser/tests/legacy/html/script/inline_defer.js b/src/browser/tests/legacy/html/script/inline_defer.js new file mode 100644 index 000000000..1e0ee1a4f --- /dev/null +++ b/src/browser/tests/legacy/html/script/inline_defer.js @@ -0,0 +1 @@ +dyn1_loaded += 1; diff --git a/src/browser/tests/legacy/html/script/order.html b/src/browser/tests/legacy/html/script/order.html new file mode 100644 index 000000000..7efbbef32 --- /dev/null +++ b/src/browser/tests/legacy/html/script/order.html @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + diff --git a/src/browser/tests/legacy/html/script/order.js b/src/browser/tests/legacy/html/script/order.js new file mode 100644 index 000000000..31e602fc9 --- /dev/null +++ b/src/browser/tests/legacy/html/script/order.js @@ -0,0 +1,2 @@ +list += 'a'; +testing.expectEqual('a', list); diff --git a/src/browser/tests/legacy/html/script/order_async.js b/src/browser/tests/legacy/html/script/order_async.js new file mode 100644 index 000000000..97c9adac5 --- /dev/null +++ b/src/browser/tests/legacy/html/script/order_async.js @@ -0,0 +1,3 @@ +list += 'f'; +testing.expectEqual('abcdef', list); + diff --git a/src/browser/tests/legacy/html/script/order_defer.js b/src/browser/tests/legacy/html/script/order_defer.js new file mode 100644 index 000000000..3911b6445 --- /dev/null +++ b/src/browser/tests/legacy/html/script/order_defer.js @@ -0,0 +1,2 @@ +list += 'e'; +testing.expectEqual('abcde', list); diff --git a/src/browser/tests/legacy/html/script/script.html b/src/browser/tests/legacy/html/script/script.html new file mode 100644 index 000000000..5049e4bbb --- /dev/null +++ b/src/browser/tests/legacy/html/script/script.html @@ -0,0 +1,21 @@ + + + + + + diff --git a/src/browser/tests/legacy/html/select.html b/src/browser/tests/legacy/html/select.html new file mode 100644 index 000000000..f18dfdab3 --- /dev/null +++ b/src/browser/tests/legacy/html/select.html @@ -0,0 +1,80 @@ + + + +
+ +
+ + + diff --git a/src/browser/tests/legacy/html/slot.html b/src/browser/tests/legacy/html/slot.html new file mode 100644 index 000000000..026e13e08 --- /dev/null +++ b/src/browser/tests/legacy/html/slot.html @@ -0,0 +1,179 @@ + + + + + + +default +

default

+

default

xx other
+More

default2

!!
+ + + + + + + +
hello
+ + +
hello
+ + + + + +
hello
+ diff --git a/src/browser/tests/legacy/html/style.html b/src/browser/tests/legacy/html/style.html new file mode 100644 index 000000000..6463cd815 --- /dev/null +++ b/src/browser/tests/legacy/html/style.html @@ -0,0 +1,8 @@ + + + + diff --git a/src/browser/tests/legacy/html/svg.html b/src/browser/tests/legacy/html/svg.html new file mode 100644 index 000000000..368546493 --- /dev/null +++ b/src/browser/tests/legacy/html/svg.html @@ -0,0 +1,38 @@ + + + + + + OVER 9000!! + + + + + OVER 9000!!! + + + diff --git a/src/browser/tests/legacy/html/template.html b/src/browser/tests/legacy/html/template.html new file mode 100644 index 000000000..058c1dd32 --- /dev/null +++ b/src/browser/tests/legacy/html/template.html @@ -0,0 +1,38 @@ + + + +
+ + + + + + diff --git a/src/browser/tests/legacy/polyfill/webcomponents.html b/src/browser/tests/legacy/polyfill/webcomponents.html new file mode 100644 index 000000000..5854bc82c --- /dev/null +++ b/src/browser/tests/legacy/polyfill/webcomponents.html @@ -0,0 +1,23 @@ + + + +
+ + diff --git a/src/browser/tests/legacy/storage/local_storage.html b/src/browser/tests/legacy/storage/local_storage.html new file mode 100644 index 000000000..4ad0b14f9 --- /dev/null +++ b/src/browser/tests/legacy/storage/local_storage.html @@ -0,0 +1,29 @@ + + + + diff --git a/src/browser/tests/legacy/streams/readable_stream.html b/src/browser/tests/legacy/streams/readable_stream.html new file mode 100644 index 000000000..a8339cc50 --- /dev/null +++ b/src/browser/tests/legacy/streams/readable_stream.html @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + diff --git a/src/browser/tests/legacy/testing.js b/src/browser/tests/legacy/testing.js new file mode 100644 index 000000000..891d9cc2d --- /dev/null +++ b/src/browser/tests/legacy/testing.js @@ -0,0 +1,206 @@ +// Note: this code tries to make sure that we don't fail to execute a tags we have have had at least + // 1 assertion. This helps ensure that if a script tag fails to execute, + // we'll report an error, even if no assertions failed. + const scripts = document.getElementsByTagName('script'); + for (script of scripts) { + const id = script.id; + if (!id) { + continue; + } + + if (!testing._executed_scripts.has(id)) { + console.warn(`Failed to execute any expectations for `); + throw new Error('Failed'); + } + } + + if (testing._status != 'ok') { + throw new Error(testing._status); + } + } + + // Set expectations to happen at some point in the future. Necessary for + // testing callbacks which will only be executed after page.wait is called. + function eventually(fn) { + // capture the current state (script id, stack) so that, when we do run this + // we can display more meaningful details on failure. + testing._eventually.push([fn, { + script_id: document.currentScript.id, + stack: new Error().stack, + }]); + + _registerErrorCallback(); + } + + async function async(promise, cb) { + const script_id = document.currentScript ? document.currentScript.id : '.\n There should be a eval error printed above this.`, + ); + } + } + + function _equal(a, b) { + if (a === b) { + return true; + } + if (a === null || b === null) { + return false; + } + if (typeof a !== 'object' || typeof b !== 'object') { + return false; + } + + if (Object.keys(a).length != Object.keys(b).length) { + return false; + } + + for (property in a) { + if (b.hasOwnProperty(property) === false) { + return false; + } + if (_equal(a[property], b[property]) === false) { + return false; + } + } + + return true; + } + + window.testing = { + _status: 'empty', + _eventually: [], + _executed_scripts: new Set(), + _captured: null, + skip: skip, + async: async, + assertOk: assertOk, + eventually: eventually, + expectEqual: expectEqual, + expectError: expectError, + withError: withError, + }; + + // Helper, so you can do $(sel) in a test + window.$ = function(sel) { + return document.querySelector(sel); + } + + // Helper, so you can do $$(sel) in a test + window.$$ = function(sel) { + return document.querySelectorAll(sel); + } + + if (!console.lp) { + // make this work in the browser + console.lp = console.log; + } +})(); diff --git a/src/browser/tests/legacy/url/url.html b/src/browser/tests/legacy/url/url.html new file mode 100644 index 000000000..ef770e461 --- /dev/null +++ b/src/browser/tests/legacy/url/url.html @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + diff --git a/src/browser/tests/legacy/url/url_search_params.html b/src/browser/tests/legacy/url/url_search_params.html new file mode 100644 index 000000000..03f22bcda --- /dev/null +++ b/src/browser/tests/legacy/url/url_search_params.html @@ -0,0 +1,94 @@ + + + + + diff --git a/src/browser/tests/legacy/window/frames.html b/src/browser/tests/legacy/window/frames.html new file mode 100644 index 000000000..fc4b7abc4 --- /dev/null +++ b/src/browser/tests/legacy/window/frames.html @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/src/browser/tests/legacy/window/window.html b/src/browser/tests/legacy/window/window.html new file mode 100644 index 000000000..aac911718 --- /dev/null +++ b/src/browser/tests/legacy/window/window.html @@ -0,0 +1,167 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/browser/tests/legacy/xhr/form_data.html b/src/browser/tests/legacy/xhr/form_data.html new file mode 100644 index 000000000..94bf8a272 --- /dev/null +++ b/src/browser/tests/legacy/xhr/form_data.html @@ -0,0 +1,130 @@ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ diff --git a/src/browser/tests/legacy/xhr/progress_event.html b/src/browser/tests/legacy/xhr/progress_event.html new file mode 100644 index 000000000..4b7f5df4a --- /dev/null +++ b/src/browser/tests/legacy/xhr/progress_event.html @@ -0,0 +1,17 @@ + + + diff --git a/src/browser/tests/legacy/xhr/xhr.html b/src/browser/tests/legacy/xhr/xhr.html new file mode 100644 index 000000000..13ab6216e --- /dev/null +++ b/src/browser/tests/legacy/xhr/xhr.html @@ -0,0 +1,110 @@ + + + + + + + + + + + diff --git a/src/browser/tests/legacy/xmlserializer.html b/src/browser/tests/legacy/xmlserializer.html new file mode 100644 index 000000000..0d3d46284 --- /dev/null +++ b/src/browser/tests/legacy/xmlserializer.html @@ -0,0 +1,8 @@ + + +

And

+ diff --git a/src/browser/webapi/DOMParser.zig b/src/browser/webapi/DOMParser.zig index 358312955..051f004b3 100644 --- a/src/browser/webapi/DOMParser.zig +++ b/src/browser/webapi/DOMParser.zig @@ -24,8 +24,6 @@ const Document = @import("Document.zig"); const HTMLDocument = @import("HTMLDocument.zig"); const DOMParser = @This(); -// @ZIGDOM support empty structs -_: u8 = 0, pub fn init() DOMParser { return .{}; @@ -63,6 +61,7 @@ pub const JsApi = struct { pub const name = "DOMParser"; pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; + pub const empty_with_no_proto = true; }; pub const constructor = bridge.constructor(DOMParser.init, .{}); diff --git a/src/browser/webapi/NodeFilter.zig b/src/browser/webapi/NodeFilter.zig index c9fab4155..232355dc5 100644 --- a/src/browser/webapi/NodeFilter.zig +++ b/src/browser/webapi/NodeFilter.zig @@ -85,6 +85,7 @@ pub const JsApi = struct { pub const name = "NodeFilter"; pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; + pub const empty_with_no_proto = true; }; pub const FILTER_ACCEPT = bridge.property(NodeFilter.FILTER_ACCEPT); diff --git a/src/lightpanda.zig b/src/lightpanda.zig index 57d277934..9c15f7224 100644 --- a/src/lightpanda.zig +++ b/src/lightpanda.zig @@ -19,8 +19,12 @@ const std = @import("std"); pub const App = @import("App.zig"); pub const Server = @import("Server.zig"); +pub const Page = @import("browser/Page.zig"); +pub const Browser = @import("browser/Browser.zig"); +pub const Session = @import("browser/Session.zig"); pub const log = @import("log.zig"); +pub const js = @import("browser/js/js.zig"); pub const dump = @import("browser/dump.zig"); pub const build_config = @import("build_config"); @@ -32,7 +36,6 @@ pub const FetchOpts = struct { writer: ?*std.Io.Writer = null, }; pub fn fetch(app: *App, url: [:0]const u8, opts: FetchOpts) !void { - const Browser = @import("browser/Browser.zig"); var browser = try Browser.init(app); defer browser.deinit(); diff --git a/src/main_legacy_test.zig b/src/main_legacy_test.zig new file mode 100644 index 000000000..4dc93e7e2 --- /dev/null +++ b/src/main_legacy_test.zig @@ -0,0 +1,238 @@ +const std = @import("std"); +const lp = @import("lightpanda"); + +const Allocator = std.mem.Allocator; + +// used in custom panic handler +var current_test: ?[]const u8 = null; + +pub fn main() !void { + var gpa: std.heap.DebugAllocator(.{}) = .init; + defer _ = gpa.deinit(); + + const allocator = gpa.allocator(); + + var http_server = try TestHTTPServer.init(); + defer http_server.deinit(); + + { + var wg: std.Thread.WaitGroup = .{}; + wg.startMany(1); + var thrd = try std.Thread.spawn(.{}, TestHTTPServer.run, .{ &http_server, &wg }); + thrd.detach(); + wg.wait(); + } + lp.log.opts.level = .warn; + + var app = try lp.App.init(allocator, .{ + .run_mode = .serve, + .tls_verify_host = false, + .user_agent = "User-Agent: Lightpanda/1.0 internal-tester", + }); + defer app.deinit(); + + var test_arena = std.heap.ArenaAllocator.init(allocator); + defer test_arena.deinit(); + + var browser = try lp.Browser.init(app); + defer browser.deinit(); + + const session = try browser.newSession(); + + var dir = try std.fs.cwd().openDir("src/browser/tests/legacy/", .{ .iterate = true, .no_follow = true }); + defer dir.close(); + var walker = try dir.walk(allocator); + defer walker.deinit(); + while (try walker.next()) |entry| { + _ = test_arena.reset(.retain_capacity); + if (entry.kind != .file) { + continue; + } + + if (!std.mem.endsWith(u8, entry.basename, ".html")) { + continue; + } + std.debug.print("\n===={s}====\n", .{entry.path}); + current_test = entry.path; + run(test_arena.allocator(), entry.path, session) catch |err| { + std.debug.print("Failure: {s} - {any}\n", .{ entry.path, err }); + }; + } +} + +pub fn run(allocator: Allocator, file: []const u8, session: *lp.Session) !void { + const url = try std.fmt.allocPrintSentinel(allocator, "http://localhost:9589/{s}", .{file}, 0); + + const page = try session.createPage(); + defer session.removePage(); + + const js_context = page.js; + var try_catch: lp.js.TryCatch = undefined; + try_catch.init(js_context); + defer try_catch.deinit(); + + try page.navigate(url, .{}); + session.fetchWait(2000); + + page._session.browser.runMicrotasks(); + page._session.browser.runMessageLoop(); + + js_context.eval("testing.assertOk()", "testing.assertOk()") catch |err| { + const msg = try_catch.err(allocator) catch @errorName(err) orelse "unknown"; + + std.debug.print("{s}: test failure\nError: {s}\n", .{ file, msg }); + return err; + }; +} + +const TestHTTPServer = struct { + shutdown: bool, + dir: std.fs.Dir, + listener: ?std.net.Server, + + pub fn init() !TestHTTPServer { + return .{ + .dir = try std.fs.cwd().openDir("src/browser/tests/legacy/", .{}), + .shutdown = true, + .listener = null, + }; + } + + pub fn deinit(self: *TestHTTPServer) void { + self.shutdown = true; + if (self.listener) |*listener| { + listener.deinit(); + } + self.dir.close(); + } + + pub fn run(self: *TestHTTPServer, wg: *std.Thread.WaitGroup) !void { + const address = try std.net.Address.parseIp("127.0.0.1", 9589); + + self.listener = try address.listen(.{ .reuse_address = true }); + var listener = &self.listener.?; + + wg.finish(); + + while (true) { + const conn = listener.accept() catch |err| { + if (self.shutdown) { + return; + } + return err; + }; + const thrd = try std.Thread.spawn(.{}, handleConnection, .{ self, conn }); + thrd.detach(); + } + } + + fn handleConnection(self: *TestHTTPServer, conn: std.net.Server.Connection) !void { + defer conn.stream.close(); + + var req_buf: [2048]u8 = undefined; + var conn_reader = conn.stream.reader(&req_buf); + var conn_writer = conn.stream.writer(&req_buf); + + var http_server = std.http.Server.init(conn_reader.interface(), &conn_writer.interface); + + while (true) { + var req = http_server.receiveHead() catch |err| switch (err) { + error.ReadFailed => continue, + error.HttpConnectionClosing => continue, + else => { + std.debug.print("Test HTTP Server error: {}\n", .{err}); + 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 }); + return; + }; + } + } + + fn handler(server: *TestHTTPServer, req: *std.http.Server.Request) !void { + const path = req.head.target; + + // strip out leading '/' to make the path relative + const file = try server.dir.openFile(path[1..], .{}); + defer file.close(); + + const stat = try file.stat(); + var send_buffer: [4096]u8 = undefined; + + var res = try req.respondStreaming(&send_buffer, .{ + .content_length = stat.size, + .respond_options = .{ + .extra_headers = &.{ + .{ .name = "content-type", .value = getContentType(path) }, + }, + }, + }); + + var read_buffer: [4096]u8 = undefined; + var reader = file.reader(&read_buffer); + _ = try res.writer.sendFileAll(&reader, .unlimited); + try res.writer.flush(); + try res.end(); + } + + pub fn sendFile(req: *std.http.Server.Request, file_path: []const u8) !void { + var file = std.fs.cwd().openFile(file_path, .{}) catch |err| switch (err) { + error.FileNotFound => return req.respond("server error", .{ .status = .not_found }), + else => return err, + }; + defer file.close(); + + const stat = try file.stat(); + var send_buffer: [4096]u8 = undefined; + + var res = try req.respondStreaming(&send_buffer, .{ + .content_length = stat.size, + .respond_options = .{ + .extra_headers = &.{ + .{ .name = "content-type", .value = getContentType(file_path) }, + }, + }, + }); + + var read_buffer: [4096]u8 = undefined; + var reader = file.reader(&read_buffer); + _ = try res.writer.sendFileAll(&reader, .unlimited); + try res.writer.flush(); + try res.end(); + } + + fn getContentType(file_path: []const u8) []const u8 { + if (std.mem.endsWith(u8, file_path, ".js")) { + return "application/json"; + } + + if (std.mem.endsWith(u8, file_path, ".html")) { + return "text/html"; + } + + if (std.mem.endsWith(u8, file_path, ".htm")) { + return "text/html"; + } + + if (std.mem.endsWith(u8, file_path, ".xml")) { + // some wpt tests do this + return "text/xml"; + } + + std.debug.print("TestHTTPServer asked to serve an unknown file type: {s}\n", .{file_path}); + return "text/html"; + } +}; + +pub const panic = std.debug.FullPanic(struct { + pub fn panicFn(msg: []const u8, first_trace_addr: ?usize) noreturn { + if (current_test) |ct| { + std.debug.print("===panic running: {s}===\n", .{ct}); + } + std.debug.defaultPanic(msg, first_trace_addr); + } +}.panicFn); diff --git a/tests/html/bug-html-parsing-4.html b/tests/html/bug-html-parsing-4.html deleted file mode 100644 index 391ac0c7d..000000000 --- a/tests/html/bug-html-parsing-4.html +++ /dev/null @@ -1,6 +0,0 @@ - - - - - From 04f719c33c244da33ffa61f4193eeee88a13dc01 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Fri, 14 Nov 2025 16:14:12 +0800 Subject: [PATCH 25/30] wpt runner --- src/browser/parser/Parser.zig | 1 - src/browser/parser/html5ever.zig | 1 - src/main_wpt.zig | 190 ++++++++++++++++++++++++++----- 3 files changed, 160 insertions(+), 32 deletions(-) diff --git a/src/browser/parser/Parser.zig b/src/browser/parser/Parser.zig index f4c6232fd..f7cd5c557 100644 --- a/src/browser/parser/Parser.zig +++ b/src/browser/parser/Parser.zig @@ -16,7 +16,6 @@ // 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 h5e = @import("html5ever.zig"); diff --git a/src/browser/parser/html5ever.zig b/src/browser/parser/html5ever.zig index ea3e7668b..529852902 100644 --- a/src/browser/parser/html5ever.zig +++ b/src/browser/parser/html5ever.zig @@ -16,7 +16,6 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . - const ParsedNode = @import("Parser.zig").ParsedNode; pub extern "c" fn html5ever_parse_document( diff --git a/src/main_wpt.zig b/src/main_wpt.zig index 99d7adc6b..ff512e408 100644 --- a/src/main_wpt.zig +++ b/src/main_wpt.zig @@ -17,17 +17,11 @@ // along with this program. If not, see . const std = @import("std"); - -const log = @import("log.zig"); -const js = @import("browser/js/js.zig"); +const lp = @import("lightpanda"); const Allocator = std.mem.Allocator; const ArenaAllocator = std.heap.ArenaAllocator; -const App = @import("app.zig").App; -const Browser = @import("browser/browser.zig").Browser; -const TestHTTPServer = @import("TestHTTPServer.zig"); - const WPT_DIR = "tests/wpt"; // use in custom panic handler @@ -38,9 +32,8 @@ pub fn main() !void { defer _ = gpa.deinit(); const allocator = gpa.allocator(); - log.opts.level = .err; - var http_server = TestHTTPServer.init(httpHandler); + var http_server = try TestHTTPServer.init(); defer http_server.deinit(); { @@ -64,19 +57,21 @@ pub fn main() !void { var writer = try Writer.init(allocator, cmd.format); defer writer.deinit(); - // An arena for running each tests. Is reset after every test. - var test_arena = ArenaAllocator.init(allocator); - defer test_arena.deinit(); - - var app = try App.init(allocator, .{ - .run_mode = .fetch, - .user_agent = "User-Agent: Lightpanda/1.0 Lightpanda/WPT", + lp.log.opts.level = .warn; + var app = try lp.App.init(allocator, .{ + .run_mode = .serve, + .tls_verify_host = false, + .user_agent = "User-Agent: Lightpanda/1.0 internal-tester", }); defer app.deinit(); - var browser = try Browser.init(app); + var browser = try lp.Browser.init(app); defer browser.deinit(); + // An arena for running each tests. Is reset after every test. + var test_arena = ArenaAllocator.init(allocator); + defer test_arena.deinit(); + var i: usize = 0; while (try it.next()) |test_file| { defer _ = test_arena.reset(.retain_capacity); @@ -108,7 +103,7 @@ pub fn main() !void { fn run( arena: Allocator, - browser: *Browser, + browser: *lp.Browser, test_file: []const u8, err_out: *?[]const u8, ) ![]const u8 { @@ -118,13 +113,13 @@ fn run( const page = try session.createPage(); defer session.removePage(); - const url = try std.fmt.allocPrint(arena, "http://localhost:9582/{s}", .{test_file}); + const url = try std.fmt.allocPrintSentinel(arena, "http://localhost:9582/{s}", .{test_file}, 0); try page.navigate(url, .{}); _ = page.wait(2000); const js_context = page.js; - var try_catch: js.TryCatch = undefined; + var try_catch: lp.js.TryCatch = undefined; try_catch.init(js_context); defer try_catch.deinit(); @@ -442,19 +437,154 @@ const Test = struct { cases: []Case, }; -fn httpHandler(req: *std.http.Server.Request) !void { - const path = req.head.target; +const TestHTTPServer = struct { + shutdown: bool, + dir: std.fs.Dir, + listener: ?std.net.Server, - if (std.mem.eql(u8, path, "/")) { - // There's 1 test that does an XHR request to this, and it just seems - // to want a 200 success. - return req.respond("Hello!", .{}); + pub fn init() !TestHTTPServer { + return .{ + .dir = try std.fs.cwd().openDir(WPT_DIR, .{}), + .shutdown = true, + .listener = null, + }; } - var buf: [1024]u8 = undefined; - const file_path = try std.fmt.bufPrint(&buf, WPT_DIR ++ "{s}", .{path}); - return TestHTTPServer.sendFile(req, file_path); -} + pub fn deinit(self: *TestHTTPServer) void { + self.shutdown = true; + if (self.listener) |*listener| { + listener.deinit(); + } + self.dir.close(); + } + + pub fn run(self: *TestHTTPServer, wg: *std.Thread.WaitGroup) !void { + const address = try std.net.Address.parseIp("127.0.0.1", 9582); + + self.listener = try address.listen(.{ .reuse_address = true }); + var listener = &self.listener.?; + + wg.finish(); + + while (true) { + const conn = listener.accept() catch |err| { + if (self.shutdown) { + return; + } + return err; + }; + const thrd = try std.Thread.spawn(.{}, handleConnection, .{ self, conn }); + thrd.detach(); + } + } + + fn handleConnection(self: *TestHTTPServer, conn: std.net.Server.Connection) !void { + defer conn.stream.close(); + + var req_buf: [2048]u8 = undefined; + var conn_reader = conn.stream.reader(&req_buf); + var conn_writer = conn.stream.writer(&req_buf); + + var http_server = std.http.Server.init(conn_reader.interface(), &conn_writer.interface); + + while (true) { + var req = http_server.receiveHead() catch |err| switch (err) { + error.ReadFailed => continue, + error.HttpConnectionClosing => continue, + else => { + std.debug.print("Test HTTP Server error: {}\n", .{err}); + 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 }); + return; + }; + } + } + + fn handler(server: *TestHTTPServer, req: *std.http.Server.Request) !void { + const path = req.head.target; + + if (std.mem.eql(u8, path, "/")) { + // There's 1 test that does an XHR request to this, and it just seems + // to want a 200 success. + return req.respond("Hello!", .{}); + } + + // strip out leading '/' to make the path relative + const file = try server.dir.openFile(path[1..], .{}); + defer file.close(); + + const stat = try file.stat(); + var send_buffer: [4096]u8 = undefined; + + var res = try req.respondStreaming(&send_buffer, .{ + .content_length = stat.size, + .respond_options = .{ + .extra_headers = &.{ + .{ .name = "content-type", .value = getContentType(path) }, + }, + }, + }); + + var read_buffer: [4096]u8 = undefined; + var reader = file.reader(&read_buffer); + _ = try res.writer.sendFileAll(&reader, .unlimited); + try res.writer.flush(); + try res.end(); + } + + pub fn sendFile(req: *std.http.Server.Request, file_path: []const u8) !void { + var file = std.fs.cwd().openFile(file_path, .{}) catch |err| switch (err) { + error.FileNotFound => return req.respond("server error", .{ .status = .not_found }), + else => return err, + }; + defer file.close(); + + const stat = try file.stat(); + var send_buffer: [4096]u8 = undefined; + + var res = try req.respondStreaming(&send_buffer, .{ + .content_length = stat.size, + .respond_options = .{ + .extra_headers = &.{ + .{ .name = "content-type", .value = getContentType(file_path) }, + }, + }, + }); + + var read_buffer: [4096]u8 = undefined; + var reader = file.reader(&read_buffer); + _ = try res.writer.sendFileAll(&reader, .unlimited); + try res.writer.flush(); + try res.end(); + } + + fn getContentType(file_path: []const u8) []const u8 { + if (std.mem.endsWith(u8, file_path, ".js")) { + return "application/json"; + } + + if (std.mem.endsWith(u8, file_path, ".html")) { + return "text/html"; + } + + if (std.mem.endsWith(u8, file_path, ".htm")) { + return "text/html"; + } + + if (std.mem.endsWith(u8, file_path, ".xml")) { + // some wpt tests do this + return "text/xml"; + } + + std.debug.print("TestHTTPServer asked to serve an unknown file type: {s}\n", .{file_path}); + return "text/html"; + } +}; pub const panic = std.debug.FullPanic(struct { pub fn panicFn(msg: []const u8, first_trace_addr: ?usize) noreturn { From 5ae74d6924ca549c5eb7934fffb42d9153731e4d Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Fri, 14 Nov 2025 17:56:09 +0800 Subject: [PATCH 26/30] improve form element support --- src/browser/tests/element/html/button.html | 73 +++++ src/browser/tests/element/html/input.html | 107 ++++++- src/browser/tests/element/html/option.html | 34 +++ src/browser/tests/element/html/select.html | 283 ++++++++++++++++++ src/browser/tests/element/html/textarea.html | 73 +++++ src/browser/webapi/Element.zig | 1 + src/browser/webapi/collections.zig | 2 + .../webapi/collections/HTMLCollection.zig | 10 + .../collections/HTMLOptionsCollection.zig | 106 +++++++ src/browser/webapi/collections/node_live.zig | 18 +- src/browser/webapi/element/Attribute.zig | 2 +- src/browser/webapi/element/html/Button.zig | 22 ++ src/browser/webapi/element/html/Input.zig | 54 +++- src/browser/webapi/element/html/Option.zig | 41 ++- src/browser/webapi/element/html/Select.zig | 177 ++++++++--- src/browser/webapi/element/html/TextArea.zig | 22 ++ 16 files changed, 975 insertions(+), 50 deletions(-) create mode 100644 src/browser/webapi/collections/HTMLOptionsCollection.zig diff --git a/src/browser/tests/element/html/button.html b/src/browser/tests/element/html/button.html index dc7d5855c..76e5be8bd 100644 --- a/src/browser/tests/element/html/button.html +++ b/src/browser/tests/element/html/button.html @@ -53,3 +53,76 @@ const buttonInvalidFormAttr = $('#button_invalid_form_attr') testing.expectEqual(null, buttonInvalidFormAttr.form) + + + + + + + + + + + + + + + + + + diff --git a/src/browser/tests/element/html/input.html b/src/browser/tests/element/html/input.html index 9f762d421..dbd79aa3d 100644 --- a/src/browser/tests/element/html/input.html +++ b/src/browser/tests/element/html/input.html @@ -15,7 +15,7 @@ - + + + + + diff --git a/src/browser/tests/element/html/option.html b/src/browser/tests/element/html/option.html index 30d023780..6e7f72c8d 100644 --- a/src/browser/tests/element/html/option.html +++ b/src/browser/tests/element/html/option.html @@ -65,3 +65,37 @@ $('#opt4').disabled = false testing.expectEqual(false, $('#opt4').disabled) + + + + + + + + + diff --git a/src/browser/tests/element/html/select.html b/src/browser/tests/element/html/select.html index a6a835a64..ceb46c16b 100644 --- a/src/browser/tests/element/html/select.html +++ b/src/browser/tests/element/html/select.html @@ -81,3 +81,286 @@ const selectNoForm = $('#select_no_form') testing.expectEqual(null, selectNoForm.form) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/browser/tests/element/html/textarea.html b/src/browser/tests/element/html/textarea.html index f20e182e9..f820288eb 100644 --- a/src/browser/tests/element/html/textarea.html +++ b/src/browser/tests/element/html/textarea.html @@ -76,3 +76,76 @@ const textareaInvalidFormAttr = $('#textarea_invalid_form_attr') testing.expectEqual(null, textareaInvalidFormAttr.form) + + + + + + + + + + + + + + + + + + diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index 9f0fdd5f5..0ea4783ea 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -407,6 +407,7 @@ pub fn replaceChildren(self: *Element, nodes: []const Node.NodeOrText, page: *Pa } pub fn remove(self: *Element, page: *Page) void { + page.domChanged(); const node = self.asNode(); const parent = node._parent orelse return; page.removeNode(parent, node, .{ .will_be_reconnected = false }); diff --git a/src/browser/webapi/collections.zig b/src/browser/webapi/collections.zig index 0e091cbda..eead81b92 100644 --- a/src/browser/webapi/collections.zig +++ b/src/browser/webapi/collections.zig @@ -20,6 +20,7 @@ pub const NodeLive = @import("collections/node_live.zig").NodeLive; pub const ChildNodes = @import("collections/ChildNodes.zig"); pub const DOMTokenList = @import("collections/DOMTokenList.zig"); pub const HTMLAllCollection = @import("collections/HTMLAllCollection.zig"); +pub const HTMLOptionsCollection = @import("collections/HTMLOptionsCollection.zig"); pub fn registerTypes() []const type { return &.{ @@ -31,6 +32,7 @@ pub fn registerTypes() []const type { @import("collections/NodeList.zig").EntryIterator, @import("collections/HTMLAllCollection.zig"), @import("collections/HTMLAllCollection.zig").Iterator, + HTMLOptionsCollection, DOMTokenList, DOMTokenList.Iterator, }; diff --git a/src/browser/webapi/collections/HTMLCollection.zig b/src/browser/webapi/collections/HTMLCollection.zig index e3f42a904..54c99bffa 100644 --- a/src/browser/webapi/collections/HTMLCollection.zig +++ b/src/browser/webapi/collections/HTMLCollection.zig @@ -29,6 +29,8 @@ const Mode = enum { tag_name, class_name, child_elements, + child_tag, + selected_options, }; const HTMLCollection = @This(); @@ -38,6 +40,8 @@ data: union(Mode) { tag_name: NodeLive(.tag_name), class_name: NodeLive(.class_name), child_elements: NodeLive(.child_elements), + child_tag: NodeLive(.child_tag), + selected_options: NodeLive(.selected_options), }, pub fn length(self: *HTMLCollection, page: *const Page) u32 { @@ -66,6 +70,8 @@ pub fn iterator(self: *HTMLCollection, page: *Page) !*Iterator { .tag_name => |*impl| .{ .tag_name = impl._tw.clone() }, .class_name => |*impl| .{ .class_name = impl._tw.clone() }, .child_elements => |*impl| .{ .child_elements = impl._tw.clone() }, + .child_tag => |*impl| .{ .child_tag = impl._tw.clone() }, + .selected_options => |*impl| .{ .selected_options = impl._tw.clone() }, }, }, page); } @@ -78,6 +84,8 @@ pub const Iterator = GenericIterator(struct { tag_name: TreeWalker.FullExcludeSelf, class_name: TreeWalker.FullExcludeSelf, child_elements: TreeWalker.Children, + child_tag: TreeWalker.Children, + selected_options: TreeWalker.Children, }, pub fn next(self: *@This(), _: *Page) ?*Element { @@ -86,6 +94,8 @@ pub const Iterator = GenericIterator(struct { .tag_name => |*impl| impl.nextTw(&self.tw.tag_name), .class_name => |*impl| impl.nextTw(&self.tw.class_name), .child_elements => |*impl| impl.nextTw(&self.tw.child_elements), + .child_tag => |*impl| impl.nextTw(&self.tw.child_tag), + .selected_options => |*impl| impl.nextTw(&self.tw.selected_options), }; } }, null); diff --git a/src/browser/webapi/collections/HTMLOptionsCollection.zig b/src/browser/webapi/collections/HTMLOptionsCollection.zig new file mode 100644 index 000000000..4c9d59c44 --- /dev/null +++ b/src/browser/webapi/collections/HTMLOptionsCollection.zig @@ -0,0 +1,106 @@ +// 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 js = @import("../../js/js.zig"); +const Page = @import("../../Page.zig"); +const Element = @import("../Element.zig"); +const HTMLCollection = @import("HTMLCollection.zig"); +const NodeLive = @import("node_live.zig").NodeLive; + +const HTMLOptionsCollection = @This(); + +_proto: *HTMLCollection, +_select: *@import("../element/html/Select.zig"), + +pub fn deinit(self: *HTMLOptionsCollection) void { + const page = Page.current; + page._factory.destroy(self); +} + +// Forward length to HTMLCollection +pub fn length(self: *HTMLOptionsCollection, page: *Page) u32 { + return self._proto.length(page); +} + +// Forward indexed access to HTMLCollection +pub fn getAtIndex(self: *HTMLOptionsCollection, index: usize, page: *Page) ?*Element { + return self._proto.getAtIndex(index, page); +} + +pub fn getByName(self: *HTMLOptionsCollection, name: []const u8, page: *Page) ?*Element { + return self._proto.getByName(name, page); +} + +// Forward selectedIndex to the owning select element +pub fn getSelectedIndex(self: *const HTMLOptionsCollection) i32 { + return self._select.getSelectedIndex(); +} + +pub fn setSelectedIndex(self: *HTMLOptionsCollection, index: i32) !void { + return self._select.setSelectedIndex(index); +} + +const Option = @import("../element/html/Option.zig"); + +// Add a new option element +pub fn add(self: *HTMLOptionsCollection, element: *Option, before: ?*Option, page: *Page) !void { + const select_node = self._select.asNode(); + const element_node = element.asElement().asNode(); + + if (before) |before_option| { + const before_node = before_option.asElement().asNode(); + _ = try select_node.insertBefore(element_node, before_node, page); + } else { + _ = try select_node.appendChild(element_node, page); + } +} + +// Remove an option element by index +pub fn remove(self: *HTMLOptionsCollection, index: i32, page: *Page) void { + if (index < 0) { + return; + } + + if (self._proto.getAtIndex(@intCast(index), page)) |element| { + element.remove(page); + } +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(HTMLOptionsCollection); + + pub const Meta = struct { + pub const name = "HTMLOptionsCollection"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + pub const finalizer = HTMLOptionsCollection.deinit; + pub const manage = false; + }; + + pub const length = bridge.accessor(HTMLOptionsCollection.length, null, .{}); + + // Indexed access + pub const @"[int]" = bridge.indexed(HTMLOptionsCollection.getAtIndex, .{ .null_as_undefined = true }); + pub const @"[str]" = bridge.namedIndexed(HTMLOptionsCollection.getByName, null, null, .{ .null_as_undefined = true }); + + pub const selectedIndex = bridge.accessor(HTMLOptionsCollection.getSelectedIndex, HTMLOptionsCollection.setSelectedIndex, .{}); + pub const add = bridge.function(HTMLOptionsCollection.add, .{}); + pub const remove = bridge.function(HTMLOptionsCollection.remove, .{}); +}; diff --git a/src/browser/webapi/collections/node_live.zig b/src/browser/webapi/collections/node_live.zig index 45eea51c9..ee123b4c4 100644 --- a/src/browser/webapi/collections/node_live.zig +++ b/src/browser/webapi/collections/node_live.zig @@ -36,6 +36,8 @@ const Mode = enum { tag_name, class_name, child_elements, + child_tag, + selected_options, }; const Filters = union(Mode) { @@ -43,6 +45,8 @@ const Filters = union(Mode) { tag_name: String, class_name: []const u8, child_elements, + child_tag: Element.Tag, + selected_options, fn TypeOf(comptime mode: Mode) type { @setEvalBranchQuota(2000); @@ -71,7 +75,7 @@ pub fn NodeLive(comptime mode: Mode) type { const Filter = Filters.TypeOf(mode); const TW = switch (mode) { .tag, .tag_name, .class_name => TreeWalker.FullExcludeSelf, - .child_elements => TreeWalker.Children, + .child_elements, .child_tag, .selected_options => TreeWalker.Children, }; return struct { _tw: TW, @@ -213,6 +217,16 @@ pub fn NodeLive(comptime mode: Mode) type { return Selector.classAttributeContains(class_attr, self._filter); }, .child_elements => return node._type == .element, + .child_tag => { + const el = node.is(Element) orelse return false; + return el.getTag() == self._filter; + }, + .selected_options => { + const el = node.is(Element) orelse return false; + const Option = Element.Html.Option; + const opt = el.is(Option) orelse return false; + return opt.getSelected(); + }, } } @@ -236,6 +250,8 @@ pub fn NodeLive(comptime mode: Mode) type { .tag_name => HTMLCollection{ .data = .{ .tag_name = self } }, .class_name => HTMLCollection{ .data = .{ .class_name = self } }, .child_elements => HTMLCollection{ .data = .{ .child_elements = self } }, + .child_tag => HTMLCollection{ .data = .{ .child_tag = self } }, + .selected_options => HTMLCollection{ .data = .{ .selected_options = self } }, }; return page._factory.create(collection); } diff --git a/src/browser/webapi/element/Attribute.zig b/src/browser/webapi/element/Attribute.zig index 66357754e..3d37f173c 100644 --- a/src/browser/webapi/element/Attribute.zig +++ b/src/browser/webapi/element/Attribute.zig @@ -174,7 +174,7 @@ pub const List = struct { if (is_id) { try page.document._elements_by_id.put(page.arena, entry._value.str(), element); } - page.attributeChange(element, result.normalized, value); + page.attributeChange(element, result.normalized, entry._value.str()); return entry; } diff --git a/src/browser/webapi/element/html/Button.zig b/src/browser/webapi/element/html/Button.zig index 2e1a40165..acf076e61 100644 --- a/src/browser/webapi/element/html/Button.zig +++ b/src/browser/webapi/element/html/Button.zig @@ -50,6 +50,26 @@ pub fn setDisabled(self: *Button, disabled: bool, page: *Page) !void { } } +pub fn getName(self: *const Button) []const u8 { + return self.asConstElement().getAttributeSafe("name") orelse ""; +} + +pub fn setName(self: *Button, name: []const u8, page: *Page) !void { + try self.asElement().setAttributeSafe("name", name, page); +} + +pub fn getRequired(self: *const Button) bool { + return self.asConstElement().getAttributeSafe("required") != null; +} + +pub fn setRequired(self: *Button, required: bool, page: *Page) !void { + if (required) { + try self.asElement().setAttributeSafe("required", "", page); + } else { + try self.asElement().removeAttribute("required", page); + } +} + pub fn getForm(self: *Button, page: *Page) ?*Form { const element = self.asElement(); @@ -84,6 +104,8 @@ pub const JsApi = struct { }; pub const disabled = bridge.accessor(Button.getDisabled, Button.setDisabled, .{}); + pub const name = bridge.accessor(Button.getName, Button.setName, .{}); + pub const required = bridge.accessor(Button.getRequired, Button.setRequired, .{}); pub const form = bridge.accessor(Button.getForm, null, .{}); }; diff --git a/src/browser/webapi/element/html/Input.zig b/src/browser/webapi/element/html/Input.zig index d805ba6fe..9c4593a92 100644 --- a/src/browser/webapi/element/html/Input.zig +++ b/src/browser/webapi/element/html/Input.zig @@ -109,6 +109,10 @@ pub fn getDefaultValue(self: *const Input) []const u8 { return self._default_value orelse ""; } +pub fn setDefaultValue(self: *Input, value: []const u8, page: *Page) !void { + try self.asElement().setAttributeSafe("value", value, page); +} + pub fn getChecked(self: *const Input) bool { return self._checked; } @@ -126,6 +130,14 @@ pub fn getDefaultChecked(self: *const Input) bool { return self._default_checked; } +pub fn setDefaultChecked(self: *Input, checked: bool, page: *Page) !void { + if (checked) { + try self.asElement().setAttributeSafe("checked", "", page); + } else { + try self.asElement().removeAttribute("checked", page); + } +} + pub fn getDisabled(self: *const Input) bool { // TODO: Also check for disabled fieldset ancestors // (but not if we're inside a of that fieldset) @@ -140,6 +152,26 @@ pub fn setDisabled(self: *Input, disabled: bool, page: *Page) !void { } } +pub fn getName(self: *const Input) []const u8 { + return self.asConstElement().getAttributeSafe("name") orelse ""; +} + +pub fn setName(self: *Input, name: []const u8, page: *Page) !void { + try self.asElement().setAttributeSafe("name", name, page); +} + +pub fn getRequired(self: *const Input) bool { + return self.asConstElement().getAttributeSafe("required") != null; +} + +pub fn setRequired(self: *Input, required: bool, page: *Page) !void { + if (required) { + try self.asElement().setAttributeSafe("required", "", page); + } else { + try self.asElement().removeAttribute("required", page); + } +} + pub fn getForm(self: *Input, page: *Page) ?*Form { const element = self.asElement(); @@ -218,10 +250,12 @@ pub const JsApi = struct { pub const @"type" = bridge.accessor(Input.getType, Input.setType, .{}); pub const value = bridge.accessor(Input.getValue, Input.setValue, .{}); - pub const defaultValue = bridge.accessor(Input.getDefaultValue, null, .{}); + pub const defaultValue = bridge.accessor(Input.getDefaultValue, Input.setDefaultValue, .{}); pub const checked = bridge.accessor(Input.getChecked, Input.setChecked, .{}); - pub const defaultChecked = bridge.accessor(Input.getDefaultChecked, null, .{}); + pub const defaultChecked = bridge.accessor(Input.getDefaultChecked, Input.setDefaultChecked, .{}); pub const disabled = bridge.accessor(Input.getDisabled, Input.setDisabled, .{}); + pub const name = bridge.accessor(Input.getName, Input.setName, .{}); + pub const required = bridge.accessor(Input.getRequired, Input.setRequired, .{}); pub const form = bridge.accessor(Input.getForm, null, .{}); }; @@ -249,13 +283,20 @@ pub const Build = struct { } } - pub fn attributeChange(element: *Element, name: []const u8, value: []const u8, _: *Page) !void { + pub fn attributeChange(element: *Element, name: []const u8, value: []const u8, page: *Page) !void { const attribute = std.meta.stringToEnum(enum { type, value, checked }, name) orelse return; const self = element.as(Input); switch (attribute) { .type => self._input_type = Type.fromString(value), .value => self._default_value = value, - .checked => self._default_checked = true, + .checked => { + self._default_checked = true; + self._checked = true; + // If setting a radio button to checked, uncheck others in the group + if (self._input_type == .radio) { + try self.uncheckRadioGroup(page); + } + }, } } @@ -265,7 +306,10 @@ pub const Build = struct { switch (attribute) { .type => self._input_type = .text, .value => self._default_value = null, - .checked => self._default_checked = false, + .checked => { + self._default_checked = false; + self._checked = false; + }, } } }; diff --git a/src/browser/webapi/element/html/Option.zig b/src/browser/webapi/element/html/Option.zig index 5123e088e..b5718a1ec 100644 --- a/src/browser/webapi/element/html/Option.zig +++ b/src/browser/webapi/element/html/Option.zig @@ -16,6 +16,7 @@ // 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 js = @import("../../../js/js.zig"); const Page = @import("../../../Page.zig"); @@ -35,6 +36,9 @@ _disabled: bool = false, pub fn asElement(self: *Option) *Element { return self._proto._proto; } +pub fn asConstElement(self: *const Option) *const Element { + return self._proto._proto; +} pub fn asNode(self: *Option) *Node { return self.asElement().asNode(); } @@ -45,7 +49,7 @@ pub fn getValue(self: *const Option) []const u8 { } pub fn setValue(self: *Option, value: []const u8, page: *Page) !void { - const owned = try page.arena.dupe(u8, value); + const owned = try page.dupeString(value); try self.asElement().setAttributeSafe("value", owned, page); self._value = owned; } @@ -59,10 +63,10 @@ pub fn getSelected(self: *const Option) bool { } pub fn setSelected(self: *Option, selected: bool, page: *Page) !void { - _ = page; // TODO: When setting selected=true, may need to unselect other options // in the parent