From b047cb6dc166fca24325947e50a10be0c072f865 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Sat, 18 Oct 2025 07:49:15 +0800 Subject: [PATCH 001/219] 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 002/219] 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 003/219] 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 004/219] 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 005/219] 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 007/219] 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 018/219] 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 019/219] 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 020/219] 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 021/219] 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 022/219] 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 023/219] 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 024/219] 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 025/219] 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 026/219] 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 - - - - - + + diff --git a/src/browser/webapi/selector/List.zig b/src/browser/webapi/selector/List.zig index 7174ecc20..481a3697a 100644 --- a/src/browser/webapi/selector/List.zig +++ b/src/browser/webapi/selector/List.zig @@ -500,6 +500,10 @@ fn attributeContainsWord(value: []const u8, word: []const u8) bool { fn matchesPseudoClass(el: *Node.Element, pseudo: Selector.PseudoClass) bool { switch (pseudo) { .modal => return false, + .checked => { + const input = el.is(Node.Element.Html.Input) orelse return false; + return input.getChecked(); + }, .first_child => return isFirstChild(el), .last_child => return isLastChild(el), .only_child => return isFirstChild(el) and isLastChild(el), diff --git a/src/browser/webapi/selector/Parser.zig b/src/browser/webapi/selector/Parser.zig index 0b465e1d2..498132910 100644 --- a/src/browser/webapi/selector/Parser.zig +++ b/src/browser/webapi/selector/Parser.zig @@ -395,6 +395,8 @@ fn pseudoClass(self: *Parser, arena: Allocator, page: *Page) !Selector.PseudoCla return .{ .not = selectors.items }; } + + return error.UnknownPseudoClass; } @@ -402,6 +404,9 @@ fn pseudoClass(self: *Parser, arena: Allocator, page: *Page) !Selector.PseudoCla 5 => { if (fastEql(name, "modal")) return .modal; }, + 7 => { + if (fastEql(name, "checked")) return .checked; + }, 10 => { if (fastEql(name, "only-child")) return .only_child; if (fastEql(name, "last-child")) return .last_child; diff --git a/src/browser/webapi/selector/Selector.zig b/src/browser/webapi/selector/Selector.zig index defc30148..15579b65b 100644 --- a/src/browser/webapi/selector/Selector.zig +++ b/src/browser/webapi/selector/Selector.zig @@ -132,6 +132,7 @@ pub const AttributeMatcher = union(enum) { pub const PseudoClass = union(enum) { modal, + checked, first_child, last_child, only_child, From 470f5b5029bccdf77494abb717f1a07ae150607a Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Fri, 21 Nov 2025 20:22:24 +0800 Subject: [PATCH 062/219] Headers and improved Request --- src/browser/js/bridge.zig | 1 + src/browser/tests/net/headers.html | 31 +++++++++ src/browser/tests/net/request.html | 104 +++++++++++++++++++++++++++++ src/browser/webapi/net/Fetch.zig | 2 +- src/browser/webapi/net/Headers.zig | 63 +++++++++++++++++ src/browser/webapi/net/Request.zig | 78 +++++++++++++++++++++- 6 files changed, 275 insertions(+), 4 deletions(-) create mode 100644 src/browser/tests/net/headers.html create mode 100644 src/browser/tests/net/request.html create mode 100644 src/browser/webapi/net/Headers.zig diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index f9ba64f6e..7acb84739 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -550,6 +550,7 @@ pub const JsApis = flattenTypes(&.{ @import("../webapi/Location.zig"), @import("../webapi/Navigator.zig"), @import("../webapi/net/FormData.zig"), + @import("../webapi/net/Headers.zig"), @import("../webapi/net/Request.zig"), @import("../webapi/net/Response.zig"), @import("../webapi/net/URLSearchParams.zig"), diff --git a/src/browser/tests/net/headers.html b/src/browser/tests/net/headers.html new file mode 100644 index 000000000..d0d1c35ea --- /dev/null +++ b/src/browser/tests/net/headers.html @@ -0,0 +1,31 @@ + + + diff --git a/src/browser/tests/net/request.html b/src/browser/tests/net/request.html new file mode 100644 index 000000000..c0028cf83 --- /dev/null +++ b/src/browser/tests/net/request.html @@ -0,0 +1,104 @@ + + + + + + + + + diff --git a/src/browser/webapi/net/Fetch.zig b/src/browser/webapi/net/Fetch.zig index 7bbc2da94..547a6ab1c 100644 --- a/src/browser/webapi/net/Fetch.zig +++ b/src/browser/webapi/net/Fetch.zig @@ -39,7 +39,7 @@ pub const Input = Request.Input; // @ZIGDOM just enough to get campire demo working pub fn init(input: Input, page: *Page) !js.Promise { - const request = try Request.init(input, page); + const request = try Request.init(input, null, page); const fetch = try page.arena.create(Fetch); fetch.* = .{ diff --git a/src/browser/webapi/net/Headers.zig b/src/browser/webapi/net/Headers.zig new file mode 100644 index 000000000..2f2fa68f2 --- /dev/null +++ b/src/browser/webapi/net/Headers.zig @@ -0,0 +1,63 @@ +const std = @import("std"); +const js = @import("../../js/js.zig"); + +const Page = @import("../../Page.zig"); +const KeyValueList = @import("../KeyValueList.zig"); + +const Headers = @This(); + +_list: KeyValueList, + +pub fn init(page: *Page) !*Headers { + return page._factory.create(Headers{ + ._list = KeyValueList.init(), + }); +} + + +pub fn append(self: *Headers, name: []const u8, value: []const u8, page: *Page) !void { + try self._list.append(page.arena, name, value); +} + +pub fn delete(self: *Headers, name: []const u8) void { + self._list.delete(name, null); +} + +pub fn get(self: *const Headers, name: []const u8) ?[]const u8 { + return self._list.get(name); +} + +pub fn getAll(self: *const Headers, name: []const u8, page: *Page) ![]const []const u8 { + return self._list.getAll(name, page); +} + +pub fn has(self: *const Headers, name: []const u8) bool { + return self._list.has(name); +} + +pub fn set(self: *Headers, name: []const u8, value: []const u8, page: *Page) !void { + try self._list.set(page.arena, name, value); +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(Headers); + + pub const Meta = struct { + pub const name = "Headers"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; + + pub const constructor = bridge.constructor(Headers.init, .{}); + pub const append = bridge.function(Headers.append, .{}); + pub const delete = bridge.function(Headers.delete, .{}); + pub const get = bridge.function(Headers.get, .{}); + pub const getAll = bridge.function(Headers.getAll, .{}); + pub const has = bridge.function(Headers.has, .{}); + pub const set = bridge.function(Headers.set, .{}); +}; + +const testing = @import("../../../testing.zig"); +test "WebApi: Headers" { + try testing.htmlRunner("net/headers.html", .{}); +} diff --git a/src/browser/webapi/net/Request.zig b/src/browser/webapi/net/Request.zig index d715c53b2..d1524afe1 100644 --- a/src/browser/webapi/net/Request.zig +++ b/src/browser/webapi/net/Request.zig @@ -22,28 +22,92 @@ const js = @import("../../js/js.zig"); const URL = @import("../URL.zig"); const Page = @import("../../Page.zig"); +const Headers = @import("Headers.zig"); const Allocator = std.mem.Allocator; const Request = @This(); _url: [:0]const u8, +_method: std.http.Method, +_headers: ?*Headers, _arena: Allocator, pub const Input = union(enum) { + request: *Request, url: [:0]const u8, - // request: *Request, TODO }; -pub fn init(input: Input, page: *Page) !*Request { +pub const Options = struct { + method: ?[]const u8 = null, + headers: ?*Headers = null, +}; + +pub fn init(input: Input, opts_: ?Options, page: *Page) !*Request { const arena = page.arena; - const url = try URL.resolve(arena, page.url, input.url, .{ .always_dupe = true }); + const url = switch (input) { + .url => |u| try URL.resolve(arena, page.url, u, .{ .always_dupe = true }), + .request => |r| try arena.dupeZ(u8, r._url), + }; + + const opts = opts_ orelse Options{}; + const method = if (opts.method) |m| + try parseMethod(m, page) + else switch (input) { + .url => .GET, + .request => |r| r._method, + }; + + const headers = if (opts.headers) |h| + h + else switch (input) { + .url => null, + .request => |r| r._headers, + }; return page._factory.create(Request{ ._url = url, ._arena = arena, + ._method = method, + ._headers = headers, }); } +fn parseMethod(method: []const u8, page: *Page) !std.http.Method { + if (method.len > "options".len) { + return error.InvalidMethod; + } + + const lower = std.ascii.lowerString(&page.buf, method); + + if (std.mem.eql(u8, lower, "get")) return .GET; + if (std.mem.eql(u8, lower, "post")) return .POST; + if (std.mem.eql(u8, lower, "delete")) return .DELETE; + if (std.mem.eql(u8, lower, "put")) return .PUT; + if (std.mem.eql(u8, lower, "patch")) return .PATCH; + if (std.mem.eql(u8, lower, "head")) return .HEAD; + if (std.mem.eql(u8, lower, "options")) return .OPTIONS; + + return error.InvalidMethod; +} + +pub fn getUrl(self: *const Request) []const u8 { + return self._url; +} + +pub fn getMethod(self: *const Request) []const u8 { + return @tagName(self._method); +} + +pub fn getHeaders(self: *Request, page: *Page) !*Headers { + if (self._headers) |headers| { + return headers; + } + + const headers = try Headers.init(page); + self._headers = headers; + return headers; +} + pub const JsApi = struct { pub const bridge = js.Bridge(Request); @@ -54,4 +118,12 @@ pub const JsApi = struct { }; pub const constructor = bridge.constructor(Request.init, .{}); + pub const url = bridge.accessor(Request.getUrl, null, .{}); + pub const method = bridge.accessor(Request.getMethod, null, .{}); + pub const headers = bridge.accessor(Request.getHeaders, null, .{}); }; + +const testing = @import("../../../testing.zig"); +test "WebApi: Request" { + try testing.htmlRunner("net/request.html", .{}); +} From 357df22fabf37ccb9e91778c5f12d02427c1525f Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Fri, 21 Nov 2025 22:23:34 +0800 Subject: [PATCH 063/219] more pseudoclass support --- src/browser/tests/element/pseudo_classes.html | 82 ++++++++ .../tests/element/selector_invalid.html | 62 ++++++ src/browser/webapi/selector/List.zig | 196 +++++++++++++++--- src/browser/webapi/selector/Parser.zig | 130 ++++++++++++ src/browser/webapi/selector/Selector.zig | 44 +++- 5 files changed, 481 insertions(+), 33 deletions(-) create mode 100644 src/browser/tests/element/pseudo_classes.html create mode 100644 src/browser/tests/element/selector_invalid.html diff --git a/src/browser/tests/element/pseudo_classes.html b/src/browser/tests/element/pseudo_classes.html new file mode 100644 index 000000000..8114cae0a --- /dev/null +++ b/src/browser/tests/element/pseudo_classes.html @@ -0,0 +1,82 @@ + + + +
+

First

+ + + Content +
+ + + + + + + + + + + + + + diff --git a/src/browser/tests/element/selector_invalid.html b/src/browser/tests/element/selector_invalid.html new file mode 100644 index 000000000..826ce7b7b --- /dev/null +++ b/src/browser/tests/element/selector_invalid.html @@ -0,0 +1,62 @@ + + + +
+

Test

+
+ + + + + + + + + + diff --git a/src/browser/webapi/selector/List.zig b/src/browser/webapi/selector/List.zig index 481a3697a..8ce759a84 100644 --- a/src/browser/webapi/selector/List.zig +++ b/src/browser/webapi/selector/List.zig @@ -53,9 +53,8 @@ pub fn collect( _ = tw.next(); } - const boundary = if (result.exclude_root) result.root else null; while (tw.next()) |node| { - if (matches(node, result.selector, boundary)) { + if (matches(node, result.selector, page)) { try nodes.put(allocator, node, {}); } } @@ -71,9 +70,8 @@ pub fn initOne(root: *Node, selector: Selector.Selector, page: *Page) ?*Node { if (result.exclude_root) { _ = tw.next(); } - const boundary = if (result.exclude_root) result.root else null; while (tw.next()) |node| { - if (matches(node, optimized_selector, boundary)) { + if (matches(node, optimized_selector, page)) { return node; } } @@ -175,7 +173,7 @@ fn optimizeSelector(root: *Node, selector: *const Selector.Selector, page: *Page .segments = selector.segments[0 .. seg_idx + 1], }; - if (!matches(id_node, prefix_selector, null)) { + if (!matches(id_node, prefix_selector, page)) { return null; } @@ -250,23 +248,23 @@ fn findIdSelector(selector: *const Selector.Selector) ?IdAnchor { return null; } -pub fn matches(node: *Node, selector: Selector.Selector, root: ?*Node) bool { +pub fn matches(node: *Node, selector: Selector.Selector, page: *Page) bool { const el = node.is(Node.Element) orelse return false; if (selector.segments.len == 0) { - return matchesCompound(el, selector.first); + return matchesCompound(el, selector.first, page); } const last_segment = selector.segments[selector.segments.len - 1]; - if (!matchesCompound(el, last_segment.compound)) { + if (!matchesCompound(el, last_segment.compound, page)) { return false; } - return matchSegments(node, selector, selector.segments.len - 1, root); + return matchSegments(node, selector, selector.segments.len - 1, null, page); } // Match segments backward, with support for backtracking on subsequent_sibling -fn matchSegments(node: *Node, selector: Selector.Selector, segment_index: usize, root: ?*Node) bool { +fn matchSegments(node: *Node, selector: Selector.Selector, segment_index: usize, root: ?*Node, page: *Page) bool { const segment = selector.segments[segment_index]; const target_compound = if (segment_index == 0) selector.first @@ -274,9 +272,9 @@ fn matchSegments(node: *Node, selector: Selector.Selector, segment_index: usize, selector.segments[segment_index - 1].compound; const matched: ?*Node = switch (segment.combinator) { - .descendant => matchDescendant(node, target_compound, root), - .child => matchChild(node, target_compound, root), - .next_sibling => matchNextSibling(node, target_compound), + .descendant => matchDescendant(node, target_compound, root, page), + .child => matchChild(node, target_compound, root, page), + .next_sibling => matchNextSibling(node, target_compound, page), .subsequent_sibling => { // For subsequent_sibling, try all matching siblings with backtracking var sibling = node.previousSibling(); @@ -286,13 +284,13 @@ fn matchSegments(node: *Node, selector: Selector.Selector, segment_index: usize, continue; }; - if (matchesCompound(sibling_el, target_compound)) { + if (matchesCompound(sibling_el, target_compound, page)) { // If we're at the first segment, we found a match if (segment_index == 0) { return true; } // Try to match remaining segments from this sibling - if (matchSegments(s, selector, segment_index - 1, root)) { + if (matchSegments(s, selector, segment_index - 1, root, page)) { return true; } // This sibling didn't work, try the next one @@ -309,7 +307,7 @@ fn matchSegments(node: *Node, selector: Selector.Selector, segment_index: usize, if (segment_index == 0) { return true; } - return matchSegments(current, selector, segment_index - 1, root); + return matchSegments(current, selector, segment_index - 1, root, page); } // subsequent_sibling already handled its recursion above @@ -317,12 +315,12 @@ fn matchSegments(node: *Node, selector: Selector.Selector, segment_index: usize, } // Find an ancestor that matches the compound (any distance up the tree) -fn matchDescendant(node: *Node, compound: Selector.Compound, root: ?*Node) ?*Node { +fn matchDescendant(node: *Node, compound: Selector.Compound, root: ?*Node, page: *Page) ?*Node { var current = node._parent; while (current) |ancestor| { if (ancestor.is(Node.Element)) |ancestor_el| { - if (matchesCompound(ancestor_el, compound)) { + if (matchesCompound(ancestor_el, compound, page)) { return ancestor; } } @@ -341,7 +339,7 @@ fn matchDescendant(node: *Node, compound: Selector.Compound, root: ?*Node) ?*Nod } // Find the direct parent if it matches the compound -fn matchChild(node: *Node, compound: Selector.Compound, root: ?*Node) ?*Node { +fn matchChild(node: *Node, compound: Selector.Compound, root: ?*Node, page: *Page) ?*Node { const parent = node._parent orelse return null; // Don't match beyond the root boundary @@ -354,7 +352,7 @@ fn matchChild(node: *Node, compound: Selector.Compound, root: ?*Node) ?*Node { const parent_el = parent.is(Node.Element) orelse return null; - if (matchesCompound(parent_el, compound)) { + if (matchesCompound(parent_el, compound, page)) { return parent; } @@ -362,7 +360,7 @@ fn matchChild(node: *Node, compound: Selector.Compound, root: ?*Node) ?*Node { } // Find the immediately preceding sibling if it matches the compound -fn matchNextSibling(node: *Node, compound: Selector.Compound) ?*Node { +fn matchNextSibling(node: *Node, compound: Selector.Compound, page: *Page) ?*Node { var sibling = node.previousSibling(); // For next_sibling (+), we need the immediately preceding element sibling @@ -374,7 +372,7 @@ fn matchNextSibling(node: *Node, compound: Selector.Compound) ?*Node { }; // Found an element - check if it matches - if (matchesCompound(sibling_el, compound)) { + if (matchesCompound(sibling_el, compound, page)) { return s; } // we found an element, it wasn't a match, we're done @@ -385,7 +383,7 @@ fn matchNextSibling(node: *Node, compound: Selector.Compound) ?*Node { } // Find any preceding sibling that matches the compound -fn matchSubsequentSibling(node: *Node, compound: Selector.Compound) ?*Node { +fn matchSubsequentSibling(node: *Node, compound: Selector.Compound, page: *Page) ?*Node { var sibling = node.previousSibling(); // For subsequent_sibling (~), check all preceding element siblings @@ -396,7 +394,7 @@ fn matchSubsequentSibling(node: *Node, compound: Selector.Compound) ?*Node { continue; }; - if (matchesCompound(sibling_el, compound)) { + if (matchesCompound(sibling_el, compound, page)) { return s; } @@ -406,17 +404,17 @@ fn matchSubsequentSibling(node: *Node, compound: Selector.Compound) ?*Node { return null; } -fn matchesCompound(el: *Node.Element, compound: Selector.Compound) bool { +fn matchesCompound(el: *Node.Element, compound: Selector.Compound, page: *Page) bool { // For compound selectors, ALL parts must match for (compound.parts) |part| { - if (!matchesPart(el, part)) { + if (!matchesPart(el, part, page)) { return false; } } return true; } -fn matchesPart(el: *Node.Element, part: Part) bool { +fn matchesPart(el: *Node.Element, part: Part, page: *Page) bool { switch (part) { .id => |id| { const element_id = el.getAttributeSafe("id") orelse return false; @@ -437,7 +435,7 @@ fn matchesPart(el: *Node.Element, part: Part) bool { return std.mem.eql(u8, element_tag, tag_name); }, .universal => return true, - .pseudo_class => |pseudo| return matchesPseudoClass(el, pseudo), + .pseudo_class => |pseudo| return matchesPseudoClass(el, pseudo, page), .attribute => |attr| return matchesAttribute(el, attr), } } @@ -497,13 +495,79 @@ fn attributeContainsWord(value: []const u8, word: []const u8) bool { return false; } -fn matchesPseudoClass(el: *Node.Element, pseudo: Selector.PseudoClass) bool { +fn matchesPseudoClass(el: *Node.Element, pseudo: Selector.PseudoClass, page: *Page) bool { + const node = el.asNode(); switch (pseudo) { + // State pseudo-classes .modal => return false, .checked => { const input = el.is(Node.Element.Html.Input) orelse return false; return input.getChecked(); }, + .disabled => { + return el.getAttributeSafe("disabled") != null; + }, + .enabled => { + return el.getAttributeSafe("disabled") == null; + }, + .indeterminate => return false, + + // Form validation + .valid => return false, + .invalid => return false, + .required => { + return el.getAttributeSafe("required") != null; + }, + .optional => { + return el.getAttributeSafe("required") == null; + }, + .in_range => return false, + .out_of_range => return false, + .placeholder_shown => return false, + .read_only => { + return el.getAttributeSafe("readonly") != null; + }, + .read_write => { + return el.getAttributeSafe("readonly") == null; + }, + .default => return false, + + // User interaction + .hover => return false, + .active => return false, + .focus => { + const active = page.document._active_element orelse return false; + return active == el; + }, + .focus_within => { + const active = page.document._active_element orelse return false; + return node.contains(active.asNode()); + }, + .focus_visible => return false, + + // Link states + .link => return false, + .visited => return false, + .any_link => { + if (el.getTag() != .anchor) return false; + return el.getAttributeSafe("href") != null; + }, + .target => { + const element_id = el.getAttributeSafe("id") orelse return false; + const location = page.document._location orelse return false; + const hash = location.getHash(); + if (hash.len <= 1) return false; + return std.mem.eql(u8, element_id, hash[1..]); + }, + + // Tree structural + .root => { + const parent = node.parentNode() orelse return false; + return parent._type == .document; + }, + .empty => { + return node.firstChild() == null; + }, .first_child => return isFirstChild(el), .last_child => return isLastChild(el), .only_child => return isFirstChild(el) and isLastChild(el), @@ -514,19 +578,87 @@ fn matchesPseudoClass(el: *Node.Element, pseudo: Selector.PseudoClass) bool { .nth_last_child => |pattern| return matchesNthLastChild(el, pattern), .nth_of_type => |pattern| return matchesNthOfType(el, pattern), .nth_last_of_type => |pattern| return matchesNthLastOfType(el, pattern), + + // Custom elements + .defined => { + const tag_name = el.getTagNameLower(); + if (std.mem.indexOfScalar(u8, tag_name, '-') == null) return true; + const registry = &page.window._custom_elements; + return registry.get(tag_name) != null; + }, + + // Functional + .lang => return false, .not => |selectors| { - // CSS Level 4: :not() matches if NONE of the selectors match - // Each selector in the list is evaluated independently for (selectors) |selector| { - if (matches(el.asNode(), selector, null)) { + if (matches(node, selector, page)) { return false; } } return true; }, + .is => |selectors| { + for (selectors) |selector| { + if (matches(node, selector, page)) { + return true; + } + } + return false; + }, + .where => |selectors| { + for (selectors) |selector| { + if (matches(node, selector, page)) { + return true; + } + } + return false; + }, + .has => |selectors| { + for (selectors) |selector| { + var child = node.firstChild(); + while (child) |c| { + const child_el = c.is(Node.Element) orelse { + child = c.nextSibling(); + continue; + }; + + if (matches(child_el.asNode(), selector, page)) { + return true; + } + + if (matchesHasDescendant(child_el, selector, page)) { + return true; + } + + child = c.nextSibling(); + } + } + return false; + }, } } +fn matchesHasDescendant(el: *Node.Element, selector: Selector.Selector, page: *Page) bool { + var child = el.asNode().firstChild(); + while (child) |c| { + const child_el = c.is(Node.Element) orelse { + child = c.nextSibling(); + continue; + }; + + if (matches(child_el.asNode(), selector, page)) { + return true; + } + + if (matchesHasDescendant(child_el, selector, page)) { + return true; + } + + child = c.nextSibling(); + } + return false; +} + fn isFirstChild(el: *Node.Element) bool { const node = el.asNode(); var sibling = node.previousSibling(); diff --git a/src/browser/webapi/selector/Parser.zig b/src/browser/webapi/selector/Parser.zig index 498132910..41075ca5e 100644 --- a/src/browser/webapi/selector/Parser.zig +++ b/src/browser/webapi/selector/Parser.zig @@ -395,21 +395,144 @@ fn pseudoClass(self: *Parser, arena: Allocator, page: *Page) !Selector.PseudoCla return .{ .not = selectors.items }; } + if (std.mem.eql(u8, name, "is")) { + var selectors: std.ArrayList(Selector.Selector) = .empty; + + _ = self.skipSpaces(); + while (true) { + if (self.peek() == ')') break; + if (self.peek() == 0) return error.InvalidPseudoClass; + + const selector = try parse(arena, self.consumeUntilCommaOrParen(), page); + try selectors.append(arena, selector); + + _ = self.skipSpaces(); + if (self.peek() == ',') { + self.input = self.input[1..]; + _ = self.skipSpaces(); + continue; + } + break; + } + + if (self.peek() != ')') return error.InvalidPseudoClass; + self.input = self.input[1..]; + + if (selectors.items.len == 0) return error.InvalidPseudoClass; + return .{ .is = selectors.items }; + } + + if (std.mem.eql(u8, name, "where")) { + var selectors: std.ArrayList(Selector.Selector) = .empty; + _ = self.skipSpaces(); + while (true) { + if (self.peek() == ')') break; + if (self.peek() == 0) return error.InvalidPseudoClass; + + const selector = try parse(arena, self.consumeUntilCommaOrParen(), page); + try selectors.append(arena, selector); + + _ = self.skipSpaces(); + if (self.peek() == ',') { + self.input = self.input[1..]; + _ = self.skipSpaces(); + continue; + } + break; + } + + if (self.peek() != ')') return error.InvalidPseudoClass; + self.input = self.input[1..]; + + if (selectors.items.len == 0) return error.InvalidPseudoClass; + return .{ .where = selectors.items }; + } + + if (std.mem.eql(u8, name, "has")) { + var selectors: std.ArrayList(Selector.Selector) = .empty; + + _ = self.skipSpaces(); + while (true) { + if (self.peek() == ')') break; + if (self.peek() == 0) return error.InvalidPseudoClass; + + const selector = try parse(arena, self.consumeUntilCommaOrParen(), page); + try selectors.append(arena, selector); + + _ = self.skipSpaces(); + if (self.peek() == ',') { + self.input = self.input[1..]; + _ = self.skipSpaces(); + continue; + } + break; + } + + if (self.peek() != ')') return error.InvalidPseudoClass; + self.input = self.input[1..]; + + if (selectors.items.len == 0) return error.InvalidPseudoClass; + return .{ .has = selectors.items }; + } + + if (std.mem.eql(u8, name, "lang")) { + _ = self.skipSpaces(); + const lang_start = self.input; + var lang_i: usize = 0; + while (lang_i < lang_start.len and lang_start[lang_i] != ')') : (lang_i += 1) {} + if (lang_i == 0 or self.peek() == 0) return error.InvalidPseudoClass; + + const lang = try arena.dupe(u8, std.mem.trim(u8, lang_start[0..lang_i], &std.ascii.whitespace)); + self.input = lang_start[lang_i..]; + + if (self.peek() != ')') return error.InvalidPseudoClass; + self.input = self.input[1..]; + + return .{ .lang = lang }; + } return error.UnknownPseudoClass; } switch (name.len) { + 4 => { + if (fastEql(name, "root")) return .root; + if (fastEql(name, "link")) return .link; + }, 5 => { if (fastEql(name, "modal")) return .modal; + if (fastEql(name, "hover")) return .hover; + if (fastEql(name, "focus")) return .focus; + if (fastEql(name, "empty")) return .empty; + if (fastEql(name, "valid")) return .valid; + }, + 6 => { + if (fastEql(name, "active")) return .active; + if (fastEql(name, "target")) return .target; }, 7 => { if (fastEql(name, "checked")) return .checked; + if (fastEql(name, "visited")) return .visited; + if (fastEql(name, "enabled")) return .enabled; + if (fastEql(name, "invalid")) return .invalid; + if (fastEql(name, "default")) return .default; + if (fastEql(name, "defined")) return .defined; + }, + 8 => { + if (fastEql(name, "disabled")) return .disabled; + if (fastEql(name, "required")) return .required; + if (fastEql(name, "optional")) return .optional; + if (fastEql(name, "any-link")) return .any_link; + if (fastEql(name, "in-range")) return .in_range; + }, + 9 => { + if (fastEql(name, "read-only")) return .read_only; }, 10 => { if (fastEql(name, "only-child")) return .only_child; if (fastEql(name, "last-child")) return .last_child; + if (fastEql(name, "read-write")) return .read_write; }, 11 => { if (fastEql(name, "first-child")) return .first_child; @@ -417,9 +540,16 @@ fn pseudoClass(self: *Parser, arena: Allocator, page: *Page) !Selector.PseudoCla 12 => { if (fastEql(name, "only-of-type")) return .only_of_type; if (fastEql(name, "last-of-type")) return .last_of_type; + if (fastEql(name, "focus-within")) return .focus_within; + if (fastEql(name, "out-of-range")) return .out_of_range; }, 13 => { if (fastEql(name, "first-of-type")) return .first_of_type; + if (fastEql(name, "focus-visible")) return .focus_visible; + if (fastEql(name, "indeterminate")) return .indeterminate; + }, + 17 => { + if (fastEql(name, "placeholder-shown")) return .placeholder_shown; }, else => {}, } diff --git a/src/browser/webapi/selector/Selector.zig b/src/browser/webapi/selector/Selector.zig index 15579b65b..5360cd3fe 100644 --- a/src/browser/webapi/selector/Selector.zig +++ b/src/browser/webapi/selector/Selector.zig @@ -82,7 +82,7 @@ pub fn matches(el: *Node.Element, input: []const u8, page: *Page) !bool { const selectors = try Parser.parseList(arena, input, page); for (selectors) |selector| { - if (List.matches(el.asNode(), selector, null)) { + if (List.matches(el.asNode(), selector, page)) { return true; } } @@ -131,8 +131,41 @@ pub const AttributeMatcher = union(enum) { }; pub const PseudoClass = union(enum) { + // State pseudo-classes modal, checked, + disabled, + enabled, + indeterminate, + + // Form validation + valid, + invalid, + required, + optional, + in_range, + out_of_range, + placeholder_shown, + read_only, + read_write, + default, + + // User interaction + hover, + active, + focus, + focus_within, + focus_visible, + + // Link states + link, + visited, + any_link, + target, + + // Tree structural + root, + empty, first_child, last_child, only_child, @@ -143,7 +176,16 @@ pub const PseudoClass = union(enum) { nth_last_child: NthPattern, nth_of_type: NthPattern, nth_last_of_type: NthPattern, + + // Custom elements + defined, + + // Functional + lang: []const u8, not: []const Selector, // :not() - CSS Level 4: supports full selectors and comma-separated lists + is: []const Selector, // :is() - matches any of the selectors + where: []const Selector, // :where() - like :is() but with zero specificity + has: []const Selector, // :has() - element containing descendants matching selector }; pub const NthPattern = struct { From 3c010f0e73a9503b4574b0b52b6dd444ae56f6b8 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Sat, 22 Nov 2025 12:25:12 +0800 Subject: [PATCH 064/219] tweak custom element callbacks --- src/browser/Page.zig | 18 +++++++--- src/browser/webapi/CustomElementRegistry.zig | 4 +++ src/browser/webapi/element/html/Custom.zig | 36 ++++++++++++++++++++ 3 files changed, 54 insertions(+), 4 deletions(-) diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 8c75f2bc2..19dd7f782 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -107,6 +107,8 @@ _intersection_delivery_scheduled: bool = false, // Lookup for customized built-in elements. Maps element pointer to definition. _customized_builtin_definitions: std.AutoHashMapUnmanaged(*Element, *CustomElementDefinition) = .{}, +_customized_builtin_connected_callback_invoked: std.AutoHashMapUnmanaged(*Element, void) = .{}, +_customized_builtin_disconnected_callback_invoked: std.AutoHashMapUnmanaged(*Element, void) = .{}, // This is set when an element is being upgraded (constructor is called). // The constructor can access this to get the element being upgraded. @@ -223,6 +225,8 @@ fn reset(self: *Page, comptime initializing: bool) !void { self._intersection_observers = .{}; self._intersection_delivery_scheduled = false; self._customized_builtin_definitions = .{}; + self._customized_builtin_connected_callback_invoked = .{}; + self._customized_builtin_disconnected_callback_invoked = .{}; self._undefined_custom_elements = .{}; try self.registerBackgroundTasks(); @@ -1380,13 +1384,14 @@ pub fn appendNode(self: *Page, parent: *Node, child: *Node, opts: InsertNodeOpts pub fn appendAllChildren(self: *Page, parent: *Node, target: *Node) !void { self.domChanged(); - const is_connected = parent.isConnected(); const dest_connected = target.isConnected(); var it = parent.childrenIterator(); while (it.next()) |child| { + // Check if child was connected BEFORE removing it from parent + const child_was_connected = child.isConnected(); self.removeNode(parent, child, .{ .will_be_reconnected = dest_connected }); - try self.appendNode(target, child, .{ .child_already_connected = is_connected }); + try self.appendNode(target, child, .{ .child_already_connected = child_was_connected }); } } @@ -1500,14 +1505,19 @@ pub fn _insertNodeRelative(self: *Page, comptime from_parser: bool, parent: *Nod // 1. A disconnected child became connected (parent.isConnected() == true) // 2. Child is being added to a shadow tree (parent_in_shadow == true) // In both cases, we need to update ID maps and invoke callbacks + + // Only invoke connectedCallback if the root child is transitioning from + // disconnected to connected. When that happens, all descendants should also + // get connectedCallback invoked (they're becoming connected as a group). + const should_invoke_connected = parent_is_connected and !opts.child_already_connected; + var tw = @import("webapi/TreeWalker.zig").Full.Elements.init(child, .{}); while (tw.next()) |el| { if (el.getAttributeSafe("id")) |id| { try self.addElementId(el.asNode()._parent.?, el, id); } - // Only invoke connected callback if actually connected to document - if (parent_is_connected) { + if (should_invoke_connected) { Element.Html.Custom.invokeConnectedCallbackOnElement(el, self); } } diff --git a/src/browser/webapi/CustomElementRegistry.zig b/src/browser/webapi/CustomElementRegistry.zig index 2028bda8a..c97fab0db 100644 --- a/src/browser/webapi/CustomElementRegistry.zig +++ b/src/browser/webapi/CustomElementRegistry.zig @@ -136,6 +136,10 @@ fn upgradeElement(self: *CustomElementRegistry, element: *Element, page: *Page) fn upgradeCustomElement(custom: *@import("element/html/Custom.zig"), definition: *CustomElementDefinition, page: *Page) !void { custom._definition = definition; + // Reset callback flags since this is a fresh upgrade + custom._connected_callback_invoked = false; + custom._disconnected_callback_invoked = false; + const node = custom.asNode(); const prev_upgrading = page._upgrading_element; page._upgrading_element = node; diff --git a/src/browser/webapi/element/html/Custom.zig b/src/browser/webapi/element/html/Custom.zig index 31e46bcf0..50e8518eb 100644 --- a/src/browser/webapi/element/html/Custom.zig +++ b/src/browser/webapi/element/html/Custom.zig @@ -32,6 +32,8 @@ const Custom = @This(); _proto: *HtmlElement, _tag_name: String, _definition: ?*CustomElementDefinition, +_connected_callback_invoked: bool = false, +_disconnected_callback_invoked: bool = false, pub fn asElement(self: *Custom) *Element { return self._proto._proto; @@ -41,10 +43,20 @@ pub fn asNode(self: *Custom) *Node { } pub fn invokeConnectedCallback(self: *Custom, page: *Page) void { + // Only invoke if we haven't already called it while connected + if (self._connected_callback_invoked) return; + + self._connected_callback_invoked = true; + self._disconnected_callback_invoked = false; self.invokeCallback("connectedCallback", .{}, page); } pub fn invokeDisconnectedCallback(self: *Custom, page: *Page) void { + // Only invoke if we haven't already called it while disconnected + if (self._disconnected_callback_invoked) return; + + self._disconnected_callback_invoked = true; + self._connected_callback_invoked = false; self.invokeCallback("disconnectedCallback", .{}, page); } @@ -63,6 +75,16 @@ pub fn invokeConnectedCallbackOnElement(element: *Element, page: *Page) void { } // Customized built-in element + // Check if we've already invoked connectedCallback while connected + if (page._customized_builtin_connected_callback_invoked.contains(element)) return; + + page._customized_builtin_connected_callback_invoked.put( + page.arena, + element, + {}, + ) catch return; + _ = page._customized_builtin_disconnected_callback_invoked.remove(element); + invokeCallbackOnElement(element, "connectedCallback", .{}, page); } @@ -74,6 +96,16 @@ pub fn invokeDisconnectedCallbackOnElement(element: *Element, page: *Page) void } // Customized built-in element + // Check if we've already invoked disconnectedCallback while disconnected + if (page._customized_builtin_disconnected_callback_invoked.contains(element)) return; + + page._customized_builtin_disconnected_callback_invoked.put( + page.arena, + element, + {}, + ) catch return; + _ = page._customized_builtin_connected_callback_invoked.remove(element); + invokeCallbackOnElement(element, "disconnectedCallback", .{}, page); } @@ -119,6 +151,10 @@ pub fn checkAndAttachBuiltIn(element: *Element, page: *Page) !void { // Attach the definition try page.setCustomizedBuiltInDefinition(element, definition); + // Reset callback flags since this is a fresh upgrade + _ = page._customized_builtin_connected_callback_invoked.remove(element); + _ = page._customized_builtin_disconnected_callback_invoked.remove(element); + // Invoke constructor const prev_upgrading = page._upgrading_element; const node = element.asNode(); From 6b990f8f123f5f9a3c9ee887f01502db9e075e38 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Sat, 22 Nov 2025 12:33:29 +0800 Subject: [PATCH 065/219] CustomEvent and document.createEvent --- src/browser/js/bridge.zig | 1 + src/browser/tests/event/custom_event.html | 95 +++++++++++++++++++++++ src/browser/webapi/Document.zig | 21 +++++ src/browser/webapi/Event.zig | 1 + src/browser/webapi/event/CustomEvent.zig | 78 +++++++++++++++++++ 5 files changed, 196 insertions(+) create mode 100644 src/browser/tests/event/custom_event.html create mode 100644 src/browser/webapi/event/CustomEvent.zig diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index 7acb84739..bc380d1a0 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -544,6 +544,7 @@ pub const JsApis = flattenTypes(&.{ @import("../webapi/encoding/TextDecoder.zig"), @import("../webapi/encoding/TextEncoder.zig"), @import("../webapi/Event.zig"), + @import("../webapi/event/CustomEvent.zig"), @import("../webapi/event/ErrorEvent.zig"), @import("../webapi/event/ProgressEvent.zig"), @import("../webapi/EventTarget.zig"), diff --git a/src/browser/tests/event/custom_event.html b/src/browser/tests/event/custom_event.html new file mode 100644 index 000000000..97f114d88 --- /dev/null +++ b/src/browser/tests/event/custom_event.html @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/browser/webapi/Document.zig b/src/browser/webapi/Document.zig index eaa9b6e0f..e895bfd40 100644 --- a/src/browser/webapi/Document.zig +++ b/src/browser/webapi/Document.zig @@ -175,6 +175,26 @@ pub fn createTextNode(_: *const Document, data: []const u8, page: *Page) !*Node return page.createTextNode(data); } +pub fn createEvent(_: *const Document, event_type: []const u8, page: *Page) !*@import("Event.zig") { + const Event = @import("Event.zig"); + + if (std.ascii.eqlIgnoreCase(event_type, "event") or std.ascii.eqlIgnoreCase(event_type, "events") or std.ascii.eqlIgnoreCase(event_type, "htmlevents")) { + return Event.init("", null, page); + } + + if (std.ascii.eqlIgnoreCase(event_type, "customevent") or std.ascii.eqlIgnoreCase(event_type, "customevents")) { + const CustomEvent = @import("event/CustomEvent.zig"); + const custom_event = try CustomEvent.init("", null, page); + return custom_event.asEvent(); + } + + if (std.ascii.eqlIgnoreCase(event_type, "messageevent")) { + return error.NotSupported; + } + + return error.NotSupported; +} + pub fn createTreeWalker(_: *const Document, root: *Node, what_to_show: ?u32, filter: ?DOMTreeWalker.FilterOpts, page: *Page) !*DOMTreeWalker { const show = what_to_show orelse NodeFilter.SHOW_ALL; return DOMTreeWalker.init(root, show, filter, page); @@ -239,6 +259,7 @@ pub const JsApi = struct { pub const createDocumentFragment = bridge.function(Document.createDocumentFragment, .{}); pub const createComment = bridge.function(Document.createComment, .{}); pub const createTextNode = bridge.function(Document.createTextNode, .{}); + pub const createEvent = bridge.function(Document.createEvent, .{ .dom_exception = true }); pub const createTreeWalker = bridge.function(Document.createTreeWalker, .{}); pub const createNodeIterator = bridge.function(Document.createNodeIterator, .{}); pub const getElementById = bridge.function(Document.getElementById, .{}); diff --git a/src/browser/webapi/Event.zig b/src/browser/webapi/Event.zig index 9884ff855..70de6e078 100644 --- a/src/browser/webapi/Event.zig +++ b/src/browser/webapi/Event.zig @@ -48,6 +48,7 @@ pub const Type = union(enum) { generic, progress_event: *@import("event/ProgressEvent.zig"), error_event: *@import("event/ErrorEvent.zig"), + custom_event: *@import("event/CustomEvent.zig"), }; const Options = struct { diff --git a/src/browser/webapi/event/CustomEvent.zig b/src/browser/webapi/event/CustomEvent.zig new file mode 100644 index 000000000..1c36fc33e --- /dev/null +++ b/src/browser/webapi/event/CustomEvent.zig @@ -0,0 +1,78 @@ +// 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 Event = @import("../Event.zig"); +const Allocator = std.mem.Allocator; + +const CustomEvent = @This(); + +_proto: *Event, +_detail: ?js.Object = null, +_arena: Allocator, + +pub const InitOptions = struct { + detail: ?js.Object = null, + bubbles: bool = false, + cancelable: bool = false, +}; + +pub fn init(typ: []const u8, opts_: ?InitOptions, page: *Page) !*CustomEvent { + const arena = page.arena; + const opts = opts_ orelse InitOptions{}; + + const event = try page._factory.event(typ, CustomEvent{ + ._arena = arena, + ._proto = undefined, + ._detail = if (opts.detail) |detail| try detail.persist() else null, + }); + + event._proto._bubbles = opts.bubbles; + event._proto._cancelable = opts.cancelable; + + return event; +} + +pub fn asEvent(self: *CustomEvent) *Event { + return self._proto; +} + +pub fn getDetail(self: *const CustomEvent) ?js.Object { + return self._detail; +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(CustomEvent); + + pub const Meta = struct { + pub const name = "CustomEvent"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; + + pub const constructor = bridge.constructor(CustomEvent.init, .{}); + pub const detail = bridge.accessor(CustomEvent.getDetail, null, .{}); +}; + +const testing = @import("../../../testing.zig"); +test "WebApi: CustomEvent" { + try testing.htmlRunner("event/custom_event.html", .{}); +} From d3c00cdd527e9273c1a4a838c97665c2310fa77b Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Sat, 22 Nov 2025 22:56:58 +0800 Subject: [PATCH 066/219] Link get/set href --- src/browser/webapi/element/html/Link.zig | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/browser/webapi/element/html/Link.zig b/src/browser/webapi/element/html/Link.zig index 3fbfdaa06..65e879179 100644 --- a/src/browser/webapi/element/html/Link.zig +++ b/src/browser/webapi/element/html/Link.zig @@ -17,6 +17,9 @@ // along with this program. If not, see . const js = @import("../../../js/js.zig"); +const Page = @import("../../../Page.zig"); + +const URL = @import("../../URL.zig"); const Node = @import("../../Node.zig"); const Element = @import("../../Element.zig"); const HtmlElement = @import("../Html.zig"); @@ -31,6 +34,15 @@ pub fn asNode(self: *Link) *Node { return self.asElement().asNode(); } +pub fn getHref(self: *Link, page: *Page) ![]const u8 { + const href = self.asElement().getAttributeSafe("href"); + return URL.resolve(page.call_arena, page.url, href orelse "", .{}); +} + +pub fn setHref(self: *Link, value: []const u8, page: *Page) !void { + try self.asElement().setAttributeSafe("href", value, page); +} + pub const JsApi = struct { pub const bridge = js.Bridge(Link); @@ -39,4 +51,6 @@ pub const JsApi = struct { pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; }; + + pub const href = bridge.accessor(Link.getHref, Link.setHref, .{}); }; From f536f169266ae84592362ef341329bf439752431 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Sat, 22 Nov 2025 23:04:17 +0800 Subject: [PATCH 067/219] Correct exception on custom element re-definition --- src/browser/webapi/Crypto.zig | 4 ++-- src/browser/webapi/CustomElementRegistry.zig | 3 ++- src/browser/webapi/DOMException.zig | 2 +- src/browser/webapi/IntersectionObserver.zig | 9 ++------- src/browser/webapi/net/Headers.zig | 1 - 5 files changed, 7 insertions(+), 12 deletions(-) diff --git a/src/browser/webapi/Crypto.zig b/src/browser/webapi/Crypto.zig index 2c54689cd..069715b86 100644 --- a/src/browser/webapi/Crypto.zig +++ b/src/browser/webapi/Crypto.zig @@ -73,8 +73,8 @@ pub const JsApi = struct { pub const empty_with_no_proto = true; }; - pub const getRandomValues = bridge.function(Crypto.getRandomValues, .{ }); - pub const randomUUID = bridge.function(Crypto.randomUUID, .{ }); + pub const getRandomValues = bridge.function(Crypto.getRandomValues, .{}); + pub const randomUUID = bridge.function(Crypto.randomUUID, .{}); }; const testing = @import("../../testing.zig"); diff --git a/src/browser/webapi/CustomElementRegistry.zig b/src/browser/webapi/CustomElementRegistry.zig index c97fab0db..361aced55 100644 --- a/src/browser/webapi/CustomElementRegistry.zig +++ b/src/browser/webapi/CustomElementRegistry.zig @@ -51,7 +51,8 @@ pub fn define(self: *CustomElementRegistry, name: []const u8, constructor: js.Fu const gop = try self._definitions.getOrPut(page.arena, name); if (gop.found_existing) { - return error.AlreadyDefined; + // Yes, this is the correct error to return when trying to redefine a name + return error.NotSupported; } const owned_name = try page.dupeString(name); diff --git a/src/browser/webapi/DOMException.zig b/src/browser/webapi/DOMException.zig index 07c7137f1..2f1cc789f 100644 --- a/src/browser/webapi/DOMException.zig +++ b/src/browser/webapi/DOMException.zig @@ -47,7 +47,7 @@ pub fn getName(self: *const DOMException) []const u8 { .invalid_character_error => "InvalidCharacterError", .syntax_error => "SyntaxError", .not_found => "NotFoundError", - .not_supported => "NotSupported", + .not_supported => "NotSupportedError", .hierarchy_error => "HierarchyError", }; } diff --git a/src/browser/webapi/IntersectionObserver.zig b/src/browser/webapi/IntersectionObserver.zig index d9722be76..c6940899e 100644 --- a/src/browser/webapi/IntersectionObserver.zig +++ b/src/browser/webapi/IntersectionObserver.zig @@ -53,7 +53,7 @@ var zero_rect: DOMRect = .{ pub const ObserverInit = struct { root: ?*Element = null, rootMargin: ?[]const u8 = null, - threshold: Threshold = .{.scalar = 0.0}, + threshold: Threshold = .{ .scalar = 0.0 }, const Threshold = union(enum) { scalar: f64, @@ -74,12 +74,7 @@ pub fn init(callback: js.Function, options: ?ObserverInit, page: *Page) !*Inters .array => |arr| try page.arena.dupe(f64, arr), }; - return page._factory.create(IntersectionObserver{ - ._callback = callback, - ._root = opts.root, - ._root_margin = root_margin, - ._threshold = threshold - }); + return page._factory.create(IntersectionObserver{ ._callback = callback, ._root = opts.root, ._root_margin = root_margin, ._threshold = threshold }); } pub fn observe(self: *IntersectionObserver, target: *Element, page: *Page) !void { diff --git a/src/browser/webapi/net/Headers.zig b/src/browser/webapi/net/Headers.zig index 2f2fa68f2..9dea0b958 100644 --- a/src/browser/webapi/net/Headers.zig +++ b/src/browser/webapi/net/Headers.zig @@ -14,7 +14,6 @@ pub fn init(page: *Page) !*Headers { }); } - pub fn append(self: *Headers, name: []const u8, value: []const u8, page: *Page) !void { try self._list.append(page.arena, name, value); } From 871fd46c892ece94997aa51f46870635bcd7c957 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Mon, 24 Nov 2025 15:11:16 +0800 Subject: [PATCH 068/219] fix 0-size structs all having the same identity (the same pointer --- src/browser/Page.zig | 2 ++ src/browser/tests/navigator.html | 29 --------------------- src/browser/webapi/Console.zig | 1 + src/browser/webapi/Crypto.zig | 3 +++ src/browser/webapi/Navigator.zig | 5 +--- src/browser/webapi/Window.zig | 22 +++++++++++----- src/browser/webapi/encoding/TextEncoder.zig | 1 + 7 files changed, 24 insertions(+), 39 deletions(-) delete mode 100644 src/browser/tests/navigator.html diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 19dd7f782..e9eda3f23 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -176,6 +176,8 @@ pub fn deinit(self: *Page) void { log.debug(.page, "page.deinit", .{ .url = self.url }); } self.js.deinit(); + self._script_manager.shutdown = true; + self._session.browser.http_client.abort(); self._script_manager.deinit(); } diff --git a/src/browser/tests/navigator.html b/src/browser/tests/navigator.html deleted file mode 100644 index 7547b0168..000000000 --- a/src/browser/tests/navigator.html +++ /dev/null @@ -1,29 +0,0 @@ - - - diff --git a/src/browser/webapi/Console.zig b/src/browser/webapi/Console.zig index e3e856ab9..3563f1c57 100644 --- a/src/browser/webapi/Console.zig +++ b/src/browser/webapi/Console.zig @@ -22,6 +22,7 @@ const js = @import("../js/js.zig"); const logger = @import("../../log.zig"); const Console = @This(); +_pad: bool = false, pub const init: Console = .{}; diff --git a/src/browser/webapi/Crypto.zig b/src/browser/webapi/Crypto.zig index 069715b86..e8f987b55 100644 --- a/src/browser/webapi/Crypto.zig +++ b/src/browser/webapi/Crypto.zig @@ -20,6 +20,9 @@ const std = @import("std"); const js = @import("../js/js.zig"); const Crypto = @This(); +_pad: bool = false, + +pub const init: Crypto = .{}; // We take a js.Value, because we want to return the same instance, not a new // TypedArray diff --git a/src/browser/webapi/Navigator.zig b/src/browser/webapi/Navigator.zig index 981fc2e1c..63b4cfc9a 100644 --- a/src/browser/webapi/Navigator.zig +++ b/src/browser/webapi/Navigator.zig @@ -20,6 +20,7 @@ const builtin = @import("builtin"); const js = @import("../js/js.zig"); const Navigator = @This(); +_pad: bool = false, pub const init: Navigator = .{}; @@ -120,7 +121,3 @@ pub const JsApi = struct { pub const javaEnabled = bridge.function(Navigator.javaEnabled, .{}); }; -const testing = @import("../../testing.zig"); -test "WebApi: Navigator" { - try testing.htmlRunner("navigator.html", .{}); -} diff --git a/src/browser/webapi/Window.zig b/src/browser/webapi/Window.zig index 1379f4ad3..ecf3793d2 100644 --- a/src/browser/webapi/Window.zig +++ b/src/browser/webapi/Window.zig @@ -34,12 +34,15 @@ const EventTarget = @import("EventTarget.zig"); const ErrorEvent = @import("event/ErrorEvent.zig"); const MediaQueryList = @import("css/MediaQueryList.zig"); const storage = @import("storage/storage.zig"); +const Element = @import("Element.zig"); +const CSSStyleDeclaration = @import("css/CSSStyleDeclaration.zig"); const CustomElementRegistry = @import("CustomElementRegistry.zig"); const Window = @This(); _proto: *EventTarget, _document: *Document, +_crypto: Crypto = .init, _console: Console = .init, _navigator: Navigator = .init, _performance: Performance, @@ -67,16 +70,17 @@ pub fn getDocument(self: *Window) *Document { return self._document; } -pub fn getConsole(_: *const Window) Console { - return .{}; +pub fn getConsole(self: *Window) *Console { + std.debug.print("getConsole\n", .{}); + return &self._console; } -pub fn getNavigator(_: *const Window) Navigator { - return .{}; +pub fn getNavigator(self: *Window) *Navigator { + return &self._navigator; } -pub fn getCrypto(_: *const Window) Crypto { - return .{}; +pub fn getCrypto(self: *Window) *Crypto { + return &self._crypto; } pub fn getPerformance(self: *Window) *Performance { @@ -210,6 +214,10 @@ pub fn matchMedia(_: *const Window, query: []const u8, page: *Page) !*MediaQuery }); } +pub fn getComputedStyle(_: *const Window, _: *Element, page: *Page) !@import("css/CSSStyleDeclaration.zig") { + return CSSStyleDeclaration.init(null, page); +} + pub fn btoa(_: *const Window, input: []const u8, page: *Page) ![]const u8 { const encoded_len = std.base64.standard.Encoder.calcSize(input.len); const encoded = try page.call_arena.alloc(u8, encoded_len); @@ -223,6 +231,7 @@ pub fn atob(_: *const Window, input: []const u8, page: *Page) ![]const u8 { return decoded; } + const ScheduleOpts = struct { repeat: bool, params: []js.Object, @@ -384,6 +393,7 @@ pub const JsApi = struct { return 1080; } }.wrap, null, .{ .cache = "innerHeight" }); + pub const getComputedStyle = bridge.function(Window.getComputedStyle, .{}); }; const testing = @import("../../testing.zig"); diff --git a/src/browser/webapi/encoding/TextEncoder.zig b/src/browser/webapi/encoding/TextEncoder.zig index c7066d5ec..614187cd0 100644 --- a/src/browser/webapi/encoding/TextEncoder.zig +++ b/src/browser/webapi/encoding/TextEncoder.zig @@ -20,6 +20,7 @@ const std = @import("std"); const js = @import("../../js/js.zig"); const TextEncoder = @This(); +_pad: bool = false, pub fn init() TextEncoder { return .{}; From e336c67857b92d6a5ec5fcbfad7f159f8937b3eb Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Mon, 24 Nov 2025 20:12:43 +0800 Subject: [PATCH 069/219] various small api fixes/tweaks --- src/browser/Factory.zig | 6 +- src/browser/Page.zig | 14 +- src/browser/ScriptManager.zig | 2 +- src/browser/dump.zig | 73 ++++- src/browser/js/Function.zig | 1 + src/browser/tests/element/pseudo_classes.html | 8 + src/browser/webapi/History.zig | 4 +- src/browser/webapi/Window.zig | 17 +- src/browser/webapi/css/MediaQueryList.zig | 5 + src/browser/webapi/element/html/Script.zig | 10 + src/browser/webapi/net/XMLHttpRequest.zig | 2 +- src/browser/webapi/selector/Parser.zig | 261 ++++++++++++------ src/browser/webapi/storage/cookie.zig | 4 +- src/cdp/domains/log.zig | 4 +- src/lightpanda.zig | 4 +- src/log.zig | 13 +- src/main.zig | 22 +- 17 files changed, 323 insertions(+), 127 deletions(-) diff --git a/src/browser/Factory.zig b/src/browser/Factory.zig index 696ae98c3..8c9c3b58c 100644 --- a/src/browser/Factory.zig +++ b/src/browser/Factory.zig @@ -63,7 +63,7 @@ _size_144_8: MemoryPoolAligned([144]u8, .@"8"), _size_152_8: MemoryPoolAligned([152]u8, .@"8"), _size_160_8: MemoryPoolAligned([160]u8, .@"8"), _size_184_8: MemoryPoolAligned([184]u8, .@"8"), -_size_192_8: MemoryPoolAligned([192]u8, .@"8"), +_size_232_8: MemoryPoolAligned([232]u8, .@"8"), _size_648_8: MemoryPoolAligned([648]u8, .@"8"), pub fn init(page: *Page) Factory { @@ -86,7 +86,7 @@ pub fn init(page: *Page) Factory { ._size_152_8 = MemoryPoolAligned([152]u8, .@"8").init(page.arena), ._size_160_8 = MemoryPoolAligned([160]u8, .@"8").init(page.arena), ._size_184_8 = MemoryPoolAligned([184]u8, .@"8").init(page.arena), - ._size_192_8 = MemoryPoolAligned([192]u8, .@"8").init(page.arena), + ._size_232_8 = MemoryPoolAligned([232]u8, .@"8").init(page.arena), ._size_648_8 = MemoryPoolAligned([648]u8, .@"8").init(page.arena), }; } @@ -265,7 +265,7 @@ pub fn createT(self: *Factory, comptime T: type) !*T { if (comptime SO == 152) return @ptrCast(try self._size_152_8.create()); if (comptime SO == 160) return @ptrCast(try self._size_160_8.create()); if (comptime SO == 184) return @ptrCast(try self._size_184_8.create()); - if (comptime SO == 192) return @ptrCast(try self._size_192_8.create()); + if (comptime SO == 232) return @ptrCast(try self._size_232_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) })); } diff --git a/src/browser/Page.zig b/src/browser/Page.zig index e9eda3f23..b93a9f946 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -266,7 +266,7 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi try self.reset(false); } - log.info(.http, "navigate", .{ + log.info(.page, "navigate", .{ .url = request_url, .method = opts.method, .reason = opts.reason, @@ -329,7 +329,7 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi .done_callback = pageDoneCallback, .error_callback = pageErrorCallback, }) catch |err| { - log.err(.http, "navigate request", .{ .url = self.url, .err = err }); + log.err(.page, "navigate request", .{ .url = self.url, .err = err }); return err; }; } @@ -412,7 +412,7 @@ fn pageHeaderDoneCallback(transfer: *Http.Transfer) !void { self.window._location = try Location.init(self.url, self); self.document._location = self.window._location; - log.debug(.http, "navigate header", .{ + log.debug(.page, "navigate header", .{ .url = self.url, .status = header.status, .content_type = header.contentType(), @@ -433,7 +433,7 @@ fn pageDataCallback(transfer: *Http.Transfer, data: []const u8) !void { } orelse .unknown; if (comptime IS_DEBUG) { - log.debug(.http, "navigate first chunk", .{ .content_type = mime.content_type, .len = data.len }); + log.debug(.page, "navigate first chunk", .{ .content_type = mime.content_type, .len = data.len }); } switch (mime.content_type) { @@ -475,7 +475,7 @@ fn pageDataCallback(transfer: *Http.Transfer, data: []const u8) !void { fn pageDoneCallback(ctx: *anyopaque) !void { if (comptime IS_DEBUG) { - log.debug(.http, "navigate done", .{}); + log.debug(.page, "navigate done", .{}); } var self: *Page = @ptrCast(@alignCast(ctx)); @@ -522,7 +522,7 @@ fn pageDoneCallback(ctx: *anyopaque) !void { } fn pageErrorCallback(ctx: *anyopaque, err: anyerror) void { - log.err(.http, "navigate failed", .{ .err = err }); + log.err(.page, "navigate failed", .{ .err = err }); var self: *Page = @ptrCast(@alignCast(ctx)); self.clearTransferArena(); @@ -624,7 +624,7 @@ fn _wait(self: *Page, wait_ms: u32) !Session.WaitResult { if (try_catch.hasCaught()) { const msg = (try try_catch.err(self.arena)) orelse "unknown"; - log.warn(.user_script, "page wait", .{ .err = msg, .src = "scheduler" }); + log.warn(.js, "page wait", .{ .err = msg, .src = "scheduler" }); return error.JsError; } diff --git a/src/browser/ScriptManager.zig b/src/browser/ScriptManager.zig index 0d8740c80..632be5f2e 100644 --- a/src/browser/ScriptManager.zig +++ b/src/browser/ScriptManager.zig @@ -757,7 +757,7 @@ const Script = struct { // } const msg = try_catch.err(page.arena) catch |err| @errorName(err) orelse "unknown"; - log.warn(.user_script, "eval script", .{ + log.warn(.js, "eval script", .{ .url = url, .err = msg, .cacheable = cacheable, diff --git a/src/browser/dump.zig b/src/browser/dump.zig index 8efc8da49..2e00ba39e 100644 --- a/src/browser/dump.zig +++ b/src/browser/dump.zig @@ -17,24 +17,45 @@ // along with this program. If not, see . const std = @import("std"); +const Page = @import("Page.zig"); const Node = @import("webapi/Node.zig"); -pub const Opts = struct { - // @ZIGDOM (none of these do anything) +pub const RootOpts = struct { with_base: bool = false, - strip_mode: StripMode = .{}, + strip: Opts.Strip = .{}, +}; - pub const StripMode = struct { +pub const Opts = struct { + strip: Strip = .{}, + pub const Strip = struct { js: bool = false, ui: bool = false, css: bool = false, }; }; +pub fn root(opts: RootOpts, writer: *std.Io.Writer, page: *Page) !void { + const doc = page.document; + if (opts.with_base) { + if (doc.is(Node.Document.HTMLDocument)) |html_doc| { + const parent = if (html_doc.getHead()) |head| head.asNode() else doc.asNode(); + const base = try doc.createElement("base", null, page); + try base.setAttributeSafe("base", page.url, page); + _ = try parent.insertBefore(base.asNode(), parent.firstChild(), page); + } + } + + return deep(doc.asNode(), .{.strip = opts.strip}, writer); +} + pub fn deep(node: *Node, opts: Opts, writer: *std.Io.Writer) error{WriteFailed}!void { switch (node._type) { .cdata => |cd| try writer.writeAll(cd.getData()), .element => |el| { + if (shouldStripElement(el, opts)) { + return; + } + try el.format(writer); try children(node, opts, writer); if (!isVoidElement(el)) { @@ -106,3 +127,47 @@ fn isVoidElement(el: *const Node.Element) bool { .svg => false, }; } + +fn shouldStripElement(el: *const Node.Element, opts: Opts) bool { + const tag_name = el.getTagNameDump(); + + if (opts.strip.js) { + if (std.mem.eql(u8, tag_name, "script")) return true; + if (std.mem.eql(u8, tag_name, "noscript")) return true; + + if (std.mem.eql(u8, tag_name, "link")) { + if (el.getAttributeSafe("as")) |as| { + if (std.mem.eql(u8, as, "script")) return true; + } + if (el.getAttributeSafe("rel")) |rel| { + if (std.mem.eql(u8, rel, "modulepreload") or std.mem.eql(u8, rel, "preload")) { + if (el.getAttributeSafe("as")) |as| { + if (std.mem.eql(u8, as, "script")) return true; + } + } + } + } + } + + if (opts.strip.css or opts.strip.ui) { + if (std.mem.eql(u8, tag_name, "style")) return true; + + if (std.mem.eql(u8, tag_name, "link")) { + if (el.getAttributeSafe("rel")) |rel| { + if (std.mem.eql(u8, rel, "stylesheet")) return true; + } + } + } + + if (opts.strip.ui) { + if (std.mem.eql(u8, tag_name, "img")) return true; + if (std.mem.eql(u8, tag_name, "picture")) return true; + if (std.mem.eql(u8, tag_name, "video")) return true; + if (std.mem.eql(u8, tag_name, "audio")) return true; + if (std.mem.eql(u8, tag_name, "svg")) return true; + if (std.mem.eql(u8, tag_name, "canvas")) return true; + if (std.mem.eql(u8, tag_name, "iframe")) return true; + } + + return false; +} diff --git a/src/browser/js/Function.zig b/src/browser/js/Function.zig index 73c029117..41d8fa2ca 100644 --- a/src/browser/js/Function.zig +++ b/src/browser/js/Function.zig @@ -144,6 +144,7 @@ pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args const result = self.func.castToFunction().call(context.v8_context, js_this, js_args); if (result == null) { + std.debug.print("CB ERR: {s}\n", .{self.src() catch "???"}); return error.JSExecCallback; } diff --git a/src/browser/tests/element/pseudo_classes.html b/src/browser/tests/element/pseudo_classes.html index 8114cae0a..fe75ab844 100644 --- a/src/browser/tests/element/pseudo_classes.html +++ b/src/browser/tests/element/pseudo_classes.html @@ -80,3 +80,11 @@ testing.expectTrue(whereResult.length >= 3); } + +
+ diff --git a/src/browser/webapi/History.zig b/src/browser/webapi/History.zig index 3bc568662..d80fe3ba7 100644 --- a/src/browser/webapi/History.zig +++ b/src/browser/webapi/History.zig @@ -52,7 +52,7 @@ pub fn pushState(self: *History, state: js.Object, _title: []const u8, url: ?[]c _ = url; // For minimal implementation, we don't actually navigate _ = page; - self._state = state; + self._state = try state.persist(); self._length += 1; } @@ -60,7 +60,7 @@ pub fn replaceState(self: *History, state: js.Object, _title: []const u8, url: ? _ = _title; _ = url; _ = page; - self._state = state; + self._state = try state.persist(); // Note: replaceState doesn't change length } diff --git a/src/browser/webapi/Window.zig b/src/browser/webapi/Window.zig index ecf3793d2..b65359e44 100644 --- a/src/browser/webapi/Window.zig +++ b/src/browser/webapi/Window.zig @@ -49,6 +49,7 @@ _performance: Performance, _history: History, _storage_bucket: *storage.Bucket, _on_load: ?js.Function = null, +_on_error: ?js.Function = null, // TODO: invoke on error? _location: *Location, _timer_id: u30 = 0, _timers: std.AutoHashMapUnmanaged(u32, *ScheduleCallback) = .{}, @@ -71,7 +72,6 @@ pub fn getDocument(self: *Window) *Document { } pub fn getConsole(self: *Window) *Console { - std.debug.print("getConsole\n", .{}); return &self._console; } @@ -119,6 +119,18 @@ pub fn setOnLoad(self: *Window, cb_: ?js.Function) !void { } } +pub fn getOnError(self: *const Window) ?js.Function { + return self._on_error; +} + +pub fn setOnError(self: *Window, cb_: ?js.Function) !void { + if (cb_) |cb| { + self._on_error = cb; + } else { + self._on_error = null; + } +} + pub fn fetch(_: *const Window, input: Fetch.Input, page: *Page) !js.Promise { return Fetch.init(input, page); } @@ -214,7 +226,7 @@ pub fn matchMedia(_: *const Window, query: []const u8, page: *Page) !*MediaQuery }); } -pub fn getComputedStyle(_: *const Window, _: *Element, page: *Page) !@import("css/CSSStyleDeclaration.zig") { +pub fn getComputedStyle(_: *const Window, _: *Element, page: *Page) !*CSSStyleDeclaration { return CSSStyleDeclaration.init(null, page); } @@ -362,6 +374,7 @@ pub const JsApi = struct { pub const crypto = bridge.accessor(Window.getCrypto, null, .{ .cache = "crypto" }); pub const customElements = bridge.accessor(Window.getCustomElements, null, .{ .cache = "customElements" }); pub const onload = bridge.accessor(Window.getOnLoad, Window.setOnLoad, .{}); + pub const onerror = bridge.accessor(Window.getOnError, Window.getOnError, .{}); pub const fetch = bridge.function(Window.fetch, .{}); pub const queueMicrotask = bridge.function(Window.queueMicrotask, .{}); pub const setTimeout = bridge.function(Window.setTimeout, .{}); diff --git a/src/browser/webapi/css/MediaQueryList.zig b/src/browser/webapi/css/MediaQueryList.zig index 4e0da9710..46304ccc5 100644 --- a/src/browser/webapi/css/MediaQueryList.zig +++ b/src/browser/webapi/css/MediaQueryList.zig @@ -43,6 +43,9 @@ pub fn getMatches(_: *const MediaQueryList) bool { return false; } +pub fn addListener(_: *const MediaQueryList, _: js.Function) void {} +pub fn removeListener(_: *const MediaQueryList, _: js.Function) void {} + pub const JsApi = struct { pub const bridge = js.Bridge(MediaQueryList); @@ -54,6 +57,8 @@ pub const JsApi = struct { pub const media = bridge.accessor(MediaQueryList.getMedia, null, .{}); pub const matches = bridge.accessor(MediaQueryList.getMatches, null, .{}); + pub const addListener = bridge.function(MediaQueryList.addListener, .{}); + pub const removeListener = bridge.function(MediaQueryList.removeListener, .{}); }; const testing = @import("../../../testing.zig"); diff --git a/src/browser/webapi/element/html/Script.zig b/src/browser/webapi/element/html/Script.zig index 1e548c4e3..c12038a64 100644 --- a/src/browser/webapi/element/html/Script.zig +++ b/src/browser/webapi/element/html/Script.zig @@ -35,6 +35,11 @@ _executed: bool = false, pub fn asElement(self: *Script) *Element { return self._proto._proto; } + +pub fn asConstElement(self: *const Script) *const Element { + return self._proto._proto; +} + pub fn asNode(self: *Script) *Node { return self.asElement().asNode(); } @@ -76,6 +81,10 @@ pub fn setOnError(self: *Script, cb_: ?js.Function) !void { } } +pub fn getNoModule(self: *const Script) bool { + return self.asConstElement().getAttributeSafe("nomodule") != null; +} + pub const JsApi = struct { pub const bridge = js.Bridge(Script); @@ -88,6 +97,7 @@ pub const JsApi = struct { pub const src = bridge.accessor(Script.getSrc, Script.setSrc, .{}); pub const onload = bridge.accessor(Script.getOnLoad, Script.setOnLoad, .{}); pub const onerorr = bridge.accessor(Script.getOnError, Script.setOnError, .{}); + pub const noModule = bridge.accessor(Script.getNoModule, null, .{}); }; pub const Build = struct { diff --git a/src/browser/webapi/net/XMLHttpRequest.zig b/src/browser/webapi/net/XMLHttpRequest.zig index dfb848e66..6239ddc42 100644 --- a/src/browser/webapi/net/XMLHttpRequest.zig +++ b/src/browser/webapi/net/XMLHttpRequest.zig @@ -125,7 +125,7 @@ pub fn open(self: *XMLHttpRequest, method_: []const u8, url: [:0]const u8) !void pub fn send(self: *XMLHttpRequest, body_: ?[]const u8) !void { if (comptime IS_DEBUG) { - log.debug(.xhr, "XMLHttpRequest.send", .{ .url = self._url }); + log.debug(.http, "XMLHttpRequest.send", .{ .url = self._url }); } if (body_) |b| { diff --git a/src/browser/webapi/selector/Parser.zig b/src/browser/webapi/selector/Parser.zig index 41075ca5e..b97f7c004 100644 --- a/src/browser/webapi/selector/Parser.zig +++ b/src/browser/webapi/selector/Parser.zig @@ -226,8 +226,8 @@ pub fn parse(arena: Allocator, input: []const u8, page: *Page) ParseError!Select fn parsePart(self: *Parser, arena: Allocator, page: *Page) !Part { return switch (self.peek()) { - '#' => .{ .id = try self.id() }, - '.' => .{ .class = try self.class() }, + '#' => .{ .id = try self.id(arena) }, + '.' => .{ .class = try self.class(arena) }, '*' => blk: { self.input = self.input[1..]; break :blk .universal; @@ -655,7 +655,7 @@ fn parseNthPattern(self: *Parser) !Selector.NthPattern { return .{ .a = a, .b = b }; } -pub fn id(self: *Parser) ![]const u8 { +pub fn id(self: *Parser, arena: Allocator) ![]const u8 { // Must be called when we're at a '#' std.debug.assert(self.peek() == '#'); @@ -667,26 +667,46 @@ pub fn id(self: *Parser) ![]const u8 { return error.InvalidIDSelector; } - // First character: must be letter, underscore, or non-ASCII (>= 0x80) - // Can also be hyphen if not followed by digit or another hyphen - const first = input[0]; - if (first == '-') { - if (input.len < 2) { - @branchHint(.cold); - return error.InvalidIDSelector; + // First pass: find the end of the id and check if there are escape sequences + var i: usize = 0; + var has_escape = false; + var first_char_validated = false; + + while (i < input.len) { + const b = input[i]; + + if (b == '\\') { + // Escape sequence + if (i + 1 >= input.len) { + @branchHint(.cold); + return error.InvalidIDSelector; + } + has_escape = true; + i += 2; // Skip backslash and escaped char + first_char_validated = true; + continue; } - const second = input[1]; - if (second == '-' or std.ascii.isDigit(second)) { - @branchHint(.cold); - return error.InvalidIDSelector; + + // Validate first character if not yet validated + if (!first_char_validated) { + if (b == '-') { + if (i + 1 >= input.len) { + @branchHint(.cold); + return error.InvalidIDSelector; + } + const second = input[i + 1]; + if (second == '-' or std.ascii.isDigit(second)) { + @branchHint(.cold); + return error.InvalidIDSelector; + } + } else if (!std.ascii.isAlphabetic(b) and b != '_' and b < 0x80) { + @branchHint(.cold); + return error.InvalidIDSelector; + } + first_char_validated = true; } - } else if (!std.ascii.isAlphabetic(first) and first != '_' and first < 0x80) { - @branchHint(.cold); - return error.InvalidIDSelector; - } - var i: usize = 1; - for (input[1..]) |b| { + // Check if this is a valid id character switch (b) { 'a'...'z', 'A'...'Z', '0'...'9', '-', '_' => {}, 0x80...0xFF => {}, // non-ASCII characters @@ -701,11 +721,39 @@ pub fn id(self: *Parser) ![]const u8 { i += 1; } + if (i == 0) { + @branchHint(.cold); + return error.InvalidIDSelector; + } + + const raw = input[0..i]; self.input = input[i..]; - return input[0..i]; + + // If no escape sequences, return the slice as-is + if (!has_escape) { + return raw; + } + + // Build unescaped string + var result = try std.ArrayList(u8).initCapacity(arena, raw.len); + var j: usize = 0; + while (j < raw.len) { + if (raw[j] == '\\') { + j += 1; // Skip backslash + if (j < raw.len) { + try result.append(arena, raw[j]); // Add escaped char + j += 1; + } + } else { + try result.append(arena, raw[j]); + j += 1; + } + } + + return result.items; } -fn class(self: *Parser) ![]const u8 { +fn class(self: *Parser, arena: Allocator) ![]const u8 { // Must be called when we're at a '.' std.debug.assert(self.peek() == '.'); @@ -717,26 +765,46 @@ fn class(self: *Parser) ![]const u8 { return error.InvalidClassSelector; } - // First character: must be letter, underscore, or non-ASCII (>= 0x80) - // Can also be hyphen if not followed by digit or another hyphen - const first = input[0]; - if (first == '-') { - if (input.len < 2) { - @branchHint(.cold); - return error.InvalidClassSelector; + // First pass: find the end of the class name and check if there are escape sequences + var i: usize = 0; + var has_escape = false; + var first_char_validated = false; + + while (i < input.len) { + const b = input[i]; + + if (b == '\\') { + // Escape sequence + if (i + 1 >= input.len) { + @branchHint(.cold); + return error.InvalidClassSelector; + } + has_escape = true; + i += 2; // Skip backslash and escaped char + first_char_validated = true; + continue; } - const second = input[1]; - if (second == '-' or std.ascii.isDigit(second)) { - @branchHint(.cold); - return error.InvalidClassSelector; + + // Validate first character if not yet validated + if (!first_char_validated) { + if (b == '-') { + if (i + 1 >= input.len) { + @branchHint(.cold); + return error.InvalidClassSelector; + } + const second = input[i + 1]; + if (second == '-' or std.ascii.isDigit(second)) { + @branchHint(.cold); + return error.InvalidClassSelector; + } + } else if (!std.ascii.isAlphabetic(b) and b != '_' and b < 0x80) { + @branchHint(.cold); + return error.InvalidClassSelector; + } + first_char_validated = true; } - } else if (!std.ascii.isAlphabetic(first) and first != '_' and first < 0x80) { - @branchHint(.cold); - return error.InvalidClassSelector; - } - var i: usize = 1; - for (input[1..]) |b| { + // Check if this is a valid class name character switch (b) { 'a'...'z', 'A'...'Z', '0'...'9', '-', '_' => {}, 0x80...0xFF => {}, // non-ASCII characters @@ -751,8 +819,36 @@ fn class(self: *Parser) ![]const u8 { i += 1; } + if (i == 0) { + @branchHint(.cold); + return error.InvalidClassSelector; + } + + const raw = input[0..i]; self.input = input[i..]; - return input[0..i]; + + // If no escape sequences, return the slice as-is + if (!has_escape) { + return raw; + } + + // Build unescaped string + var result = try std.ArrayList(u8).initCapacity(arena, raw.len); + var j: usize = 0; + while (j < raw.len) { + if (raw[j] == '\\') { + j += 1; // Skip backslash + if (j < raw.len) { + try result.append(arena, raw[j]); // Add escaped char + j += 1; + } + } else { + try result.append(arena, raw[j]); + j += 1; + } + } + + return result.items; } fn tag(self: *Parser) ![]const u8 { @@ -941,227 +1037,231 @@ fn fastEql(a: []const u8, comptime b: []const u8) bool { const testing = @import("../../../testing.zig"); test "Selector: Parser.ID" { + const arena = testing.allocator; + { var parser = Parser{ .input = "#" }; - try testing.expectError(error.InvalidIDSelector, parser.id()); + try testing.expectError(error.InvalidIDSelector, parser.id(arena)); } { var parser = Parser{ .input = "# " }; - try testing.expectError(error.InvalidIDSelector, parser.id()); + try testing.expectError(error.InvalidIDSelector, parser.id(arena)); } { var parser = Parser{ .input = "#1" }; - try testing.expectError(error.InvalidIDSelector, parser.id()); + try testing.expectError(error.InvalidIDSelector, parser.id(arena)); } { var parser = Parser{ .input = "#9abc" }; - try testing.expectError(error.InvalidIDSelector, parser.id()); + try testing.expectError(error.InvalidIDSelector, parser.id(arena)); } { var parser = Parser{ .input = "#-1" }; - try testing.expectError(error.InvalidIDSelector, parser.id()); + try testing.expectError(error.InvalidIDSelector, parser.id(arena)); } { var parser = Parser{ .input = "#-5abc" }; - try testing.expectError(error.InvalidIDSelector, parser.id()); + try testing.expectError(error.InvalidIDSelector, parser.id(arena)); } { var parser = Parser{ .input = "#--" }; - try testing.expectError(error.InvalidIDSelector, parser.id()); + try testing.expectError(error.InvalidIDSelector, parser.id(arena)); } { var parser = Parser{ .input = "#--test" }; - try testing.expectError(error.InvalidIDSelector, parser.id()); + try testing.expectError(error.InvalidIDSelector, parser.id(arena)); } { var parser = Parser{ .input = "#-" }; - try testing.expectError(error.InvalidIDSelector, parser.id()); + try testing.expectError(error.InvalidIDSelector, parser.id(arena)); } { var parser = Parser{ .input = "#over" }; - try testing.expectEqual("over", try parser.id()); + try testing.expectEqual("over", try parser.id(arena)); try testing.expectEqual("", parser.input); } { var parser = Parser{ .input = "#myID123" }; - try testing.expectEqual("myID123", try parser.id()); + try testing.expectEqual("myID123", try parser.id(arena)); try testing.expectEqual("", parser.input); } { var parser = Parser{ .input = "#_test" }; - try testing.expectEqual("_test", try parser.id()); + try testing.expectEqual("_test", try parser.id(arena)); try testing.expectEqual("", parser.input); } { var parser = Parser{ .input = "#test_123" }; - try testing.expectEqual("test_123", try parser.id()); + try testing.expectEqual("test_123", try parser.id(arena)); try testing.expectEqual("", parser.input); } { var parser = Parser{ .input = "#-test" }; - try testing.expectEqual("-test", try parser.id()); + try testing.expectEqual("-test", try parser.id(arena)); try testing.expectEqual("", parser.input); } { var parser = Parser{ .input = "#my-id" }; - try testing.expectEqual("my-id", try parser.id()); + try testing.expectEqual("my-id", try parser.id(arena)); try testing.expectEqual("", parser.input); } { var parser = Parser{ .input = "#test other" }; - try testing.expectEqual("test", try parser.id()); + try testing.expectEqual("test", try parser.id(arena)); try testing.expectEqual(" other", parser.input); } { var parser = Parser{ .input = "#id.class" }; - try testing.expectEqual("id", try parser.id()); + try testing.expectEqual("id", try parser.id(arena)); try testing.expectEqual(".class", parser.input); } { var parser = Parser{ .input = "#id:hover" }; - try testing.expectEqual("id", try parser.id()); + try testing.expectEqual("id", try parser.id(arena)); try testing.expectEqual(":hover", parser.input); } { var parser = Parser{ .input = "#id>child" }; - try testing.expectEqual("id", try parser.id()); + try testing.expectEqual("id", try parser.id(arena)); try testing.expectEqual(">child", parser.input); } { var parser = Parser{ .input = "#id[attr]" }; - try testing.expectEqual("id", try parser.id()); + try testing.expectEqual("id", try parser.id(arena)); try testing.expectEqual("[attr]", parser.input); } } test "Selector: Parser.class" { + const arena = testing.allocator; + { var parser = Parser{ .input = "." }; - try testing.expectError(error.InvalidClassSelector, parser.class()); + try testing.expectError(error.InvalidClassSelector, parser.class(arena)); } { var parser = Parser{ .input = ". " }; - try testing.expectError(error.InvalidClassSelector, parser.class()); + try testing.expectError(error.InvalidClassSelector, parser.class(arena)); } { var parser = Parser{ .input = ".1" }; - try testing.expectError(error.InvalidClassSelector, parser.class()); + try testing.expectError(error.InvalidClassSelector, parser.class(arena)); } { var parser = Parser{ .input = ".9abc" }; - try testing.expectError(error.InvalidClassSelector, parser.class()); + try testing.expectError(error.InvalidClassSelector, parser.class(arena)); } { var parser = Parser{ .input = ".-1" }; - try testing.expectError(error.InvalidClassSelector, parser.class()); + try testing.expectError(error.InvalidClassSelector, parser.class(arena)); } { var parser = Parser{ .input = ".-5abc" }; - try testing.expectError(error.InvalidClassSelector, parser.class()); + try testing.expectError(error.InvalidClassSelector, parser.class(arena)); } { var parser = Parser{ .input = ".--" }; - try testing.expectError(error.InvalidClassSelector, parser.class()); + try testing.expectError(error.InvalidClassSelector, parser.class(arena)); } { var parser = Parser{ .input = ".--test" }; - try testing.expectError(error.InvalidClassSelector, parser.class()); + try testing.expectError(error.InvalidClassSelector, parser.class(arena)); } { var parser = Parser{ .input = ".-" }; - try testing.expectError(error.InvalidClassSelector, parser.class()); + try testing.expectError(error.InvalidClassSelector, parser.class(arena)); } { var parser = Parser{ .input = ".active" }; - try testing.expectEqual("active", try parser.class()); + try testing.expectEqual("active", try parser.class(arena)); try testing.expectEqual("", parser.input); } { var parser = Parser{ .input = ".myClass123" }; - try testing.expectEqual("myClass123", try parser.class()); + try testing.expectEqual("myClass123", try parser.class(arena)); try testing.expectEqual("", parser.input); } { var parser = Parser{ .input = "._test" }; - try testing.expectEqual("_test", try parser.class()); + try testing.expectEqual("_test", try parser.class(arena)); try testing.expectEqual("", parser.input); } { var parser = Parser{ .input = ".test_123" }; - try testing.expectEqual("test_123", try parser.class()); + try testing.expectEqual("test_123", try parser.class(arena)); try testing.expectEqual("", parser.input); } { var parser = Parser{ .input = ".-test" }; - try testing.expectEqual("-test", try parser.class()); + try testing.expectEqual("-test", try parser.class(arena)); try testing.expectEqual("", parser.input); } { var parser = Parser{ .input = ".my-class" }; - try testing.expectEqual("my-class", try parser.class()); + try testing.expectEqual("my-class", try parser.class(arena)); try testing.expectEqual("", parser.input); } { var parser = Parser{ .input = ".test other" }; - try testing.expectEqual("test", try parser.class()); + try testing.expectEqual("test", try parser.class(arena)); try testing.expectEqual(" other", parser.input); } { var parser = Parser{ .input = ".class1.class2" }; - try testing.expectEqual("class1", try parser.class()); + try testing.expectEqual("class1", try parser.class(arena)); try testing.expectEqual(".class2", parser.input); } { var parser = Parser{ .input = ".class:hover" }; - try testing.expectEqual("class", try parser.class()); + try testing.expectEqual("class", try parser.class(arena)); try testing.expectEqual(":hover", parser.input); } { var parser = Parser{ .input = ".class>child" }; - try testing.expectEqual("class", try parser.class()); + try testing.expectEqual("class", try parser.class(arena)); try testing.expectEqual(">child", parser.input); } { var parser = Parser{ .input = ".class[attr]" }; - try testing.expectEqual("class", try parser.class()); + try testing.expectEqual("class", try parser.class(arena)); try testing.expectEqual("[attr]", parser.input); } } @@ -1354,3 +1454,4 @@ test "Selector: Parser.parseNthPattern" { try testing.expectEqual(" )", parser.input); } } + diff --git a/src/browser/webapi/storage/cookie.zig b/src/browser/webapi/storage/cookie.zig index 25d6f51dd..436d258b2 100644 --- a/src/browser/webapi/storage/cookie.zig +++ b/src/browser/webapi/storage/cookie.zig @@ -129,7 +129,7 @@ pub const Jar = struct { pub fn populateFromResponse(self: *Jar, uri: *const Uri, set_cookie: []const u8) !void { const c = Cookie.parse(self.allocator, uri, set_cookie) catch |err| { - log.warn(.web_api, "cookie parse failed", .{ .raw = set_cookie, .err = err }); + log.warn(.page, "cookie parse failed", .{ .raw = set_cookie, .err = err }); return; }; @@ -312,7 +312,7 @@ pub const Cookie = struct { // Algolia, for example, will call document.setCookie with // an expired value which is literally 'Invalid Date' // (it's trying to do something like: `new Date() + undefined`). - log.debug(.web_api, "cookie expires date", .{ .date = expires_ }); + log.debug(.page, "cookie expires date", .{ .date = expires_ }); } } } diff --git a/src/cdp/domains/log.zig b/src/cdp/domains/log.zig index 07d3c6d65..66b8b79f1 100644 --- a/src/cdp/domains/log.zig +++ b/src/cdp/domains/log.zig @@ -88,8 +88,8 @@ pub fn LogInterceptor(comptime BC: type) type { self.bc.cdp.sendEvent("Log.entryAdded", .{ .entry = .{ .source = switch (scope) { - .js, .user_script, .console, .web_api, .script_event => "javascript", - .http, .fetch, .xhr => "network", + .js, .console => "javascript", + .http => "network", .telemetry, .unknown_prop, .interceptor => unreachable, // filtered out in writer above else => "other", }, diff --git a/src/lightpanda.zig b/src/lightpanda.zig index 9c15f7224..ddc815fdd 100644 --- a/src/lightpanda.zig +++ b/src/lightpanda.zig @@ -32,7 +32,7 @@ const Allocator = std.mem.Allocator; pub const FetchOpts = struct { wait_ms: u32 = 5000, - dump: dump.Opts, + dump: dump.RootOpts, writer: ?*std.Io.Writer = null, }; pub fn fetch(app: *App, url: [:0]const u8, opts: FetchOpts) !void { @@ -64,7 +64,7 @@ pub fn fetch(app: *App, url: [:0]const u8, opts: FetchOpts) !void { _ = session.fetchWait(opts.wait_ms); const writer = opts.writer orelse return; - try dump.deep(page.document.asNode(), opts.dump, writer); + try dump.root(opts.dump, writer, page); try writer.flush(); } diff --git a/src/log.zig b/src/log.zig index e34329e8b..d3ab7c760 100644 --- a/src/log.zig +++ b/src/log.zig @@ -33,19 +33,12 @@ pub const Scope = enum { http, page, js, - loop, event, scheduler, not_implemented, - script_event, telemetry, - user_script, - unknown_prop, - web_api, - xhr, - fetch, - polyfill, interceptor, + unknown_prop, }; const Opts = struct { @@ -394,7 +387,7 @@ test "log: data" { const string = try testing.allocator.dupe(u8, "spice_must_flow"); defer testing.allocator.free(string); - try logTo(.http, .warn, "a msg", .{ + try logTo(.page, .warn, "a msg", .{ .cint = 5, .cfloat = 3.43, .int = @as(i16, -49), @@ -409,7 +402,7 @@ test "log: data" { .level = Level.warn, }, &aw.writer); - try testing.expectEqual("$time=1739795092929 $scope=http $level=warn $msg=\"a msg\" " ++ + try testing.expectEqual("$time=1739795092929 $scope=page $level=warn $msg=\"a msg\" " ++ "cint=5 cfloat=3.43 int=-49 float=0.0003232 bt=true bf=false " ++ "nn=33 n=null lit=over9000! slice=spice_must_flow " ++ "err=Nope level=warn\n", aw.written()); diff --git a/src/main.zig b/src/main.zig index 42ad8d0f6..1f7bd57e3 100644 --- a/src/main.zig +++ b/src/main.zig @@ -125,8 +125,8 @@ fn run(allocator: Allocator, main_arena: Allocator) !void { var fetch_opts = lp.FetchOpts{ .wait_ms = 5000, .dump = .{ + .strip = opts.strip, .with_base = opts.withbase, - .strip_mode = opts.strip_mode, }, }; @@ -245,7 +245,7 @@ const Command = struct { dump: bool = false, common: Common, withbase: bool = false, - strip_mode: lp.dump.Opts.StripMode = .{}, + strip: lp.dump.Opts.Strip = .{}, }; const Common = struct { @@ -511,7 +511,7 @@ fn parseFetchArgs( var withbase: bool = false; var url: ?[:0]const u8 = null; var common: Command.Common = .{}; - var strip_mode: lp.dump.Opts.StripMode = .{}; + var strip: lp.dump.Opts.Strip = .{}; while (args.next()) |opt| { if (std.mem.eql(u8, "--dump", opt)) { @@ -524,7 +524,7 @@ fn parseFetchArgs( .feature = "--noscript argument", .hint = "use '--strip_mode js' instead", }); - strip_mode.js = true; + strip.js = true; continue; } @@ -543,15 +543,15 @@ fn parseFetchArgs( while (it.next()) |part| { const trimmed = std.mem.trim(u8, part, &std.ascii.whitespace); if (std.mem.eql(u8, trimmed, "js")) { - strip_mode.js = true; + strip.js = true; } else if (std.mem.eql(u8, trimmed, "ui")) { - strip_mode.ui = true; + strip.ui = true; } else if (std.mem.eql(u8, trimmed, "css")) { - strip_mode.css = true; + strip.css = true; } else if (std.mem.eql(u8, trimmed, "full")) { - strip_mode.js = true; - strip_mode.ui = true; - strip_mode.css = true; + strip.js = true; + strip.ui = true; + strip.css = true; } else { log.fatal(.app, "invalid option choice", .{ .arg = "--strip_mode", .value = trimmed }); } @@ -583,9 +583,9 @@ fn parseFetchArgs( return .{ .url = url.?, .dump = dump, + .strip = strip, .common = common, .withbase = withbase, - .strip_mode = strip_mode, }; } From aa1742db639a88559a1e5522dc796d4cc639c477 Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Mon, 24 Nov 2025 10:43:08 -0800 Subject: [PATCH 070/219] use SlabAllocator --- src/browser/Factory.zig | 96 +----- src/slab.zig | 651 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 661 insertions(+), 86 deletions(-) create mode 100644 src/slab.zig diff --git a/src/browser/Factory.zig b/src/browser/Factory.zig index 8c9c3b58c..6013a2ff6 100644 --- a/src/browser/Factory.zig +++ b/src/browser/Factory.zig @@ -24,6 +24,8 @@ const IS_DEBUG = builtin.mode == .Debug; const log = @import("../log.zig"); const String = @import("../string.zig").String; +const SlabAllocator = @import("../slab.zig").SlabAllocator(16); + const Page = @import("Page.zig"); const Node = @import("webapi/Node.zig"); const Event = @import("webapi/Event.zig"); @@ -46,48 +48,12 @@ const MemoryPoolAligned = std.heap.MemoryPoolAligned; // (and alignment) based pools. const Factory = @This(); _page: *Page, -_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_80_16: MemoryPoolAligned([80]u8, .@"16"), -_size_88_8: MemoryPoolAligned([88]u8, .@"8"), -_size_96_16: MemoryPoolAligned([96]u8, .@"16"), -_size_128_8: MemoryPoolAligned([128]u8, .@"8"), -_size_144_8: MemoryPoolAligned([144]u8, .@"8"), -_size_152_8: MemoryPoolAligned([152]u8, .@"8"), -_size_160_8: MemoryPoolAligned([160]u8, .@"8"), -_size_184_8: MemoryPoolAligned([184]u8, .@"8"), -_size_232_8: MemoryPoolAligned([232]u8, .@"8"), -_size_648_8: MemoryPoolAligned([648]u8, .@"8"), +_slab: SlabAllocator, pub fn init(page: *Page) Factory { return .{ ._page = page, - ._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_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_128_8 = MemoryPoolAligned([128]u8, .@"8").init(page.arena), - ._size_144_8 = MemoryPoolAligned([144]u8, .@"8").init(page.arena), - ._size_152_8 = MemoryPoolAligned([152]u8, .@"8").init(page.arena), - ._size_160_8 = MemoryPoolAligned([160]u8, .@"8").init(page.arena), - ._size_184_8 = MemoryPoolAligned([184]u8, .@"8").init(page.arena), - ._size_232_8 = MemoryPoolAligned([232]u8, .@"8").init(page.arena), - ._size_648_8 = MemoryPoolAligned([648]u8, .@"8").init(page.arena), + ._slab = SlabAllocator.init(page.arena), }; } @@ -246,28 +212,8 @@ pub fn create(self: *Factory, value: anytype) !*@TypeOf(value) { } pub fn createT(self: *Factory, comptime T: type) !*T { - const SO = @sizeOf(T); - 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 == 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 == 128) return @ptrCast(try self._size_128_8.create()); - if (comptime SO == 152) return @ptrCast(try self._size_152_8.create()); - if (comptime SO == 160) return @ptrCast(try self._size_160_8.create()); - if (comptime SO == 184) return @ptrCast(try self._size_184_8.create()); - if (comptime SO == 232) return @ptrCast(try self._size_232_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) })); + const allocator = self._slab.allocator(); + return try allocator.create(T); } pub fn destroy(self: *Factory, value: anytype) void { @@ -291,6 +237,8 @@ pub fn destroy(self: *Factory, value: anytype) void { fn destroyChain(self: *Factory, value: anytype, comptime first: bool) void { const S = reflect.Struct(@TypeOf(value)); + const allocator = self._slab.allocator(); + // 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) @@ -311,7 +259,7 @@ fn destroyChain(self: *Factory, value: anytype, comptime first: bool) void { } 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)); + allocator.destroy(tagged); } } @@ -319,31 +267,7 @@ fn destroyChain(self: *Factory, value: anytype, comptime first: bool) void { // (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)) { - 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))), - 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))), - 128 => self._size_128_8.destroy(@ptrCast(value)), - 144 => self._size_144_8.destroy(@ptrCast(value)), - 152 => self._size_152_8.destroy(@ptrCast(value)), - 160 => self._size_160_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) })), - } + allocator.destroy(value); } } diff --git a/src/slab.zig b/src/slab.zig new file mode 100644 index 000000000..0af4f6168 --- /dev/null +++ b/src/slab.zig @@ -0,0 +1,651 @@ +const std = @import("std"); +const assert = std.debug.assert; + +const Allocator = std.mem.Allocator; +const Alignment = std.mem.Alignment; + +pub fn SlabAllocator(comptime slot_count: usize) type { + comptime assert(std.math.isPowerOfTwo(slot_count)); + + const Slab = struct { + const Slab = @This(); + const chunk_shift = std.math.log2_int(usize, slot_count); + const chunk_mask = slot_count - 1; + + alignment: Alignment, + item_size: usize, + + bitset: std.bit_set.DynamicBitSetUnmanaged, + chunks: std.ArrayListUnmanaged([]u8), + + pub fn init( + allocator: Allocator, + alignment: Alignment, + item_size: usize, + ) !Slab { + return .{ + .alignment = alignment, + .item_size = item_size, + .bitset = try .initFull(allocator, 0), + .chunks = .empty, + }; + } + + pub fn deinit(self: *Slab, allocator: Allocator) void { + self.bitset.deinit(allocator); + + for (self.chunks.items) |chunk| { + allocator.rawFree(chunk, self.alignment, @returnAddress()); + } + + self.chunks.deinit(allocator); + } + + inline fn toBitsetIndex(chunk_index: usize, slot_index: usize) usize { + return chunk_index * slot_count + slot_index; + } + + inline fn chunkIndex(bitset_index: usize) usize { + return bitset_index >> chunk_shift; + } + + inline fn slotIndex(bitset_index: usize) usize { + return bitset_index & chunk_mask; + } + + fn alloc(self: *Slab, allocator: Allocator) ![]u8 { + if (self.bitset.findFirstSet()) |index| { + // if we have a free slot + const chunk_index = chunkIndex(index); + const slot_index = slotIndex(index); + self.bitset.unset(index); + + const chunk = self.chunks.items[chunk_index]; + const offset = slot_index * self.item_size; + return chunk.ptr[offset..][0..self.item_size]; + } else { + const old_capacity = self.bitset.bit_length; + + // if we have don't have a free slot + try self.allocateChunk(allocator); + + const first_slot_index = old_capacity; + self.bitset.unset(first_slot_index); + + const new_chunk = self.chunks.items[self.chunks.items.len - 1]; + return new_chunk.ptr[0..self.item_size]; + } + } + + fn free(self: *Slab, ptr: [*]u8) void { + const addr = @intFromPtr(ptr); + + for (self.chunks.items, 0..) |chunk, i| { + const chunk_start = @intFromPtr(chunk.ptr); + const chunk_end = chunk_start + (slot_count * self.item_size); + + if (addr >= chunk_start and addr < chunk_end) { + const offset = addr - chunk_start; + const slot_index = offset / self.item_size; + + const bitset_index = toBitsetIndex(i, slot_index); + assert(!self.bitset.isSet(bitset_index)); + + self.bitset.set(bitset_index); + return; + } + } + + unreachable; + } + + fn allocateChunk(self: *Slab, allocator: Allocator) !void { + const chunk_len = self.item_size * slot_count; + + const chunk_ptr = allocator.rawAlloc( + chunk_len, + self.alignment, + @returnAddress(), + ) orelse return error.FailedChildAllocation; + + const chunk = chunk_ptr[0..chunk_len]; + try self.chunks.append(allocator, chunk); + + const new_capacity = self.chunks.items.len * slot_count; + try self.bitset.resize(allocator, new_capacity, true); + } + }; + + const SlabKey = struct { + size: usize, + alignment: Alignment, + }; + + return struct { + const Self = @This(); + + child_allocator: Allocator, + slabs: std.ArrayHashMapUnmanaged(SlabKey, Slab, struct { + const Context = @This(); + + pub fn hash(_: Context, key: SlabKey) u32 { + var hasher = std.hash.Wyhash.init(0); + std.hash.autoHash(&hasher, key.size); + std.hash.autoHash(&hasher, key.alignment); + return @truncate(hasher.final()); + } + + pub fn eql(_: Context, a: SlabKey, b: SlabKey, _: usize) bool { + return a.size == b.size and a.alignment == b.alignment; + } + }, false) = .empty, + + pub fn init(child_allocator: Allocator) Self { + return .{ + .child_allocator = child_allocator, + .slabs = .empty, + }; + } + + pub fn deinit(self: *Self) void { + for (self.slabs.values()) |*slab| { + slab.deinit(self.child_allocator); + } + + self.slabs.deinit(self.child_allocator); + } + + pub const ResetKind = enum { + /// Free all chunks and release all memory. + clear, + /// Keep all chunks, reset trees to reuse memory. + retain_capacity, + }; + + /// This clears all of the stored memory, freeing the currently used chunks. + pub fn reset(self: *Self, kind: ResetKind) void { + switch (kind) { + .clear => { + for (self.slabs.values()) |*slab| { + for (slab.chunks.items) |chunk| { + self.child_allocator.free(chunk); + } + + slab.chunks.clearAndFree(self.child_allocator); + slab.bitset.deinit(self.child_allocator); + } + + self.slabs.clearAndFree(self.child_allocator); + }, + .retain_capacity => { + for (self.slabs.values()) |*slab| { + slab.bitset.setAll(); + } + }, + } + } + + pub const vtable = Allocator.VTable{ + .alloc = alloc, + .free = free, + .remap = Allocator.noRemap, + .resize = Allocator.noResize, + }; + + pub fn allocator(self: *Self) Allocator { + return .{ + .ptr = self, + .vtable = &vtable, + }; + } + + fn alloc(ctx: *anyopaque, len: usize, alignment: Alignment, ret_addr: usize) ?[*]u8 { + const self: *Self = @ptrCast(@alignCast(ctx)); + _ = ret_addr; + + const list_gop = self.slabs.getOrPut( + self.child_allocator, + SlabKey{ .size = len, .alignment = alignment }, + ) catch return null; + + if (!list_gop.found_existing) { + list_gop.value_ptr.* = Slab.init( + self.child_allocator, + alignment, + len, + ) catch return null; + } + + const list = list_gop.value_ptr; + const buf = list.alloc(self.child_allocator) catch return null; + return buf.ptr; + } + + fn free(ctx: *anyopaque, memory: []u8, alignment: Alignment, ret_addr: usize) void { + const self: *Self = @ptrCast(@alignCast(ctx)); + _ = ret_addr; + + const ptr = memory.ptr; + const len = memory.len; + + const list = self.slabs.getPtr(.{ .size = len, .alignment = alignment }).?; + list.free(ptr); + } + }; +} + +const testing = std.testing; + +const TestSlabAllocator = SlabAllocator(32); + +test "slab allocator - basic allocation and free" { + var seg = TestSlabAllocator.init(testing.allocator); + defer seg.deinit(); + + const allocator = seg.allocator(); + + // Allocate some memory + const ptr1 = try allocator.alloc(u8, 100); + try testing.expect(ptr1.len == 100); + + // Write to it to ensure it's valid + @memset(ptr1, 42); + try testing.expectEqual(@as(u8, 42), ptr1[50]); + + // Free it + allocator.free(ptr1); +} + +test "slab allocator - multiple allocations" { + var seg = TestSlabAllocator.init(testing.allocator); + defer seg.deinit(); + + const allocator = seg.allocator(); + + const ptr1 = try allocator.alloc(u8, 64); + const ptr2 = try allocator.alloc(u8, 128); + const ptr3 = try allocator.alloc(u8, 256); + + // Ensure they don't overlap + const addr1 = @intFromPtr(ptr1.ptr); + const addr2 = @intFromPtr(ptr2.ptr); + const addr3 = @intFromPtr(ptr3.ptr); + + try testing.expect(addr1 + 64 <= addr2 or addr2 + 128 <= addr1); + try testing.expect(addr2 + 128 <= addr3 or addr3 + 256 <= addr2); + + allocator.free(ptr1); + allocator.free(ptr2); + allocator.free(ptr3); +} + +test "slab allocator - no coalescing (different size classes)" { + var seg = TestSlabAllocator.init(testing.allocator); + defer seg.deinit(); + + const allocator = seg.allocator(); + + // Allocate two blocks of same size + const ptr1 = try allocator.alloc(u8, 128); + const ptr2 = try allocator.alloc(u8, 128); + + // Free them (no coalescing in slab allocator) + allocator.free(ptr1); + allocator.free(ptr2); + + // Can't allocate larger block from these freed 128-byte blocks + const ptr3 = try allocator.alloc(u8, 256); + + // ptr3 will be from a different size class, not coalesced from ptr1+ptr2 + const addr1 = @intFromPtr(ptr1.ptr); + const addr3 = @intFromPtr(ptr3.ptr); + + // They should NOT be adjacent (different size classes) + try testing.expect(addr3 < addr1 or addr3 >= addr1 + 256); + + allocator.free(ptr3); +} + +test "slab allocator - reuse freed memory" { + var seg = TestSlabAllocator.init(testing.allocator); + defer seg.deinit(); + + const allocator = seg.allocator(); + + const ptr1 = try allocator.alloc(u8, 64); + const addr1 = @intFromPtr(ptr1.ptr); + allocator.free(ptr1); + + // Allocate same size, should reuse from same slab + const ptr2 = try allocator.alloc(u8, 64); + const addr2 = @intFromPtr(ptr2.ptr); + + try testing.expectEqual(addr1, addr2); + allocator.free(ptr2); +} + +test "slab allocator - multiple size classes" { + var seg = TestSlabAllocator.init(testing.allocator); + defer seg.deinit(); + + const allocator = seg.allocator(); + + // Allocate various sizes - each creates a new slab + var ptrs: [10][]u8 = undefined; + const sizes = [_]usize{ 24, 40, 64, 88, 128, 144, 200, 256, 512, 1000 }; + + for (&ptrs, sizes) |*ptr, size| { + ptr.* = try allocator.alloc(u8, size); + @memset(ptr.*, 0xFF); + } + + // Should have created multiple slabs + try testing.expect(seg.slabs.count() >= 10); + + // Free all + for (ptrs) |ptr| { + allocator.free(ptr); + } +} + +test "slab allocator - various sizes" { + var seg = TestSlabAllocator.init(testing.allocator); + defer seg.deinit(); + + const allocator = seg.allocator(); + + // Test different sizes (not limited to powers of 2!) + const sizes = [_]usize{ 8, 16, 24, 32, 40, 64, 88, 128, 144, 256 }; + + for (sizes) |size| { + const ptr = try allocator.alloc(u8, size); + try testing.expect(ptr.len == size); + @memset(ptr, @intCast(size & 0xFF)); + allocator.free(ptr); + } +} + +test "slab allocator - exact sizes (no rounding)" { + var seg = TestSlabAllocator.init(testing.allocator); + defer seg.deinit(); + + const allocator = seg.allocator(); + + // Odd sizes stay exact (unlike buddy which rounds to power of 2) + const ptr1 = try allocator.alloc(u8, 100); + const ptr2 = try allocator.alloc(u8, 200); + const ptr3 = try allocator.alloc(u8, 50); + + // Exact sizes! + try testing.expect(ptr1.len == 100); + try testing.expect(ptr2.len == 200); + try testing.expect(ptr3.len == 50); + + allocator.free(ptr1); + allocator.free(ptr2); + allocator.free(ptr3); +} + +test "slab allocator - chunk allocation" { + var seg = TestSlabAllocator.init(testing.allocator); + defer seg.deinit(); + + const allocator = seg.allocator(); + + // Allocate many items of same size to force multiple chunks + var ptrs: [100][]u8 = undefined; + for (&ptrs) |*ptr| { + ptr.* = try allocator.alloc(u8, 64); + } + + // Should have allocated multiple chunks (32 items per chunk) + const slab = seg.slabs.getPtr(.{ .size = 64, .alignment = Alignment.@"1" }).?; + try testing.expect(slab.chunks.items.len > 1); + + // Free all + for (ptrs) |ptr| { + allocator.free(ptr); + } +} + +test "slab allocator - reset with retain_capacity" { + var seg = TestSlabAllocator.init(testing.allocator); + defer seg.deinit(); + + const allocator = seg.allocator(); + + // Allocate some memory + const ptr1 = try allocator.alloc(u8, 128); + const ptr2 = try allocator.alloc(u8, 256); + _ = ptr1; + _ = ptr2; + + const slabs_before = seg.slabs.count(); + const slab_128 = seg.slabs.getPtr(.{ .size = 128, .alignment = Alignment.@"1" }).?; + const chunks_before = slab_128.chunks.items.len; + + // Reset but keep chunks + seg.reset(.retain_capacity); + + try testing.expectEqual(slabs_before, seg.slabs.count()); + try testing.expectEqual(chunks_before, slab_128.chunks.items.len); + + // Should be able to allocate again + const ptr3 = try allocator.alloc(u8, 512); + allocator.free(ptr3); +} + +test "slab allocator - reset with clear" { + var seg = TestSlabAllocator.init(testing.allocator); + defer seg.deinit(); + + const allocator = seg.allocator(); + + // Allocate some memory + const ptr1 = try allocator.alloc(u8, 128); + _ = ptr1; + + try testing.expect(seg.slabs.count() > 0); + + // Reset and free everything + seg.reset(.clear); + + try testing.expectEqual(@as(usize, 0), seg.slabs.count()); + + // Should still work after reset + const ptr2 = try allocator.alloc(u8, 256); + allocator.free(ptr2); +} + +test "slab allocator - stress test" { + var seg = TestSlabAllocator.init(testing.allocator); + defer seg.deinit(); + + const allocator = seg.allocator(); + + var prng = std.Random.DefaultPrng.init(0); + const random = prng.random(); + + var ptrs: std.ArrayList([]u8) = .empty; + + defer { + for (ptrs.items) |ptr| { + allocator.free(ptr); + } + ptrs.deinit(allocator); + } + + // Random allocations and frees + var i: usize = 0; + while (i < 100) : (i += 1) { + if (random.boolean() and ptrs.items.len > 0) { + // Free a random allocation + const index = random.uintLessThan(usize, ptrs.items.len); + allocator.free(ptrs.swapRemove(index)); + } else { + // Allocate random size (8 to 512) + const size = random.uintAtMost(usize, 504) + 8; + const ptr = try allocator.alloc(u8, size); + try ptrs.append(allocator, ptr); + + // Write to ensure it's valid + @memset(ptr, @intCast(i & 0xFF)); + } + } +} + +test "slab allocator - alignment" { + var seg = TestSlabAllocator.init(testing.allocator); + defer seg.deinit(); + + const allocator = seg.allocator(); + + const ptr1 = try allocator.create(u64); + const ptr2 = try allocator.create(u32); + const ptr3 = try allocator.create([100]u8); + + allocator.destroy(ptr1); + allocator.destroy(ptr2); + allocator.destroy(ptr3); +} + +test "slab allocator - no resize support" { + var seg = TestSlabAllocator.init(testing.allocator); + defer seg.deinit(); + + const allocator = seg.allocator(); + + const slice = try allocator.alloc(u8, 100); + @memset(slice, 42); + + // Resize should fail (not supported) + try testing.expect(!allocator.resize(slice, 90)); + try testing.expect(!allocator.resize(slice, 200)); + + allocator.free(slice); +} + +test "slab allocator - fragmentation pattern" { + var seg = TestSlabAllocator.init(testing.allocator); + defer seg.deinit(); + + const allocator = seg.allocator(); + + // Allocate 10 items + var items: [10][]u8 = undefined; + for (&items) |*item| { + item.* = try allocator.alloc(u8, 64); + @memset(item.*, 0xFF); + } + + // Free every other one + allocator.free(items[0]); + allocator.free(items[2]); + allocator.free(items[4]); + allocator.free(items[6]); + allocator.free(items[8]); + + // Allocate new items - should reuse freed slots + const new1 = try allocator.alloc(u8, 64); + const new2 = try allocator.alloc(u8, 64); + const new3 = try allocator.alloc(u8, 64); + + // Should get some of the freed slots back + const addrs = [_]usize{ + @intFromPtr(items[0].ptr), + @intFromPtr(items[2].ptr), + @intFromPtr(items[4].ptr), + @intFromPtr(items[6].ptr), + @intFromPtr(items[8].ptr), + }; + + const new1_addr = @intFromPtr(new1.ptr); + var found = false; + for (addrs) |addr| { + if (new1_addr == addr) found = true; + } + try testing.expect(found); + + // Cleanup + allocator.free(items[1]); + allocator.free(items[3]); + allocator.free(items[5]); + allocator.free(items[7]); + allocator.free(items[9]); + allocator.free(new1); + allocator.free(new2); + allocator.free(new3); +} + +test "slab allocator - many small allocations" { + var seg = TestSlabAllocator.init(testing.allocator); + defer seg.deinit(); + + const allocator = seg.allocator(); + + // Allocate 1000 small items + var ptrs: std.ArrayList([]u8) = .empty; + defer { + for (ptrs.items) |ptr| { + allocator.free(ptr); + } + ptrs.deinit(allocator); + } + + var i: usize = 0; + while (i < 1000) : (i += 1) { + const ptr = try allocator.alloc(u8, 24); + try ptrs.append(allocator, ptr); + } + + // Should have created multiple chunks + const slab = seg.slabs.getPtr(.{ .size = 24, .alignment = Alignment.@"1" }).?; + try testing.expect(slab.chunks.items.len > 10); +} + +test "slab allocator - zero waste for exact sizes" { + var seg = TestSlabAllocator.init(testing.allocator); + defer seg.deinit(); + + const allocator = seg.allocator(); + + // These sizes have zero internal fragmentation (unlike buddy) + const sizes = [_]usize{ 24, 40, 56, 88, 144, 152, 184, 232, 648 }; + + for (sizes) |size| { + const ptr = try allocator.alloc(u8, size); + + // Exact size returned! + try testing.expectEqual(size, ptr.len); + + @memset(ptr, 0xFF); + allocator.free(ptr); + } +} + +test "slab allocator - different size classes don't interfere" { + var seg = TestSlabAllocator.init(testing.allocator); + defer seg.deinit(); + + const allocator = seg.allocator(); + + // Allocate size 64 + const ptr_64 = try allocator.alloc(u8, 64); + const addr_64 = @intFromPtr(ptr_64.ptr); + allocator.free(ptr_64); + + // Allocate size 128 - should NOT reuse size-64 slot + const ptr_128 = try allocator.alloc(u8, 128); + const addr_128 = @intFromPtr(ptr_128.ptr); + + try testing.expect(addr_64 != addr_128); + + // Allocate size 64 again - SHOULD reuse original slot + const ptr_64_again = try allocator.alloc(u8, 64); + const addr_64_again = @intFromPtr(ptr_64_again.ptr); + + try testing.expectEqual(addr_64, addr_64_again); + + allocator.free(ptr_128); + allocator.free(ptr_64_again); +} From 219245be9534f71043a79d7f352d74711c25bd6e Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Mon, 24 Nov 2025 20:36:15 -0800 Subject: [PATCH 071/219] standardize slab testing names --- src/slab.zig | 122 +++++++++++++++++++++++++-------------------------- 1 file changed, 61 insertions(+), 61 deletions(-) diff --git a/src/slab.zig b/src/slab.zig index 0af4f6168..52d63c825 100644 --- a/src/slab.zig +++ b/src/slab.zig @@ -239,10 +239,10 @@ const testing = std.testing; const TestSlabAllocator = SlabAllocator(32); test "slab allocator - basic allocation and free" { - var seg = TestSlabAllocator.init(testing.allocator); - defer seg.deinit(); + var slab_alloc = TestSlabAllocator.init(testing.allocator); + defer slab_alloc.deinit(); - const allocator = seg.allocator(); + const allocator = slab_alloc.allocator(); // Allocate some memory const ptr1 = try allocator.alloc(u8, 100); @@ -257,10 +257,10 @@ test "slab allocator - basic allocation and free" { } test "slab allocator - multiple allocations" { - var seg = TestSlabAllocator.init(testing.allocator); - defer seg.deinit(); + var slab_alloc = TestSlabAllocator.init(testing.allocator); + defer slab_alloc.deinit(); - const allocator = seg.allocator(); + const allocator = slab_alloc.allocator(); const ptr1 = try allocator.alloc(u8, 64); const ptr2 = try allocator.alloc(u8, 128); @@ -280,10 +280,10 @@ test "slab allocator - multiple allocations" { } test "slab allocator - no coalescing (different size classes)" { - var seg = TestSlabAllocator.init(testing.allocator); - defer seg.deinit(); + var slab_alloc = TestSlabAllocator.init(testing.allocator); + defer slab_alloc.deinit(); - const allocator = seg.allocator(); + const allocator = slab_alloc.allocator(); // Allocate two blocks of same size const ptr1 = try allocator.alloc(u8, 128); @@ -307,10 +307,10 @@ test "slab allocator - no coalescing (different size classes)" { } test "slab allocator - reuse freed memory" { - var seg = TestSlabAllocator.init(testing.allocator); - defer seg.deinit(); + var slab_alloc = TestSlabAllocator.init(testing.allocator); + defer slab_alloc.deinit(); - const allocator = seg.allocator(); + const allocator = slab_alloc.allocator(); const ptr1 = try allocator.alloc(u8, 64); const addr1 = @intFromPtr(ptr1.ptr); @@ -325,10 +325,10 @@ test "slab allocator - reuse freed memory" { } test "slab allocator - multiple size classes" { - var seg = TestSlabAllocator.init(testing.allocator); - defer seg.deinit(); + var slab_alloc = TestSlabAllocator.init(testing.allocator); + defer slab_alloc.deinit(); - const allocator = seg.allocator(); + const allocator = slab_alloc.allocator(); // Allocate various sizes - each creates a new slab var ptrs: [10][]u8 = undefined; @@ -340,7 +340,7 @@ test "slab allocator - multiple size classes" { } // Should have created multiple slabs - try testing.expect(seg.slabs.count() >= 10); + try testing.expect(slab_alloc.slabs.count() >= 10); // Free all for (ptrs) |ptr| { @@ -349,10 +349,10 @@ test "slab allocator - multiple size classes" { } test "slab allocator - various sizes" { - var seg = TestSlabAllocator.init(testing.allocator); - defer seg.deinit(); + var slab_alloc = TestSlabAllocator.init(testing.allocator); + defer slab_alloc.deinit(); - const allocator = seg.allocator(); + const allocator = slab_alloc.allocator(); // Test different sizes (not limited to powers of 2!) const sizes = [_]usize{ 8, 16, 24, 32, 40, 64, 88, 128, 144, 256 }; @@ -366,10 +366,10 @@ test "slab allocator - various sizes" { } test "slab allocator - exact sizes (no rounding)" { - var seg = TestSlabAllocator.init(testing.allocator); - defer seg.deinit(); + var slab_alloc = TestSlabAllocator.init(testing.allocator); + defer slab_alloc.deinit(); - const allocator = seg.allocator(); + const allocator = slab_alloc.allocator(); // Odd sizes stay exact (unlike buddy which rounds to power of 2) const ptr1 = try allocator.alloc(u8, 100); @@ -387,10 +387,10 @@ test "slab allocator - exact sizes (no rounding)" { } test "slab allocator - chunk allocation" { - var seg = TestSlabAllocator.init(testing.allocator); - defer seg.deinit(); + var slab_alloc = TestSlabAllocator.init(testing.allocator); + defer slab_alloc.deinit(); - const allocator = seg.allocator(); + const allocator = slab_alloc.allocator(); // Allocate many items of same size to force multiple chunks var ptrs: [100][]u8 = undefined; @@ -399,7 +399,7 @@ test "slab allocator - chunk allocation" { } // Should have allocated multiple chunks (32 items per chunk) - const slab = seg.slabs.getPtr(.{ .size = 64, .alignment = Alignment.@"1" }).?; + const slab = slab_alloc.slabs.getPtr(.{ .size = 64, .alignment = Alignment.@"1" }).?; try testing.expect(slab.chunks.items.len > 1); // Free all @@ -409,10 +409,10 @@ test "slab allocator - chunk allocation" { } test "slab allocator - reset with retain_capacity" { - var seg = TestSlabAllocator.init(testing.allocator); - defer seg.deinit(); + var slab_alloc = TestSlabAllocator.init(testing.allocator); + defer slab_alloc.deinit(); - const allocator = seg.allocator(); + const allocator = slab_alloc.allocator(); // Allocate some memory const ptr1 = try allocator.alloc(u8, 128); @@ -420,14 +420,14 @@ test "slab allocator - reset with retain_capacity" { _ = ptr1; _ = ptr2; - const slabs_before = seg.slabs.count(); - const slab_128 = seg.slabs.getPtr(.{ .size = 128, .alignment = Alignment.@"1" }).?; + const slabs_before = slab_alloc.slabs.count(); + const slab_128 = slab_alloc.slabs.getPtr(.{ .size = 128, .alignment = Alignment.@"1" }).?; const chunks_before = slab_128.chunks.items.len; // Reset but keep chunks - seg.reset(.retain_capacity); + slab_alloc.reset(.retain_capacity); - try testing.expectEqual(slabs_before, seg.slabs.count()); + try testing.expectEqual(slabs_before, slab_alloc.slabs.count()); try testing.expectEqual(chunks_before, slab_128.chunks.items.len); // Should be able to allocate again @@ -436,21 +436,21 @@ test "slab allocator - reset with retain_capacity" { } test "slab allocator - reset with clear" { - var seg = TestSlabAllocator.init(testing.allocator); - defer seg.deinit(); + var slab_alloc = TestSlabAllocator.init(testing.allocator); + defer slab_alloc.deinit(); - const allocator = seg.allocator(); + const allocator = slab_alloc.allocator(); // Allocate some memory const ptr1 = try allocator.alloc(u8, 128); _ = ptr1; - try testing.expect(seg.slabs.count() > 0); + try testing.expect(slab_alloc.slabs.count() > 0); // Reset and free everything - seg.reset(.clear); + slab_alloc.reset(.clear); - try testing.expectEqual(@as(usize, 0), seg.slabs.count()); + try testing.expectEqual(@as(usize, 0), slab_alloc.slabs.count()); // Should still work after reset const ptr2 = try allocator.alloc(u8, 256); @@ -458,10 +458,10 @@ test "slab allocator - reset with clear" { } test "slab allocator - stress test" { - var seg = TestSlabAllocator.init(testing.allocator); - defer seg.deinit(); + var slab_alloc = TestSlabAllocator.init(testing.allocator); + defer slab_alloc.deinit(); - const allocator = seg.allocator(); + const allocator = slab_alloc.allocator(); var prng = std.Random.DefaultPrng.init(0); const random = prng.random(); @@ -495,10 +495,10 @@ test "slab allocator - stress test" { } test "slab allocator - alignment" { - var seg = TestSlabAllocator.init(testing.allocator); - defer seg.deinit(); + var slab_alloc = TestSlabAllocator.init(testing.allocator); + defer slab_alloc.deinit(); - const allocator = seg.allocator(); + const allocator = slab_alloc.allocator(); const ptr1 = try allocator.create(u64); const ptr2 = try allocator.create(u32); @@ -510,10 +510,10 @@ test "slab allocator - alignment" { } test "slab allocator - no resize support" { - var seg = TestSlabAllocator.init(testing.allocator); - defer seg.deinit(); + var slab_alloc = TestSlabAllocator.init(testing.allocator); + defer slab_alloc.deinit(); - const allocator = seg.allocator(); + const allocator = slab_alloc.allocator(); const slice = try allocator.alloc(u8, 100); @memset(slice, 42); @@ -526,10 +526,10 @@ test "slab allocator - no resize support" { } test "slab allocator - fragmentation pattern" { - var seg = TestSlabAllocator.init(testing.allocator); - defer seg.deinit(); + var slab_alloc = TestSlabAllocator.init(testing.allocator); + defer slab_alloc.deinit(); - const allocator = seg.allocator(); + const allocator = slab_alloc.allocator(); // Allocate 10 items var items: [10][]u8 = undefined; @@ -578,10 +578,10 @@ test "slab allocator - fragmentation pattern" { } test "slab allocator - many small allocations" { - var seg = TestSlabAllocator.init(testing.allocator); - defer seg.deinit(); + var slab_alloc = TestSlabAllocator.init(testing.allocator); + defer slab_alloc.deinit(); - const allocator = seg.allocator(); + const allocator = slab_alloc.allocator(); // Allocate 1000 small items var ptrs: std.ArrayList([]u8) = .empty; @@ -599,15 +599,15 @@ test "slab allocator - many small allocations" { } // Should have created multiple chunks - const slab = seg.slabs.getPtr(.{ .size = 24, .alignment = Alignment.@"1" }).?; + const slab = slab_alloc.slabs.getPtr(.{ .size = 24, .alignment = Alignment.@"1" }).?; try testing.expect(slab.chunks.items.len > 10); } test "slab allocator - zero waste for exact sizes" { - var seg = TestSlabAllocator.init(testing.allocator); - defer seg.deinit(); + var slab_alloc = TestSlabAllocator.init(testing.allocator); + defer slab_alloc.deinit(); - const allocator = seg.allocator(); + const allocator = slab_alloc.allocator(); // These sizes have zero internal fragmentation (unlike buddy) const sizes = [_]usize{ 24, 40, 56, 88, 144, 152, 184, 232, 648 }; @@ -624,10 +624,10 @@ test "slab allocator - zero waste for exact sizes" { } test "slab allocator - different size classes don't interfere" { - var seg = TestSlabAllocator.init(testing.allocator); - defer seg.deinit(); + var slab_alloc = TestSlabAllocator.init(testing.allocator); + defer slab_alloc.deinit(); - const allocator = seg.allocator(); + const allocator = slab_alloc.allocator(); // Allocate size 64 const ptr_64 = try allocator.alloc(u8, 64); From 218d08b1f68ab03111950800602cd9bd7b867290 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Tue, 25 Nov 2025 13:00:32 +0800 Subject: [PATCH 072/219] add some skeleton implementations for various CSS WebAPIs --- src/browser/dump.zig | 2 +- src/browser/js/bridge.zig | 5 + src/browser/tests/css/stylesheet.html | 207 ++++++++++++++++++ src/browser/webapi/Document.zig | 13 +- src/browser/webapi/Navigator.zig | 1 - src/browser/webapi/Window.zig | 1 - src/browser/webapi/css/CSSRule.zig | 90 ++++++++ src/browser/webapi/css/CSSRuleList.zig | 36 +++ .../webapi/css/CSSStyleDeclaration.zig | 44 ++-- src/browser/webapi/css/CSSStyleProperties.zig | 4 +- src/browser/webapi/css/CSSStyleRule.zig | 48 ++++ src/browser/webapi/css/CSSStyleSheet.zig | 87 ++++++++ src/browser/webapi/css/StyleSheetList.zig | 34 +++ src/browser/webapi/selector/Parser.zig | 1 - src/cdp/domains/log.zig | 2 +- src/html5ever/lib.rs | 1 + 16 files changed, 547 insertions(+), 29 deletions(-) create mode 100644 src/browser/tests/css/stylesheet.html create mode 100644 src/browser/webapi/css/CSSRule.zig create mode 100644 src/browser/webapi/css/CSSRuleList.zig create mode 100644 src/browser/webapi/css/CSSStyleRule.zig create mode 100644 src/browser/webapi/css/CSSStyleSheet.zig create mode 100644 src/browser/webapi/css/StyleSheetList.zig diff --git a/src/browser/dump.zig b/src/browser/dump.zig index 2e00ba39e..0617b4880 100644 --- a/src/browser/dump.zig +++ b/src/browser/dump.zig @@ -45,7 +45,7 @@ pub fn root(opts: RootOpts, writer: *std.Io.Writer, page: *Page) !void { } } - return deep(doc.asNode(), .{.strip = opts.strip}, writer); + return deep(doc.asNode(), .{ .strip = opts.strip }, writer); } pub fn deep(node: *Node, opts: Opts, writer: *std.Io.Writer) error{WriteFailed}!void { diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index bc380d1a0..d4b6b6fed 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -488,9 +488,14 @@ pub const JsApis = flattenTypes(&.{ @import("../webapi/collections.zig"), @import("../webapi/Console.zig"), @import("../webapi/Crypto.zig"), + @import("../webapi/css/CSSRule.zig"), + @import("../webapi/css/CSSRuleList.zig"), @import("../webapi/css/CSSStyleDeclaration.zig"), + @import("../webapi/css/CSSStyleRule.zig"), + @import("../webapi/css/CSSStyleSheet.zig"), @import("../webapi/css/CSSStyleProperties.zig"), @import("../webapi/css/MediaQueryList.zig"), + @import("../webapi/css/StyleSheetList.zig"), @import("../webapi/Document.zig"), @import("../webapi/HTMLDocument.zig"), @import("../webapi/History.zig"), diff --git a/src/browser/tests/css/stylesheet.html b/src/browser/tests/css/stylesheet.html new file mode 100644 index 000000000..abc1ed92f --- /dev/null +++ b/src/browser/tests/css/stylesheet.html @@ -0,0 +1,207 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/browser/webapi/Document.zig b/src/browser/webapi/Document.zig index e895bfd40..05223fdec 100644 --- a/src/browser/webapi/Document.zig +++ b/src/browser/webapi/Document.zig @@ -31,6 +31,7 @@ const NodeFilter = @import("NodeFilter.zig"); const DOMTreeWalker = @import("DOMTreeWalker.zig"); const DOMNodeIterator = @import("DOMNodeIterator.zig"); const DOMImplementation = @import("DOMImplementation.zig"); +const StyleSheetList = @import("css/StyleSheetList.zig"); pub const HTMLDocument = @import("HTMLDocument.zig"); @@ -43,6 +44,7 @@ _ready_state: ReadyState = .loading, _current_script: ?*Element.Html.Script = null, _elements_by_id: std.StringHashMapUnmanaged(*Element) = .empty, _active_element: ?*Element = null, +_style_sheets: ?*StyleSheetList = null, pub const Type = union(enum) { generic, @@ -225,6 +227,15 @@ pub fn getActiveElement(self: *Document) ?*Element { return self.getDocumentElement(); } +pub fn getStyleSheets(self: *Document, page: *Page) !*StyleSheetList { + if (self._style_sheets) |sheets| { + return sheets; + } + const sheets = try StyleSheetList.init(page); + self._style_sheets = sheets; + return sheets; +} + const ReadyState = enum { loading, interactive, @@ -253,7 +264,7 @@ pub const JsApi = struct { 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 styleSheets = bridge.accessor(Document.getStyleSheets, null, .{}); pub const createElement = bridge.function(Document.createElement, .{}); pub const createElementNS = bridge.function(Document.createElementNS, .{}); pub const createDocumentFragment = bridge.function(Document.createDocumentFragment, .{}); diff --git a/src/browser/webapi/Navigator.zig b/src/browser/webapi/Navigator.zig index 63b4cfc9a..3fa8154f1 100644 --- a/src/browser/webapi/Navigator.zig +++ b/src/browser/webapi/Navigator.zig @@ -120,4 +120,3 @@ pub const JsApi = struct { // Methods pub const javaEnabled = bridge.function(Navigator.javaEnabled, .{}); }; - diff --git a/src/browser/webapi/Window.zig b/src/browser/webapi/Window.zig index b65359e44..1607bb79c 100644 --- a/src/browser/webapi/Window.zig +++ b/src/browser/webapi/Window.zig @@ -243,7 +243,6 @@ pub fn atob(_: *const Window, input: []const u8, page: *Page) ![]const u8 { return decoded; } - const ScheduleOpts = struct { repeat: bool, params: []js.Object, diff --git a/src/browser/webapi/css/CSSRule.zig b/src/browser/webapi/css/CSSRule.zig new file mode 100644 index 000000000..dcf41db9e --- /dev/null +++ b/src/browser/webapi/css/CSSRule.zig @@ -0,0 +1,90 @@ +const std = @import("std"); +const js = @import("../../js/js.zig"); +const Page = @import("../../Page.zig"); + +const CSSRule = @This(); + +pub const Type = enum(u16) { + style = 1, + charset = 2, + import = 3, + media = 4, + font_face = 5, + page = 6, + keyframes = 7, + keyframe = 8, + margin = 9, + namespace = 10, + counter_style = 11, + supports = 12, + document = 13, + font_feature_values = 14, + viewport = 15, + region_style = 16, +}; + +_type: Type, + +pub fn init(rule_type: Type, page: *Page) !*CSSRule { + return page._factory.create(CSSRule{ + ._type = rule_type, + }); +} + +pub fn getType(self: *const CSSRule) u16 { + return @intFromEnum(self._type); +} + +pub fn getCssText(self: *const CSSRule, page: *Page) []const u8 { + _ = self; + _ = page; + return ""; +} + +pub fn setCssText(self: *CSSRule, text: []const u8, page: *Page) !void { + _ = self; + _ = text; + _ = page; +} + +pub fn getParentRule(self: *const CSSRule) ?*CSSRule { + _ = self; + return null; +} + +pub fn getParentStyleSheet(self: *const CSSRule) ?*CSSRule { + _ = self; + return null; +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(CSSRule); + + pub const Meta = struct { + pub const name = "CSSRule"; + pub var class_id: bridge.ClassId = undefined; + pub const prototype_chain = bridge.prototypeChain(); + }; + + pub const STYLE_RULE = 1; + pub const CHARSET_RULE = 2; + pub const IMPORT_RULE = 3; + pub const MEDIA_RULE = 4; + pub const FONT_FACE_RULE = 5; + pub const PAGE_RULE = 6; + pub const KEYFRAMES_RULE = 7; + pub const KEYFRAME_RULE = 8; + pub const MARGIN_RULE = 9; + pub const NAMESPACE_RULE = 10; + pub const COUNTER_STYLE_RULE = 11; + pub const SUPPORTS_RULE = 12; + pub const DOCUMENT_RULE = 13; + pub const FONT_FEATURE_VALUES_RULE = 14; + pub const VIEWPORT_RULE = 15; + pub const REGION_STYLE_RULE = 16; + + pub const @"type" = bridge.accessor(CSSRule.getType, null, .{}); + pub const cssText = bridge.accessor(CSSRule.getCssText, CSSRule.setCssText, .{}); + pub const parentRule = bridge.accessor(CSSRule.getParentRule, null, .{}); + pub const parentStyleSheet = bridge.accessor(CSSRule.getParentStyleSheet, null, .{}); +}; diff --git a/src/browser/webapi/css/CSSRuleList.zig b/src/browser/webapi/css/CSSRuleList.zig new file mode 100644 index 000000000..4a700237c --- /dev/null +++ b/src/browser/webapi/css/CSSRuleList.zig @@ -0,0 +1,36 @@ +const std = @import("std"); +const js = @import("../../js/js.zig"); +const Page = @import("../../Page.zig"); +const CSSRule = @import("CSSRule.zig"); + +const CSSRuleList = @This(); + +_rules: []*CSSRule = &.{}, + +pub fn init(page: *Page) !*CSSRuleList { + return page._factory.create(CSSRuleList{}); +} + +pub fn length(self: *const CSSRuleList) u32 { + return @intCast(self._rules.len); +} + +pub fn item(self: *const CSSRuleList, index: usize) ?*CSSRule { + if (index >= self._rules.len) { + return null; + } + return self._rules[index]; +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(CSSRuleList); + + pub const Meta = struct { + pub const name = "CSSRuleList"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; + + pub const length = bridge.accessor(CSSRuleList.length, null, .{}); + pub const @"[]" = bridge.indexed(CSSRuleList.item, .{ .null_as_undefined = true }); +}; diff --git a/src/browser/webapi/css/CSSStyleDeclaration.zig b/src/browser/webapi/css/CSSStyleDeclaration.zig index 887a8098d..536fa7376 100644 --- a/src/browser/webapi/css/CSSStyleDeclaration.zig +++ b/src/browser/webapi/css/CSSStyleDeclaration.zig @@ -29,28 +29,6 @@ const CSSStyleDeclaration = @This(); _element: ?*Element = null, _properties: std.DoublyLinkedList = .{}, -pub const Property = struct { - _name: String, - _value: String, - _important: bool = false, - _node: std.DoublyLinkedList.Node, - - fn fromNodeLink(n: *std.DoublyLinkedList.Node) *Property { - return @alignCast(@fieldParentPtr("_node", n)); - } - - pub fn format(self: *const Property, writer: *std.Io.Writer) !void { - try self._name.format(writer); - try writer.writeAll(": "); - try self._value.format(writer); - - if (self._important) { - try writer.writeAll(" !important"); - } - try writer.writeByte(';'); - } -}; - pub fn init(element: ?*Element, page: *Page) !*CSSStyleDeclaration { return page._factory.create(CSSStyleDeclaration{ ._element = element, @@ -214,6 +192,28 @@ fn normalizePropertyName(name: []const u8, buf: []u8) []const u8 { return std.ascii.lowerString(buf, name); } +pub const Property = struct { + _name: String, + _value: String, + _important: bool = false, + _node: std.DoublyLinkedList.Node, + + fn fromNodeLink(n: *std.DoublyLinkedList.Node) *Property { + return @alignCast(@fieldParentPtr("_node", n)); + } + + pub fn format(self: *const Property, writer: *std.Io.Writer) !void { + try self._name.format(writer); + try writer.writeAll(": "); + try self._value.format(writer); + + if (self._important) { + try writer.writeAll(" !important"); + } + try writer.writeByte(';'); + } +}; + pub const JsApi = struct { pub const bridge = js.Bridge(CSSStyleDeclaration); diff --git a/src/browser/webapi/css/CSSStyleProperties.zig b/src/browser/webapi/css/CSSStyleProperties.zig index f595838e1..199d12140 100644 --- a/src/browser/webapi/css/CSSStyleProperties.zig +++ b/src/browser/webapi/css/CSSStyleProperties.zig @@ -72,7 +72,9 @@ fn isKnownCSSProperty(dash_case: []const u8) bool { } fn camelCaseToDashCase(name: []const u8, buf: []u8) []const u8 { - if (name.len == 0) return name; + if (name.len == 0) { + return name; + } // Special case: cssFloat -> float const lower_name = std.ascii.lowerString(buf, name); diff --git a/src/browser/webapi/css/CSSStyleRule.zig b/src/browser/webapi/css/CSSStyleRule.zig new file mode 100644 index 000000000..c477621c7 --- /dev/null +++ b/src/browser/webapi/css/CSSStyleRule.zig @@ -0,0 +1,48 @@ +const std = @import("std"); +const js = @import("../../js/js.zig"); +const Page = @import("../../Page.zig"); +const CSSRule = @import("CSSRule.zig"); +const CSSStyleDeclaration = @import("CSSStyleDeclaration.zig"); + +const CSSStyleRule = @This(); + +_proto: *CSSRule, +_selector_text: []const u8 = "", +_style: ?*CSSStyleDeclaration = null, + +pub fn init(page: *Page) !*CSSStyleRule { + const rule = try CSSRule.init(.style, page); + return page._factory.create(CSSStyleRule{ + ._proto = rule, + }); +} + +pub fn getSelectorText(self: *const CSSStyleRule) []const u8 { + return self._selector_text; +} + +pub fn setSelectorText(self: *CSSStyleRule, text: []const u8, page: *Page) !void { + self._selector_text = try page.dupeString(text); +} + +pub fn getStyle(self: *CSSStyleRule, page: *Page) !*CSSStyleDeclaration { + if (self._style) |style| { + return style; + } + const style = try CSSStyleDeclaration.init(null, page); + self._style = style; + return style; +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(CSSStyleRule); + + pub const Meta = struct { + pub const name = "CSSStyleRule"; + pub const prototype_chain = bridge.prototypeChain(CSSRule); + pub var class_id: bridge.ClassId = undefined; + }; + + pub const selectorText = bridge.accessor(CSSStyleRule.getSelectorText, CSSStyleRule.setSelectorText, .{}); + pub const style = bridge.accessor(CSSStyleRule.getStyle, null, .{}); +}; diff --git a/src/browser/webapi/css/CSSStyleSheet.zig b/src/browser/webapi/css/CSSStyleSheet.zig new file mode 100644 index 000000000..a377618d5 --- /dev/null +++ b/src/browser/webapi/css/CSSStyleSheet.zig @@ -0,0 +1,87 @@ +const std = @import("std"); +const js = @import("../../js/js.zig"); +const Page = @import("../../Page.zig"); +const CSSRuleList = @import("CSSRuleList.zig"); +const CSSRule = @import("CSSRule.zig"); + +const CSSStyleSheet = @This(); + +_href: ?[]const u8 = null, +_title: []const u8 = "", +_disabled: bool = false, +_css_rules: ?*CSSRuleList = null, +_owner_rule: ?*CSSRule = null, + +pub fn init(page: *Page) !*CSSStyleSheet { + return page._factory.create(CSSStyleSheet{}); +} + +pub fn getOwnerNode(self: *const CSSStyleSheet) ?*CSSStyleSheet { + _ = self; + return null; +} + +pub fn getHref(self: *const CSSStyleSheet) ?[]const u8 { + return self._href; +} + +pub fn getTitle(self: *const CSSStyleSheet) []const u8 { + return self._title; +} + +pub fn getDisabled(self: *const CSSStyleSheet) bool { + return self._disabled; +} + +pub fn setDisabled(self: *CSSStyleSheet, disabled: bool) void { + self._disabled = disabled; +} + +pub fn getCssRules(self: *CSSStyleSheet, page: *Page) !*CSSRuleList { + if (self._css_rules) |rules| return rules; + const rules = try CSSRuleList.init(page); + self._css_rules = rules; + return rules; +} + +pub fn getOwnerRule(self: *const CSSStyleSheet) ?*CSSRule { + return self._owner_rule; +} + +pub fn insertRule(self: *CSSStyleSheet, rule: []const u8, index: u32, page: *Page) !u32 { + _ = self; + _ = rule; + _ = index; + _ = page; + return 0; +} + +pub fn deleteRule(self: *CSSStyleSheet, index: u32, page: *Page) !void { + _ = self; + _ = index; + _ = page; +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(CSSStyleSheet); + + pub const Meta = struct { + pub const name = "CSSStyleSheet"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; + + pub const ownerNode = bridge.accessor(CSSStyleSheet.getOwnerNode, null, .{ .null_as_undefined = true }); + pub const href = bridge.accessor(CSSStyleSheet.getHref, null, .{ .null_as_undefined = true }); + pub const title = bridge.accessor(CSSStyleSheet.getTitle, null, .{}); + pub const disabled = bridge.accessor(CSSStyleSheet.getDisabled, CSSStyleSheet.setDisabled, .{}); + pub const cssRules = bridge.accessor(CSSStyleSheet.getCssRules, null, .{}); + pub const ownerRule = bridge.accessor(CSSStyleSheet.getOwnerRule, null, .{ .null_as_undefined = true }); + pub const insertRule = bridge.function(CSSStyleSheet.insertRule, .{}); + pub const deleteRule = bridge.function(CSSStyleSheet.deleteRule, .{}); +}; + +const testing = @import("../../../testing.zig"); +test "WebApi: CSSStyleSheet" { + try testing.htmlRunner("css/stylesheet.html", .{}); +} diff --git a/src/browser/webapi/css/StyleSheetList.zig b/src/browser/webapi/css/StyleSheetList.zig new file mode 100644 index 000000000..8a019a183 --- /dev/null +++ b/src/browser/webapi/css/StyleSheetList.zig @@ -0,0 +1,34 @@ +const std = @import("std"); +const js = @import("../../js/js.zig"); +const Page = @import("../../Page.zig"); +const CSSStyleSheet = @import("CSSStyleSheet.zig"); + +const StyleSheetList = @This(); + +_sheets: []*CSSStyleSheet = &.{}, + +pub fn init(page: *Page) !*StyleSheetList { + return page._factory.create(StyleSheetList{}); +} + +pub fn length(self: *const StyleSheetList) u32 { + return @intCast(self._sheets.len); +} + +pub fn item(self: *const StyleSheetList, index: usize) ?*CSSStyleSheet { + if (index >= self._sheets.len) return null; + return self._sheets[index]; +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(StyleSheetList); + + pub const Meta = struct { + pub const name = "StyleSheetList"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; + + pub const length = bridge.accessor(StyleSheetList.length, null, .{}); + pub const @"[]" = bridge.indexed(StyleSheetList.item, .{ .null_as_undefined = true }); +}; diff --git a/src/browser/webapi/selector/Parser.zig b/src/browser/webapi/selector/Parser.zig index b97f7c004..a793e7c82 100644 --- a/src/browser/webapi/selector/Parser.zig +++ b/src/browser/webapi/selector/Parser.zig @@ -1454,4 +1454,3 @@ test "Selector: Parser.parseNthPattern" { try testing.expectEqual(" )", parser.input); } } - diff --git a/src/cdp/domains/log.zig b/src/cdp/domains/log.zig index 66b8b79f1..2eca6847a 100644 --- a/src/cdp/domains/log.zig +++ b/src/cdp/domains/log.zig @@ -88,7 +88,7 @@ pub fn LogInterceptor(comptime BC: type) type { self.bc.cdp.sendEvent("Log.entryAdded", .{ .entry = .{ .source = switch (scope) { - .js, .console => "javascript", + .js, .console => "javascript", .http => "network", .telemetry, .unknown_prop, .interceptor => unreachable, // filtered out in writer above else => "other", diff --git a/src/html5ever/lib.rs b/src/html5ever/lib.rs index 992b00fd3..69f6b399d 100644 --- a/src/html5ever/lib.rs +++ b/src/html5ever/lib.rs @@ -184,6 +184,7 @@ pub extern "C" fn html5ever_get_memory_usage() -> Memory { // Streaming parser API // The Parser type from html5ever implements TendrilSink and supports streaming pub struct StreamingParser { + #[allow(dead_code)] arena: Box>, parser: Box, } From 35a728e69f18fe9f2c02ce71693d9828c7304d97 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Tue, 25 Nov 2025 15:54:25 +0800 Subject: [PATCH 073/219] explicitly run microtasks --- src/browser/EventManager.zig | 39 ++++++++++++++++++++++++----------- src/browser/Page.zig | 14 +++---------- src/browser/js/Context.zig | 7 +++++++ src/browser/js/js.zig | 2 ++ src/browser/webapi/Window.zig | 10 +++++---- 5 files changed, 45 insertions(+), 27 deletions(-) diff --git a/src/browser/EventManager.zig b/src/browser/EventManager.zig index aa5f023ad..e6d1ec0b3 100644 --- a/src/browser/EventManager.zig +++ b/src/browser/EventManager.zig @@ -107,12 +107,19 @@ 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; + var was_handled = false; + + defer if (was_handled) { + self.page.js.runMicrotasks(); + }; + switch (target._type) { - .node => |node| try self.dispatchNode(node, event), + .node => |node| try self.dispatchNode(node, event, &was_handled), .xhr, .window, .abort_signal, .media_query_list => { const list = self.lookup.getPtr(@intFromPtr(target)) orelse return; - try self.dispatchAll(list, target, event); + try self.dispatchAll(list, target, event, &was_handled); }, } } @@ -135,19 +142,26 @@ pub fn dispatchWithFunction(self: *EventManager, target: *EventTarget, event: *E event._target = target; } + var was_dispatched = false; + defer if (was_dispatched) { + self.page.js.runMicrotasks(); + }; + if (function_) |func| { event._current_target = target; - func.call(void, .{event}) catch |err| { + if (func.call(void, .{event})) { + was_dispatched = true; + } else |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); + try self.dispatchAll(list, target, event, &was_dispatched); } -fn dispatchNode(self: *EventManager, target: *Node, event: *Event) !void { +fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled: *bool) !void { var path_len: usize = 0; var path_buffer: [128]*EventTarget = undefined; @@ -175,7 +189,7 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event) !void { i -= 1; const current_target = path[i]; if (self.lookup.getPtr(@intFromPtr(current_target))) |list| { - try self.dispatchPhase(list, current_target, event, true); + try self.dispatchPhase(list, current_target, event, was_handled, true); if (event._stop_propagation) { event._event_phase = .none; return; @@ -187,7 +201,7 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event) !void { 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); + try self.dispatchPhase(list, target_et, event, was_handled, null); if (event._stop_propagation) { event._event_phase = .none; return; @@ -200,7 +214,7 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event) !void { 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); + try self.dispatchPhase(list, current_target, event, was_handled, false); if (event._stop_propagation) { break; } @@ -211,7 +225,7 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event) !void { event._event_phase = .none; } -fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_target: *EventTarget, event: *Event, comptime capture_only: ?bool) !void { +fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_target: *EventTarget, event: *Event, was_handled: *bool, comptime capture_only: ?bool) !void { const page = self.page; const typ = event._type_string; @@ -240,6 +254,7 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe } } + was_handled.* = true; event._current_target = current_target; switch (listener.function) { @@ -261,8 +276,8 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe } // 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 dispatchAll(self: *EventManager, list: *std.DoublyLinkedList, current_target: *EventTarget, event: *Event, was_handled: *bool) !void { + return self.dispatchPhase(list, current_target, event, was_handled, null); } fn removeListener(self: *EventManager, list: *std.DoublyLinkedList, listener: *Listener) void { diff --git a/src/browser/Page.zig b/src/browser/Page.zig index b93a9f946..28c1fcac8 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -242,21 +242,13 @@ fn registerBackgroundTasks(self: *Page) !void { const Browser = @import("Browser.zig"); - try self.scheduler.add(self._session.browser, struct { - fn runMicrotasks(ctx: *anyopaque) !?u32 { - const b: *Browser = @ptrCast(@alignCast(ctx)); - b.runMicrotasks(); - return 5; - } - }.runMicrotasks, 5, .{ .name = "page.microtasks" }); - try self.scheduler.add(self._session.browser, struct { fn runMessageLoop(ctx: *anyopaque) !?u32 { const b: *Browser = @ptrCast(@alignCast(ctx)); b.runMessageLoop(); - return 100; + return 250; } - }.runMessageLoop, 5, .{ .name = "page.messageLoop" }); + }.runMessageLoop, 250, .{ .name = "page.messageLoop" }); } pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !void { @@ -705,10 +697,10 @@ fn _wait(self: *Page, wait_ms: u32) !Session.WaitResult { } pub fn tick(self: *Page) void { - self._session.browser.runMicrotasks(); _ = self.scheduler.run() catch |err| { log.err(.page, "tick", .{ .err = err }); }; + self.js.runMicrotasks(); } pub fn scriptAddedCallback(self: *Page, script: *HtmlScript) !void { diff --git a/src/browser/js/Context.zig b/src/browser/js/Context.zig index 8c37b47eb..fddcdfa48 100644 --- a/src/browser/js/Context.zig +++ b/src/browser/js/Context.zig @@ -1162,6 +1162,10 @@ pub fn resolvePromise(self: *Context, value: anytype) !js.Promise { return resolver.getPromise(); } +pub fn runMicrotasks(self: *Context) void { + self.isolate.performMicrotasksCheckpoint(); +} + // creates a PersistentPromiseResolver, taking in a lifetime parameter. // If the lifetime is page, the page will clean up the PersistentPromiseResolver. // If the lifetime is self, you will be expected to deinitalize the PersistentPromiseResolver. @@ -1444,6 +1448,7 @@ fn dynamicModuleSourceCallback(ctx: *anyopaque, fetch_result_: anyerror!ScriptMa } fn resolveDynamicModule(self: *Context, state: *DynamicModuleResolveState, module_entry: ModuleEntry) void { + defer self.runMicrotasks(); const ctx = self.v8_context; const isolate = self.isolate; const external = v8.External.init(self.isolate, @ptrCast(state)); @@ -1479,6 +1484,7 @@ fn resolveDynamicModule(self: *Context, state: *DynamicModuleResolveState, modul return; } + defer caller.context.runMicrotasks(); const namespace = s.module.?.getModuleNamespace(); _ = s.resolver.castToPromiseResolver().resolve(caller.context.v8_context, namespace); } @@ -1494,6 +1500,7 @@ fn resolveDynamicModule(self: *Context, state: *DynamicModuleResolveState, modul if (s.context_id != caller.context.id) { return; } + defer caller.context.runMicrotasks(); _ = s.resolver.castToPromiseResolver().reject(caller.context.v8_context, info.getData()); } }.callback, external); diff --git a/src/browser/js/js.zig b/src/browser/js/js.zig index 6a50576c5..71e192865 100644 --- a/src/browser/js/js.zig +++ b/src/browser/js/js.zig @@ -107,6 +107,7 @@ pub const PersistentPromiseResolver = struct { pub fn resolve(self: PersistentPromiseResolver, value: anytype) !void { const context = self.context; const js_value = try context.zigValueToJs(value, .{}); + defer context.runMicrotasks(); // resolver.resolve will return null if the promise isn't pending const ok = self.resolver.castToPromiseResolver().resolve(context.v8_context, js_value) orelse return; @@ -118,6 +119,7 @@ pub const PersistentPromiseResolver = struct { pub fn reject(self: PersistentPromiseResolver, value: anytype) !void { const context = self.context; const js_value = try context.zigValueToJs(value, .{}); + defer context.runMicrotasks(); // 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/webapi/Window.zig b/src/browser/webapi/Window.zig index 1607bb79c..2ebae996c 100644 --- a/src/browser/webapi/Window.zig +++ b/src/browser/webapi/Window.zig @@ -321,14 +321,15 @@ const ScheduleCallback = struct { fn run(ctx: *anyopaque) !?u32 { const self: *ScheduleCallback = @ptrCast(@alignCast(ctx)); + const page = self.page; if (self.removed) { - _ = self.page.window._timers.remove(self.timer_id); + _ = page.window._timers.remove(self.timer_id); self.deinit(); return null; } if (self.animation_frame) { - self.cb.call(void, .{self.page.window._performance.now()}) catch |err| { + self.cb.call(void, .{page.window._performance.now()}) catch |err| { // a non-JS error log.warn(.js, "window.RAF", .{ .name = self.name, .err = err }); }; @@ -342,9 +343,10 @@ const ScheduleCallback = struct { if (self.repeat_ms) |ms| { return ms; } + defer self.deinit(); - _ = self.page.window._timers.remove(self.timer_id); - self.deinit(); + _ = page.window._timers.remove(self.timer_id); + page.js.runMicrotasks(); return null; } }; From 6d6f1340af3553c34708c25f236565b7c82e2d5a Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Tue, 25 Nov 2025 15:58:34 +0800 Subject: [PATCH 074/219] window.screen --- src/browser/js/bridge.zig | 1 + src/browser/tests/window/window.html | 10 ++++ src/browser/webapi/Screen.zig | 73 ++++++++++++++++++++++++++++ src/browser/webapi/Window.zig | 7 +++ 4 files changed, 91 insertions(+) create mode 100644 src/browser/webapi/Screen.zig diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index d4b6b6fed..7ac4f0ad0 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -573,4 +573,5 @@ pub const JsApis = flattenTypes(&.{ @import("../webapi/ResizeObserver.zig"), @import("../webapi/Blob.zig"), @import("../webapi/File.zig"), + @import("../webapi/Screen.zig"), }); diff --git a/src/browser/tests/window/window.html b/src/browser/tests/window/window.html index c482ae289..c378c130c 100644 --- a/src/browser/tests/window/window.html +++ b/src/browser/tests/window/window.html @@ -93,3 +93,13 @@ } + + diff --git a/src/browser/webapi/Screen.zig b/src/browser/webapi/Screen.zig new file mode 100644 index 000000000..1ed5b1396 --- /dev/null +++ b/src/browser/webapi/Screen.zig @@ -0,0 +1,73 @@ +// 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 Screen = @This(); +_pad: bool = false, + +pub const init: Screen = .{}; + +/// Total width of the screen in pixels +pub fn getWidth(_: *const Screen) u32 { + return 1920; +} + +/// Total height of the screen in pixels +pub fn getHeight(_: *const Screen) u32 { + return 1080; +} + +/// Available width (excluding OS UI elements like taskbar) +pub fn getAvailWidth(_: *const Screen) u32 { + return 1920; +} + +/// Available height (excluding OS UI elements like taskbar) +pub fn getAvailHeight(_: *const Screen) u32 { + return 1040; // 40px reserved for taskbar/dock +} + +/// Color depth in bits per pixel +pub fn getColorDepth(_: *const Screen) u32 { + return 24; +} + +/// Pixel depth in bits per pixel (typically same as colorDepth) +pub fn getPixelDepth(_: *const Screen) u32 { + return 24; +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(Screen); + + pub const Meta = struct { + pub const name = "Screen"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + pub const empty_with_no_proto = true; + }; + + // Read-only properties + pub const width = bridge.accessor(Screen.getWidth, null, .{}); + pub const height = bridge.accessor(Screen.getHeight, null, .{}); + pub const availWidth = bridge.accessor(Screen.getAvailWidth, null, .{}); + pub const availHeight = bridge.accessor(Screen.getAvailHeight, null, .{}); + pub const colorDepth = bridge.accessor(Screen.getColorDepth, null, .{}); + pub const pixelDepth = bridge.accessor(Screen.getPixelDepth, null, .{}); +}; diff --git a/src/browser/webapi/Window.zig b/src/browser/webapi/Window.zig index 2ebae996c..503dc008f 100644 --- a/src/browser/webapi/Window.zig +++ b/src/browser/webapi/Window.zig @@ -26,6 +26,7 @@ const Console = @import("Console.zig"); const History = @import("History.zig"); const Crypto = @import("Crypto.zig"); const Navigator = @import("Navigator.zig"); +const Screen = @import("Screen.zig"); const Performance = @import("Performance.zig"); const Document = @import("Document.zig"); const Location = @import("Location.zig"); @@ -45,6 +46,7 @@ _document: *Document, _crypto: Crypto = .init, _console: Console = .init, _navigator: Navigator = .init, +_screen: Screen = .init, _performance: Performance, _history: History, _storage_bucket: *storage.Bucket, @@ -79,6 +81,10 @@ pub fn getNavigator(self: *Window) *Navigator { return &self._navigator; } +pub fn getScreen(self: *Window) *Screen { + return &self._screen; +} + pub fn getCrypto(self: *Window) *Crypto { return &self._crypto; } @@ -366,6 +372,7 @@ 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 screen = bridge.accessor(Window.getScreen, null, .{ .cache = "screen" }); pub const performance = bridge.accessor(Window.getPerformance, null, .{ .cache = "performance" }); pub const localStorage = bridge.accessor(Window.getLocalStorage, null, .{ .cache = "localStorage" }); pub const sessionStorage = bridge.accessor(Window.getSessionStorage, null, .{ .cache = "sessionStorage" }); From 4a4602137b5448a9e5fce8f09d079fd8b0f0f688 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Tue, 25 Nov 2025 11:46:54 +0100 Subject: [PATCH 075/219] element: add prefix and localName accessors --- .../tests/document/create_element_ns.html | 8 ++++++++ src/browser/webapi/Element.zig | 20 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/browser/tests/document/create_element_ns.html b/src/browser/tests/document/create_element_ns.html index c14a27734..5a75c359b 100644 --- a/src/browser/tests/document/create_element_ns.html +++ b/src/browser/tests/document/create_element_ns.html @@ -28,5 +28,13 @@ const regularDiv = document.createElement('div'); testing.expectEqual('DIV', regularDiv.tagName); + testing.expectEqual('div', regularDiv.localName); + testing.expectEqual(null, regularDiv.prefix); testing.expectEqual('http://www.w3.org/1999/xhtml', regularDiv.namespaceURI); + + const custom = document.createElementNS('test', 'te:ST'); + testing.expectEqual('TE:ST', custom.tagName); // Should be te:ST + testing.expectEqual('te', custom.prefix); + testing.expectEqual('ST', custom.localName); + testing.expectEqual('http://www.w3.org/1999/xhtml', custom.namespaceURI); // Should be test diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index 023da5109..81427e0ba 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -913,6 +913,26 @@ pub const JsApi = struct { return buf.written(); } + pub const prefix = bridge.accessor(_prefix, null, .{}); + fn _prefix(self: *Element) ?[]const u8 { + const name = self.getTagNameLower(); + if (std.mem.indexOfPos(u8, name, 0, ":")) |pos| { + return name[0..pos]; + } + + return null; + } + + pub const localName = bridge.accessor(_localName, null, .{}); + fn _localName(self: *Element) []const u8 { + const name = self.getTagNameLower(); + if (std.mem.indexOfPos(u8, name, 0, ":")) |pos| { + return name[pos + 1 ..]; + } + + return name; + } + 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, .{}); From a0fa232a3a3852bc163fbbce081784644b36aaef Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Tue, 25 Nov 2025 12:12:00 +0100 Subject: [PATCH 076/219] element: upper case only the suffix part of the tagname --- src/browser/tests/document/create_element_ns.html | 2 +- src/browser/webapi/Element.zig | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/browser/tests/document/create_element_ns.html b/src/browser/tests/document/create_element_ns.html index 5a75c359b..46773ebcf 100644 --- a/src/browser/tests/document/create_element_ns.html +++ b/src/browser/tests/document/create_element_ns.html @@ -33,7 +33,7 @@ testing.expectEqual('http://www.w3.org/1999/xhtml', regularDiv.namespaceURI); const custom = document.createElementNS('test', 'te:ST'); - testing.expectEqual('TE:ST', custom.tagName); // Should be te:ST + testing.expectEqual('te:ST', custom.tagName); testing.expectEqual('te', custom.prefix); testing.expectEqual('ST', custom.localName); testing.expectEqual('http://www.w3.org/1999/xhtml', custom.namespaceURI); // Should be test diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index 81427e0ba..c16084250 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -767,7 +767,15 @@ fn upperTagName(tag_name: *String, buf: []u8) []const u8 { log.info(.dom, "tag.long.name", .{ .name = tag_name.str() }); return tag_name.str(); } - return std.ascii.upperString(buf, tag_name.str()); + const tag = tag_name.str(); + // If the tag_name has a prefix, we must uppercase only the suffix part. + // example: te:st should be returned as te:ST. + if (std.mem.indexOfPos(u8, tag, 0, ":")) |pos| { + @memcpy(buf[0 .. pos + 1], tag[0 .. pos + 1]); + _ = std.ascii.upperString(buf[pos..tag.len], tag[pos..tag.len]); + return buf[0..tag.len]; + } + return std.ascii.upperString(buf, tag); } pub fn getTag(self: *const Element) Tag { From be0a808f01732e069433c9a5e4575d35f0455520 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Tue, 25 Nov 2025 19:50:53 +0800 Subject: [PATCH 077/219] Add HTMLSlotElement, PerformanceObserver and Script get/set type --- src/browser/Page.zig | 22 +- src/browser/ScriptManager.zig | 57 ++- src/browser/js/bridge.zig | 2 + src/browser/tests/element/html/slot.html | 384 +++++++++++++++ .../custom_element_composition.html | 456 ++++++++++++++++++ src/browser/tests/window/window.html | 1 - src/browser/webapi/Element.zig | 4 + src/browser/webapi/Performance.zig | 66 +++ src/browser/webapi/PerformanceObserver.zig | 67 +++ src/browser/webapi/element/Html.zig | 3 + src/browser/webapi/element/html/Script.zig | 9 + src/browser/webapi/element/html/Slot.zig | 151 ++++++ 12 files changed, 1192 insertions(+), 30 deletions(-) create mode 100644 src/browser/tests/element/html/slot.html create mode 100644 src/browser/tests/integration/custom_element_composition.html create mode 100644 src/browser/webapi/PerformanceObserver.zig create mode 100644 src/browser/webapi/element/html/Slot.zig diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 28c1fcac8..805003168 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -1024,6 +1024,12 @@ pub fn createElement(self: *Page, ns_: ?[]const u8, name: []const u8, attribute_ else => {}, }, 4 => switch (@as(u32, @bitCast(name[0..4].*))) { + asUint("span") => return self.createHtmlElementT( + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = String.init(undefined, "span", .{}) catch unreachable, ._tag = .span }, + ), asUint("meta") => return self.createHtmlElementT( Element.Html.Meta, namespace, @@ -1036,6 +1042,12 @@ pub fn createElement(self: *Page, ns_: ?[]const u8, name: []const u8, attribute_ attribute_iterator, .{ ._proto = undefined }, ), + asUint("slot") => return self.createHtmlElementT( + Element.Html.Slot, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), asUint("html") => return self.createHtmlElementT( Element.Html.Html, namespace, @@ -1066,12 +1078,6 @@ pub fn createElement(self: *Page, ns_: ?[]const u8, name: []const u8, attribute_ attribute_iterator, .{ ._proto = undefined, ._tag_name = String.init(undefined, "main", .{}) catch unreachable, ._tag = .main }, ), - asUint("span") => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = String.init(undefined, "span", .{}) catch unreachable, ._tag = .span }, - ), else => {}, }, 5 => switch (@as(u40, @bitCast(name[0..5].*))) { @@ -1787,3 +1793,7 @@ const testing = @import("../testing.zig"); test "WebApi: Page" { try testing.htmlRunner("page", .{}); } + +test "WebApi: Integration" { + try testing.htmlRunner("integration", .{}); +} diff --git a/src/browser/ScriptManager.zig b/src/browser/ScriptManager.zig index 632be5f2e..0d421db14 100644 --- a/src/browser/ScriptManager.zig +++ b/src/browser/ScriptManager.zig @@ -249,11 +249,14 @@ pub fn addFromElement(self: *ScriptManager, script_element: *Element.Html.Script .error_callback = Script.errorCallback, }); - log.debug(.http, "script queue", .{ - .ctx = ctx, - .url = remote_url.?, - .stack = page.js.stackTrace() catch "???", - }); + if (comptime IS_DEBUG) { + log.debug(.http, "script queue", .{ + .ctx = ctx, + .url = remote_url.?, + .element = element, + .stack = page.js.stackTrace() catch "???", + }); + } } if (script.mode != .normal) { @@ -326,12 +329,14 @@ pub fn preloadImport(self: *ScriptManager, url: [:0]const u8, referrer: []const var headers = try self.client.newHeaders(); try self.page.requestCookie(.{}).headersForRequest(self.page.arena, url, &headers); - log.debug(.http, "script queue", .{ - .url = url, - .ctx = "module", - .referrer = referrer, - .stack = self.page.js.stackTrace() catch "???", - }); + if (comptime IS_DEBUG) { + log.debug(.http, "script queue", .{ + .url = url, + .ctx = "module", + .referrer = referrer, + .stack = self.page.js.stackTrace() catch "???", + }); + } try self.client.request(.{ .url = url, @@ -403,12 +408,14 @@ pub fn getAsyncImport(self: *ScriptManager, url: [:0]const u8, cb: ImportAsync.C var headers = try self.client.newHeaders(); try self.page.requestCookie(.{}).headersForRequest(self.page.arena, url, &headers); - log.debug(.http, "script queue", .{ - .url = url, - .ctx = "dynamic module", - .referrer = referrer, - .stack = self.page.js.stackTrace() catch "???", - }); + if (comptime IS_DEBUG) { + log.debug(.http, "script queue", .{ + .url = url, + .ctx = "dynamic module", + .referrer = referrer, + .stack = self.page.js.stackTrace() catch "???", + }); + } // It's possible, but unlikely, for client.request to immediately finish // a request, thus calling our callback. We generally don't want a call @@ -617,11 +624,13 @@ const Script = struct { return; } - log.debug(.http, "script header", .{ - .req = transfer, - .status = header.status, - .content_type = header.contentType(), - }); + if (comptime IS_DEBUG) { + log.debug(.http, "script header", .{ + .req = transfer, + .status = header.status, + .content_type = header.contentType(), + }); + } // If this isn't true, then we'll likely leak memory. If you don't // set `CURLOPT_SUPPRESS_CONNECT_HEADERS` and CONNECT to a proxy, this @@ -649,7 +658,9 @@ const Script = struct { fn doneCallback(ctx: *anyopaque) !void { const self: *Script = @ptrCast(@alignCast(ctx)); self.complete = true; - log.debug(.http, "script fetch complete", .{ .req = self.url }); + if (comptime IS_DEBUG) { + log.debug(.http, "script fetch complete", .{ .req = self.url }); + } const manager = self.manager; if (self.mode == .async or self.mode == .import_async) { diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index 7ac4f0ad0..b93c3ec07 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -538,6 +538,7 @@ pub const JsApis = flattenTypes(&.{ @import("../webapi/element/html/Paragraph.zig"), @import("../webapi/element/html/Script.zig"), @import("../webapi/element/html/Select.zig"), + @import("../webapi/element/html/Slot.zig"), @import("../webapi/element/html/Style.zig"), @import("../webapi/element/html/Template.zig"), @import("../webapi/element/html/TextArea.zig"), @@ -574,4 +575,5 @@ pub const JsApis = flattenTypes(&.{ @import("../webapi/Blob.zig"), @import("../webapi/File.zig"), @import("../webapi/Screen.zig"), + @import("../webapi/PerformanceObserver.zig"), }); diff --git a/src/browser/tests/element/html/slot.html b/src/browser/tests/element/html/slot.html new file mode 100644 index 000000000..af2b08086 --- /dev/null +++ b/src/browser/tests/element/html/slot.html @@ -0,0 +1,384 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/browser/tests/integration/custom_element_composition.html b/src/browser/tests/integration/custom_element_composition.html new file mode 100644 index 000000000..3559d9f8b --- /dev/null +++ b/src/browser/tests/integration/custom_element_composition.html @@ -0,0 +1,456 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/browser/tests/window/window.html b/src/browser/tests/window/window.html index c378c130c..9cd74b371 100644 --- a/src/browser/tests/window/window.html +++ b/src/browser/tests/window/window.html @@ -102,4 +102,3 @@ testing.expectEqual(24, screen.pixelDepth); testing.expectEqual(screen, window.screen); - diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index 023da5109..fb9b927e9 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -148,6 +148,7 @@ pub fn getTagNameLower(self: *const Element) []const u8 { .p => "p", .script => "script", .select => "select", + .slot => "slot", .style => "style", .template => "template", .text_area => "textarea", @@ -192,6 +193,7 @@ pub fn getTagNameSpec(self: *const Element, buf: []u8) []const u8 { .p => "P", .script => "SCRIPT", .select => "SELECT", + .slot => "SLOT", .style => "STYLE", .template => "TEMPLATE", .text_area => "TEXTAREA", @@ -790,6 +792,7 @@ pub fn getTag(self: *const Element) Tag { .generic => |g| g._tag, .script => .script, .select => .select, + .slot => .slot, .option => .option, .template => .template, .text_area => .textarea, @@ -855,6 +858,7 @@ pub const Tag = enum { rect, script, select, + slot, span, strong, style, diff --git a/src/browser/webapi/Performance.zig b/src/browser/webapi/Performance.zig index 3ac87871f..60b972a30 100644 --- a/src/browser/webapi/Performance.zig +++ b/src/browser/webapi/Performance.zig @@ -1,6 +1,13 @@ const js = @import("../js/js.zig"); const datetime = @import("../../datetime.zig"); +pub fn registerTypes() []const type { + return &.{ + Performance, + Entry, + }; +} + const Performance = @This(); _time_origin: u64, @@ -34,6 +41,65 @@ pub const JsApi = struct { pub const timeOrigin = bridge.accessor(Performance.getTimeOrigin, null, .{}); }; +pub const Entry = struct { + _duration: f64 = 0.0, + _entry_type: Type, + _name: []const u8, + _start_time: f64 = 0.0, + + const Type = enum { + element, + event, + first_input, + largest_contentful_paint, + layout_shift, + long_animation_frame, + longtask, + mark, + measure, + navigation, + paint, + resource, + taskattribution, + visibility_state, + }; + + pub fn getDuration(self: *const Entry) f64 { + return self._duration; + } + + pub fn getEntryType(self: *const Entry) []const u8 { + return switch (self._entry_type) { + .first_input => "first-input", + .largest_contentful_paint => "largest-contentful-paint", + .layout_shift => "layout-shift", + .long_animation_frame => "long-animation-frame", + .visibility_state => "visibility-state", + else => |t| @tagName(t), + }; + } + + pub fn getName(self: *const Entry) []const u8 { + return self._name; + } + + pub fn getStartTime(self: *const Entry) f64 { + return self._start_time; + } + + pub const JsApi = struct { + pub const bridge = js.Bridge(Entry); + + pub const Meta = struct { + pub const name = "PerformanceEntry"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; + pub const duration = bridge.accessor(Entry.getDuration, null, .{}); + pub const entryType = bridge.accessor(Entry.getEntryType, null, .{}); + }; +}; + const testing = @import("../../testing.zig"); test "WebApi: Performance" { try testing.htmlRunner("performance.html", .{}); diff --git a/src/browser/webapi/PerformanceObserver.zig b/src/browser/webapi/PerformanceObserver.zig new file mode 100644 index 000000000..08bc2733a --- /dev/null +++ b/src/browser/webapi/PerformanceObserver.zig @@ -0,0 +1,67 @@ +// 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 Entry = @import("Performance.zig").Entry; + +// https://developer.mozilla.org/en-US/docs/Web/API/PerformanceObserver +const PerformanceObserver = @This(); + +pub fn init(callback: js.Function) PerformanceObserver { + _ = callback; + return .{}; +} + +const ObserverOptions = struct { + buffered: ?bool = null, + durationThreshold: ?f64 = null, + entryTypes: ?[]const []const u8 = null, + type: ?[]const u8 = null, +}; + +pub fn observe(self: *const PerformanceObserver, opts_: ?ObserverOptions) void { + _ = self; + _ = opts_; + return; +} + +pub fn disconnect(self: *PerformanceObserver) void { + _ = self; +} + +pub fn takeRecords(_: *const PerformanceObserver) []const Entry { + return &.{}; +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(PerformanceObserver); + + pub const Meta = struct { + pub const name = "PerformanceObserver"; + 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(PerformanceObserver.init, .{}); + + pub const observe = bridge.function(PerformanceObserver.observe, .{}); + pub const disconnect = bridge.function(PerformanceObserver.disconnect, .{}); + pub const takeRecords = bridge.function(PerformanceObserver.takeRecords, .{}); +}; diff --git a/src/browser/webapi/element/Html.zig b/src/browser/webapi/element/Html.zig index 475c1f210..4468b553a 100644 --- a/src/browser/webapi/element/Html.zig +++ b/src/browser/webapi/element/Html.zig @@ -51,6 +51,7 @@ pub const Template = @import("html/Template.zig"); pub const TextArea = @import("html/TextArea.zig"); pub const Paragraph = @import("html/Paragraph.zig"); pub const Select = @import("html/Select.zig"); +pub const Slot = @import("html/Slot.zig"); pub const Option = @import("html/Option.zig"); pub const IFrame = @import("html/IFrame.zig"); @@ -90,6 +91,7 @@ pub const Type = union(enum) { p: Paragraph, script: *Script, select: Select, + slot: Slot, style: Style, template: *Template, text_area: *TextArea, @@ -131,6 +133,7 @@ pub fn className(self: *const HtmlElement) []const u8 { .generic => "[object HTMLElement]", .script => "[object HtmlScriptElement]", .select => "[object HTMLSelectElement]", + .slot => "[object HTMLSlotElement]", .template => "[object HTMLTemplateElement]", .option => "[object HTMLOptionElement]", .text_area => "[object HtmlTextAreaElement]", diff --git a/src/browser/webapi/element/html/Script.zig b/src/browser/webapi/element/html/Script.zig index c12038a64..e1f559888 100644 --- a/src/browser/webapi/element/html/Script.zig +++ b/src/browser/webapi/element/html/Script.zig @@ -57,6 +57,14 @@ pub fn setSrc(self: *Script, src: []const u8, page: *Page) !void { } } +pub fn getType(self: *const Script) []const u8 { + return self.asConstElement().getAttributeSafe("type") orelse ""; +} + +pub fn setType(self: *Script, value: []const u8, page: *Page) !void { + return self.asElement().setAttributeSafe("type", value, page); +} + pub fn getOnLoad(self: *const Script) ?js.Function { return self._on_load; } @@ -95,6 +103,7 @@ pub const JsApi = struct { }; pub const src = bridge.accessor(Script.getSrc, Script.setSrc, .{}); + pub const @"type" = bridge.accessor(Script.getType, Script.setType, .{}); pub const onload = bridge.accessor(Script.getOnLoad, Script.setOnLoad, .{}); pub const onerorr = bridge.accessor(Script.getOnError, Script.setOnError, .{}); pub const noModule = bridge.accessor(Script.getNoModule, null, .{}); diff --git a/src/browser/webapi/element/html/Slot.zig b/src/browser/webapi/element/html/Slot.zig new file mode 100644 index 000000000..1089ad565 --- /dev/null +++ b/src/browser/webapi/element/html/Slot.zig @@ -0,0 +1,151 @@ +const std = @import("std"); + +const log = @import("../../../../log.zig"); +const js = @import("../../../js/js.zig"); +const Page = @import("../../../Page.zig"); +const Node = @import("../../Node.zig"); +const Element = @import("../../Element.zig"); +const HtmlElement = @import("../Html.zig"); +const ShadowRoot = @import("../../ShadowRoot.zig"); + +const Slot = @This(); + +_proto: *HtmlElement, + +pub fn asElement(self: *Slot) *Element { + return self._proto._proto; +} + +pub fn asConstElement(self: *const Slot) *const Element { + return self._proto._proto; +} + +pub fn asNode(self: *Slot) *Node { + return self.asElement().asNode(); +} + +pub fn getName(self: *const Slot) []const u8 { + return self.asConstElement().getAttributeSafe("name") orelse ""; +} + +pub fn setName(self: *Slot, name: []const u8, page: *Page) !void { + try self.asElement().setAttributeSafe("name", name, page); +} + +const AssignedNodesOptions = struct { + flatten: bool = false, +}; + +pub fn assignedNodes(self: *Slot, opts_: ?AssignedNodesOptions, page: *Page) ![]const *Node { + const opts = opts_ orelse AssignedNodesOptions{}; + var nodes: std.ArrayList(*Node) = .empty; + try self.collectAssignedNodes(false, &nodes, opts, page); + return nodes.items; +} + +pub fn assignedElements(self: *Slot, opts_: ?AssignedNodesOptions, page: *Page) ![]const *Element { + const opts = opts_ orelse AssignedNodesOptions{}; + var elements: std.ArrayList(*Element) = .empty; + try self.collectAssignedNodes(true, &elements, opts, page); + return elements.items; +} + +fn CollectionType(comptime elements: bool) type { + return if (elements) *std.ArrayList(*Element) else *std.ArrayList(*Node); +} + +fn collectAssignedNodes(self: *Slot, comptime elements: bool, coll: CollectionType(elements), opts: AssignedNodesOptions, page: *Page) !void { + // Find the shadow root this slot belongs to + const shadow_root = self.findShadowRoot() orelse return; + + const slot_name = self.getName(); + const allocator = page.call_arena; + + const host = shadow_root.getHost(); + var it = host.asNode().childrenIterator(); + while (it.next()) |child| { + if (!isAssignedToSlot(child, slot_name)) { + continue; + } + + if (opts.flatten) { + if (child.is(Slot)) |child_slot| { + // Only flatten if the child slot is actually in a shadow tree + if (child_slot.findShadowRoot()) |_| { + try child_slot.collectAssignedNodes(elements, coll, opts, page); + continue; + } + // Otherwise, treat it as a regular element and fall through + } + } + + if (comptime elements) { + if (child.is(Element)) |el| { + try coll.append(allocator, el); + } + } else { + try coll.append(allocator, child); + } + } +} + +pub fn assign(self: *Slot, nodes: []const *Node) void { + // Imperative slot assignment API + // This would require storing manually assigned nodes + // For now, this is a placeholder for the API + _ = self; + _ = nodes; + + // let's see if this is ever actually used + log.warn(.not_implemented, "Slot.assign", .{ }); +} + +fn findShadowRoot(self: *Slot) ?*ShadowRoot { + // Walk up the parent chain to find the shadow root + var parent = self.asNode()._parent; + while (parent) |p| { + if (p.is(ShadowRoot)) |shadow_root| { + return shadow_root; + } + parent = p._parent; + } + return null; +} + +fn isAssignedToSlot(node: *Node, slot_name: []const u8) bool { + // Check if a node should be assigned to a slot with the given name + if (node.is(Element)) |element| { + // Get the slot attribute from the element + const node_slot = element.getAttributeSafe("slot") orelse ""; + + // Match if: + // - Both are empty (default slot) + // - They match exactly + return std.mem.eql(u8, node_slot, slot_name); + } + + // Text nodes, comments, etc. are only assigned to the default slot + // (when they have no preceding/following element siblings with slot attributes) + // For simplicity, text nodes go to default slot if slot_name is empty + return slot_name.len == 0; +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(Slot); + + pub const Meta = struct { + pub const name = "HTMLSlotElement"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; + + pub const name = bridge.accessor(Slot.getName, Slot.setName, .{}); + pub const assignedNodes = bridge.function(Slot.assignedNodes, .{}); + pub const assignedElements = bridge.function(Slot.assignedElements, .{}); + pub const assign = bridge.function(Slot.assign, .{}); +}; + +const testing = @import("../../../../testing.zig"); +test "WebApi: HTMLSlotElement" { + try testing.htmlRunner("element/html/slot.html", .{}); +} From 0da87e1d5ea79fc4b00ae3f1d9c1d237474975c3 Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Tue, 25 Nov 2025 12:13:13 -0800 Subject: [PATCH 078/219] add slab statistics --- src/browser/Page.zig | 5 ++ src/slab.zig | 160 +++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 161 insertions(+), 4 deletions(-) diff --git a/src/browser/Page.zig b/src/browser/Page.zig index b93a9f946..b988f2eed 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -174,7 +174,12 @@ pub fn init(arena: Allocator, call_arena: Allocator, session: *Session) !*Page { pub fn deinit(self: *Page) void { if (comptime IS_DEBUG) { log.debug(.page, "page.deinit", .{ .url = self.url }); + + // Uncomment if you want slab statistics to print. + // const stats = self._factory._slab.getStats(self.arena) catch unreachable; + // stats.print() catch unreachable; } + self.js.deinit(); self._script_manager.shutdown = true; self._session.browser.http_client.abort(); diff --git a/src/slab.zig b/src/slab.zig index 52d63c825..509a18791 100644 --- a/src/slab.zig +++ b/src/slab.zig @@ -7,6 +7,11 @@ const Alignment = std.mem.Alignment; pub fn SlabAllocator(comptime slot_count: usize) type { comptime assert(std.math.isPowerOfTwo(slot_count)); + const SlabKey = struct { + size: usize, + alignment: Alignment, + }; + const Slab = struct { const Slab = @This(); const chunk_shift = std.math.log2_int(usize, slot_count); @@ -114,11 +119,45 @@ pub fn SlabAllocator(comptime slot_count: usize) type { const new_capacity = self.chunks.items.len * slot_count; try self.bitset.resize(allocator, new_capacity, true); } - }; - const SlabKey = struct { - size: usize, - alignment: Alignment, + const Stats = struct { + key: SlabKey, + item_size: usize, + chunk_count: usize, + total_slots: usize, + slots_in_use: usize, + slots_free: usize, + bytes_allocated: usize, + bytes_in_use: usize, + bytes_free: usize, + utilization_ratio: f64, + }; + + fn getStats(self: *const Slab, key: SlabKey) Stats { + const total_slots = self.bitset.bit_length; + const free_slots = self.bitset.count(); + const used_slots = total_slots - free_slots; + const bytes_allocated = self.chunks.items.len * slot_count * self.item_size; + const bytes_in_use = used_slots * self.item_size; + + const utilization_ratio = if (bytes_allocated > 0) + @as(f64, @floatFromInt(bytes_in_use)) / @as(f64, @floatFromInt(bytes_allocated)) + else + 0.0; + + return .{ + .key = key, + .item_size = self.item_size, + .chunk_count = self.chunks.items.len, + .total_slots = total_slots, + .slots_in_use = used_slots, + .slots_free = free_slots, + .bytes_allocated = bytes_allocated, + .bytes_in_use = bytes_in_use, + .bytes_free = free_slots * self.item_size, + .utilization_ratio = utilization_ratio, + }; + } }; return struct { @@ -185,6 +224,119 @@ pub fn SlabAllocator(comptime slot_count: usize) type { } } + const Stats = struct { + total_allocated_bytes: usize, + bytes_in_use: usize, + bytes_free: usize, + slab_count: usize, + total_chunks: usize, + total_slots: usize, + slots_in_use: usize, + slots_free: usize, + fragmentation_ratio: f64, + utilization_ratio: f64, + slabs: []const Slab.Stats, + + pub fn print(self: *const Stats) !void { + std.debug.print("\n", .{}); + std.debug.print("\n=== Slab Allocator Statistics ===\n", .{}); + std.debug.print("Overall Memory:\n", .{}); + std.debug.print(" Total allocated: {} bytes ({d:.2} MB)\n", .{ + self.total_allocated_bytes, + @as(f64, @floatFromInt(self.total_allocated_bytes)) / 1_048_576.0, + }); + std.debug.print(" In use: {} bytes ({d:.2} MB)\n", .{ + self.bytes_in_use, + @as(f64, @floatFromInt(self.bytes_in_use)) / 1_048_576.0, + }); + std.debug.print(" Free: {} bytes ({d:.2} MB)\n", .{ + self.bytes_free, + @as(f64, @floatFromInt(self.bytes_free)) / 1_048_576.0, + }); + + std.debug.print("\nOverall Structure:\n", .{}); + std.debug.print(" Slab Count: {}\n", .{self.slab_count}); + std.debug.print(" Total chunks: {}\n", .{self.total_chunks}); + std.debug.print(" Total slots: {}\n", .{self.total_slots}); + std.debug.print(" Slots in use: {}\n", .{self.slots_in_use}); + std.debug.print(" Slots free: {}\n", .{self.slots_free}); + + std.debug.print("\nOverall Efficiency:\n", .{}); + std.debug.print(" Utilization: {d:.1}%\n", .{self.utilization_ratio * 100.0}); + std.debug.print(" Fragmentation: {d:.1}%\n", .{self.fragmentation_ratio * 100.0}); + + if (self.slabs.len > 0) { + std.debug.print("\nPer-Slab Breakdown:\n", .{}); + std.debug.print( + " {s:>5} | {s:>4} | {s:>6} | {s:>6} | {s:>6} | {s:>10} | {s:>6}\n", + .{ "Size", "Algn", "Chunks", "Slots", "InUse", "Bytes", "Util%" }, + ); + std.debug.print( + " {s:-<5}-+-{s:-<4}-+-{s:-<6}-+-{s:-<6}-+-{s:-<6}-+-{s:-<10}-+-{s:-<6}\n", + .{ "", "", "", "", "", "", "" }, + ); + + for (self.slabs) |slab| { + std.debug.print(" {d:5} | {d:4} | {d:6} | {d:6} | {d:6} | {d:10} | {d:5.1}%\n", .{ + slab.key.size, + @intFromEnum(slab.key.alignment), + slab.chunk_count, + slab.total_slots, + slab.slots_in_use, + slab.bytes_allocated, + slab.utilization_ratio * 100.0, + }); + } + } + } + }; + + pub fn getStats(self: *Self, a: std.mem.Allocator) !Stats { + var slab_stats: std.ArrayList(Slab.Stats) = try .initCapacity(a, self.slabs.entries.len); + errdefer slab_stats.deinit(a); + + var stats = Stats{ + .total_allocated_bytes = 0, + .bytes_in_use = 0, + .bytes_free = 0, + .slab_count = self.slabs.count(), + .total_chunks = 0, + .total_slots = 0, + .slots_in_use = 0, + .slots_free = 0, + .fragmentation_ratio = 0.0, + .utilization_ratio = 0.0, + .slabs = &.{}, + }; + + var it = self.slabs.iterator(); + while (it.next()) |entry| { + const key = entry.key_ptr.*; + const slab = entry.value_ptr; + const slab_stat = slab.getStats(key); + + slab_stats.appendAssumeCapacity(slab_stat); + + stats.total_allocated_bytes += slab_stat.bytes_allocated; + stats.bytes_in_use += slab_stat.bytes_in_use; + stats.bytes_free += slab_stat.bytes_free; + stats.total_chunks += slab_stat.chunk_count; + stats.total_slots += slab_stat.total_slots; + stats.slots_in_use += slab_stat.slots_in_use; + stats.slots_free += slab_stat.slots_free; + } + + if (stats.total_allocated_bytes > 0) { + stats.fragmentation_ratio = @as(f64, @floatFromInt(stats.bytes_free)) / + @as(f64, @floatFromInt(stats.total_allocated_bytes)); + stats.utilization_ratio = @as(f64, @floatFromInt(stats.bytes_in_use)) / + @as(f64, @floatFromInt(stats.total_allocated_bytes)); + } + + stats.slabs = try slab_stats.toOwnedSlice(a); + return stats; + } + pub const vtable = Allocator.VTable{ .alloc = alloc, .free = free, From 058f86ec5f45371ec9a2bdce00a5ca4d30056b35 Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Tue, 25 Nov 2025 13:40:51 -0800 Subject: [PATCH 079/219] new exponential SlabAllocator --- src/browser/Factory.zig | 4 +- src/slab.zig | 689 +++++++++++++++++++++------------------- 2 files changed, 357 insertions(+), 336 deletions(-) diff --git a/src/browser/Factory.zig b/src/browser/Factory.zig index 6013a2ff6..336924b60 100644 --- a/src/browser/Factory.zig +++ b/src/browser/Factory.zig @@ -24,7 +24,7 @@ const IS_DEBUG = builtin.mode == .Debug; const log = @import("../log.zig"); const String = @import("../string.zig").String; -const SlabAllocator = @import("../slab.zig").SlabAllocator(16); +const SlabAllocator = @import("../slab.zig").SlabAllocator; const Page = @import("Page.zig"); const Node = @import("webapi/Node.zig"); @@ -53,7 +53,7 @@ _slab: SlabAllocator, pub fn init(page: *Page) Factory { return .{ ._page = page, - ._slab = SlabAllocator.init(page.arena), + ._slab = SlabAllocator.init(page.arena, 128), }; } diff --git a/src/slab.zig b/src/slab.zig index 509a18791..02d10aa72 100644 --- a/src/slab.zig +++ b/src/slab.zig @@ -4,394 +4,415 @@ const assert = std.debug.assert; const Allocator = std.mem.Allocator; const Alignment = std.mem.Alignment; -pub fn SlabAllocator(comptime slot_count: usize) type { - comptime assert(std.math.isPowerOfTwo(slot_count)); +const Slab = struct { + alignment: Alignment, + item_size: usize, + max_slot_count: usize, - const SlabKey = struct { - size: usize, - alignment: Alignment, - }; - - const Slab = struct { - const Slab = @This(); - const chunk_shift = std.math.log2_int(usize, slot_count); - const chunk_mask = slot_count - 1; + bitset: std.bit_set.DynamicBitSetUnmanaged, + chunks: std.ArrayListUnmanaged([]u8), + pub fn init( + allocator: Allocator, alignment: Alignment, item_size: usize, + max_slot_count: usize, + ) !Slab { + return .{ + .alignment = alignment, + .item_size = item_size, + .bitset = try .initFull(allocator, 0), + .chunks = .empty, + .max_slot_count = max_slot_count, + }; + } - bitset: std.bit_set.DynamicBitSetUnmanaged, - chunks: std.ArrayListUnmanaged([]u8), - - pub fn init( - allocator: Allocator, - alignment: Alignment, - item_size: usize, - ) !Slab { - return .{ - .alignment = alignment, - .item_size = item_size, - .bitset = try .initFull(allocator, 0), - .chunks = .empty, - }; + pub fn deinit(self: *Slab, allocator: Allocator) void { + self.bitset.deinit(allocator); + + for (self.chunks.items) |chunk| { + allocator.rawFree(chunk, self.alignment, @returnAddress()); } - pub fn deinit(self: *Slab, allocator: Allocator) void { - self.bitset.deinit(allocator); + self.chunks.deinit(allocator); + } - for (self.chunks.items) |chunk| { - allocator.rawFree(chunk, self.alignment, @returnAddress()); - } + inline fn calculateChunkSize(self: *Slab, chunk_index: usize) usize { + const safe_index: u6 = @intCast(@min(std.math.maxInt(u6), chunk_index)); + const exponential = @as(usize, 1) << safe_index; + return @min(exponential, self.max_slot_count); + } - self.chunks.deinit(allocator); + inline fn toBitsetIndex(self: *Slab, chunk_index: usize, slot_index: usize) usize { + var offset: usize = 0; + for (0..chunk_index) |i| { + const chunk_size = self.calculateChunkSize(i); + offset += chunk_size; } + return offset + slot_index; + } - inline fn toBitsetIndex(chunk_index: usize, slot_index: usize) usize { - return chunk_index * slot_count + slot_index; - } + inline fn toChunkAndSlotIndices(self: *Slab, bitset_index: usize) struct { usize, usize } { + var offset: usize = 0; + var chunk_index: usize = 0; - inline fn chunkIndex(bitset_index: usize) usize { - return bitset_index >> chunk_shift; - } + while (chunk_index < self.chunks.items.len) : (chunk_index += 1) { + const chunk_size = self.calculateChunkSize(chunk_index); + if (bitset_index < offset + chunk_size) { + return .{ chunk_index, bitset_index - offset }; + } - inline fn slotIndex(bitset_index: usize) usize { - return bitset_index & chunk_mask; + offset += chunk_size; } - fn alloc(self: *Slab, allocator: Allocator) ![]u8 { - if (self.bitset.findFirstSet()) |index| { - // if we have a free slot - const chunk_index = chunkIndex(index); - const slot_index = slotIndex(index); - self.bitset.unset(index); + unreachable; + } - const chunk = self.chunks.items[chunk_index]; - const offset = slot_index * self.item_size; - return chunk.ptr[offset..][0..self.item_size]; - } else { - const old_capacity = self.bitset.bit_length; + fn alloc(self: *Slab, allocator: Allocator) ![]u8 { + if (self.bitset.findFirstSet()) |index| { + const chunk_index, const slot_index = self.toChunkAndSlotIndices(index); - // if we have don't have a free slot - try self.allocateChunk(allocator); + // if we have a free slot + self.bitset.unset(index); - const first_slot_index = old_capacity; - self.bitset.unset(first_slot_index); + const chunk = self.chunks.items[chunk_index]; + const offset = slot_index * self.item_size; + return chunk.ptr[offset..][0..self.item_size]; + } else { + const old_capacity = self.bitset.bit_length; - const new_chunk = self.chunks.items[self.chunks.items.len - 1]; - return new_chunk.ptr[0..self.item_size]; - } + // if we have don't have a free slot + try self.allocateChunk(allocator); + + const first_slot_index = old_capacity; + self.bitset.unset(first_slot_index); + + const new_chunk = self.chunks.items[self.chunks.items.len - 1]; + return new_chunk.ptr[0..self.item_size]; } + } - fn free(self: *Slab, ptr: [*]u8) void { - const addr = @intFromPtr(ptr); + fn free(self: *Slab, ptr: [*]u8) void { + const addr = @intFromPtr(ptr); - for (self.chunks.items, 0..) |chunk, i| { - const chunk_start = @intFromPtr(chunk.ptr); - const chunk_end = chunk_start + (slot_count * self.item_size); + for (self.chunks.items, 0..) |chunk, i| { + const chunk_start = @intFromPtr(chunk.ptr); + const chunk_end = chunk_start + chunk.len; - if (addr >= chunk_start and addr < chunk_end) { - const offset = addr - chunk_start; - const slot_index = offset / self.item_size; + if (addr >= chunk_start and addr < chunk_end) { + const offset = addr - chunk_start; + const slot_index = offset / self.item_size; - const bitset_index = toBitsetIndex(i, slot_index); - assert(!self.bitset.isSet(bitset_index)); + const bitset_index = self.toBitsetIndex(i, slot_index); + assert(!self.bitset.isSet(bitset_index)); - self.bitset.set(bitset_index); - return; - } + self.bitset.set(bitset_index); + return; } - - unreachable; } - fn allocateChunk(self: *Slab, allocator: Allocator) !void { - const chunk_len = self.item_size * slot_count; + unreachable; + } - const chunk_ptr = allocator.rawAlloc( - chunk_len, - self.alignment, - @returnAddress(), - ) orelse return error.FailedChildAllocation; + fn allocateChunk(self: *Slab, allocator: Allocator) !void { + const next_chunk_size = self.calculateChunkSize(self.chunks.items.len); + const chunk_len = self.item_size * next_chunk_size; - const chunk = chunk_ptr[0..chunk_len]; - try self.chunks.append(allocator, chunk); + const chunk_ptr = allocator.rawAlloc( + chunk_len, + self.alignment, + @returnAddress(), + ) orelse return error.FailedChildAllocation; - const new_capacity = self.chunks.items.len * slot_count; - try self.bitset.resize(allocator, new_capacity, true); - } + const chunk = chunk_ptr[0..chunk_len]; + try self.chunks.append(allocator, chunk); - const Stats = struct { - key: SlabKey, - item_size: usize, - chunk_count: usize, - total_slots: usize, - slots_in_use: usize, - slots_free: usize, - bytes_allocated: usize, - bytes_in_use: usize, - bytes_free: usize, - utilization_ratio: f64, - }; + const new_capacity = self.bitset.bit_length + next_chunk_size; + try self.bitset.resize(allocator, new_capacity, true); + } - fn getStats(self: *const Slab, key: SlabKey) Stats { - const total_slots = self.bitset.bit_length; - const free_slots = self.bitset.count(); - const used_slots = total_slots - free_slots; - const bytes_allocated = self.chunks.items.len * slot_count * self.item_size; - const bytes_in_use = used_slots * self.item_size; - - const utilization_ratio = if (bytes_allocated > 0) - @as(f64, @floatFromInt(bytes_in_use)) / @as(f64, @floatFromInt(bytes_allocated)) - else - 0.0; - - return .{ - .key = key, - .item_size = self.item_size, - .chunk_count = self.chunks.items.len, - .total_slots = total_slots, - .slots_in_use = used_slots, - .slots_free = free_slots, - .bytes_allocated = bytes_allocated, - .bytes_in_use = bytes_in_use, - .bytes_free = free_slots * self.item_size, - .utilization_ratio = utilization_ratio, - }; - } + const Stats = struct { + key: SlabKey, + item_size: usize, + chunk_count: usize, + total_slots: usize, + slots_in_use: usize, + slots_free: usize, + bytes_allocated: usize, + bytes_in_use: usize, + bytes_free: usize, + utilization_ratio: f64, }; - return struct { - const Self = @This(); + fn getStats(self: *const Slab, key: SlabKey) Stats { + const total_slots = self.bitset.bit_length; + const free_slots = self.bitset.count(); + const used_slots = total_slots - free_slots; + const bytes_allocated = total_slots * self.item_size; + const bytes_in_use = used_slots * self.item_size; + + const utilization_ratio = if (bytes_allocated > 0) + @as(f64, @floatFromInt(bytes_in_use)) / @as(f64, @floatFromInt(bytes_allocated)) + else + 0.0; + + return .{ + .key = key, + .item_size = self.item_size, + .chunk_count = self.chunks.items.len, + .total_slots = total_slots, + .slots_in_use = used_slots, + .slots_free = free_slots, + .bytes_allocated = bytes_allocated, + .bytes_in_use = bytes_in_use, + .bytes_free = free_slots * self.item_size, + .utilization_ratio = utilization_ratio, + }; + } +}; - child_allocator: Allocator, - slabs: std.ArrayHashMapUnmanaged(SlabKey, Slab, struct { - const Context = @This(); +const SlabKey = struct { + size: usize, + alignment: Alignment, +}; - pub fn hash(_: Context, key: SlabKey) u32 { - var hasher = std.hash.Wyhash.init(0); - std.hash.autoHash(&hasher, key.size); - std.hash.autoHash(&hasher, key.alignment); - return @truncate(hasher.final()); - } +pub const SlabAllocator = struct { + const Self = @This(); - pub fn eql(_: Context, a: SlabKey, b: SlabKey, _: usize) bool { - return a.size == b.size and a.alignment == b.alignment; - } - }, false) = .empty, + child_allocator: Allocator, + max_slot_count: usize, - pub fn init(child_allocator: Allocator) Self { - return .{ - .child_allocator = child_allocator, - .slabs = .empty, - }; - } + slabs: std.ArrayHashMapUnmanaged(SlabKey, Slab, struct { + const Context = @This(); - pub fn deinit(self: *Self) void { - for (self.slabs.values()) |*slab| { - slab.deinit(self.child_allocator); - } + pub fn hash(_: Context, key: SlabKey) u32 { + var hasher = std.hash.Wyhash.init(0); + std.hash.autoHash(&hasher, key.size); + std.hash.autoHash(&hasher, key.alignment); + return @truncate(hasher.final()); + } - self.slabs.deinit(self.child_allocator); + pub fn eql(_: Context, a: SlabKey, b: SlabKey, _: usize) bool { + return a.size == b.size and a.alignment == b.alignment; } + }, false) = .empty, - pub const ResetKind = enum { - /// Free all chunks and release all memory. - clear, - /// Keep all chunks, reset trees to reuse memory. - retain_capacity, + pub fn init(child_allocator: Allocator, max_slot_count: usize) Self { + assert(std.math.isPowerOfTwo(max_slot_count)); + + return .{ + .child_allocator = child_allocator, + .slabs = .empty, + .max_slot_count = max_slot_count, }; + } - /// This clears all of the stored memory, freeing the currently used chunks. - pub fn reset(self: *Self, kind: ResetKind) void { - switch (kind) { - .clear => { - for (self.slabs.values()) |*slab| { - for (slab.chunks.items) |chunk| { - self.child_allocator.free(chunk); - } - - slab.chunks.clearAndFree(self.child_allocator); - slab.bitset.deinit(self.child_allocator); - } + pub fn deinit(self: *Self) void { + for (self.slabs.values()) |*slab| { + slab.deinit(self.child_allocator); + } - self.slabs.clearAndFree(self.child_allocator); - }, - .retain_capacity => { - for (self.slabs.values()) |*slab| { - slab.bitset.setAll(); + self.slabs.deinit(self.child_allocator); + } + + pub const ResetKind = enum { + /// Free all chunks and release all memory. + clear, + /// Keep all chunks, reset trees to reuse memory. + retain_capacity, + }; + + /// This clears all of the stored memory, freeing the currently used chunks. + pub fn reset(self: *Self, kind: ResetKind) void { + switch (kind) { + .clear => { + for (self.slabs.values()) |*slab| { + for (slab.chunks.items) |chunk| { + self.child_allocator.free(chunk); } - }, - } + + slab.chunks.clearAndFree(self.child_allocator); + slab.bitset.deinit(self.child_allocator); + } + + self.slabs.clearAndFree(self.child_allocator); + }, + .retain_capacity => { + for (self.slabs.values()) |*slab| { + slab.bitset.setAll(); + } + }, } + } - const Stats = struct { - total_allocated_bytes: usize, - bytes_in_use: usize, - bytes_free: usize, - slab_count: usize, - total_chunks: usize, - total_slots: usize, - slots_in_use: usize, - slots_free: usize, - fragmentation_ratio: f64, - utilization_ratio: f64, - slabs: []const Slab.Stats, - - pub fn print(self: *const Stats) !void { - std.debug.print("\n", .{}); - std.debug.print("\n=== Slab Allocator Statistics ===\n", .{}); - std.debug.print("Overall Memory:\n", .{}); - std.debug.print(" Total allocated: {} bytes ({d:.2} MB)\n", .{ - self.total_allocated_bytes, - @as(f64, @floatFromInt(self.total_allocated_bytes)) / 1_048_576.0, - }); - std.debug.print(" In use: {} bytes ({d:.2} MB)\n", .{ - self.bytes_in_use, - @as(f64, @floatFromInt(self.bytes_in_use)) / 1_048_576.0, - }); - std.debug.print(" Free: {} bytes ({d:.2} MB)\n", .{ - self.bytes_free, - @as(f64, @floatFromInt(self.bytes_free)) / 1_048_576.0, - }); - - std.debug.print("\nOverall Structure:\n", .{}); - std.debug.print(" Slab Count: {}\n", .{self.slab_count}); - std.debug.print(" Total chunks: {}\n", .{self.total_chunks}); - std.debug.print(" Total slots: {}\n", .{self.total_slots}); - std.debug.print(" Slots in use: {}\n", .{self.slots_in_use}); - std.debug.print(" Slots free: {}\n", .{self.slots_free}); - - std.debug.print("\nOverall Efficiency:\n", .{}); - std.debug.print(" Utilization: {d:.1}%\n", .{self.utilization_ratio * 100.0}); - std.debug.print(" Fragmentation: {d:.1}%\n", .{self.fragmentation_ratio * 100.0}); - - if (self.slabs.len > 0) { - std.debug.print("\nPer-Slab Breakdown:\n", .{}); - std.debug.print( - " {s:>5} | {s:>4} | {s:>6} | {s:>6} | {s:>6} | {s:>10} | {s:>6}\n", - .{ "Size", "Algn", "Chunks", "Slots", "InUse", "Bytes", "Util%" }, - ); - std.debug.print( - " {s:-<5}-+-{s:-<4}-+-{s:-<6}-+-{s:-<6}-+-{s:-<6}-+-{s:-<10}-+-{s:-<6}\n", - .{ "", "", "", "", "", "", "" }, - ); - - for (self.slabs) |slab| { - std.debug.print(" {d:5} | {d:4} | {d:6} | {d:6} | {d:6} | {d:10} | {d:5.1}%\n", .{ - slab.key.size, - @intFromEnum(slab.key.alignment), - slab.chunk_count, - slab.total_slots, - slab.slots_in_use, - slab.bytes_allocated, - slab.utilization_ratio * 100.0, - }); - } + const Stats = struct { + total_allocated_bytes: usize, + bytes_in_use: usize, + bytes_free: usize, + slab_count: usize, + total_chunks: usize, + total_slots: usize, + slots_in_use: usize, + slots_free: usize, + fragmentation_ratio: f64, + utilization_ratio: f64, + slabs: []const Slab.Stats, + + pub fn print(self: *const Stats) !void { + std.debug.print("\n", .{}); + std.debug.print("\n=== Slab Allocator Statistics ===\n", .{}); + std.debug.print("Overall Memory:\n", .{}); + std.debug.print(" Total allocated: {} bytes ({d:.2} MB)\n", .{ + self.total_allocated_bytes, + @as(f64, @floatFromInt(self.total_allocated_bytes)) / 1_048_576.0, + }); + std.debug.print(" In use: {} bytes ({d:.2} MB)\n", .{ + self.bytes_in_use, + @as(f64, @floatFromInt(self.bytes_in_use)) / 1_048_576.0, + }); + std.debug.print(" Free: {} bytes ({d:.2} MB)\n", .{ + self.bytes_free, + @as(f64, @floatFromInt(self.bytes_free)) / 1_048_576.0, + }); + + std.debug.print("\nOverall Structure:\n", .{}); + std.debug.print(" Slab Count: {}\n", .{self.slab_count}); + std.debug.print(" Total chunks: {}\n", .{self.total_chunks}); + std.debug.print(" Total slots: {}\n", .{self.total_slots}); + std.debug.print(" Slots in use: {}\n", .{self.slots_in_use}); + std.debug.print(" Slots free: {}\n", .{self.slots_free}); + + std.debug.print("\nOverall Efficiency:\n", .{}); + std.debug.print(" Utilization: {d:.1}%\n", .{self.utilization_ratio * 100.0}); + std.debug.print(" Fragmentation: {d:.1}%\n", .{self.fragmentation_ratio * 100.0}); + + if (self.slabs.len > 0) { + std.debug.print("\nPer-Slab Breakdown:\n", .{}); + std.debug.print( + " {s:>5} | {s:>4} | {s:>6} | {s:>6} | {s:>6} | {s:>10} | {s:>6}\n", + .{ "Size", "Algn", "Chunks", "Slots", "InUse", "Bytes", "Util%" }, + ); + std.debug.print( + " {s:-<5}-+-{s:-<4}-+-{s:-<6}-+-{s:-<6}-+-{s:-<6}-+-{s:-<10}-+-{s:-<6}\n", + .{ "", "", "", "", "", "", "" }, + ); + + for (self.slabs) |slab| { + std.debug.print(" {d:5} | {d:4} | {d:6} | {d:6} | {d:6} | {d:10} | {d:5.1}%\n", .{ + slab.key.size, + @intFromEnum(slab.key.alignment), + slab.chunk_count, + slab.total_slots, + slab.slots_in_use, + slab.bytes_allocated, + slab.utilization_ratio * 100.0, + }); } } - }; + } + }; - pub fn getStats(self: *Self, a: std.mem.Allocator) !Stats { - var slab_stats: std.ArrayList(Slab.Stats) = try .initCapacity(a, self.slabs.entries.len); - errdefer slab_stats.deinit(a); - - var stats = Stats{ - .total_allocated_bytes = 0, - .bytes_in_use = 0, - .bytes_free = 0, - .slab_count = self.slabs.count(), - .total_chunks = 0, - .total_slots = 0, - .slots_in_use = 0, - .slots_free = 0, - .fragmentation_ratio = 0.0, - .utilization_ratio = 0.0, - .slabs = &.{}, - }; - - var it = self.slabs.iterator(); - while (it.next()) |entry| { - const key = entry.key_ptr.*; - const slab = entry.value_ptr; - const slab_stat = slab.getStats(key); - - slab_stats.appendAssumeCapacity(slab_stat); - - stats.total_allocated_bytes += slab_stat.bytes_allocated; - stats.bytes_in_use += slab_stat.bytes_in_use; - stats.bytes_free += slab_stat.bytes_free; - stats.total_chunks += slab_stat.chunk_count; - stats.total_slots += slab_stat.total_slots; - stats.slots_in_use += slab_stat.slots_in_use; - stats.slots_free += slab_stat.slots_free; - } + pub fn getStats(self: *Self, a: std.mem.Allocator) !Stats { + var slab_stats: std.ArrayList(Slab.Stats) = try .initCapacity(a, self.slabs.entries.len); + errdefer slab_stats.deinit(a); + + var stats = Stats{ + .total_allocated_bytes = 0, + .bytes_in_use = 0, + .bytes_free = 0, + .slab_count = self.slabs.count(), + .total_chunks = 0, + .total_slots = 0, + .slots_in_use = 0, + .slots_free = 0, + .fragmentation_ratio = 0.0, + .utilization_ratio = 0.0, + .slabs = &.{}, + }; - if (stats.total_allocated_bytes > 0) { - stats.fragmentation_ratio = @as(f64, @floatFromInt(stats.bytes_free)) / - @as(f64, @floatFromInt(stats.total_allocated_bytes)); - stats.utilization_ratio = @as(f64, @floatFromInt(stats.bytes_in_use)) / - @as(f64, @floatFromInt(stats.total_allocated_bytes)); - } + var it = self.slabs.iterator(); + while (it.next()) |entry| { + const key = entry.key_ptr.*; + const slab = entry.value_ptr; + const slab_stat = slab.getStats(key); + + slab_stats.appendAssumeCapacity(slab_stat); + + stats.total_allocated_bytes += slab_stat.bytes_allocated; + stats.bytes_in_use += slab_stat.bytes_in_use; + stats.bytes_free += slab_stat.bytes_free; + stats.total_chunks += slab_stat.chunk_count; + stats.total_slots += slab_stat.total_slots; + stats.slots_in_use += slab_stat.slots_in_use; + stats.slots_free += slab_stat.slots_free; + } - stats.slabs = try slab_stats.toOwnedSlice(a); - return stats; + if (stats.total_allocated_bytes > 0) { + stats.fragmentation_ratio = @as(f64, @floatFromInt(stats.bytes_free)) / + @as(f64, @floatFromInt(stats.total_allocated_bytes)); + stats.utilization_ratio = @as(f64, @floatFromInt(stats.bytes_in_use)) / + @as(f64, @floatFromInt(stats.total_allocated_bytes)); } - pub const vtable = Allocator.VTable{ - .alloc = alloc, - .free = free, - .remap = Allocator.noRemap, - .resize = Allocator.noResize, + stats.slabs = try slab_stats.toOwnedSlice(a); + return stats; + } + + pub const vtable = Allocator.VTable{ + .alloc = alloc, + .free = free, + .remap = Allocator.noRemap, + .resize = Allocator.noResize, + }; + + pub fn allocator(self: *Self) Allocator { + return .{ + .ptr = self, + .vtable = &vtable, }; + } - pub fn allocator(self: *Self) Allocator { - return .{ - .ptr = self, - .vtable = &vtable, - }; - } + fn alloc(ctx: *anyopaque, len: usize, alignment: Alignment, ret_addr: usize) ?[*]u8 { + const self: *Self = @ptrCast(@alignCast(ctx)); + _ = ret_addr; - fn alloc(ctx: *anyopaque, len: usize, alignment: Alignment, ret_addr: usize) ?[*]u8 { - const self: *Self = @ptrCast(@alignCast(ctx)); - _ = ret_addr; + const list_gop = self.slabs.getOrPut( + self.child_allocator, + SlabKey{ .size = len, .alignment = alignment }, + ) catch return null; - const list_gop = self.slabs.getOrPut( + if (!list_gop.found_existing) { + list_gop.value_ptr.* = Slab.init( self.child_allocator, - SlabKey{ .size = len, .alignment = alignment }, + alignment, + len, + self.max_slot_count, ) catch return null; - - if (!list_gop.found_existing) { - list_gop.value_ptr.* = Slab.init( - self.child_allocator, - alignment, - len, - ) catch return null; - } - - const list = list_gop.value_ptr; - const buf = list.alloc(self.child_allocator) catch return null; - return buf.ptr; } - fn free(ctx: *anyopaque, memory: []u8, alignment: Alignment, ret_addr: usize) void { - const self: *Self = @ptrCast(@alignCast(ctx)); - _ = ret_addr; + const list = list_gop.value_ptr; + const buf = list.alloc(self.child_allocator) catch return null; + return buf.ptr; + } - const ptr = memory.ptr; - const len = memory.len; + fn free(ctx: *anyopaque, memory: []u8, alignment: Alignment, ret_addr: usize) void { + const self: *Self = @ptrCast(@alignCast(ctx)); + _ = ret_addr; - const list = self.slabs.getPtr(.{ .size = len, .alignment = alignment }).?; - list.free(ptr); - } - }; -} + const ptr = memory.ptr; + const len = memory.len; + + const list = self.slabs.getPtr(.{ .size = len, .alignment = alignment }).?; + list.free(ptr); + } +}; const testing = std.testing; -const TestSlabAllocator = SlabAllocator(32); +const TestSlabAllocator = SlabAllocator; test "slab allocator - basic allocation and free" { - var slab_alloc = TestSlabAllocator.init(testing.allocator); + var slab_alloc = TestSlabAllocator.init(testing.allocator, 16); defer slab_alloc.deinit(); const allocator = slab_alloc.allocator(); @@ -409,7 +430,7 @@ test "slab allocator - basic allocation and free" { } test "slab allocator - multiple allocations" { - var slab_alloc = TestSlabAllocator.init(testing.allocator); + var slab_alloc = TestSlabAllocator.init(testing.allocator, 16); defer slab_alloc.deinit(); const allocator = slab_alloc.allocator(); @@ -432,7 +453,7 @@ test "slab allocator - multiple allocations" { } test "slab allocator - no coalescing (different size classes)" { - var slab_alloc = TestSlabAllocator.init(testing.allocator); + var slab_alloc = TestSlabAllocator.init(testing.allocator, 16); defer slab_alloc.deinit(); const allocator = slab_alloc.allocator(); @@ -459,7 +480,7 @@ test "slab allocator - no coalescing (different size classes)" { } test "slab allocator - reuse freed memory" { - var slab_alloc = TestSlabAllocator.init(testing.allocator); + var slab_alloc = TestSlabAllocator.init(testing.allocator, 16); defer slab_alloc.deinit(); const allocator = slab_alloc.allocator(); @@ -477,7 +498,7 @@ test "slab allocator - reuse freed memory" { } test "slab allocator - multiple size classes" { - var slab_alloc = TestSlabAllocator.init(testing.allocator); + var slab_alloc = TestSlabAllocator.init(testing.allocator, 16); defer slab_alloc.deinit(); const allocator = slab_alloc.allocator(); @@ -501,7 +522,7 @@ test "slab allocator - multiple size classes" { } test "slab allocator - various sizes" { - var slab_alloc = TestSlabAllocator.init(testing.allocator); + var slab_alloc = TestSlabAllocator.init(testing.allocator, 16); defer slab_alloc.deinit(); const allocator = slab_alloc.allocator(); @@ -518,7 +539,7 @@ test "slab allocator - various sizes" { } test "slab allocator - exact sizes (no rounding)" { - var slab_alloc = TestSlabAllocator.init(testing.allocator); + var slab_alloc = TestSlabAllocator.init(testing.allocator, 16); defer slab_alloc.deinit(); const allocator = slab_alloc.allocator(); @@ -539,7 +560,7 @@ test "slab allocator - exact sizes (no rounding)" { } test "slab allocator - chunk allocation" { - var slab_alloc = TestSlabAllocator.init(testing.allocator); + var slab_alloc = TestSlabAllocator.init(testing.allocator, 16); defer slab_alloc.deinit(); const allocator = slab_alloc.allocator(); @@ -561,7 +582,7 @@ test "slab allocator - chunk allocation" { } test "slab allocator - reset with retain_capacity" { - var slab_alloc = TestSlabAllocator.init(testing.allocator); + var slab_alloc = TestSlabAllocator.init(testing.allocator, 16); defer slab_alloc.deinit(); const allocator = slab_alloc.allocator(); @@ -588,7 +609,7 @@ test "slab allocator - reset with retain_capacity" { } test "slab allocator - reset with clear" { - var slab_alloc = TestSlabAllocator.init(testing.allocator); + var slab_alloc = TestSlabAllocator.init(testing.allocator, 16); defer slab_alloc.deinit(); const allocator = slab_alloc.allocator(); @@ -610,7 +631,7 @@ test "slab allocator - reset with clear" { } test "slab allocator - stress test" { - var slab_alloc = TestSlabAllocator.init(testing.allocator); + var slab_alloc = TestSlabAllocator.init(testing.allocator, 16); defer slab_alloc.deinit(); const allocator = slab_alloc.allocator(); @@ -647,7 +668,7 @@ test "slab allocator - stress test" { } test "slab allocator - alignment" { - var slab_alloc = TestSlabAllocator.init(testing.allocator); + var slab_alloc = TestSlabAllocator.init(testing.allocator, 16); defer slab_alloc.deinit(); const allocator = slab_alloc.allocator(); @@ -662,7 +683,7 @@ test "slab allocator - alignment" { } test "slab allocator - no resize support" { - var slab_alloc = TestSlabAllocator.init(testing.allocator); + var slab_alloc = TestSlabAllocator.init(testing.allocator, 16); defer slab_alloc.deinit(); const allocator = slab_alloc.allocator(); @@ -678,7 +699,7 @@ test "slab allocator - no resize support" { } test "slab allocator - fragmentation pattern" { - var slab_alloc = TestSlabAllocator.init(testing.allocator); + var slab_alloc = TestSlabAllocator.init(testing.allocator, 16); defer slab_alloc.deinit(); const allocator = slab_alloc.allocator(); @@ -730,7 +751,7 @@ test "slab allocator - fragmentation pattern" { } test "slab allocator - many small allocations" { - var slab_alloc = TestSlabAllocator.init(testing.allocator); + var slab_alloc = TestSlabAllocator.init(testing.allocator, 16); defer slab_alloc.deinit(); const allocator = slab_alloc.allocator(); @@ -752,11 +773,11 @@ test "slab allocator - many small allocations" { // Should have created multiple chunks const slab = slab_alloc.slabs.getPtr(.{ .size = 24, .alignment = Alignment.@"1" }).?; - try testing.expect(slab.chunks.items.len > 10); + try testing.expect(slab.chunks.items.len > 1); } test "slab allocator - zero waste for exact sizes" { - var slab_alloc = TestSlabAllocator.init(testing.allocator); + var slab_alloc = TestSlabAllocator.init(testing.allocator, 16); defer slab_alloc.deinit(); const allocator = slab_alloc.allocator(); @@ -776,7 +797,7 @@ test "slab allocator - zero waste for exact sizes" { } test "slab allocator - different size classes don't interfere" { - var slab_alloc = TestSlabAllocator.init(testing.allocator); + var slab_alloc = TestSlabAllocator.init(testing.allocator, 16); defer slab_alloc.deinit(); const allocator = slab_alloc.allocator(); From e1d9732a6008c8bb5e4181695951a18ff1616f2f Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Wed, 26 Nov 2025 07:42:19 +0800 Subject: [PATCH 080/219] PerformanceObserver.supportedEntryTypes --- build.zig.zon | 6 ++--- src/browser/js/Env.zig | 26 +++++++++++++----- src/browser/js/bridge.zig | 31 ++++++++++------------ src/browser/webapi/PerformanceObserver.zig | 5 ++++ 4 files changed, 42 insertions(+), 26 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 682f823cf..6d3b20617 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -5,9 +5,9 @@ .fingerprint = 0xda130f3af836cea0, .dependencies = .{ .v8 = .{ - .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/beb187f3337a8c458e1917dc0105003fb7ae1b2f.tar.gz", - .hash = "v8-0.0.0-xddH6x_gAwAgDtdWGHjv52NsW07MQnfpUQDpZn7RR43Y", + .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/0d19781ccec829640e4f07591cbc166fa7dbe139.tar.gz", + .hash = "v8-0.0.0-xddH6wTgAwALFCYoZbUIqtsRyP6mr69N7aKT_cySHKN2", }, - // .v8 = .{ .path = "../zig-v8-fork" } + //.v8 = .{ .path = "../zig-v8-fork" } }, } diff --git a/src/browser/js/Env.zig b/src/browser/js/Env.zig index 973ef43eb..3dc59ab54 100644 --- a/src/browser/js/Env.zig +++ b/src/browser/js/Env.zig @@ -196,9 +196,15 @@ fn promiseRejectCallback(v8_msg: v8.C_PromiseRejectMessage) callconv(.c) void { const context = Context.fromIsolate(isolate); const value = - if (msg.getValue()) |v8_value| context.valueToString(v8_value, .{}) catch |err| @errorName(err) else "no value"; - - log.debug(.js, "unhandled rejection", .{ .value = value }); + if (msg.getValue()) |v8_value| + context.valueToString(v8_value, .{}) catch |err| @errorName(err) + else "no value" + ; + + log.debug(.js, "unhandled rejection", .{ + .value = value, + .stack = context.stackTrace() catch |err| @errorName(err) orelse "???" + }); } // Give it a Zig struct, get back a v8.FunctionTemplate. @@ -232,8 +238,13 @@ pub fn attachClass(comptime JsApi: type, isolate: v8.Isolate, template: v8.Funct const js_name = v8.String.initUtf8(isolate, name).toName(); const getter_callback = v8.FunctionTemplate.initCallback(isolate, value.getter); if (value.setter == null) { - template_proto.setAccessorGetter(js_name, getter_callback); + if (value.static) { + template.setAccessorGetter(js_name, getter_callback); + } else { + template_proto.setAccessorGetter(js_name, getter_callback); + } } else { + std.debug.assert(value.static == false); const setter_callback = v8.FunctionTemplate.initCallback(isolate, value.setter); template_proto.setAccessorGetterAndSetter(js_name, getter_callback, setter_callback); } @@ -265,8 +276,11 @@ pub fn attachClass(comptime JsApi: type, isolate: v8.Isolate, template: v8.Funct const js_name = v8.Symbol.getIterator(isolate).toName(); template_proto.set(js_name, function_template, v8.PropertyAttribute.None); }, - bridge.Property.Int => { - const js_value = js.simpleZigValueToJs(isolate, value.int, true, false); + bridge.Property => { + const js_value = switch (value) { + .int => |v| js.simpleZigValueToJs(isolate, v, true, false), + }; + const js_name = v8.String.initUtf8(isolate, name).toName(); // apply it both to the type itself template.set(js_name, js_value, v8.PropertyAttribute.ReadOnly + v8.PropertyAttribute.DontDelete); diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index b93c3ec07..63aef20a2 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -57,8 +57,12 @@ pub fn Builder(comptime T: type) type { return Callable.init(T, func, opts); } - pub fn property(value: anytype) Property.GetType(@TypeOf(value)) { - return Property.GetType(@TypeOf(value)).init(value); + pub fn property(value: anytype) Property { + switch (@typeInfo(@TypeOf(value))) { + .comptime_int, .int => return .{.int = value}, + else => {}, + } + @compileError("Property for " ++ @typeName(@TypeOf(value)) ++ " hasn't been defined yet"); } pub fn prototypeChain() [prototypeChainLength(T)]js.PrototypeChainEntry { @@ -146,17 +150,22 @@ pub const Function = struct { }; pub const Accessor = struct { + static: bool = false, getter: ?*const fn (?*const v8.C_FunctionCallbackInfo) callconv(.c) void = null, setter: ?*const fn (?*const v8.C_FunctionCallbackInfo) callconv(.c) void = null, const Opts = struct { + static: bool = false, cache: ?[]const u8 = null, // @ZIGDOM as_typed_array: bool = false, null_as_undefined: bool = false, }; fn init(comptime T: type, comptime getter: anytype, comptime setter: anytype, comptime opts: Opts) Accessor { - var accessor = Accessor{}; + var accessor = Accessor{ + .static = opts.static, + }; + if (@typeInfo(@TypeOf(getter)) != .null) { accessor.getter = struct { fn wrap(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void { @@ -321,20 +330,8 @@ pub const Callable = struct { } }; -pub const Property = struct { - fn GetType(comptime T: type) type { - switch (@typeInfo(T)) { - .comptime_int, .int => return Int, - else => @compileError("Property for " ++ @typeName(T) ++ " hasn't been defined yet"), - } - } - - pub const Int = struct { - int: i64, - pub fn init(value: i64) Int { - return .{ .int = value }; - } - }; +pub const Property = union(enum) { + int: i64, }; // Given a Type, returns the length of the prototype chain, including self diff --git a/src/browser/webapi/PerformanceObserver.zig b/src/browser/webapi/PerformanceObserver.zig index 08bc2733a..68eafe015 100644 --- a/src/browser/webapi/PerformanceObserver.zig +++ b/src/browser/webapi/PerformanceObserver.zig @@ -49,6 +49,10 @@ pub fn takeRecords(_: *const PerformanceObserver) []const Entry { return &.{}; } +pub fn getSupportedEntryTypes(_: *const PerformanceObserver) [][]const u8 { + return &.{}; +} + pub const JsApi = struct { pub const bridge = js.Bridge(PerformanceObserver); @@ -64,4 +68,5 @@ pub const JsApi = struct { pub const observe = bridge.function(PerformanceObserver.observe, .{}); pub const disconnect = bridge.function(PerformanceObserver.disconnect, .{}); pub const takeRecords = bridge.function(PerformanceObserver.takeRecords, .{}); + pub const supportedEntryTypes = bridge.accessor(PerformanceObserver.getSupportedEntryTypes, null, .{.static = true}); }; From 71af78caea6a8cf8665c0ec1c7004201b0ba3b98 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Wed, 26 Nov 2025 07:46:24 +0800 Subject: [PATCH 081/219] adoptNode and importNode --- src/browser/tests/document/adopt_import.html | 217 +++++++++++++++++++ src/browser/webapi/Document.zig | 23 ++ 2 files changed, 240 insertions(+) create mode 100644 src/browser/tests/document/adopt_import.html diff --git a/src/browser/tests/document/adopt_import.html b/src/browser/tests/document/adopt_import.html new file mode 100644 index 000000000..32f640b63 --- /dev/null +++ b/src/browser/tests/document/adopt_import.html @@ -0,0 +1,217 @@ + + +
+

+ Child 1 + Child 2 +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/browser/webapi/Document.zig b/src/browser/webapi/Document.zig index 05223fdec..2643e26c0 100644 --- a/src/browser/webapi/Document.zig +++ b/src/browser/webapi/Document.zig @@ -236,6 +236,26 @@ pub fn getStyleSheets(self: *Document, page: *Page) !*StyleSheetList { return sheets; } +pub fn adoptNode(_: *const Document, node: *Node, page: *Page) !*Node { + if (node._type == .document) { + return error.NotSupported; + } + + if (node._parent) |parent| { + page.removeNode(parent, node, .{ .will_be_reconnected = false }); + } + + return node; +} + +pub fn importNode(_: *const Document, node: *Node, deep_: ?bool, page: *Page) !*Node { + if (node._type == .document) { + return error.NotSupported; + } + + return node.cloneNode(deep_, page); +} + const ReadyState = enum { loading, interactive, @@ -278,6 +298,9 @@ 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 adoptNode = bridge.function(Document.adoptNode, .{ .dom_exception = true }); + pub const importNode = bridge.function(Document.importNode, .{ .dom_exception = true }); + pub const defaultView = bridge.accessor(struct { fn defaultView(_: *const Document, page: *Page) *@import("Window.zig") { return page.window; From 23e3a1d0125fcb004eb965e7984d8d9595d94bff Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Tue, 25 Nov 2025 12:42:43 +0300 Subject: [PATCH 082/219] move `html5ever/` under `vendor/` --- {src => vendor}/html5ever/Cargo.lock | 0 {src => vendor}/html5ever/Cargo.toml | 0 {src => vendor}/html5ever/lib.rs | 0 {src => vendor}/html5ever/sink.rs | 0 {src => vendor}/html5ever/types.rs | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename {src => vendor}/html5ever/Cargo.lock (100%) rename {src => vendor}/html5ever/Cargo.toml (100%) rename {src => vendor}/html5ever/lib.rs (100%) rename {src => vendor}/html5ever/sink.rs (100%) rename {src => vendor}/html5ever/types.rs (100%) diff --git a/src/html5ever/Cargo.lock b/vendor/html5ever/Cargo.lock similarity index 100% rename from src/html5ever/Cargo.lock rename to vendor/html5ever/Cargo.lock diff --git a/src/html5ever/Cargo.toml b/vendor/html5ever/Cargo.toml similarity index 100% rename from src/html5ever/Cargo.toml rename to vendor/html5ever/Cargo.toml diff --git a/src/html5ever/lib.rs b/vendor/html5ever/lib.rs similarity index 100% rename from src/html5ever/lib.rs rename to vendor/html5ever/lib.rs diff --git a/src/html5ever/sink.rs b/vendor/html5ever/sink.rs similarity index 100% rename from src/html5ever/sink.rs rename to vendor/html5ever/sink.rs diff --git a/src/html5ever/types.rs b/vendor/html5ever/types.rs similarity index 100% rename from src/html5ever/types.rs rename to vendor/html5ever/types.rs From 6280232e919540d574ad68cbc0e5278a95c3e2ec Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Tue, 25 Nov 2025 12:43:52 +0300 Subject: [PATCH 083/219] add a build step for `html5ever` in `build.zig` --- .gitignore | 1 - Makefile | 11 ++--------- build.zig | 43 ++++++++++++++++++++++++++++++++++--------- 3 files changed, 36 insertions(+), 19 deletions(-) diff --git a/.gitignore b/.gitignore index 9a7968b9a..9accc0618 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,3 @@ lightpanda.id /v8/ /build/ -src/html5ever/target/ diff --git a/Makefile b/Makefile index 957705e2b..7208b9ee9 100644 --- a/Makefile +++ b/Makefile @@ -127,20 +127,13 @@ build-v8: # Install and build required dependencies commands # ------------ -.PHONY: install-html5ever install-html5ever-dev .PHONY: install install-dev ## Install and build dependencies for release -install: install-submodule install-html5ever +install: install-submodule ## Install and build dependencies for dev -install-dev: install-submodule install-html5ever-dev - -install-html5ever: - cd src/html5ever && cargo build --release --target-dir ../../build/html5ever/ - -install-html5ever-dev: - cd src/html5ever && cargo build --target-dir ../../build/html5ever/ +install-dev: install-submodule data: cd src/data && go run public_suffix_list_gen.go > public_suffix_list.zig diff --git a/build.zig b/build.zig index 704203d2a..b1ef11667 100644 --- a/build.zig +++ b/build.zig @@ -39,6 +39,9 @@ pub fn build(b: *Build) !void { }, } + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + var opts = b.addOptions(); opts.addOption( []const u8, @@ -46,8 +49,30 @@ pub fn build(b: *Build) !void { b.option([]const u8, "git_commit", "Current git commit") orelse "dev", ); - const target = b.standardTargetOptions(.{}); - const optimize = b.standardOptimizeOption(.{}); + // Build step to install html5ever dependency. + const html5ever_argv = blk: { + const argv: []const []const u8 = &.{ + "cargo", + "build", + // Seems cargo can figure out required paths out of Cargo.toml. + "--manifest-path", + "vendor/html5ever/Cargo.toml", + // TODO: We can prefer `--artifact-dir` once it become stable. + "--target-dir", + b.getInstallPath(.prefix, "html5ever"), + // This must be the last argument. + "--release", + }; + + break :blk switch (optimize) { + // Consider these as dev builds. + .Debug, .ReleaseSafe => argv[0 .. argv.len - 1], + .ReleaseFast, .ReleaseSmall => argv, + }; + }; + const html5ever_exec_cargo = b.addSystemCommand(html5ever_argv); + const html5ever_step = b.step("html5ever", "Install html5ever dependency (requires cargo)"); + html5ever_step.dependOn(&html5ever_exec_cargo.step); const enable_tsan = b.option(bool, "tsan", "Enable Thread Sanitizer"); const enable_csan = b.option(std.zig.SanitizeC, "csan", "Enable C Sanitizers"); @@ -65,16 +90,16 @@ pub fn build(b: *Build) !void { 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; }; + const html5ever_obj = switch (optimize) { + .Debug, .ReleaseSafe => b.getInstallPath(.prefix, "html5ever/debug/liblitefetch_html5ever.a"), + .ReleaseFast, .ReleaseSmall => b.getInstallPath(.prefix, "html5ever/release/liblitefetch_html5ever.a"), + }; + + lightpanda_module.addObjectFile(.{ .cwd_relative = html5ever_obj }); + { // browser const exe = b.addExecutable(.{ From 444ae001299f663a5eddee4db92110f98af932b1 Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Wed, 26 Nov 2025 14:27:28 +0300 Subject: [PATCH 084/219] mv `vendor/html5ever` `src/html5ever` --- build.zig | 2 +- {vendor => src}/html5ever/Cargo.lock | 0 {vendor => src}/html5ever/Cargo.toml | 0 {vendor => src}/html5ever/lib.rs | 0 {vendor => src}/html5ever/sink.rs | 0 {vendor => src}/html5ever/types.rs | 0 6 files changed, 1 insertion(+), 1 deletion(-) rename {vendor => src}/html5ever/Cargo.lock (100%) rename {vendor => src}/html5ever/Cargo.toml (100%) rename {vendor => src}/html5ever/lib.rs (100%) rename {vendor => src}/html5ever/sink.rs (100%) rename {vendor => src}/html5ever/types.rs (100%) diff --git a/build.zig b/build.zig index b1ef11667..3632f98db 100644 --- a/build.zig +++ b/build.zig @@ -56,7 +56,7 @@ pub fn build(b: *Build) !void { "build", // Seems cargo can figure out required paths out of Cargo.toml. "--manifest-path", - "vendor/html5ever/Cargo.toml", + "src/html5ever/Cargo.toml", // TODO: We can prefer `--artifact-dir` once it become stable. "--target-dir", b.getInstallPath(.prefix, "html5ever"), diff --git a/vendor/html5ever/Cargo.lock b/src/html5ever/Cargo.lock similarity index 100% rename from vendor/html5ever/Cargo.lock rename to src/html5ever/Cargo.lock diff --git a/vendor/html5ever/Cargo.toml b/src/html5ever/Cargo.toml similarity index 100% rename from vendor/html5ever/Cargo.toml rename to src/html5ever/Cargo.toml diff --git a/vendor/html5ever/lib.rs b/src/html5ever/lib.rs similarity index 100% rename from vendor/html5ever/lib.rs rename to src/html5ever/lib.rs diff --git a/vendor/html5ever/sink.rs b/src/html5ever/sink.rs similarity index 100% rename from vendor/html5ever/sink.rs rename to src/html5ever/sink.rs diff --git a/vendor/html5ever/types.rs b/src/html5ever/types.rs similarity index 100% rename from vendor/html5ever/types.rs rename to src/html5ever/types.rs From d23eacbd373b0e634c8596f68c2b07f8ed7f493e Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Wed, 26 Nov 2025 14:28:18 +0300 Subject: [PATCH 085/219] update `.gitignore` LSPs seem to generate the `target` directory when navigating these files through editor. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 9accc0618..59d6886ca 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ lightpanda.id /v8/ /build/ +/src/html5ever/target/ From 67f63a6bb325dd96bf3fed60a6acf284fb3e7b52 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Wed, 26 Nov 2025 19:43:08 +0800 Subject: [PATCH 086/219] improve parsed (i.e. static) custom element callbacks --- src/browser/Page.zig | 25 ++- src/browser/dump.zig | 54 ++++++- src/browser/js/Function.zig | 2 +- .../tests/custom_elements/connected.html | 1 + .../connected_from_parser.html | 122 ++++++++++++++ src/browser/tests/shadowroot/dump.html | 151 ++++++++++++++++++ .../tests/shadowroot/innerHTML_spec.html | 84 ++++++++++ src/browser/webapi/CustomElementRegistry.zig | 1 - src/browser/webapi/DocumentFragment.zig | 6 +- src/browser/webapi/Element.zig | 12 +- src/browser/webapi/element/html/Custom.zig | 72 ++++++--- 11 files changed, 486 insertions(+), 44 deletions(-) create mode 100644 src/browser/tests/custom_elements/connected_from_parser.html create mode 100644 src/browser/tests/shadowroot/dump.html create mode 100644 src/browser/tests/shadowroot/innerHTML_spec.html diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 805003168..b84e297e7 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -1216,6 +1216,22 @@ pub fn createElement(self: *Page, ns_: ?[]const u8, name: []const u8, attribute_ return node; }; + + // After constructor runs, invoke attributeChangedCallback for initial attributes + const element = node.as(Element); + if (element._attributes) |attributes| { + var it = attributes.iterator(); + while (it.next()) |attr| { + Element.Html.Custom.invokeAttributeChangedCallbackOnElement( + element, + attr._name.str(), + null, // old_value is null for initial attributes + attr._value.str(), + self, + ); + } + } + return node; } @@ -1485,6 +1501,13 @@ pub fn _insertNodeRelative(self: *Page, comptime from_parser: bool, parent: *Nod if (el.getAttributeSafe("id")) |id| { try self.addElementId(parent, el, id); } + + // Invoke connectedCallback for custom elements during parsing + // For main document parsing, we know nodes are connected (fast path) + // For fragment parsing (innerHTML), we need to check connectivity + if (self._parse_mode == .document or child.isConnected()) { + try Element.Html.Custom.invokeConnectedCallbackOnElement(true, el, self); + } } return; } @@ -1518,7 +1541,7 @@ pub fn _insertNodeRelative(self: *Page, comptime from_parser: bool, parent: *Nod } if (should_invoke_connected) { - Element.Html.Custom.invokeConnectedCallbackOnElement(el, self); + try Element.Html.Custom.invokeConnectedCallbackOnElement(false, el, self); } } } diff --git a/src/browser/dump.zig b/src/browser/dump.zig index 0617b4880..73ebe42b9 100644 --- a/src/browser/dump.zig +++ b/src/browser/dump.zig @@ -19,6 +19,7 @@ const std = @import("std"); const Page = @import("Page.zig"); const Node = @import("webapi/Node.zig"); +const Slot = @import("webapi/element/html/Slot.zig"); pub const RootOpts = struct { with_base: bool = false, @@ -27,11 +28,24 @@ pub const RootOpts = struct { pub const Opts = struct { strip: Strip = .{}, + shadow: Shadow = .rendered, + pub const Strip = struct { js: bool = false, ui: bool = false, css: bool = false, }; + + pub const Shadow = enum { + // Skip shadow DOM entirely (innerHTML/outerHTML) + skip, + + // Dump everyhting (like "view source") + complete, + + // Resolve slot elements (like what actually gets rendered) + rendered, + }; }; pub fn root(opts: RootOpts, writer: *std.Io.Writer, page: *Page) !void { @@ -45,10 +59,10 @@ pub fn root(opts: RootOpts, writer: *std.Io.Writer, page: *Page) !void { } } - return deep(doc.asNode(), .{ .strip = opts.strip }, writer); + return deep(doc.asNode(), .{ .strip = opts.strip }, writer, page); } -pub fn deep(node: *Node, opts: Opts, writer: *std.Io.Writer) error{WriteFailed}!void { +pub fn deep(node: *Node, opts: Opts, writer: *std.Io.Writer, page: *Page) error{WriteFailed}!void { switch (node._type) { .cdata => |cd| try writer.writeAll(cd.getData()), .element => |el| { @@ -56,25 +70,39 @@ pub fn deep(node: *Node, opts: Opts, writer: *std.Io.Writer) error{WriteFailed}! return; } + // Handle elements in rendered mode + if (opts.shadow == .rendered) { + if (el.is(Slot)) |slot| { + return dumpSlotContent(slot, opts, writer, page); + } + } + try el.format(writer); - try children(node, opts, writer); + + if (opts.shadow != .skip) { + if (page._element_shadow_roots.get(el)) |shadow| { + try children(shadow.asNode(), opts, writer, page); + } + } + + try children(node, opts, writer, page); if (!isVoidElement(el)) { try writer.writeAll("'); } }, - .document => try children(node, opts, writer), + .document => try children(node, opts, writer, page), .document_type => {}, - .document_fragment => try children(node, opts, writer), + .document_fragment => try children(node, opts, writer, page), .attribute => unreachable, } } -pub fn children(parent: *Node, opts: Opts, writer: *std.Io.Writer) !void { +pub fn children(parent: *Node, opts: Opts, writer: *std.Io.Writer, page: *Page) !void { var it = parent.childrenIterator(); while (it.next()) |child| { - try deep(child, opts, writer); + try deep(child, opts, writer, page); } } @@ -118,6 +146,18 @@ pub fn toJSON(node: *Node, writer: *std.json.Stringify) !void { try writer.endObject(); } +fn dumpSlotContent(slot: *Slot, opts: Opts, writer: *std.Io.Writer, page: *Page) !void { + const assigned = slot.assignedNodes(null, page) catch return; + + if (assigned.len > 0) { + for (assigned) |assigned_node| { + try deep(assigned_node, opts, writer, page); + } + } else { + try children(slot.asNode(), opts, writer, page); + } +} + fn isVoidElement(el: *const Node.Element) bool { return switch (el._type) { .html => |html| switch (html._type) { diff --git a/src/browser/js/Function.zig b/src/browser/js/Function.zig index 41d8fa2ca..4ab5be8a5 100644 --- a/src/browser/js/Function.zig +++ b/src/browser/js/Function.zig @@ -144,7 +144,7 @@ pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args const result = self.func.castToFunction().call(context.v8_context, js_this, js_args); if (result == null) { - std.debug.print("CB ERR: {s}\n", .{self.src() catch "???"}); + // std.debug.print("CB ERR: {s}\n", .{self.src() catch "???"}); return error.JSExecCallback; } diff --git a/src/browser/tests/custom_elements/connected.html b/src/browser/tests/custom_elements/connected.html index c126fab63..4b0abff9c 100644 --- a/src/browser/tests/custom_elements/connected.html +++ b/src/browser/tests/custom_elements/connected.html @@ -91,3 +91,4 @@ testing.expectEqual(1, connectedCount); } + diff --git a/src/browser/tests/custom_elements/connected_from_parser.html b/src/browser/tests/custom_elements/connected_from_parser.html new file mode 100644 index 000000000..770c309b5 --- /dev/null +++ b/src/browser/tests/custom_elements/connected_from_parser.html @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/browser/tests/shadowroot/dump.html b/src/browser/tests/shadowroot/dump.html new file mode 100644 index 000000000..57544393c --- /dev/null +++ b/src/browser/tests/shadowroot/dump.html @@ -0,0 +1,151 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/browser/tests/shadowroot/innerHTML_spec.html b/src/browser/tests/shadowroot/innerHTML_spec.html new file mode 100644 index 000000000..029f0e7af --- /dev/null +++ b/src/browser/tests/shadowroot/innerHTML_spec.html @@ -0,0 +1,84 @@ + + + + + + + + + diff --git a/src/browser/webapi/CustomElementRegistry.zig b/src/browser/webapi/CustomElementRegistry.zig index 361aced55..9c2951701 100644 --- a/src/browser/webapi/CustomElementRegistry.zig +++ b/src/browser/webapi/CustomElementRegistry.zig @@ -84,7 +84,6 @@ pub fn define(self: *CustomElementRegistry, name: []const u8, constructor: js.Fu var idx: usize = 0; while (idx < page._undefined_custom_elements.items.len) { const custom = page._undefined_custom_elements.items[idx]; - if (!custom._tag_name.eqlSlice(name)) { idx += 1; continue; diff --git a/src/browser/webapi/DocumentFragment.zig b/src/browser/webapi/DocumentFragment.zig index f7b20878c..6c712f556 100644 --- a/src/browser/webapi/DocumentFragment.zig +++ b/src/browser/webapi/DocumentFragment.zig @@ -160,9 +160,9 @@ pub fn replaceChildren(self: *DocumentFragment, nodes: []const Node.NodeOrText, } } -pub fn getInnerHTML(self: *DocumentFragment, writer: *std.Io.Writer) !void { +pub fn getInnerHTML(self: *DocumentFragment, writer: *std.Io.Writer, page: *Page) !void { const dump = @import("../dump.zig"); - return dump.children(self.asNode(), .{}, writer); + return dump.children(self.asNode(), .{ .shadow = .complete }, writer, page); } pub fn setInnerHTML(self: *DocumentFragment, html: []const u8, page: *Page) !void { @@ -224,7 +224,7 @@ pub const JsApi = struct { fn _innerHTML(self: *DocumentFragment, page: *Page) ![]const u8 { var buf = std.Io.Writer.Allocating.init(page.call_arena); - try self.getInnerHTML(&buf.writer); + try self.getInnerHTML(&buf.writer, page); return buf.written(); } }; diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index fb9b927e9..f73e5374e 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -227,14 +227,14 @@ pub fn getInnerText(self: *Element, writer: *std.Io.Writer) !void { } } -pub fn getOuterHTML(self: *Element, writer: *std.Io.Writer) !void { +pub fn getOuterHTML(self: *Element, writer: *std.Io.Writer, page: *Page) !void { const dump = @import("../dump.zig"); - return dump.deep(self.asNode(), .{}, writer); + return dump.deep(self.asNode(), .{ .shadow = .skip }, writer, page); } -pub fn getInnerHTML(self: *Element, writer: *std.Io.Writer) !void { +pub fn getInnerHTML(self: *Element, writer: *std.Io.Writer, page: *Page) !void { const dump = @import("../dump.zig"); - return dump.children(self.asNode(), .{}, writer); + return dump.children(self.asNode(), .{ .shadow = .skip }, writer, page); } pub fn setInnerHTML(self: *Element, html: []const u8, page: *Page) !void { @@ -906,14 +906,14 @@ pub const JsApi = struct { pub const outerHTML = bridge.accessor(_outerHTML, null, .{}); fn _outerHTML(self: *Element, page: *Page) ![]const u8 { var buf = std.Io.Writer.Allocating.init(page.call_arena); - try self.getOuterHTML(&buf.writer); + try self.getOuterHTML(&buf.writer, page); return buf.written(); } pub const innerHTML = bridge.accessor(_innerHTML, Element.setInnerHTML, .{}); fn _innerHTML(self: *Element, page: *Page) ![]const u8 { var buf = std.Io.Writer.Allocating.init(page.call_arena); - try self.getInnerHTML(&buf.writer); + try self.getInnerHTML(&buf.writer, page); return buf.written(); } diff --git a/src/browser/webapi/element/html/Custom.zig b/src/browser/webapi/element/html/Custom.zig index 50e8518eb..a8c95d5c8 100644 --- a/src/browser/webapi/element/html/Custom.zig +++ b/src/browser/webapi/element/html/Custom.zig @@ -53,7 +53,9 @@ pub fn invokeConnectedCallback(self: *Custom, page: *Page) void { pub fn invokeDisconnectedCallback(self: *Custom, page: *Page) void { // Only invoke if we haven't already called it while disconnected - if (self._disconnected_callback_invoked) return; + if (self._disconnected_callback_invoked) { + return; + } self._disconnected_callback_invoked = true; self._connected_callback_invoked = false; @@ -62,30 +64,49 @@ pub fn invokeDisconnectedCallback(self: *Custom, page: *Page) void { pub fn invokeAttributeChangedCallback(self: *Custom, name: []const u8, old_value: ?[]const u8, new_value: ?[]const u8, page: *Page) void { const definition = self._definition orelse return; - if (!definition.isAttributeObserved(name)) return; + if (!definition.isAttributeObserved(name)) { + return; + } self.invokeCallback("attributeChangedCallback", .{ name, old_value, new_value }, page); } -// Static helpers that work on any Element (autonomous or customized built-in) -pub fn invokeConnectedCallbackOnElement(element: *Element, page: *Page) void { +pub fn invokeConnectedCallbackOnElement(comptime from_parser: bool, element: *Element, page: *Page) !void { // Autonomous custom element if (element.is(Custom)) |custom| { - custom.invokeConnectedCallback(page); + if (comptime from_parser) { + // From parser, we know the element is brand new + custom._connected_callback_invoked = true; + custom.invokeCallback("connectedCallback", .{}, page); + } else { + custom.invokeConnectedCallback(page); + } return; } - // Customized built-in element - // Check if we've already invoked connectedCallback while connected - if (page._customized_builtin_connected_callback_invoked.contains(element)) return; + // Customized built-in element - check if it actually has a definition first + const definition = page.getCustomizedBuiltInDefinition(element) orelse return; - page._customized_builtin_connected_callback_invoked.put( - page.arena, - element, - {}, - ) catch return; - _ = page._customized_builtin_disconnected_callback_invoked.remove(element); + if (comptime from_parser) { + // From parser, we know the element is brand new, skip the tracking check + try page._customized_builtin_connected_callback_invoked.put( + page.arena, + element, + {}, + ); + } else { + // Not from parser, check if we've already invoked while connected + const gop = try page._customized_builtin_connected_callback_invoked.getOrPut( + page.arena, + element, + ); + if (gop.found_existing) { + return; + } + gop.value_ptr.* = {}; + } - invokeCallbackOnElement(element, "connectedCallback", .{}, page); + _ = page._customized_builtin_disconnected_callback_invoked.remove(element); + invokeCallbackOnElement(element, definition, "connectedCallback", .{}, page); } pub fn invokeDisconnectedCallbackOnElement(element: *Element, page: *Page) void { @@ -95,18 +116,20 @@ pub fn invokeDisconnectedCallbackOnElement(element: *Element, page: *Page) void return; } - // Customized built-in element - // Check if we've already invoked disconnectedCallback while disconnected - if (page._customized_builtin_disconnected_callback_invoked.contains(element)) return; + // Customized built-in element - check if it actually has a definition first + const definition = page.getCustomizedBuiltInDefinition(element) orelse return; - page._customized_builtin_disconnected_callback_invoked.put( + // Check if we've already invoked disconnectedCallback while disconnected + const gop = page._customized_builtin_disconnected_callback_invoked.getOrPut( page.arena, element, - {}, ) catch return; + if (gop.found_existing) return; + gop.value_ptr.* = {}; + _ = page._customized_builtin_connected_callback_invoked.remove(element); - invokeCallbackOnElement(element, "disconnectedCallback", .{}, page); + invokeCallbackOnElement(element, definition, "disconnectedCallback", .{}, page); } pub fn invokeAttributeChangedCallbackOnElement(element: *Element, name: []const u8, old_value: ?[]const u8, new_value: ?[]const u8, page: *Page) void { @@ -119,12 +142,11 @@ pub fn invokeAttributeChangedCallbackOnElement(element: *Element, name: []const // Customized built-in element - check if attribute is observed const definition = page.getCustomizedBuiltInDefinition(element) orelse return; if (!definition.isAttributeObserved(name)) return; - invokeCallbackOnElement(element, "attributeChangedCallback", .{ name, old_value, new_value }, page); + invokeCallbackOnElement(element, definition, "attributeChangedCallback", .{ name, old_value, new_value }, page); } -fn invokeCallbackOnElement(element: *Element, comptime callback_name: [:0]const u8, args: anytype, page: *Page) void { - // Check if this element has a customized built-in definition - _ = page.getCustomizedBuiltInDefinition(element) orelse return; +fn invokeCallbackOnElement(element: *Element, definition: *CustomElementDefinition, comptime callback_name: [:0]const u8, args: anytype, page: *Page) void { + _ = definition; const context = page.js; From 63f489d39fb033ff4406eea4e1741ee1ae74a329 Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Wed, 26 Nov 2025 09:13:58 -0800 Subject: [PATCH 087/219] initial with full chain allocations --- src/browser/Factory.zig | 356 ++++++++++-------- src/browser/Page.zig | 32 +- src/browser/webapi/element/Html.zig | 44 +-- .../webapi/net/XMLHttpRequestEventTarget.zig | 2 +- 4 files changed, 230 insertions(+), 204 deletions(-) diff --git a/src/browser/Factory.zig b/src/browser/Factory.zig index 336924b60..26b30052a 100644 --- a/src/browser/Factory.zig +++ b/src/browser/Factory.zig @@ -17,6 +17,7 @@ // along with this program. If not, see . const std = @import("std"); +const assert = std.debug.assert; const builtin = @import("builtin"); const reflect = @import("reflect.zig"); const IS_DEBUG = builtin.mode == .Debug; @@ -35,21 +36,113 @@ const EventTarget = @import("webapi/EventTarget.zig"); const XMLHttpRequestEventTarget = @import("webapi/net/XMLHttpRequestEventTarget.zig"); const Blob = @import("webapi/Blob.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, _slab: SlabAllocator, +fn PrototypeChain(comptime types: []const type) type { + return struct { + const Self = @This(); + memory: []u8, + + fn totalSize() usize { + var size: usize = 0; + for (types) |T| { + size = std.mem.alignForward(usize, size, @alignOf(T)); + size += @sizeOf(T); + } + return size; + } + + fn maxAlign() std.mem.Alignment { + var alignment: std.mem.Alignment = .@"1"; + + for (types) |T| { + alignment = std.mem.Alignment.max(alignment, std.mem.Alignment.of(T)); + } + + return alignment; + } + + fn getType(comptime index: usize) type { + return types[index]; + } + + fn allocate(allocator: std.mem.Allocator) !Self { + const size = comptime Self.totalSize(); + const alignment = comptime Self.maxAlign(); + + const memory = try allocator.alignedAlloc(u8, alignment, size); + return .{ .memory = memory }; + } + + fn get(self: *const Self, comptime index: usize) *getType(index) { + var offset: usize = 0; + inline for (types, 0..) |T, i| { + offset = std.mem.alignForward(usize, offset, @alignOf(T)); + + if (i == index) { + return @as(*T, @ptrCast(@alignCast(self.memory.ptr + offset))); + } + offset += @sizeOf(T); + } + unreachable; + } + + fn set(self: *const Self, comptime index: usize, value: getType(index)) void { + const ptr = self.get(index); + ptr.* = value; + } + + fn setRoot(self: *const Self, comptime T: type) void { + const ptr = self.get(0); + ptr.* = .{ ._type = unionInit(T, self.get(1)) }; + } + + fn setMiddle(self: *const Self, comptime index: usize, comptime T: type) void { + assert(index >= 1); + assert(index < types.len); + + const ptr = self.get(index); + ptr.* = .{ ._proto = self.get(index - 1), ._type = unionInit(T, self.get(index + 1)) }; + } + + fn setMiddleWithValue(self: *const Self, comptime index: usize, comptime T: type, value: anytype) void { + assert(index >= 1); + + const ptr = self.get(index); + ptr.* = .{ ._proto = self.get(index - 1), ._type = unionInit(T, value) }; + } + + fn setLeaf(self: *const Self, comptime index: usize, value: anytype) void { + assert(index >= 1); + + const ptr = self.get(index); + ptr.* = value; + ptr._proto = self.get(index - 1); + } + }; +} + +fn AutoPrototypeChain(comptime types: []const type) type { + return struct { + fn create(allocator: std.mem.Allocator, leaf_value: anytype) !*@TypeOf(leaf_value) { + const chain = try PrototypeChain(types).allocate(allocator); + + const RootType = types[0]; + chain.setRoot(RootType.Type); + + inline for (1..types.len - 1) |i| { + const MiddleType = types[i]; + chain.setMiddle(i, MiddleType.Type); + } + + chain.setLeaf(types.len - 1, leaf_value); + return chain.get(types.len - 1); + } + }; +} + pub fn init(page: *Page) Factory { return .{ ._page = page, @@ -59,165 +152,127 @@ pub fn init(page: *Page) Factory { // 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 allocator = self._slab.allocator(); + const chain = try PrototypeChain( + &.{ EventTarget, @TypeOf(child) }, + ).allocate(allocator); - const et = try self.createT(EventTarget); - child_ptr._proto = et; - et.* = .{ ._type = unionInit(EventTarget.Type, child_ptr) }; - return child_ptr; + chain.setRoot(EventTarget.Type); + chain.setLeaf(1, child); + + return chain.get(1); } 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; + const allocator = self._slab.allocator(); + return try AutoPrototypeChain( + &.{ EventTarget, Node, @TypeOf(child) }, + ).create(allocator, child); } 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; + const allocator = self._slab.allocator(); + return try AutoPrototypeChain( + &.{ EventTarget, Node, Document, @TypeOf(child) }, + ).create(allocator, child); } pub fn documentFragment(self: *Factory, child: anytype) !*@TypeOf(child) { - const child_ptr = try self.createT(@TypeOf(child)); - child_ptr.* = child; - child_ptr._proto = try self.node(Node.DocumentFragment{ - ._proto = undefined, - ._type = unionInit(Node.DocumentFragment.Type, child_ptr), - }); - return child_ptr; + const allocator = self._slab.allocator(); + return try AutoPrototypeChain( + &.{ EventTarget, Node, Node.DocumentFragment, @TypeOf(child) }, + ).create(allocator, child); } 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; + const allocator = self._slab.allocator(); + return try AutoPrototypeChain( + &.{ EventTarget, Node, Element, @TypeOf(child) }, + ).create(allocator, child); } 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; + const allocator = self._slab.allocator(); + return try AutoPrototypeChain( + &.{ EventTarget, Node, Element, Element.Html, @TypeOf(child) }, + ).create(allocator, child); } pub fn svgElement(self: *Factory, tag_name: []const u8, child: anytype) !*@TypeOf(child) { - if (@TypeOf(child) == Element.Svg) { - return self.element(child); - } + const allocator = self._slab.allocator(); // will never allocate, can't fail const tag_name_str = String.init(self._page.arena, 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; - } + const chain = try PrototypeChain( + &.{ EventTarget, Node, Element, Element.Svg, @TypeOf(child) }, + ).allocate(allocator); + + chain.setRoot(EventTarget.Type); + chain.setMiddle(1, Node.Type); + chain.setMiddle(2, Element.Type); - // 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, + // Manually set Element.Svg with the tag_name + chain.set(3, .{ + ._proto = chain.get(2), ._tag_name = tag_name_str, - ._type = unionInit(Element.Svg.Type, child), + ._type = unionInit(Element.Svg.Type, chain.get(4)), }); - 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; + + chain.setLeaf(4, child); + return chain.get(4); } // 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 allocator = self._slab.allocator(); + + // Special case: Event has a _type_string field, so we need manual setup + const chain = try PrototypeChain( + &.{ Event, @TypeOf(child) }, + ).allocate(allocator); - const e = try self.createT(Event); - child_ptr._proto = e; - e.* = .{ - ._type = unionInit(Event.Type, child_ptr), + const event_ptr = chain.get(0); + event_ptr.* = .{ + ._type = unionInit(Event.Type, chain.get(1)), ._type_string = try String.init(self._page.arena, typ, .{}), }; - return child_ptr; + chain.setLeaf(1, child); + + return chain.get(1); } 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; + const allocator = self._slab.allocator(); + + return try AutoPrototypeChain( + &.{ EventTarget, XMLHttpRequestEventTarget, @TypeOf(child) }, + ).create(allocator, child); } pub fn blob(self: *Factory, child: anytype) !*@TypeOf(child) { - const child_ptr = try self.createT(@TypeOf(child)); - child_ptr.* = child; + const allocator = self._slab.allocator(); + + // Special case: Blob has slice and mime fields, so we need manual setup + const chain = try PrototypeChain( + &.{ Blob, @TypeOf(child) }, + ).allocate(allocator); - const b = try self.createT(Blob); - child_ptr._proto = b; - b.* = .{ - ._type = unionInit(Blob.Type, child_ptr), + const blob_ptr = chain.get(0); + blob_ptr.* = .{ + ._type = unionInit(Blob.Type, chain.get(1)), .slice = "", .mime = "", }; - return child_ptr; -} - -pub fn create(self: *Factory, value: anytype) !*@TypeOf(value) { - const ptr = try self.createT(@TypeOf(value)); - ptr.* = value; - return ptr; -} + chain.setLeaf(1, child); -pub fn createT(self: *Factory, comptime T: type) !*T { - const allocator = self._slab.allocator(); - return try allocator.create(T); + return chain.get(1); } pub fn destroy(self: *Factory, value: anytype) void { const S = reflect.Struct(@TypeOf(value)); + // const allocator = self._slab.allocator(); + if (comptime IS_DEBUG) { // We should always destroy from the leaf down. if (@hasField(S, "_type") and @typeInfo(@TypeOf(value._type)) == .@"union") { @@ -231,12 +286,13 @@ pub fn destroy(self: *Factory, value: anytype) void { } } - self.destroyChain(value, true); + const root_ptr = self.destroyChain(value, true); + _ = root_ptr; + // allocator.destroy(root_ptr); } -fn destroyChain(self: *Factory, value: anytype, comptime first: bool) void { +fn destroyChain(self: *Factory, value: anytype, comptime first: bool) *@TypeOf(value) { const S = reflect.Struct(@TypeOf(value)); - const allocator = self._slab.allocator(); // This is initially called from a deinit. We don't want to call that @@ -255,7 +311,7 @@ fn destroyChain(self: *Factory, value: anytype, comptime first: bool) void { } if (@hasField(S, "_proto")) { - self.destroyChain(value._proto, false); + return 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| { @@ -263,36 +319,18 @@ fn destroyChain(self: *Factory, value: anytype, comptime first: bool) void { } } - // 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)) { - allocator.destroy(value); - } + return @ptrCast(value); } -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); +pub fn createT(self: *Factory, comptime T: type) !*T { + const allocator = self._slab.allocator(); + return try allocator.create(T); +} - // 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; +pub fn create(self: *Factory, value: anytype) !*@TypeOf(value) { + const ptr = try self.createT(@TypeOf(value)); + ptr.* = value; + return ptr; } fn unionInit(comptime T: type, value: anytype) T { @@ -316,15 +354,3 @@ fn unionFieldName(comptime T: type, comptime V: type) []const u8 { } @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/Page.zig b/src/browser/Page.zig index 6b1198433..a11b736a7 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -1175,21 +1175,22 @@ pub fn createElement(self: *Page, ns_: ?[]const u8, name: []const u8, attribute_ else => {}, } - if (namespace == .svg) { - const tag_name = try String.init(self.arena, name, .{}); - if (std.ascii.eqlIgnoreCase(name, "svg")) { - return self.createSvgElementT(Element.Svg, name, attribute_iterator, .{ - ._proto = undefined, - ._type = .svg, - ._tag_name = tag_name, - }); - } - - // Other SVG elements (rect, circle, text, g, etc.) - const lower = std.ascii.lowerString(&self.buf, name); - const tag = std.meta.stringToEnum(Element.Tag, lower) orelse .unknown; - return self.createSvgElementT(Element.Svg.Generic, name, attribute_iterator, .{ ._proto = undefined, ._tag = tag }); - } + // TODO: uncomment + // if (namespace == .svg) { + // const tag_name = try String.init(self.arena, name, .{}); + // if (std.ascii.eqlIgnoreCase(name, "svg")) { + // return self.createSvgElementT(Element.Svg, name, attribute_iterator, .{ + // ._proto = undefined, + // ._type = .svg, + // ._tag_name = tag_name, + // }); + // } + + // // Other SVG elements (rect, circle, text, g, etc.) + // const lower = std.ascii.lowerString(&self.buf, name); + // const tag = std.meta.stringToEnum(Element.Tag, lower) orelse .unknown; + // return self.createSvgElementT(Element.Svg.Generic, name, attribute_iterator, .{ ._proto = undefined, ._tag = tag }); + // } const tag_name = try String.init(self.arena, name, .{}); @@ -1221,7 +1222,6 @@ pub fn createElement(self: *Page, ns_: ?[]const u8, name: []const u8, attribute_ return node; }; - // After constructor runs, invoke attributeChangedCallback for initial attributes const element = node.as(Element); if (element._attributes) |attributes| { diff --git a/src/browser/webapi/element/Html.zig b/src/browser/webapi/element/Html.zig index 4468b553a..e6d748c8f 100644 --- a/src/browser/webapi/element/Html.zig +++ b/src/browser/webapi/element/Html.zig @@ -67,36 +67,36 @@ pub fn construct(page: *Page) !*Element { } pub const Type = union(enum) { - anchor: Anchor, - body: Body, - br: BR, - button: Button, + anchor: *Anchor, + body: *Body, + br: *BR, + button: *Button, custom: *Custom, - dialog: Dialog, - div: Div, - form: Form, + dialog: *Dialog, + div: *Div, + form: *Form, generic: *Generic, heading: *Heading, - head: Head, - html: Html, - hr: HR, - img: Image, - iframe: IFrame, + head: *Head, + html: *Html, + hr: *HR, + img: *Image, + iframe: *IFrame, input: *Input, - li: LI, - link: Link, - meta: Meta, - ol: OL, + li: *LI, + link: *Link, + meta: *Meta, + ol: *OL, option: *Option, - p: Paragraph, + p: *Paragraph, script: *Script, - select: Select, - slot: Slot, - style: Style, + select: *Select, + slot: *Slot, + style: *Style, template: *Template, text_area: *TextArea, - title: Title, - ul: UL, + title: *Title, + ul: *UL, unknown: *Unknown, }; diff --git a/src/browser/webapi/net/XMLHttpRequestEventTarget.zig b/src/browser/webapi/net/XMLHttpRequestEventTarget.zig index c5568a9ae..4bc16b236 100644 --- a/src/browser/webapi/net/XMLHttpRequestEventTarget.zig +++ b/src/browser/webapi/net/XMLHttpRequestEventTarget.zig @@ -35,7 +35,7 @@ _on_progress: ?js.Function = null, _on_timeout: ?js.Function = null, pub const Type = union(enum) { - request: @import("XMLHttpRequest.zig"), + request: *@import("XMLHttpRequest.zig"), // TODO: xml_http_request_upload }; From 8348f2dcc84ffda4c544418fd1ea23acfeb65529 Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Wed, 26 Nov 2025 09:45:56 -0800 Subject: [PATCH 088/219] fix slot alignment in slab chunks --- src/slab.zig | 47 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/src/slab.zig b/src/slab.zig index 02d10aa72..0e574fef0 100644 --- a/src/slab.zig +++ b/src/slab.zig @@ -376,23 +376,25 @@ pub const SlabAllocator = struct { const self: *Self = @ptrCast(@alignCast(ctx)); _ = ret_addr; + const aligned_len = std.mem.alignForward(usize, len, alignment.toByteUnits()); + const list_gop = self.slabs.getOrPut( self.child_allocator, - SlabKey{ .size = len, .alignment = alignment }, + SlabKey{ .size = aligned_len, .alignment = alignment }, ) catch return null; if (!list_gop.found_existing) { list_gop.value_ptr.* = Slab.init( self.child_allocator, alignment, - len, + aligned_len, self.max_slot_count, ) catch return null; } const list = list_gop.value_ptr; const buf = list.alloc(self.child_allocator) catch return null; - return buf.ptr; + return buf[0..len].ptr; } fn free(ctx: *anyopaque, memory: []u8, alignment: Alignment, ret_addr: usize) void { @@ -401,8 +403,9 @@ pub const SlabAllocator = struct { const ptr = memory.ptr; const len = memory.len; + const aligned_len = std.mem.alignForward(usize, len, alignment.toByteUnits()); - const list = self.slabs.getPtr(.{ .size = len, .alignment = alignment }).?; + const list = self.slabs.getPtr(.{ .size = aligned_len, .alignment = alignment }).?; list.free(ptr); } }; @@ -822,3 +825,39 @@ test "slab allocator - different size classes don't interfere" { allocator.free(ptr_128); allocator.free(ptr_64_again); } + +test "slab allocator - 16-byte alignment" { + var slab_alloc = TestSlabAllocator.init(testing.allocator, 16); + defer slab_alloc.deinit(); + + const allocator = slab_alloc.allocator(); + + // Request 16-byte aligned memory + const ptr = try allocator.alignedAlloc(u8, .@"16", 152); + defer allocator.free(ptr); + + // Verify alignment + const addr = @intFromPtr(ptr.ptr); + try testing.expect(addr % 16 == 0); + + // Make sure we can use it + @memset(ptr, 0xFF); +} + +test "slab allocator - various alignments" { + var slab_alloc = TestSlabAllocator.init(testing.allocator, 16); + defer slab_alloc.deinit(); + + const allocator = slab_alloc.allocator(); + + const alignments = [_]std.mem.Alignment{ .@"1", .@"2", .@"4", .@"8", .@"16" }; + + inline for (alignments) |alignment| { + const ptr = try allocator.alignedAlloc(u8, alignment, 100); + defer allocator.free(ptr); + + const addr = @intFromPtr(ptr.ptr); + const align_value = alignment.toByteUnits(); + try testing.expect(addr % align_value == 0); + } +} From afe9ee5367a47e62b1f0e2a97a009bf50de89327 Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Wed, 26 Nov 2025 10:07:40 -0800 Subject: [PATCH 089/219] fix freeing with new combined chains --- src/browser/Factory.zig | 105 ++++++++++++++++------------- src/browser/Page.zig | 4 +- src/browser/webapi/Blob.zig | 6 ++ src/browser/webapi/Event.zig | 4 ++ src/browser/webapi/EventTarget.zig | 2 + 5 files changed, 72 insertions(+), 49 deletions(-) diff --git a/src/browser/Factory.zig b/src/browser/Factory.zig index 26b30052a..4a7333f25 100644 --- a/src/browser/Factory.zig +++ b/src/browser/Factory.zig @@ -40,6 +40,13 @@ const Factory = @This(); _page: *Page, _slab: SlabAllocator, +pub const FactoryAllocationKind = union(enum) { + /// Allocated as part of a Factory PrototypeChain + chain: []u8, + /// Allocated standalone via factory.create() + standalone, +}; + fn PrototypeChain(comptime types: []const type) type { return struct { const Self = @This(); @@ -96,7 +103,7 @@ fn PrototypeChain(comptime types: []const type) type { fn setRoot(self: *const Self, comptime T: type) void { const ptr = self.get(0); - ptr.* = .{ ._type = unionInit(T, self.get(1)) }; + ptr.* = .{ ._type = unionInit(T, self.get(1)), ._allocation = FactoryAllocationKind{ .chain = self.memory } }; } fn setMiddle(self: *const Self, comptime index: usize, comptime T: type) void { @@ -163,6 +170,46 @@ pub fn eventTarget(self: *Factory, child: anytype) !*@TypeOf(child) { return chain.get(1); } +// this is a root object +pub fn event(self: *Factory, typ: []const u8, child: anytype) !*@TypeOf(child) { + const allocator = self._slab.allocator(); + + // Special case: Event has a _type_string field, so we need manual setup + const chain = try PrototypeChain( + &.{ Event, @TypeOf(child) }, + ).allocate(allocator); + + const event_ptr = chain.get(0); + event_ptr.* = .{ + ._type = unionInit(Event.Type, chain.get(1)), + ._type_string = try String.init(self._page.arena, typ, .{}), + ._allocation = FactoryAllocationKind{ .chain = chain.memory }, + }; + chain.setLeaf(1, child); + + return chain.get(1); +} + +pub fn blob(self: *Factory, child: anytype) !*@TypeOf(child) { + const allocator = self._slab.allocator(); + + // Special case: Blob has slice and mime fields, so we need manual setup + const chain = try PrototypeChain( + &.{ Blob, @TypeOf(child) }, + ).allocate(allocator); + + const blob_ptr = chain.get(0); + blob_ptr.* = .{ + ._type = unionInit(Blob.Type, chain.get(1)), + ._allocation = FactoryAllocationKind{ .chain = chain.memory }, + .slice = "", + .mime = "", + }; + chain.setLeaf(1, child); + + return chain.get(1); +} + pub fn node(self: *Factory, child: anytype) !*@TypeOf(child) { const allocator = self._slab.allocator(); return try AutoPrototypeChain( @@ -223,25 +270,6 @@ pub fn svgElement(self: *Factory, tag_name: []const u8, child: anytype) !*@TypeO return chain.get(4); } -// this is a root object -pub fn event(self: *Factory, typ: []const u8, child: anytype) !*@TypeOf(child) { - const allocator = self._slab.allocator(); - - // Special case: Event has a _type_string field, so we need manual setup - const chain = try PrototypeChain( - &.{ Event, @TypeOf(child) }, - ).allocate(allocator); - - const event_ptr = chain.get(0); - event_ptr.* = .{ - ._type = unionInit(Event.Type, chain.get(1)), - ._type_string = try String.init(self._page.arena, typ, .{}), - }; - chain.setLeaf(1, child); - - return chain.get(1); -} - pub fn xhrEventTarget(self: *Factory, child: anytype) !*@TypeOf(child) { const allocator = self._slab.allocator(); @@ -250,28 +278,9 @@ pub fn xhrEventTarget(self: *Factory, child: anytype) !*@TypeOf(child) { ).create(allocator, child); } -pub fn blob(self: *Factory, child: anytype) !*@TypeOf(child) { - const allocator = self._slab.allocator(); - - // Special case: Blob has slice and mime fields, so we need manual setup - const chain = try PrototypeChain( - &.{ Blob, @TypeOf(child) }, - ).allocate(allocator); - - const blob_ptr = chain.get(0); - blob_ptr.* = .{ - ._type = unionInit(Blob.Type, chain.get(1)), - .slice = "", - .mime = "", - }; - chain.setLeaf(1, child); - - return chain.get(1); -} - pub fn destroy(self: *Factory, value: anytype) void { const S = reflect.Struct(@TypeOf(value)); - // const allocator = self._slab.allocator(); + const allocator = self._slab.allocator(); if (comptime IS_DEBUG) { // We should always destroy from the leaf down. @@ -286,12 +295,14 @@ pub fn destroy(self: *Factory, value: anytype) void { } } - const root_ptr = self.destroyChain(value, true); - _ = root_ptr; - // allocator.destroy(root_ptr); + const allocation_kind = self.destroyChain(value, true) orelse return; + switch (allocation_kind) { + .chain => |buf| allocator.free(buf), + .standalone => {}, + } } -fn destroyChain(self: *Factory, value: anytype, comptime first: bool) *@TypeOf(value) { +fn destroyChain(self: *Factory, value: anytype, comptime first: bool) ?FactoryAllocationKind { const S = reflect.Struct(@TypeOf(value)); const allocator = self._slab.allocator(); @@ -317,9 +328,9 @@ fn destroyChain(self: *Factory, value: anytype, comptime first: bool) *@TypeOf(v if (self._page.js.removeTaggedMapping(@intFromPtr(value))) |tagged| { allocator.destroy(tagged); } - } - - return @ptrCast(value); + } else if (@hasField(S, "_allocation")) { + return value._allocation; + } else return null; } pub fn createT(self: *Factory, comptime T: type) !*T { diff --git a/src/browser/Page.zig b/src/browser/Page.zig index a11b736a7..37bb6d1aa 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -176,8 +176,8 @@ pub fn deinit(self: *Page) void { log.debug(.page, "page.deinit", .{ .url = self.url }); // Uncomment if you want slab statistics to print. - // const stats = self._factory._slab.getStats(self.arena) catch unreachable; - // stats.print() catch unreachable; + const stats = self._factory._slab.getStats(self.arena) catch unreachable; + stats.print() catch unreachable; } self.js.deinit(); diff --git a/src/browser/webapi/Blob.zig b/src/browser/webapi/Blob.zig index 9abe6f295..144280850 100644 --- a/src/browser/webapi/Blob.zig +++ b/src/browser/webapi/Blob.zig @@ -21,12 +21,15 @@ const Writer = std.Io.Writer; const js = @import("../js/js.zig"); const Page = @import("../Page.zig"); +const FactoryAllocationKind = @import("../Factory.zig").FactoryAllocationKind; /// https://w3c.github.io/FileAPI/#blob-section /// https://developer.mozilla.org/en-US/docs/Web/API/Blob const Blob = @This(); _type: Type, +_allocation: FactoryAllocationKind, + /// Immutable slice of blob. /// Note that another blob may hold a pointer/slice to this, /// so its better to leave the deallocation of it to arena allocator. @@ -78,6 +81,7 @@ pub fn init( return page._factory.create(Blob{ ._type = .generic, + ._allocation = .standalone, .slice = slice, .mime = mime, }); @@ -267,6 +271,7 @@ pub fn getSlice( return page._factory.create(Blob{ ._type = .generic, + ._allocation = .standalone, .slice = slice[start..end], .mime = mime, }); @@ -274,6 +279,7 @@ pub fn getSlice( return page._factory.create(Blob{ ._type = .generic, + ._allocation = .standalone, .slice = slice, .mime = mime, }); diff --git a/src/browser/webapi/Event.zig b/src/browser/webapi/Event.zig index 70de6e078..c994004fd 100644 --- a/src/browser/webapi/Event.zig +++ b/src/browser/webapi/Event.zig @@ -20,12 +20,15 @@ const std = @import("std"); const js = @import("../js/js.zig"); const Page = @import("../Page.zig"); +const FactoryAllocationKind = @import("../Factory.zig").FactoryAllocationKind; const EventTarget = @import("EventTarget.zig"); const String = @import("../../string.zig").String; pub const Event = @This(); _type: Type, +_allocation: FactoryAllocationKind, + _bubbles: bool = false, _cancelable: bool = false, _type_string: String, @@ -65,6 +68,7 @@ pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*Event { return page._factory.create(Event{ ._type = .generic, + ._allocation = .standalone, ._bubbles = opts.bubbles, ._time_stamp = time_stamp, ._cancelable = opts.cancelable, diff --git a/src/browser/webapi/EventTarget.zig b/src/browser/webapi/EventTarget.zig index 23ecdf985..fd2cefe77 100644 --- a/src/browser/webapi/EventTarget.zig +++ b/src/browser/webapi/EventTarget.zig @@ -21,12 +21,14 @@ const js = @import("../js/js.zig"); const Page = @import("../Page.zig"); const RegisterOptions = @import("../EventManager.zig").RegisterOptions; +const FactoryAllocationKind = @import("../Factory.zig").FactoryAllocationKind; const Event = @import("Event.zig"); const EventTarget = @This(); _type: Type, +_allocation: FactoryAllocationKind, pub const Type = union(enum) { node: *@import("Node.zig"), From 2ddaa351abbd4f844a9c13a6f0b5e078a3b6475a Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Wed, 26 Nov 2025 10:20:27 -0800 Subject: [PATCH 090/219] use stream for logging stats --- src/browser/Page.zig | 6 ++++-- src/slab.zig | 40 ++++++++++++++++++++-------------------- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 37bb6d1aa..4cc4e3e17 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -176,8 +176,10 @@ pub fn deinit(self: *Page) void { log.debug(.page, "page.deinit", .{ .url = self.url }); // Uncomment if you want slab statistics to print. - const stats = self._factory._slab.getStats(self.arena) catch unreachable; - stats.print() catch unreachable; + // const stats = self._factory._slab.getStats(self.arena) catch unreachable; + // var buffer: [256]u8 = undefined; + // var stream = std.fs.File.stderr().writer(&buffer).interface; + // stats.print(&stream) catch unreachable; } self.js.deinit(); diff --git a/src/slab.zig b/src/slab.zig index 0e574fef0..dab2a0ef4 100644 --- a/src/slab.zig +++ b/src/slab.zig @@ -258,47 +258,47 @@ pub const SlabAllocator = struct { utilization_ratio: f64, slabs: []const Slab.Stats, - pub fn print(self: *const Stats) !void { - std.debug.print("\n", .{}); - std.debug.print("\n=== Slab Allocator Statistics ===\n", .{}); - std.debug.print("Overall Memory:\n", .{}); - std.debug.print(" Total allocated: {} bytes ({d:.2} MB)\n", .{ + pub fn print(self: *const Stats, stream: *std.io.Writer) !void { + try stream.print("\n", .{}); + try stream.print("\n=== Slab Allocator Statistics ===\n", .{}); + try stream.print("Overall Memory:\n", .{}); + try stream.print(" Total allocated: {} bytes ({d:.2} MB)\n", .{ self.total_allocated_bytes, @as(f64, @floatFromInt(self.total_allocated_bytes)) / 1_048_576.0, }); - std.debug.print(" In use: {} bytes ({d:.2} MB)\n", .{ + try stream.print(" In use: {} bytes ({d:.2} MB)\n", .{ self.bytes_in_use, @as(f64, @floatFromInt(self.bytes_in_use)) / 1_048_576.0, }); - std.debug.print(" Free: {} bytes ({d:.2} MB)\n", .{ + try stream.print(" Free: {} bytes ({d:.2} MB)\n", .{ self.bytes_free, @as(f64, @floatFromInt(self.bytes_free)) / 1_048_576.0, }); - std.debug.print("\nOverall Structure:\n", .{}); - std.debug.print(" Slab Count: {}\n", .{self.slab_count}); - std.debug.print(" Total chunks: {}\n", .{self.total_chunks}); - std.debug.print(" Total slots: {}\n", .{self.total_slots}); - std.debug.print(" Slots in use: {}\n", .{self.slots_in_use}); - std.debug.print(" Slots free: {}\n", .{self.slots_free}); + try stream.print("\nOverall Structure:\n", .{}); + try stream.print(" Slab Count: {}\n", .{self.slab_count}); + try stream.print(" Total chunks: {}\n", .{self.total_chunks}); + try stream.print(" Total slots: {}\n", .{self.total_slots}); + try stream.print(" Slots in use: {}\n", .{self.slots_in_use}); + try stream.print(" Slots free: {}\n", .{self.slots_free}); - std.debug.print("\nOverall Efficiency:\n", .{}); - std.debug.print(" Utilization: {d:.1}%\n", .{self.utilization_ratio * 100.0}); - std.debug.print(" Fragmentation: {d:.1}%\n", .{self.fragmentation_ratio * 100.0}); + try stream.print("\nOverall Efficiency:\n", .{}); + try stream.print(" Utilization: {d:.1}%\n", .{self.utilization_ratio * 100.0}); + try stream.print(" Fragmentation: {d:.1}%\n", .{self.fragmentation_ratio * 100.0}); if (self.slabs.len > 0) { - std.debug.print("\nPer-Slab Breakdown:\n", .{}); - std.debug.print( + try stream.print("\nPer-Slab Breakdown:\n", .{}); + try stream.print( " {s:>5} | {s:>4} | {s:>6} | {s:>6} | {s:>6} | {s:>10} | {s:>6}\n", .{ "Size", "Algn", "Chunks", "Slots", "InUse", "Bytes", "Util%" }, ); - std.debug.print( + try stream.print( " {s:-<5}-+-{s:-<4}-+-{s:-<6}-+-{s:-<6}-+-{s:-<6}-+-{s:-<10}-+-{s:-<6}\n", .{ "", "", "", "", "", "", "" }, ); for (self.slabs) |slab| { - std.debug.print(" {d:5} | {d:4} | {d:6} | {d:6} | {d:6} | {d:10} | {d:5.1}%\n", .{ + try stream.print(" {d:5} | {d:4} | {d:6} | {d:6} | {d:6} | {d:10} | {d:5.1}%\n", .{ slab.key.size, @intFromEnum(slab.key.alignment), slab.chunk_count, From 45c7184fdeeae41a5d754394a293849c1aaf895f Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Wed, 26 Nov 2025 11:14:30 -0800 Subject: [PATCH 091/219] use nullable slice for tracking chain allocations --- src/browser/Factory.zig | 28 +++++++++++----------------- src/browser/webapi/Blob.zig | 9 ++++----- src/browser/webapi/Event.zig | 5 ++--- src/browser/webapi/EventTarget.zig | 5 ++--- 4 files changed, 19 insertions(+), 28 deletions(-) diff --git a/src/browser/Factory.zig b/src/browser/Factory.zig index 4a7333f25..f986c8954 100644 --- a/src/browser/Factory.zig +++ b/src/browser/Factory.zig @@ -40,13 +40,6 @@ const Factory = @This(); _page: *Page, _slab: SlabAllocator, -pub const FactoryAllocationKind = union(enum) { - /// Allocated as part of a Factory PrototypeChain - chain: []u8, - /// Allocated standalone via factory.create() - standalone, -}; - fn PrototypeChain(comptime types: []const type) type { return struct { const Self = @This(); @@ -103,7 +96,7 @@ fn PrototypeChain(comptime types: []const type) type { fn setRoot(self: *const Self, comptime T: type) void { const ptr = self.get(0); - ptr.* = .{ ._type = unionInit(T, self.get(1)), ._allocation = FactoryAllocationKind{ .chain = self.memory } }; + ptr.* = .{ ._type = unionInit(T, self.get(1)), ._allocation = self.memory }; } fn setMiddle(self: *const Self, comptime index: usize, comptime T: type) void { @@ -164,7 +157,11 @@ pub fn eventTarget(self: *Factory, child: anytype) !*@TypeOf(child) { &.{ EventTarget, @TypeOf(child) }, ).allocate(allocator); - chain.setRoot(EventTarget.Type); + const event_ptr = chain.get(0); + event_ptr.* = .{ + ._type = unionInit(EventTarget.Type, chain.get(1)), + ._allocation = chain.memory, + }; chain.setLeaf(1, child); return chain.get(1); @@ -183,7 +180,7 @@ pub fn event(self: *Factory, typ: []const u8, child: anytype) !*@TypeOf(child) { event_ptr.* = .{ ._type = unionInit(Event.Type, chain.get(1)), ._type_string = try String.init(self._page.arena, typ, .{}), - ._allocation = FactoryAllocationKind{ .chain = chain.memory }, + ._allocation = chain.memory, }; chain.setLeaf(1, child); @@ -201,7 +198,7 @@ pub fn blob(self: *Factory, child: anytype) !*@TypeOf(child) { const blob_ptr = chain.get(0); blob_ptr.* = .{ ._type = unionInit(Blob.Type, chain.get(1)), - ._allocation = FactoryAllocationKind{ .chain = chain.memory }, + ._allocation = chain.memory, .slice = "", .mime = "", }; @@ -295,14 +292,11 @@ pub fn destroy(self: *Factory, value: anytype) void { } } - const allocation_kind = self.destroyChain(value, true) orelse return; - switch (allocation_kind) { - .chain => |buf| allocator.free(buf), - .standalone => {}, - } + const chain_memory = self.destroyChain(value, true) orelse return; + allocator.free(chain_memory); } -fn destroyChain(self: *Factory, value: anytype, comptime first: bool) ?FactoryAllocationKind { +fn destroyChain(self: *Factory, value: anytype, comptime first: bool) ?[]u8 { const S = reflect.Struct(@TypeOf(value)); const allocator = self._slab.allocator(); diff --git a/src/browser/webapi/Blob.zig b/src/browser/webapi/Blob.zig index 144280850..2b134e3f6 100644 --- a/src/browser/webapi/Blob.zig +++ b/src/browser/webapi/Blob.zig @@ -21,14 +21,13 @@ const Writer = std.Io.Writer; const js = @import("../js/js.zig"); const Page = @import("../Page.zig"); -const FactoryAllocationKind = @import("../Factory.zig").FactoryAllocationKind; /// https://w3c.github.io/FileAPI/#blob-section /// https://developer.mozilla.org/en-US/docs/Web/API/Blob const Blob = @This(); _type: Type, -_allocation: FactoryAllocationKind, +_allocation: ?[]u8, /// Immutable slice of blob. /// Note that another blob may hold a pointer/slice to this, @@ -81,7 +80,7 @@ pub fn init( return page._factory.create(Blob{ ._type = .generic, - ._allocation = .standalone, + ._allocation = null, .slice = slice, .mime = mime, }); @@ -271,7 +270,7 @@ pub fn getSlice( return page._factory.create(Blob{ ._type = .generic, - ._allocation = .standalone, + ._allocation = null, .slice = slice[start..end], .mime = mime, }); @@ -279,7 +278,7 @@ pub fn getSlice( return page._factory.create(Blob{ ._type = .generic, - ._allocation = .standalone, + ._allocation = null, .slice = slice, .mime = mime, }); diff --git a/src/browser/webapi/Event.zig b/src/browser/webapi/Event.zig index c994004fd..21c4f83be 100644 --- a/src/browser/webapi/Event.zig +++ b/src/browser/webapi/Event.zig @@ -20,14 +20,13 @@ const std = @import("std"); const js = @import("../js/js.zig"); const Page = @import("../Page.zig"); -const FactoryAllocationKind = @import("../Factory.zig").FactoryAllocationKind; const EventTarget = @import("EventTarget.zig"); const String = @import("../../string.zig").String; pub const Event = @This(); _type: Type, -_allocation: FactoryAllocationKind, +_allocation: ?[]u8, _bubbles: bool = false, _cancelable: bool = false, @@ -68,7 +67,7 @@ pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*Event { return page._factory.create(Event{ ._type = .generic, - ._allocation = .standalone, + ._allocation = null, ._bubbles = opts.bubbles, ._time_stamp = time_stamp, ._cancelable = opts.cancelable, diff --git a/src/browser/webapi/EventTarget.zig b/src/browser/webapi/EventTarget.zig index fd2cefe77..b9e584e19 100644 --- a/src/browser/webapi/EventTarget.zig +++ b/src/browser/webapi/EventTarget.zig @@ -21,14 +21,13 @@ const js = @import("../js/js.zig"); const Page = @import("../Page.zig"); const RegisterOptions = @import("../EventManager.zig").RegisterOptions; -const FactoryAllocationKind = @import("../Factory.zig").FactoryAllocationKind; const Event = @import("Event.zig"); const EventTarget = @This(); _type: Type, -_allocation: FactoryAllocationKind, +_allocation: ?[]u8, pub const Type = union(enum) { node: *@import("Node.zig"), @@ -124,7 +123,7 @@ pub const JsApi = struct { const testing = @import("../../testing.zig"); test "WebApi: EventTarget" { // we create thousands of these per page. Nothing should bloat it. - try testing.expectEqual(16, @sizeOf(EventTarget)); + try testing.expectEqual(32, @sizeOf(EventTarget)); try testing.htmlRunner("events.html", .{}); } From 15dff342a6088614e5b8d1f5242443688a75f783 Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Wed, 26 Nov 2025 12:07:59 -0800 Subject: [PATCH 092/219] shrink EventTarget back to 16 --- src/browser/Factory.zig | 87 +++++++++++++++++++++++++----- src/browser/webapi/Blob.zig | 5 +- src/browser/webapi/Event.zig | 3 +- src/browser/webapi/EventTarget.zig | 4 +- 4 files changed, 79 insertions(+), 20 deletions(-) diff --git a/src/browser/Factory.zig b/src/browser/Factory.zig index f986c8954..8a0893e1f 100644 --- a/src/browser/Factory.zig +++ b/src/browser/Factory.zig @@ -96,7 +96,7 @@ fn PrototypeChain(comptime types: []const type) type { fn setRoot(self: *const Self, comptime T: type) void { const ptr = self.get(0); - ptr.* = .{ ._type = unionInit(T, self.get(1)), ._allocation = self.memory }; + ptr.* = .{ ._type = unionInit(T, self.get(1)) }; } fn setMiddle(self: *const Self, comptime index: usize, comptime T: type) void { @@ -160,7 +160,6 @@ pub fn eventTarget(self: *Factory, child: anytype) !*@TypeOf(child) { const event_ptr = chain.get(0); event_ptr.* = .{ ._type = unionInit(EventTarget.Type, chain.get(1)), - ._allocation = chain.memory, }; chain.setLeaf(1, child); @@ -180,7 +179,6 @@ pub fn event(self: *Factory, typ: []const u8, child: anytype) !*@TypeOf(child) { event_ptr.* = .{ ._type = unionInit(Event.Type, chain.get(1)), ._type_string = try String.init(self._page.arena, typ, .{}), - ._allocation = chain.memory, }; chain.setLeaf(1, child); @@ -198,7 +196,6 @@ pub fn blob(self: *Factory, child: anytype) !*@TypeOf(child) { const blob_ptr = chain.get(0); blob_ptr.* = .{ ._type = unionInit(Blob.Type, chain.get(1)), - ._allocation = chain.memory, .slice = "", .mime = "", }; @@ -275,9 +272,34 @@ pub fn xhrEventTarget(self: *Factory, child: anytype) !*@TypeOf(child) { ).create(allocator, child); } +fn hasChainRoot(comptime T: type) bool { + // Check if this is a root + if (@hasDecl(T, "_prototype_root")) { + return true; + } + + // If no _proto field, we're at the top but not a recognized root + if (!@hasField(T, "_proto")) return false; + + // Get the _proto field's type and recurse + const fields = @typeInfo(T).@"struct".fields; + inline for (fields) |field| { + if (std.mem.eql(u8, field.name, "_proto")) { + const ProtoType = reflect.Struct(field.type); + return hasChainRoot(ProtoType); + } + } + + return false; +} + +fn isChainType(comptime T: type) bool { + if (@hasField(T, "_proto")) return false; + return comptime hasChainRoot(T); +} + pub fn destroy(self: *Factory, value: anytype) void { const S = reflect.Struct(@TypeOf(value)); - const allocator = self._slab.allocator(); if (comptime IS_DEBUG) { // We should always destroy from the leaf down. @@ -292,14 +314,48 @@ pub fn destroy(self: *Factory, value: anytype) void { } } - const chain_memory = self.destroyChain(value, true) orelse return; - allocator.free(chain_memory); + if (comptime isChainType(S)) { + self.destroyChain(value, true, 0, std.mem.Alignment.@"1"); + } else { + self.destroyStandalone(value); + } +} + +pub fn destroyStandalone(self: *Factory, value: anytype) void { + const S = reflect.Struct(@TypeOf(value)); + assert(!@hasDecl(S, "_prototype_root")); + + const allocator = self._slab.allocator(); + + 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"), + } + } + + allocator.destroy(value); } -fn destroyChain(self: *Factory, value: anytype, comptime first: bool) ?[]u8 { +fn destroyChain( + self: *Factory, + value: anytype, + comptime first: bool, + old_size: usize, + old_align: std.mem.Alignment, +) void { const S = reflect.Struct(@TypeOf(value)); const allocator = self._slab.allocator(); + // aligns the old size to the alignment of this element + const current_size = std.mem.alignForward(usize, old_size, @alignOf(S)); + const alignment = std.mem.Alignment.fromByteUnits(@alignOf(S)); + + const new_align = std.mem.Alignment.max(old_align, alignment); + const new_size = current_size + @sizeOf(S); + // 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) @@ -316,15 +372,22 @@ fn destroyChain(self: *Factory, value: anytype, comptime first: bool) ?[]u8 { } if (@hasField(S, "_proto")) { - return self.destroyChain(value._proto, false); + self.destroyChain(value._proto, false, new_size, new_align); } else if (@hasDecl(S, "JsApi")) { // Doesn't have a _proto, but has a JsApi. if (self._page.js.removeTaggedMapping(@intFromPtr(value))) |tagged| { allocator.destroy(tagged); } - } else if (@hasField(S, "_allocation")) { - return value._allocation; - } else return null; + } else { + // no proto so this is the head of the chain. + // we use this as the ptr to the start of the chain. + // and we have summed up the length. + assert(@hasDecl(S, "_prototype_root")); + + const memory_ptr: [*]const u8 = @ptrCast(value); + const len = std.mem.alignForward(usize, new_size, new_align.toByteUnits()); + allocator.free(memory_ptr[0..len]); + } } pub fn createT(self: *Factory, comptime T: type) !*T { diff --git a/src/browser/webapi/Blob.zig b/src/browser/webapi/Blob.zig index 2b134e3f6..a60f4b424 100644 --- a/src/browser/webapi/Blob.zig +++ b/src/browser/webapi/Blob.zig @@ -26,8 +26,8 @@ const Page = @import("../Page.zig"); /// https://developer.mozilla.org/en-US/docs/Web/API/Blob const Blob = @This(); +const _prototype_root = true; _type: Type, -_allocation: ?[]u8, /// Immutable slice of blob. /// Note that another blob may hold a pointer/slice to this, @@ -80,7 +80,6 @@ pub fn init( return page._factory.create(Blob{ ._type = .generic, - ._allocation = null, .slice = slice, .mime = mime, }); @@ -270,7 +269,6 @@ pub fn getSlice( return page._factory.create(Blob{ ._type = .generic, - ._allocation = null, .slice = slice[start..end], .mime = mime, }); @@ -278,7 +276,6 @@ pub fn getSlice( return page._factory.create(Blob{ ._type = .generic, - ._allocation = null, .slice = slice, .mime = mime, }); diff --git a/src/browser/webapi/Event.zig b/src/browser/webapi/Event.zig index 21c4f83be..b02357baa 100644 --- a/src/browser/webapi/Event.zig +++ b/src/browser/webapi/Event.zig @@ -25,8 +25,8 @@ const String = @import("../../string.zig").String; pub const Event = @This(); +const _prototype_root = true; _type: Type, -_allocation: ?[]u8, _bubbles: bool = false, _cancelable: bool = false, @@ -67,7 +67,6 @@ pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*Event { return page._factory.create(Event{ ._type = .generic, - ._allocation = null, ._bubbles = opts.bubbles, ._time_stamp = time_stamp, ._cancelable = opts.cancelable, diff --git a/src/browser/webapi/EventTarget.zig b/src/browser/webapi/EventTarget.zig index b9e584e19..4e5ab768c 100644 --- a/src/browser/webapi/EventTarget.zig +++ b/src/browser/webapi/EventTarget.zig @@ -26,8 +26,8 @@ const Event = @import("Event.zig"); const EventTarget = @This(); +const _prototype_root = true; _type: Type, -_allocation: ?[]u8, pub const Type = union(enum) { node: *@import("Node.zig"), @@ -123,7 +123,7 @@ pub const JsApi = struct { const testing = @import("../../testing.zig"); test "WebApi: EventTarget" { // we create thousands of these per page. Nothing should bloat it. - try testing.expectEqual(32, @sizeOf(EventTarget)); + try testing.expectEqual(16, @sizeOf(EventTarget)); try testing.htmlRunner("events.html", .{}); } From 8775564e04d2ad97a1924172a2f4eeff46fb5389 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 27 Nov 2025 10:53:31 +0800 Subject: [PATCH 093/219] merge module loading tweaks that were made to main --- src/browser/ScriptManager.zig | 2 +- src/browser/js/Caller.zig | 3 --- src/browser/js/Context.zig | 47 ++++++++++++++++++++++------------- 3 files changed, 31 insertions(+), 21 deletions(-) diff --git a/src/browser/ScriptManager.zig b/src/browser/ScriptManager.zig index 0d421db14..57cd65245 100644 --- a/src/browser/ScriptManager.zig +++ b/src/browser/ScriptManager.zig @@ -300,7 +300,7 @@ pub fn resolveSpecifier(self: *ScriptManager, arena: Allocator, base: [:0]const return s; } - return URL.resolve(arena, base, specifier, .{}); + return URL.resolve(arena, base, specifier, .{.always_dupe = true}); } pub fn preloadImport(self: *ScriptManager, url: [:0]const u8, referrer: []const u8) !void { diff --git a/src/browser/js/Caller.zig b/src/browser/js/Caller.zig index edd016658..efb696ec7 100644 --- a/src/browser/js/Caller.zig +++ b/src/browser/js/Caller.zig @@ -80,9 +80,6 @@ pub fn deinit(self: *Caller) void { _ = arena.reset(.{ .retain_with_limit = CALL_ARENA_RETAIN }); } - // Set this _after_ we've executed the above code, so that if the - // above code executes any callbacks, they aren't being executed - // at scope 0, which would be wrong. context.call_depth = call_depth; } diff --git a/src/browser/js/Context.zig b/src/browser/js/Context.zig index fddcdfa48..a2f358dc4 100644 --- a/src/browser/js/Context.zig +++ b/src/browser/js/Context.zig @@ -254,8 +254,8 @@ pub fn module(self: *Context, comptime want_result: bool, src: []const u8, url: } } - const m = try compileModule(self.isolate, src, url); const owned_url = try arena.dupeZ(u8, url); + const m = try compileModule(self.isolate, src, owned_url); if (cacheable) { // compileModule is synchronous - nothing can modify the cache during compilation @@ -1342,6 +1342,7 @@ fn _dynamicModuleCallback(self: *Context, specifier: [:0]const u8, referrer: []c var resolver = persistent_resolver.castToPromiseResolver(); const state = try self.arena.create(DynamicModuleResolveState); + state.* = .{ .module = null, .context = self, @@ -1379,27 +1380,39 @@ fn _dynamicModuleCallback(self: *Context, specifier: [:0]const u8, referrer: []c } // So we have a module, but no async resolver. This can only - // happen if the module was first synchronously loaded (e.g., as a - // static import dependency). You'd think we can just return the module + // happen if the module was first synchronously loaded (Does that + // ever even happen?!) You'd think we cann just return the module // but no, we need to resolve the module namespace, and the // module could still be loading! // We need to do part of what the first case is going to do in // `dynamicModuleSourceCallback`, but we can skip some steps - // since the module is already compiled. + // since the module is alrady loaded, std.debug.assert(gop.value_ptr.module != null); // If the module hasn't been evaluated yet (it was only instantiated // as a static import dependency), we need to evaluate it now. if (gop.value_ptr.module_promise == null) { const mod = gop.value_ptr.module.?.castToModule(); - const evaluated = mod.evaluate(self.v8_context) catch { - std.debug.assert(mod.getStatus() == .kErrored); - const error_msg = v8.String.initUtf8(isolate, "Module evaluation failed"); - _ = resolver.reject(self.v8_context, error_msg.toValue()); - return promise; - }; - std.debug.assert(evaluated.isPromise()); - gop.value_ptr.module_promise = PersistentPromise.init(self.isolate, .{ .handle = evaluated.handle }); + const status = mod.getStatus(); + if (status == .kEvaluated or status == .kEvaluating) { + // Module was already evaluated (shouldn't normally happen, but handle it). + // Create a pre-resolved promise with the module namespace. + const persisted_module_resolver = v8.Persistent(v8.PromiseResolver).init(isolate, v8.PromiseResolver.init(self.v8_context)); + try self.persisted_promise_resolvers.append(self.arena, persisted_module_resolver); + var module_resolver = persisted_module_resolver.castToPromiseResolver(); + _ = module_resolver.resolve(self.v8_context, mod.getModuleNamespace()); + gop.value_ptr.module_promise = PersistentPromise.init(self.isolate, module_resolver.getPromise()); + } else { + // the module was loaded, but not evaluated, we _have_ to evaluate it now + const evaluated = mod.evaluate(self.v8_context) catch { + std.debug.assert(status == .kErrored); + const error_msg = v8.String.initUtf8(isolate, "Module evaluation failed"); + _ = resolver.reject(self.v8_context, error_msg.toValue()); + return promise; + }; + std.debug.assert(evaluated.isPromise()); + gop.value_ptr.module_promise = PersistentPromise.init(self.isolate, .{ .handle = evaluated.handle }); + } } // like before, we want to set this up so that if anything else @@ -1407,30 +1420,30 @@ fn _dynamicModuleCallback(self: *Context, specifier: [:0]const u8, referrer: []c // since we're going to be doing all the work. gop.value_ptr.resolver_promise = persisted_promise; - // But we can skip directly to `resolveDynamicModule` which is + // But we can skip direclty to `resolveDynamicModule` which is // what the above callback will eventually do. self.resolveDynamicModule(state, gop.value_ptr.*); return promise; } -fn dynamicModuleSourceCallback(ctx: *anyopaque, fetch_result_: anyerror!ScriptManager.ModuleSource) void { +fn dynamicModuleSourceCallback(ctx: *anyopaque, module_source_: anyerror!ScriptManager.ModuleSource) void { const state: *DynamicModuleResolveState = @ptrCast(@alignCast(ctx)); var self = state.context; - var fetch_result = fetch_result_ catch |err| { + var ms = module_source_ catch |err| { const error_msg = v8.String.initUtf8(self.isolate, @errorName(err)); _ = state.resolver.castToPromiseResolver().reject(self.v8_context, error_msg.toValue()); return; }; const module_entry = blk: { - defer fetch_result.deinit(); + defer ms.deinit(); var try_catch: js.TryCatch = undefined; try_catch.init(self); defer try_catch.deinit(); - break :blk self.module(true, fetch_result.src(), state.specifier, true) catch { + break :blk self.module(true, ms.src(), state.specifier, true) catch { const ex = try_catch.exception(self.call_arena) catch |err| @errorName(err) orelse "unknown error"; log.err(.js, "module compilation failed", .{ .specifier = state.specifier, From 0d57356c1149d016303268a482820b03326b971d Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 27 Nov 2025 15:12:54 +0800 Subject: [PATCH 094/219] Response constructor, window.CSS --- src/browser/Page.zig | 1 - src/browser/ScriptManager.zig | 2 +- src/browser/js/Env.zig | 9 +- src/browser/js/bridge.zig | 3 +- src/browser/tests/css.html | 69 +++++++++++ src/browser/webapi/Element.zig | 6 +- src/browser/webapi/Performance.zig | 2 +- src/browser/webapi/PerformanceObserver.zig | 2 +- src/browser/webapi/Window.zig | 7 ++ src/browser/webapi/css.zig | 138 +++++++++++++++++++++ src/browser/webapi/element/html/Slot.zig | 2 +- src/browser/webapi/net/Response.zig | 18 +++ 12 files changed, 244 insertions(+), 15 deletions(-) create mode 100644 src/browser/tests/css.html diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 6b1198433..37d947c49 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -1221,7 +1221,6 @@ pub fn createElement(self: *Page, ns_: ?[]const u8, name: []const u8, attribute_ return node; }; - // After constructor runs, invoke attributeChangedCallback for initial attributes const element = node.as(Element); if (element._attributes) |attributes| { diff --git a/src/browser/ScriptManager.zig b/src/browser/ScriptManager.zig index 57cd65245..f037713f7 100644 --- a/src/browser/ScriptManager.zig +++ b/src/browser/ScriptManager.zig @@ -300,7 +300,7 @@ pub fn resolveSpecifier(self: *ScriptManager, arena: Allocator, base: [:0]const return s; } - return URL.resolve(arena, base, specifier, .{.always_dupe = true}); + return URL.resolve(arena, base, specifier, .{ .always_dupe = true }); } pub fn preloadImport(self: *ScriptManager, url: [:0]const u8, referrer: []const u8) !void { diff --git a/src/browser/js/Env.zig b/src/browser/js/Env.zig index 3dc59ab54..2c87510fa 100644 --- a/src/browser/js/Env.zig +++ b/src/browser/js/Env.zig @@ -198,13 +198,10 @@ fn promiseRejectCallback(v8_msg: v8.C_PromiseRejectMessage) callconv(.c) void { const value = if (msg.getValue()) |v8_value| context.valueToString(v8_value, .{}) catch |err| @errorName(err) - else "no value" - ; + else + "no value"; - log.debug(.js, "unhandled rejection", .{ - .value = value, - .stack = context.stackTrace() catch |err| @errorName(err) orelse "???" - }); + log.debug(.js, "unhandled rejection", .{ .value = value, .stack = context.stackTrace() catch |err| @errorName(err) orelse "???" }); } // Give it a Zig struct, get back a v8.FunctionTemplate. diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index 63aef20a2..850c80703 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -59,7 +59,7 @@ pub fn Builder(comptime T: type) type { pub fn property(value: anytype) Property { switch (@typeInfo(@TypeOf(value))) { - .comptime_int, .int => return .{.int = value}, + .comptime_int, .int => return .{ .int = value }, else => {}, } @compileError("Property for " ++ @typeName(@TypeOf(value)) ++ " hasn't been defined yet"); @@ -485,6 +485,7 @@ pub const JsApis = flattenTypes(&.{ @import("../webapi/collections.zig"), @import("../webapi/Console.zig"), @import("../webapi/Crypto.zig"), + @import("../webapi/CSS.zig"), @import("../webapi/css/CSSRule.zig"), @import("../webapi/css/CSSRuleList.zig"), @import("../webapi/css/CSSStyleDeclaration.zig"), diff --git a/src/browser/tests/css.html b/src/browser/tests/css.html new file mode 100644 index 000000000..ac0b6abae --- /dev/null +++ b/src/browser/tests/css.html @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index f73e5374e..c688d6a8b 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -32,7 +32,7 @@ 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"); +const CSS = @import("CSS.zig"); const ShadowRoot = @import("ShadowRoot.zig"); pub const Svg = @import("element/Svg.zig"); @@ -623,8 +623,8 @@ pub fn getBoundingClientRect(self: *Element, page: *Page) !*DOMRect { 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; + 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(); diff --git a/src/browser/webapi/Performance.zig b/src/browser/webapi/Performance.zig index 60b972a30..c659a7f89 100644 --- a/src/browser/webapi/Performance.zig +++ b/src/browser/webapi/Performance.zig @@ -69,7 +69,7 @@ pub const Entry = struct { } pub fn getEntryType(self: *const Entry) []const u8 { - return switch (self._entry_type) { + return switch (self._entry_type) { .first_input => "first-input", .largest_contentful_paint => "largest-contentful-paint", .layout_shift => "layout-shift", diff --git a/src/browser/webapi/PerformanceObserver.zig b/src/browser/webapi/PerformanceObserver.zig index 68eafe015..cd77ad188 100644 --- a/src/browser/webapi/PerformanceObserver.zig +++ b/src/browser/webapi/PerformanceObserver.zig @@ -68,5 +68,5 @@ pub const JsApi = struct { pub const observe = bridge.function(PerformanceObserver.observe, .{}); pub const disconnect = bridge.function(PerformanceObserver.disconnect, .{}); pub const takeRecords = bridge.function(PerformanceObserver.takeRecords, .{}); - pub const supportedEntryTypes = bridge.accessor(PerformanceObserver.getSupportedEntryTypes, null, .{.static = true}); + pub const supportedEntryTypes = bridge.accessor(PerformanceObserver.getSupportedEntryTypes, null, .{ .static = true }); }; diff --git a/src/browser/webapi/Window.zig b/src/browser/webapi/Window.zig index 503dc008f..16562980d 100644 --- a/src/browser/webapi/Window.zig +++ b/src/browser/webapi/Window.zig @@ -25,6 +25,7 @@ const Page = @import("../Page.zig"); const Console = @import("Console.zig"); const History = @import("History.zig"); const Crypto = @import("Crypto.zig"); +const CSS = @import("CSS.zig"); const Navigator = @import("Navigator.zig"); const Screen = @import("Screen.zig"); const Performance = @import("Performance.zig"); @@ -43,6 +44,7 @@ const Window = @This(); _proto: *EventTarget, _document: *Document, +_css: CSS = .init, _crypto: Crypto = .init, _console: Console = .init, _navigator: Navigator = .init, @@ -89,6 +91,10 @@ pub fn getCrypto(self: *Window) *Crypto { return &self._crypto; } +pub fn getCSS(self: *Window) *CSS { + return &self._css; +} + pub fn getPerformance(self: *Window) *Performance { return &self._performance; } @@ -380,6 +386,7 @@ pub const JsApi = struct { pub const location = bridge.accessor(Window.getLocation, null, .{ .cache = "location" }); pub const history = bridge.accessor(Window.getHistory, null, .{ .cache = "history" }); pub const crypto = bridge.accessor(Window.getCrypto, null, .{ .cache = "crypto" }); + pub const CSS = bridge.accessor(Window.getCSS, null, .{ .cache = "CSS" }); pub const customElements = bridge.accessor(Window.getCustomElements, null, .{ .cache = "customElements" }); pub const onload = bridge.accessor(Window.getOnLoad, Window.setOnLoad, .{}); pub const onerror = bridge.accessor(Window.getOnError, Window.getOnError, .{}); diff --git a/src/browser/webapi/css.zig b/src/browser/webapi/css.zig index f285e8d2d..a2f320f7d 100644 --- a/src/browser/webapi/css.zig +++ b/src/browser/webapi/css.zig @@ -17,6 +17,13 @@ // along with this program. If not, see . const std = @import("std"); +const js = @import("../js/js.zig"); +const Page = @import("../Page.zig"); + +const CSS = @This(); +_pad: bool = false, + +pub const init: CSS = .{}; pub fn parseDimension(value: []const u8) ?f64 { if (value.len == 0) { @@ -30,3 +37,134 @@ pub fn parseDimension(value: []const u8) ?f64 { return std.fmt.parseFloat(f64, num_str) catch null; } + +/// Escapes a CSS identifier string +/// https://drafts.csswg.org/cssom/#the-css.escape()-method +pub fn escape(_: *const CSS, value: []const u8, page: *Page) ![]const u8 { + if (value.len == 0) { + return error.InvalidCharacterError; + } + + const first = value[0]; + + // Count how many characters we need for the output + var out_len: usize = escapeLen(true, first); + for (value[1..]) |c| { + out_len += escapeLen(false, c); + } + + if (out_len == value.len) { + return value; + } + + const result = try page.call_arena.alloc(u8, out_len); + var pos: usize = 0; + + if (needsEscape(true, first)) { + pos = writeEscape(true, result, first); + } else { + result[0] = first; + pos = 1; + } + + for (value[1..]) |c| { + if (!needsEscape(false, c)) { + result[pos] = c; + pos += 1; + } else { + pos += writeEscape(false, result[pos..], c); + } + } + + return result; +} + +pub fn supports(_: *const CSS, property_or_condition: []const u8, value: ?[]const u8) bool { + _ = property_or_condition; + _ = value; + return true; +} + +fn escapeLen(comptime is_first: bool, c: u8) usize { + if (needsEscape(is_first, c) == false) { + return 1; + } + if (c == 0) { + return "\u{FFFD}".len; + } + if (isHexEscape(c) or ((comptime is_first) and c >= '0' and c <= '9')) { + // Will be escaped as \XX (backslash + 1-6 hex digits + space) + return 2 + hexDigitsNeeded(c); + } + // Escaped as \C (backslash + character) + return 2; +} + +fn needsEscape(comptime is_first: bool, c: u8) bool { + if (comptime is_first) { + if (c >= '0' and c <= '9') { + return true; + } + if (c == '-') { + return true; + } + } + + // Characters that need escaping + return switch (c) { + 0...0x1F, 0x7F => true, + '!', '"', '#', '$', '%', '&', '\'', '(', ')', '*', '+', ',', '.', '/', ':', ';', '<', '=', '>', '?', '@', '[', '\\', ']', '^', '`', '{', '|', '}', '~' => true, + ' ' => true, + else => false, + }; +} + +fn isHexEscape(c: u8) bool { + return (c >= 0x00 and c <= 0x1F) or c == 0x7F; +} + +fn hexDigitsNeeded(c: u8) usize { + if (c < 0x10) { + return 1; + } + return 2; +} + +fn writeEscape(comptime is_first: bool, buf: []u8, c: u8) usize { + buf[0] = '\\'; + var data = buf[1..]; + + if (c == 0) { + // NULL character becomes replacement character + const replacement = "\u{FFFD}"; + @memcpy(data[0..replacement.len], replacement); + return 1 + replacement.len; + } + + if (isHexEscape(c) or ((comptime is_first) and c >= '0' and c <= '9')) { + const hex_str = std.fmt.bufPrint(data, "{x} ", .{c}) catch unreachable; + return 1 + hex_str.len; + } + + data[0] = c; + return 2; +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(CSS); + + pub const Meta = struct { + pub const name = "Css"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + pub const empty_with_no_proto = true; + }; + + pub const escape = bridge.function(CSS.escape, .{}); + pub const supports = bridge.function(CSS.supports, .{}); +}; + +const testing = @import("../../testing.zig"); +test "WebApi: CSS" { + try testing.htmlRunner("css.html", .{}); +} diff --git a/src/browser/webapi/element/html/Slot.zig b/src/browser/webapi/element/html/Slot.zig index 1089ad565..9e0c41b9a 100644 --- a/src/browser/webapi/element/html/Slot.zig +++ b/src/browser/webapi/element/html/Slot.zig @@ -97,7 +97,7 @@ pub fn assign(self: *Slot, nodes: []const *Node) void { _ = nodes; // let's see if this is ever actually used - log.warn(.not_implemented, "Slot.assign", .{ }); + log.warn(.not_implemented, "Slot.assign", .{}); } fn findShadowRoot(self: *Slot) ?*ShadowRoot { diff --git a/src/browser/webapi/net/Response.zig b/src/browser/webapi/net/Response.zig index d072f7b6c..de1c151b1 100644 --- a/src/browser/webapi/net/Response.zig +++ b/src/browser/webapi/net/Response.zig @@ -20,6 +20,7 @@ const std = @import("std"); const js = @import("../../js/js.zig"); const Page = @import("../../Page.zig"); +const Headers = @import("Headers.zig"); const Allocator = std.mem.Allocator; const Response = @This(); @@ -28,6 +29,22 @@ _status: u16, _data: []const u8, _arena: Allocator, +const InitOpts = struct { + status: u16 = 200, + headers: ?*Headers = null, + statusText: ?[]const u8 = null, +}; + +pub fn init(body_: ?[]const u8, opts_: ?InitOpts, page: *Page) !*Response { + const opts = opts_ orelse InitOpts{}; + + return page._factory.create(Response{ + ._status = opts.status, + ._data = if (body_) |b| try page.arena.dupe(u8, b) else "", + ._arena = page.arena, + }); +} + pub fn initFromFetch(arena: Allocator, data: []const u8, page: *Page) !*Response { return page._factory.create(Response{ ._status = 200, @@ -65,6 +82,7 @@ pub const JsApi = struct { pub var class_id: bridge.ClassId = undefined; }; + pub const constructor = bridge.constructor(Response.init, .{}); pub const ok = bridge.accessor(Response.isOK, null, .{}); pub const status = bridge.accessor(Response.getStatus, null, .{}); pub const json = bridge.function(Response.getJson, .{}); From f25b8fc7b0371dc2ddd0860bc3b1cbaa641ac5ca Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 27 Nov 2025 16:57:33 +0800 Subject: [PATCH 095/219] Event.composedPath and adjusted target when crossing shadowroot boundary --- src/browser/EventManager.zig | 86 +++++++++++++- src/browser/js/Function.zig | 2 +- src/browser/tests/shadowroot/events.html | 145 +++++++++++++++++++---- src/browser/webapi/Event.zig | 73 ++++++++++++ 4 files changed, 281 insertions(+), 25 deletions(-) diff --git a/src/browser/EventManager.zig b/src/browser/EventManager.zig index e6d1ec0b3..3eb02bae6 100644 --- a/src/browser/EventManager.zig +++ b/src/browser/EventManager.zig @@ -162,18 +162,36 @@ pub fn dispatchWithFunction(self: *EventManager, target: *EventTarget, event: *E } fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled: *bool) !void { + const ShadowRoot = @import("webapi/ShadowRoot.zig"); + var path_len: usize = 0; var path_buffer: [128]*EventTarget = undefined; var node: ?*Node = target; - while (node) |n| : (node = n._parent) { + while (node) |n| { if (path_len >= path_buffer.len) break; path_buffer[path_len] = n.asEventTarget(); path_len += 1; + + // Check if this node is a shadow root + if (n.is(ShadowRoot)) |shadow| { + event._needs_retargeting = true; + + // If event is not composed, stop at shadow boundary + if (!event._composed) { + break; + } + + // Otherwise, jump to the shadow host and continue + node = shadow._host.asNode(); + continue; + } + + node = n._parent; } // Even though the window isn't part of the DOM, events always propagate - // through it in the capture phase + // through it in the capture phase (unless we stopped at a shadow boundary) if (path_len < path_buffer.len) { path_buffer[path_len] = self.page.window.asEventTarget(); path_len += 1; @@ -257,6 +275,12 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe was_handled.* = true; event._current_target = current_target; + // Compute adjusted target for shadow DOM retargeting (only if needed) + const original_target = event._target; + if (event._needs_retargeting) { + event._target = getAdjustedTarget(original_target, current_target); + } + switch (listener.function) { .value => |value| try value.call(void, .{event}), .string => |string| { @@ -265,6 +289,11 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe }, } + // Restore original target (only if we changed it) + if (event._needs_retargeting) { + event._target = original_target; + } + if (listener.once) { self.removeListener(list, listener); } @@ -325,3 +354,56 @@ const Function = union(enum) { }; } }; + +// Computes the adjusted target for shadow DOM event retargeting +// Returns the lowest shadow-including ancestor of original_target that is +// also an ancestor-or-self of current_target +fn getAdjustedTarget(original_target: ?*EventTarget, current_target: *EventTarget) ?*EventTarget { + const ShadowRoot = @import("webapi/ShadowRoot.zig"); + + const orig_node = switch ((original_target orelse return null)._type) { + .node => |n| n, + else => return original_target, + }; + const curr_node = switch (current_target._type) { + .node => |n| n, + else => return original_target, + }; + + // Walk up from original target, checking if we can reach current target + var node: ?*Node = orig_node; + while (node) |n| { + // Check if current_target is an ancestor of n (or n itself) + if (isAncestorOrSelf(curr_node, n)) { + return n.asEventTarget(); + } + + // Cross shadow boundary if needed + if (n.is(ShadowRoot)) |shadow| { + node = shadow._host.asNode(); + continue; + } + + node = n._parent; + } + + return original_target; +} + +// Check if ancestor is an ancestor of (or the same as) node +// WITHOUT crossing shadow boundaries (just regular DOM tree) +fn isAncestorOrSelf(ancestor: *Node, node: *Node) bool { + if (ancestor == node) { + return true; + } + + var current: ?*Node = node._parent; + while (current) |n| { + if (n == ancestor) { + return true; + } + current = n._parent; + } + + return false; +} diff --git a/src/browser/js/Function.zig b/src/browser/js/Function.zig index 4ab5be8a5..41d8fa2ca 100644 --- a/src/browser/js/Function.zig +++ b/src/browser/js/Function.zig @@ -144,7 +144,7 @@ pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args const result = self.func.castToFunction().call(context.v8_context, js_this, js_args); if (result == null) { - // std.debug.print("CB ERR: {s}\n", .{self.src() catch "???"}); + std.debug.print("CB ERR: {s}\n", .{self.src() catch "???"}); return error.JSExecCallback; } diff --git a/src/browser/tests/shadowroot/events.html b/src/browser/tests/shadowroot/events.html index 46285cb23..de6f7cdc1 100644 --- a/src/browser/tests/shadowroot/events.html +++ b/src/browser/tests/shadowroot/events.html @@ -52,34 +52,135 @@ + + + + + + -// const button = shadow.getElementById('btn'); + diff --git a/src/browser/webapi/Event.zig b/src/browser/webapi/Event.zig index 70de6e078..b11a83faf 100644 --- a/src/browser/webapi/Event.zig +++ b/src/browser/webapi/Event.zig @@ -21,6 +21,7 @@ const js = @import("../js/js.zig"); const Page = @import("../Page.zig"); const EventTarget = @import("EventTarget.zig"); +const Node = @import("Node.zig"); const String = @import("../../string.zig").String; pub const Event = @This(); @@ -28,6 +29,7 @@ pub const Event = @This(); _type: Type, _bubbles: bool = false, _cancelable: bool = false, +_composed: bool = false, _type_string: String, _target: ?*EventTarget = null, _current_target: ?*EventTarget = null, @@ -36,6 +38,7 @@ _stop_propagation: bool = false, _stop_immediate_propagation: bool = false, _event_phase: EventPhase = .none, _time_stamp: u64 = 0, +_needs_retargeting: bool = false, pub const EventPhase = enum(u8) { none = 0, @@ -54,6 +57,7 @@ pub const Type = union(enum) { const Options = struct { bubbles: bool = false, cancelable: bool = false, + composed: bool = false, }; pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*Event { @@ -68,6 +72,7 @@ pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*Event { ._bubbles = opts.bubbles, ._time_stamp = time_stamp, ._cancelable = opts.cancelable, + ._composed = opts.composed, ._type_string = try String.init(page.arena, typ, .{}), }); } @@ -84,6 +89,10 @@ pub fn getCancelable(self: *const Event) bool { return self._cancelable; } +pub fn getComposed(self: *const Event) bool { + return self._composed; +} + pub fn getTarget(self: *const Event) ?*EventTarget { return self._target; } @@ -117,6 +126,68 @@ pub fn getTimeStamp(self: *const Event) u64 { return self._time_stamp; } +pub fn composedPath(self: *Event, page: *Page) ![]const *EventTarget { + // Return empty array if event is not being dispatched + if (self._event_phase == .none) { + return &.{}; + } + + // If there's no target, return empty array + const target = self._target orelse return &.{}; + + // Only nodes have a propagation path + const target_node = switch (target._type) { + .node => |n| n, + else => return &.{}, + }; + + // Build the path by walking up from target + var path_len: usize = 0; + var path_buffer: [128]*EventTarget = undefined; + var stopped_at_shadow_boundary = false; + + var node: ?*Node = target_node; + while (node) |n| { + if (path_len >= path_buffer.len) { + break; + } + path_buffer[path_len] = n.asEventTarget(); + path_len += 1; + + // Check if this node is a shadow root + if (n._type == .document_fragment) { + if (n._type.document_fragment._type == .shadow_root) { + const shadow = n._type.document_fragment._type.shadow_root; + + // If event is not composed, stop at shadow boundary + if (!self._composed) { + stopped_at_shadow_boundary = true; + break; + } + + // Otherwise, jump to the shadow host and continue + node = shadow._host.asNode(); + continue; + } + } + + node = n._parent; + } + + // Add window at the end (unless we stopped at shadow boundary) + if (!stopped_at_shadow_boundary) { + if (path_len < path_buffer.len) { + path_buffer[path_len] = page.window.asEventTarget(); + path_len += 1; + } + } + + // Allocate and return the path using call_arena (short-lived) + const path = try page.call_arena.alloc(*EventTarget, path_len); + @memcpy(path, path_buffer[0..path_len]); + return path; +} + pub const JsApi = struct { pub const bridge = js.Bridge(Event); @@ -131,6 +202,7 @@ pub const JsApi = struct { pub const @"type" = bridge.accessor(Event.getType, null, .{}); pub const bubbles = bridge.accessor(Event.getBubbles, null, .{}); pub const cancelable = bridge.accessor(Event.getCancelable, null, .{}); + pub const composed = bridge.accessor(Event.getComposed, null, .{}); pub const target = bridge.accessor(Event.getTarget, null, .{}); pub const currentTarget = bridge.accessor(Event.getCurrentTarget, null, .{}); pub const eventPhase = bridge.accessor(Event.getEventPhase, null, .{}); @@ -139,6 +211,7 @@ pub const JsApi = struct { pub const preventDefault = bridge.function(Event.preventDefault, .{}); pub const stopPropagation = bridge.function(Event.stopPropagation, .{}); pub const stopImmediatePropagation = bridge.function(Event.stopImmediatePropagation, .{}); + pub const composedPath = bridge.function(Event.composedPath, .{}); // Event phase constants pub const NONE = bridge.property(@intFromEnum(EventPhase.none)); From 819424fd3b0a61bf54b88e7f6d63c3b96db1f6a4 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 27 Nov 2025 18:16:03 +0800 Subject: [PATCH 096/219] Support Image constructor (i.e. new Image(..)) --- src/browser/js/Env.zig | 13 --- src/browser/js/ExecutionWorld.zig | 5 +- src/browser/tests/element/html/image.html | 89 +++++++++++++++++ src/browser/webapi/element/html/Image.zig | 114 ++++++++++++++++++---- 4 files changed, 188 insertions(+), 33 deletions(-) create mode 100644 src/browser/tests/element/html/image.html diff --git a/src/browser/js/Env.zig b/src/browser/js/Env.zig index 2c87510fa..052c1916d 100644 --- a/src/browser/js/Env.zig +++ b/src/browser/js/Env.zig @@ -333,19 +333,6 @@ fn generateConstructor(comptime JsApi: type, isolate: v8.Isolate) v8.FunctionTem return template; } -// fn generateUndetectable(comptime Struct: type, template: v8.ObjectTemplate) void { -// const has_js_call_as_function = @hasDecl(Struct, "jsCallAsFunction"); - -// 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."); -// } -// template.markAsUndetectable(); -// } -// } - pub fn protoIndexLookup(comptime JsApi: type) ?bridge.JsApiLookup.BackingInt { @setEvalBranchQuota(2000); comptime { diff --git a/src/browser/js/ExecutionWorld.zig b/src/browser/js/ExecutionWorld.zig index 723833436..77a5865dd 100644 --- a/src/browser/js/ExecutionWorld.zig +++ b/src/browser/js/ExecutionWorld.zig @@ -116,8 +116,9 @@ pub fn createContext(self: *ExecutionWorld, page: *Page, enter: bool, global_cal // are now going to get associated with our global instance. inline for (JsApis, 0..) |JsApi, i| { if (@hasDecl(JsApi.Meta, "name")) { - const class_name = v8.String.initUtf8(isolate, JsApi.Meta.name); - global_template.set(class_name.toName(), templates[i], v8.PropertyAttribute.None); + const class_name = if (@hasDecl(JsApi.Meta, "constructor_alias")) JsApi.Meta.constructor_alias else JsApi.Meta.name; + const v8_class_name = v8.String.initUtf8(isolate, class_name); + global_template.set(v8_class_name.toName(), templates[i], v8.PropertyAttribute.None); } } diff --git a/src/browser/tests/element/html/image.html b/src/browser/tests/element/html/image.html new file mode 100644 index 000000000..5ad6454df --- /dev/null +++ b/src/browser/tests/element/html/image.html @@ -0,0 +1,89 @@ + + + + + + + + + + + + diff --git a/src/browser/webapi/element/html/Image.zig b/src/browser/webapi/element/html/Image.zig index 2cbd2634d..86f82d1d7 100644 --- a/src/browser/webapi/element/html/Image.zig +++ b/src/browser/webapi/element/html/Image.zig @@ -1,42 +1,120 @@ -// 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 Node = @import("../../Node.zig"); const Element = @import("../../Element.zig"); const HtmlElement = @import("../Html.zig"); +pub fn registerTypes() []const type { + return &.{ + Image, + // Factory, + }; +} + const Image = @This(); _proto: *HtmlElement, +pub fn constructor(w_: ?u32, h_: ?u32, page: *Page) !*Image { + const node = try page.createElement(null, "img", null); + const el = node.as(Element); + + if (w_) |w| blk: { + const w_string = std.fmt.bufPrint(&page.buf, "{d}", .{w}) catch break :blk; + try el.setAttributeSafe("width", w_string, page); + } + if (h_) |h| blk: { + const h_string = std.fmt.bufPrint(&page.buf, "{d}", .{h}) catch break :blk; + try el.setAttributeSafe("height", h_string, page); + } + return el.as(Image); +} + pub fn asElement(self: *Image) *Element { return self._proto._proto; } +pub fn asConstElement(self: *const Image) *const Element { + return self._proto._proto; +} pub fn asNode(self: *Image) *Node { return self.asElement().asNode(); } +pub fn getSrc(self: *const Image) []const u8 { + return self.asConstElement().getAttributeSafe("src") orelse ""; +} + +pub fn setSrc(self: *Image, value: []const u8, page: *Page) !void { + try self.asElement().setAttributeSafe("src", value, page); +} + +pub fn getAlt(self: *const Image) []const u8 { + return self.asConstElement().getAttributeSafe("alt") orelse ""; +} + +pub fn setAlt(self: *Image, value: []const u8, page: *Page) !void { + try self.asElement().setAttributeSafe("alt", value, page); +} + +pub fn getWidth(self: *const Image) u32 { + const attr = self.asConstElement().getAttributeSafe("width") orelse return 0; + return std.fmt.parseUnsigned(u32, attr, 10) catch 0; +} + +pub fn setWidth(self: *Image, value: u32, page: *Page) !void { + const str = try std.fmt.allocPrint(page.call_arena, "{d}", .{value}); + try self.asElement().setAttributeSafe("width", str, page); +} + +pub fn getHeight(self: *const Image) u32 { + const attr = self.asConstElement().getAttributeSafe("height") orelse return 0; + return std.fmt.parseUnsigned(u32, attr, 10) catch 0; +} + +pub fn setHeight(self: *Image, value: u32, page: *Page) !void { + const str = try std.fmt.allocPrint(page.call_arena, "{d}", .{value}); + try self.asElement().setAttributeSafe("height", str, page); +} + +pub fn getCrossOrigin(self: *const Image) ?[]const u8 { + return self.asConstElement().getAttributeSafe("crossorigin"); +} + +pub fn setCrossOrigin(self: *Image, value: ?[]const u8, page: *Page) !void { + if (value) |v| { + return self.asElement().setAttributeSafe("crossorigin", v, page); + } + return self.asElement().removeAttribute("crossorigin", page); +} + +pub fn getLoading(self: *const Image) []const u8 { + return self.asConstElement().getAttributeSafe("loading") orelse "eager"; +} + +pub fn setLoading(self: *Image, value: []const u8, page: *Page) !void { + try self.asElement().setAttributeSafe("loading", value, page); +} + pub const JsApi = struct { pub const bridge = js.Bridge(Image); pub const Meta = struct { pub const name = "HTMLImageElement"; + pub const constructor_alias = "Image"; pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; }; + + pub const constructor = bridge.constructor(Image.constructor, .{}); + pub const src = bridge.accessor(Image.getSrc, Image.setSrc, .{}); + pub const alt = bridge.accessor(Image.getAlt, Image.setAlt, .{}); + pub const width = bridge.accessor(Image.getWidth, Image.setWidth, .{}); + pub const height = bridge.accessor(Image.getHeight, Image.setHeight, .{}); + pub const crossOrigin = bridge.accessor(Image.getCrossOrigin, Image.setCrossOrigin, .{}); + pub const loading = bridge.accessor(Image.getLoading, Image.setLoading, .{}); }; + +const testing = @import("../../../../testing.zig"); +test "WebApi: HTML.Image" { + try testing.htmlRunner("element/html/image.html", .{}); +} From 94bcb30f115a65ec8050e6d9059211329ce2fbe2 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 27 Nov 2025 18:54:11 +0800 Subject: [PATCH 097/219] fetch response headers --- src/browser/webapi/element/html/Image.zig | 7 ------ src/browser/webapi/net/Fetch.zig | 29 ++++++++++++++++------- src/browser/webapi/net/Response.zig | 23 +++++++++--------- 3 files changed, 31 insertions(+), 28 deletions(-) diff --git a/src/browser/webapi/element/html/Image.zig b/src/browser/webapi/element/html/Image.zig index 86f82d1d7..9576fde75 100644 --- a/src/browser/webapi/element/html/Image.zig +++ b/src/browser/webapi/element/html/Image.zig @@ -5,13 +5,6 @@ const Node = @import("../../Node.zig"); const Element = @import("../../Element.zig"); const HtmlElement = @import("../Html.zig"); -pub fn registerTypes() []const type { - return &.{ - Image, - // Factory, - }; -} - const Image = @This(); _proto: *HtmlElement, diff --git a/src/browser/webapi/net/Fetch.zig b/src/browser/webapi/net/Fetch.zig index 547a6ab1c..d3589ee2d 100644 --- a/src/browser/webapi/net/Fetch.zig +++ b/src/browser/webapi/net/Fetch.zig @@ -24,6 +24,7 @@ const Http = @import("../../../http/Http.zig"); const js = @import("../../js/js.zig"); const Page = @import("../../Page.zig"); +const Headers = @import("Headers.zig"); const Request = @import("Request.zig"); const Response = @import("Response.zig"); @@ -32,20 +33,22 @@ const Allocator = std.mem.Allocator; const Fetch = @This(); _page: *Page, -_response: std.ArrayList(u8), +_buf: std.ArrayList(u8), +_response: *Response, _resolver: js.PersistentPromiseResolver, pub const Input = Request.Input; -// @ZIGDOM just enough to get campire demo working +// @ZIGDOM just enough to get campfire demo working pub fn init(input: Input, page: *Page) !js.Promise { const request = try Request.init(input, null, page); const fetch = try page.arena.create(Fetch); fetch.* = .{ ._page = page, - ._response = .empty, + ._buf = .empty, ._resolver = try page.js.createPromiseResolver(.page), + ._response = try Response.init(null, .{ .status = 0 }, page), }; const http_client = page._session.browser.http_client; @@ -68,20 +71,28 @@ pub fn init(input: Input, page: *Page) !js.Promise { fn httpHeaderDoneCallback(transfer: *Http.Transfer) !void { const self: *Fetch = @ptrCast(@alignCast(transfer.ctx)); - _ = self; + + if (transfer.getContentLength()) |cl| { + try self._buf.ensureTotalCapacity(self._page.arena, cl); + } + + const res = self._response; + res._status = transfer.response_header.?.status; + var it = transfer.responseHeaderIterator(); + while (it.next()) |hdr| { + try res._headers.append(hdr.name, hdr.value, self._page); + } } fn httpDataCallback(transfer: *Http.Transfer, data: []const u8) !void { const self: *Fetch = @ptrCast(@alignCast(transfer.ctx)); - try self._response.appendSlice(self._page.arena, data); + try self._buf.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); + self._response._body = self._buf.items; + return self._resolver.resolve(self._response); } fn httpErrorCallback(ctx: *anyopaque, err: anyerror) void { diff --git a/src/browser/webapi/net/Response.zig b/src/browser/webapi/net/Response.zig index de1c151b1..bc66fb001 100644 --- a/src/browser/webapi/net/Response.zig +++ b/src/browser/webapi/net/Response.zig @@ -26,8 +26,9 @@ const Allocator = std.mem.Allocator; const Response = @This(); _status: u16, -_data: []const u8, _arena: Allocator, +_headers: *Headers, +_body: []const u8, const InitOpts = struct { status: u16 = 200, @@ -39,17 +40,10 @@ pub fn init(body_: ?[]const u8, opts_: ?InitOpts, page: *Page) !*Response { const opts = opts_ orelse InitOpts{}; return page._factory.create(Response{ - ._status = opts.status, - ._data = if (body_) |b| try page.arena.dupe(u8, b) else "", ._arena = page.arena, - }); -} - -pub fn initFromFetch(arena: Allocator, data: []const u8, page: *Page) !*Response { - return page._factory.create(Response{ - ._status = 200, - ._data = data, - ._arena = arena, + ._status = opts.status, + ._body = if (body_) |b| try page.arena.dupe(u8, b) else "", + ._headers = opts.headers orelse try Headers.init(page), }); } @@ -57,6 +51,10 @@ pub fn getStatus(self: *const Response) u16 { return self._status; } +pub fn getHeaders(self: *const Response) *Headers { + return self._headers; +} + pub fn isOK(self: *const Response) bool { return self._status >= 200 and self._status <= 299; } @@ -65,7 +63,7 @@ pub fn getJson(self: *Response, page: *Page) !js.Promise { const value = std.json.parseFromSliceLeaky( std.json.Value, page.call_arena, - self._data, + self._body, .{}, ) catch |err| { return page.js.rejectPromise(.{@errorName(err)}); @@ -86,4 +84,5 @@ pub const JsApi = struct { pub const ok = bridge.accessor(Response.isOK, null, .{}); pub const status = bridge.accessor(Response.getStatus, null, .{}); pub const json = bridge.function(Response.getJson, .{}); + pub const headers = bridge.accessor(Response.getHeaders, null, .{}); }; From 8ce8c7a0f35b20af5b022a0aa255e75186566e1d Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Thu, 27 Nov 2025 12:55:48 -0800 Subject: [PATCH 098/219] use _prototype_root decl everywhere --- src/browser/Factory.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browser/Factory.zig b/src/browser/Factory.zig index 8a0893e1f..a915d74af 100644 --- a/src/browser/Factory.zig +++ b/src/browser/Factory.zig @@ -303,7 +303,7 @@ pub fn destroy(self: *Factory, value: anytype) void { if (comptime IS_DEBUG) { // We should always destroy from the leaf down. - if (@hasField(S, "_type") and @typeInfo(@TypeOf(value._type)) == .@"union") { + if (@hasDecl(S, "_prototype_root")) { // A Event{._type == .generic} (or any other similar types) // _should_ be destoyed directly. The _type = .generic is a pseudo // child From 34c10e1e4889aae5511ea677e9d9ff557f9e8097 Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Thu, 27 Nov 2025 13:10:35 -0800 Subject: [PATCH 099/219] fix svgElement + allow base tags --- src/browser/Factory.zig | 11 ++++++++--- src/browser/Page.zig | 31 +++++++++++++++---------------- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/src/browser/Factory.zig b/src/browser/Factory.zig index a915d74af..2a4a06276 100644 --- a/src/browser/Factory.zig +++ b/src/browser/Factory.zig @@ -241,18 +241,23 @@ pub fn htmlElement(self: *Factory, child: anytype) !*@TypeOf(child) { pub fn svgElement(self: *Factory, tag_name: []const u8, child: anytype) !*@TypeOf(child) { const allocator = self._slab.allocator(); + const ChildT = @TypeOf(child); - // will never allocate, can't fail - const tag_name_str = String.init(self._page.arena, tag_name, .{}) catch unreachable; + if (ChildT == Element.Svg) { + return self.element(child); + } const chain = try PrototypeChain( - &.{ EventTarget, Node, Element, Element.Svg, @TypeOf(child) }, + &.{ EventTarget, Node, Element, Element.Svg, ChildT }, ).allocate(allocator); chain.setRoot(EventTarget.Type); chain.setMiddle(1, Node.Type); chain.setMiddle(2, Element.Type); + // will never allocate, can't fail + const tag_name_str = String.init(self._page.arena, tag_name, .{}) catch unreachable; + // Manually set Element.Svg with the tag_name chain.set(3, .{ ._proto = chain.get(2), diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 4cc4e3e17..4ca3241cc 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -1177,22 +1177,21 @@ pub fn createElement(self: *Page, ns_: ?[]const u8, name: []const u8, attribute_ else => {}, } - // TODO: uncomment - // if (namespace == .svg) { - // const tag_name = try String.init(self.arena, name, .{}); - // if (std.ascii.eqlIgnoreCase(name, "svg")) { - // return self.createSvgElementT(Element.Svg, name, attribute_iterator, .{ - // ._proto = undefined, - // ._type = .svg, - // ._tag_name = tag_name, - // }); - // } - - // // Other SVG elements (rect, circle, text, g, etc.) - // const lower = std.ascii.lowerString(&self.buf, name); - // const tag = std.meta.stringToEnum(Element.Tag, lower) orelse .unknown; - // return self.createSvgElementT(Element.Svg.Generic, name, attribute_iterator, .{ ._proto = undefined, ._tag = tag }); - // } + if (namespace == .svg) { + const tag_name = try String.init(self.arena, name, .{}); + if (std.ascii.eqlIgnoreCase(name, "svg")) { + return self.createSvgElementT(Element.Svg, name, attribute_iterator, .{ + ._proto = undefined, + ._type = .svg, + ._tag_name = tag_name, + }); + } + + // Other SVG elements (rect, circle, text, g, etc.) + const lower = std.ascii.lowerString(&self.buf, name); + const tag = std.meta.stringToEnum(Element.Tag, lower) orelse .unknown; + return self.createSvgElementT(Element.Svg.Generic, name, attribute_iterator, .{ ._proto = undefined, ._tag = tag }); + } const tag_name = try String.init(self.arena, name, .{}); From 833a33678cea50792b3afdba578b24f2914d9af3 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Fri, 28 Nov 2025 13:04:42 +0800 Subject: [PATCH 100/219] call AttributeChangedCallback on upgrade --- src/browser/ScriptManager.zig | 6 ++-- .../custom_elements/attribute_changed.html | 13 ++++---- .../tests/custom_elements/upgrade.html | 30 +++++++++++++++++++ src/browser/webapi/CustomElementRegistry.zig | 18 ++++++++--- src/browser/webapi/Element.zig | 4 +-- src/browser/webapi/Window.zig | 14 +++++++++ src/browser/webapi/net/Fetch.zig | 5 ++++ src/browser/webapi/net/XMLHttpRequest.zig | 3 +- 8 files changed, 76 insertions(+), 17 deletions(-) diff --git a/src/browser/ScriptManager.zig b/src/browser/ScriptManager.zig index f037713f7..a1df242e4 100644 --- a/src/browser/ScriptManager.zig +++ b/src/browser/ScriptManager.zig @@ -215,14 +215,12 @@ pub fn addFromElement(self: *ScriptManager, script_element: *Element.Html.Script .url = remote_url orelse page.url, .mode = blk: { if (source == .@"inline") { - // inline modules are deferred, all other inline scripts have a - // normal execution flow - break :blk if (kind == .module) .@"defer" else .normal; + break :blk .normal; } if (element.getAttributeSafe("async") != null) { break :blk .async; } - if (element.getAttributeSafe("defer") != null) { + if (kind == .module or element.getAttributeSafe("defer") != null) { break :blk .@"defer"; } break :blk .normal; diff --git a/src/browser/tests/custom_elements/attribute_changed.html b/src/browser/tests/custom_elements/attribute_changed.html index 24a8a48d7..f94de7f35 100644 --- a/src/browser/tests/custom_elements/attribute_changed.html +++ b/src/browser/tests/custom_elements/attribute_changed.html @@ -122,13 +122,16 @@ testing.expectEqual(0, callbackCalls.length); customElements.define('upgrade-attr-element', UpgradeAttrElement); - testing.expectEqual(0, callbackCalls.length); - - el.setAttribute('existing', 'after-upgrade'); testing.expectEqual(1, callbackCalls.length); testing.expectEqual('existing', callbackCalls[0].name); - testing.expectEqual('before-upgrade', callbackCalls[0].oldValue); - testing.expectEqual('after-upgrade', callbackCalls[0].newValue); + testing.expectEqual(null, callbackCalls[0].oldValue); + testing.expectEqual('before-upgrade', callbackCalls[0].newValue); + + el.setAttribute('existing', 'after-upgrade'); + testing.expectEqual(2, callbackCalls.length); + testing.expectEqual('existing', callbackCalls[1].name); + testing.expectEqual('before-upgrade', callbackCalls[1].oldValue); + testing.expectEqual('after-upgrade', callbackCalls[1].newValue); } { diff --git a/src/browser/tests/custom_elements/upgrade.html b/src/browser/tests/custom_elements/upgrade.html index 44f37bd65..73f1367c4 100644 --- a/src/browser/tests/custom_elements/upgrade.html +++ b/src/browser/tests/custom_elements/upgrade.html @@ -151,4 +151,34 @@ customElements.upgrade(elem); testing.expectEqual(1, alreadyUpgradedCalled); } + +{ + let attributeChangedCalls = []; + + class UpgradeWithAttrs extends HTMLElement { + static get observedAttributes() { + return ['data-foo', 'data-bar']; + } + + attributeChangedCallback(name, oldValue, newValue) { + attributeChangedCalls.push({ name, oldValue, newValue }); + } + } + + const container = document.createElement('div'); + container.innerHTML = ''; + document.body.appendChild(container); + + testing.expectEqual(0, attributeChangedCalls.length); + + customElements.define('upgrade-with-attrs', UpgradeWithAttrs); + + testing.expectEqual(2, attributeChangedCalls.length); + testing.expectEqual('data-foo', attributeChangedCalls[0].name); + testing.expectEqual(null, attributeChangedCalls[0].oldValue); + testing.expectEqual('hello', attributeChangedCalls[0].newValue); + testing.expectEqual('data-bar', attributeChangedCalls[1].name); + testing.expectEqual(null, attributeChangedCalls[1].oldValue); + testing.expectEqual('world', attributeChangedCalls[1].newValue); +} diff --git a/src/browser/webapi/CustomElementRegistry.zig b/src/browser/webapi/CustomElementRegistry.zig index 9c2951701..318b80e6b 100644 --- a/src/browser/webapi/CustomElementRegistry.zig +++ b/src/browser/webapi/CustomElementRegistry.zig @@ -18,10 +18,13 @@ const std = @import("std"); const log = @import("../../log.zig"); + const js = @import("../js/js.zig"); const Page = @import("../Page.zig"); -const Element = @import("Element.zig"); + const Node = @import("Node.zig"); +const Element = @import("Element.zig"); +const Custom = @import("element/html/Custom.zig"); const CustomElementDefinition = @import("CustomElementDefinition.zig"); const CustomElementRegistry = @This(); @@ -119,8 +122,6 @@ fn upgradeNode(self: *CustomElementRegistry, node: *Node, page: *Page) !void { } fn upgradeElement(self: *CustomElementRegistry, element: *Element, page: *Page) !void { - const Custom = @import("element/html/Custom.zig"); - const custom = element.is(Custom) orelse { return Custom.checkAndAttachBuiltIn(element, page); }; @@ -133,7 +134,7 @@ fn upgradeElement(self: *CustomElementRegistry, element: *Element, page: *Page) try upgradeCustomElement(custom, definition, page); } -fn upgradeCustomElement(custom: *@import("element/html/Custom.zig"), definition: *CustomElementDefinition, page: *Page) !void { +fn upgradeCustomElement(custom: *Custom, definition: *CustomElementDefinition, page: *Page) !void { custom._definition = definition; // Reset callback flags since this is a fresh upgrade @@ -151,6 +152,15 @@ fn upgradeCustomElement(custom: *@import("element/html/Custom.zig"), definition: return error.CustomElementUpgradeFailed; }; + // Invoke attributeChangedCallback for existing observed attributes + var attr_it = custom.asElement().attributeIterator(); + while (attr_it.next()) |attr| { + const name = attr._name.str(); + if (definition.isAttributeObserved(name)) { + custom.invokeAttributeChangedCallback(name, null, attr._value.str(), page); + } + } + if (node.isConnected()) { custom.invokeConnectedCallback(page); } diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index c688d6a8b..36215d479 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -272,9 +272,9 @@ pub fn setClassName(self: *Element, value: []const u8, page: *Page) !void { return self.setAttributeSafe("class", value, page); } -pub fn attributeIterator(self: *Element) Attribute.Iterator { +pub fn attributeIterator(self: *Element) Attribute.InnerIterator { const attributes = self._attributes orelse return .{}; - return attributes.iterator(self); + return attributes.iterator(); } pub fn getAttribute(self: *const Element, name: []const u8, page: *Page) !?[]const u8 { diff --git a/src/browser/webapi/Window.zig b/src/browser/webapi/Window.zig index 16562980d..f025d1ed6 100644 --- a/src/browser/webapi/Window.zig +++ b/src/browser/webapi/Window.zig @@ -54,6 +54,7 @@ _history: History, _storage_bucket: *storage.Bucket, _on_load: ?js.Function = null, _on_error: ?js.Function = null, // TODO: invoke on error? +_on_unhandled_rejection: ?js.Function = null, // TODO: invoke on error _location: *Location, _timer_id: u30 = 0, _timers: std.AutoHashMapUnmanaged(u32, *ScheduleCallback) = .{}, @@ -143,6 +144,18 @@ pub fn setOnError(self: *Window, cb_: ?js.Function) !void { } } +pub fn getOnUnhandledRejection(self: *const Window) ?js.Function { + return self._on_unhandled_rejection; +} + +pub fn setOnUnhandledRejection(self: *Window, cb_: ?js.Function) !void { + if (cb_) |cb| { + self._on_unhandled_rejection = cb; + } else { + self._on_unhandled_rejection = null; + } +} + pub fn fetch(_: *const Window, input: Fetch.Input, page: *Page) !js.Promise { return Fetch.init(input, page); } @@ -390,6 +403,7 @@ pub const JsApi = struct { pub const customElements = bridge.accessor(Window.getCustomElements, null, .{ .cache = "customElements" }); pub const onload = bridge.accessor(Window.getOnLoad, Window.setOnLoad, .{}); pub const onerror = bridge.accessor(Window.getOnError, Window.getOnError, .{}); + pub const onunhandledrejection = bridge.accessor(Window.getOnUnhandledRejection, Window.setOnUnhandledRejection, .{}); pub const fetch = bridge.function(Window.fetch, .{}); pub const queueMicrotask = bridge.function(Window.queueMicrotask, .{}); pub const setTimeout = bridge.function(Window.setTimeout, .{}); diff --git a/src/browser/webapi/net/Fetch.zig b/src/browser/webapi/net/Fetch.zig index d3589ee2d..6fd4c4fde 100644 --- a/src/browser/webapi/net/Fetch.zig +++ b/src/browser/webapi/net/Fetch.zig @@ -29,6 +29,7 @@ const Request = @import("Request.zig"); const Response = @import("Response.zig"); const Allocator = std.mem.Allocator; +const IS_DEBUG = @import("builtin").mode == .Debug; const Fetch = @This(); @@ -54,6 +55,10 @@ pub fn init(input: Input, page: *Page) !js.Promise { const http_client = page._session.browser.http_client; const headers = try http_client.newHeaders(); + if (comptime IS_DEBUG) { + log.debug(.http, "fetch", .{ .url = request._url }); + } + try http_client.request(.{ .ctx = fetch, .url = request._url, diff --git a/src/browser/webapi/net/XMLHttpRequest.zig b/src/browser/webapi/net/XMLHttpRequest.zig index 6239ddc42..3bb219e2f 100644 --- a/src/browser/webapi/net/XMLHttpRequest.zig +++ b/src/browser/webapi/net/XMLHttpRequest.zig @@ -19,8 +19,6 @@ const std = @import("std"); const js = @import("../../js/js.zig"); -const IS_DEBUG = @import("builtin").mode == .Debug; - const log = @import("../../../log.zig"); const Http = @import("../../../http/Http.zig"); @@ -32,6 +30,7 @@ const EventTarget = @import("../EventTarget.zig"); const XMLHttpRequestEventTarget = @import("XMLHttpRequestEventTarget.zig"); const Allocator = std.mem.Allocator; +const IS_DEBUG = @import("builtin").mode == .Debug; const XMLHttpRequest = @This(); _page: *Page, From 8858f889b4066104947ba1d376af03f5018176b7 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Fri, 28 Nov 2025 18:01:41 +0800 Subject: [PATCH 101/219] Window.scrollX/Y, postMessage, more custom element edge cases --- src/browser/js/bridge.zig | 1 + .../custom_elements/attribute_changed.html | 4 + .../tests/custom_elements/upgrade.html | 171 ++++++++++++++++++ src/browser/tests/event/message.html | 170 +++++++++++++++++ .../event/message_multiple_listeners.html | 19 ++ src/browser/webapi/CustomElementRegistry.zig | 7 +- src/browser/webapi/Event.zig | 3 +- src/browser/webapi/Window.zig | 96 ++++++++-- src/browser/webapi/element/html/Custom.zig | 10 + src/browser/webapi/event/MessageEvent.zig | 90 +++++++++ 10 files changed, 553 insertions(+), 18 deletions(-) create mode 100644 src/browser/tests/event/message.html create mode 100644 src/browser/tests/event/message_multiple_listeners.html create mode 100644 src/browser/webapi/event/MessageEvent.zig diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index 850c80703..68f5e4891 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -550,6 +550,7 @@ pub const JsApis = flattenTypes(&.{ @import("../webapi/Event.zig"), @import("../webapi/event/CustomEvent.zig"), @import("../webapi/event/ErrorEvent.zig"), + @import("../webapi/event/MessageEvent.zig"), @import("../webapi/event/ProgressEvent.zig"), @import("../webapi/EventTarget.zig"), @import("../webapi/Location.zig"), diff --git a/src/browser/tests/custom_elements/attribute_changed.html b/src/browser/tests/custom_elements/attribute_changed.html index f94de7f35..2ebbf99e2 100644 --- a/src/browser/tests/custom_elements/attribute_changed.html +++ b/src/browser/tests/custom_elements/attribute_changed.html @@ -122,6 +122,10 @@ testing.expectEqual(0, callbackCalls.length); customElements.define('upgrade-attr-element', UpgradeAttrElement); + testing.expectEqual(0, callbackCalls.length); + + document.body.appendChild(el); + testing.expectEqual(1, callbackCalls.length); testing.expectEqual('existing', callbackCalls[0].name); testing.expectEqual(null, callbackCalls[0].oldValue); diff --git a/src/browser/tests/custom_elements/upgrade.html b/src/browser/tests/custom_elements/upgrade.html index 73f1367c4..3827ca826 100644 --- a/src/browser/tests/custom_elements/upgrade.html +++ b/src/browser/tests/custom_elements/upgrade.html @@ -181,4 +181,175 @@ testing.expectEqual(null, attributeChangedCalls[1].oldValue); testing.expectEqual('world', attributeChangedCalls[1].newValue); } + +{ + let attributeChangedCalls = []; + let connectedCalls = 0; + + class DetachedWithAttrs extends HTMLElement { + static get observedAttributes() { + return ['foo']; + } + + attributeChangedCallback(name, oldValue, newValue) { + attributeChangedCalls.push({ name, oldValue, newValue }); + } + + connectedCallback() { + connectedCalls++; + } + } + + const container = document.createElement('div'); + container.innerHTML = ''; + + testing.expectEqual(0, attributeChangedCalls.length); + + customElements.define('detached-with-attrs', DetachedWithAttrs); + + testing.expectEqual(0, attributeChangedCalls.length); + testing.expectEqual(0, connectedCalls); + + document.body.appendChild(container); + + testing.expectEqual(1, attributeChangedCalls.length); + testing.expectEqual('foo', attributeChangedCalls[0].name); + testing.expectEqual(null, attributeChangedCalls[0].oldValue); + testing.expectEqual('bar', attributeChangedCalls[0].newValue); + testing.expectEqual(1, connectedCalls); +} + +{ + let attributeChangedCalls = []; + let constructorCalled = 0; + + class ManualUpgradeWithAttrs extends HTMLElement { + static get observedAttributes() { + return ['x', 'y']; + } + + constructor() { + super(); + constructorCalled++; + } + + attributeChangedCallback(name, oldValue, newValue) { + attributeChangedCalls.push({ name, oldValue, newValue }); + } + } + + customElements.define('manual-upgrade-with-attrs', ManualUpgradeWithAttrs); + + const container = document.createElement('div'); + container.innerHTML = ''; + + testing.expectEqual(1, constructorCalled); + testing.expectEqual(2, attributeChangedCalls.length); + + const elem = container.querySelector('manual-upgrade-with-attrs'); + elem.setAttribute('z', '3'); + + customElements.upgrade(container); + + testing.expectEqual(1, constructorCalled); + testing.expectEqual(2, attributeChangedCalls.length); +} + +{ + let attributeChangedCalls = []; + + class MixedAttrs extends HTMLElement { + static get observedAttributes() { + return ['watched']; + } + + attributeChangedCallback(name, oldValue, newValue) { + attributeChangedCalls.push({ name, oldValue, newValue }); + } + } + + const container = document.createElement('div'); + container.innerHTML = ''; + document.body.appendChild(container); + + testing.expectEqual(0, attributeChangedCalls.length); + + customElements.define('mixed-attrs', MixedAttrs); + + testing.expectEqual(1, attributeChangedCalls.length); + testing.expectEqual('watched', attributeChangedCalls[0].name); + testing.expectEqual('yes', attributeChangedCalls[0].newValue); +} + +{ + let attributeChangedCalls = []; + + class EmptyAttr extends HTMLElement { + static get observedAttributes() { + return ['empty', 'non-empty']; + } + + attributeChangedCallback(name, oldValue, newValue) { + attributeChangedCalls.push({ name, oldValue, newValue }); + } + } + + const container = document.createElement('div'); + container.innerHTML = ''; + document.body.appendChild(container); + + customElements.define('empty-attr', EmptyAttr); + + testing.expectEqual(2, attributeChangedCalls.length); + testing.expectEqual('empty', attributeChangedCalls[0].name); + testing.expectEqual('', attributeChangedCalls[0].newValue); + testing.expectEqual('non-empty', attributeChangedCalls[1].name); + testing.expectEqual('value', attributeChangedCalls[1].newValue); +} + +{ + let parentCalls = []; + let childCalls = []; + + class NestedParent extends HTMLElement { + static get observedAttributes() { + return ['parent-attr']; + } + + attributeChangedCallback(name, oldValue, newValue) { + parentCalls.push({ name, oldValue, newValue }); + } + } + + class NestedChild extends HTMLElement { + static get observedAttributes() { + return ['child-attr']; + } + + attributeChangedCallback(name, oldValue, newValue) { + childCalls.push({ name, oldValue, newValue }); + } + } + + const container = document.createElement('div'); + container.innerHTML = ''; + document.body.appendChild(container); + + testing.expectEqual(0, parentCalls.length); + testing.expectEqual(0, childCalls.length); + + customElements.define('nested-parent', NestedParent); + + testing.expectEqual(1, parentCalls.length); + testing.expectEqual('parent-attr', parentCalls[0].name); + testing.expectEqual('p', parentCalls[0].newValue); + testing.expectEqual(0, childCalls.length); + + customElements.define('nested-child', NestedChild); + + testing.expectEqual(1, parentCalls.length); + testing.expectEqual(1, childCalls.length); + testing.expectEqual('child-attr', childCalls[0].name); + testing.expectEqual('c', childCalls[0].newValue); +} diff --git a/src/browser/tests/event/message.html b/src/browser/tests/event/message.html new file mode 100644 index 000000000..079f9c7a3 --- /dev/null +++ b/src/browser/tests/event/message.html @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/browser/tests/event/message_multiple_listeners.html b/src/browser/tests/event/message_multiple_listeners.html new file mode 100644 index 000000000..36f13eb29 --- /dev/null +++ b/src/browser/tests/event/message_multiple_listeners.html @@ -0,0 +1,19 @@ + + + + diff --git a/src/browser/webapi/CustomElementRegistry.zig b/src/browser/webapi/CustomElementRegistry.zig index 318b80e6b..6fc67621d 100644 --- a/src/browser/webapi/CustomElementRegistry.zig +++ b/src/browser/webapi/CustomElementRegistry.zig @@ -92,6 +92,11 @@ pub fn define(self: *CustomElementRegistry, name: []const u8, constructor: js.Fu continue; } + if (!custom.asElement().asNode().isConnected()) { + idx += 1; + continue; + } + upgradeCustomElement(custom, definition, page) catch { _ = page._undefined_custom_elements.swapRemove(idx); continue; @@ -134,7 +139,7 @@ fn upgradeElement(self: *CustomElementRegistry, element: *Element, page: *Page) try upgradeCustomElement(custom, definition, page); } -fn upgradeCustomElement(custom: *Custom, definition: *CustomElementDefinition, page: *Page) !void { +pub fn upgradeCustomElement(custom: *Custom, definition: *CustomElementDefinition, page: *Page) !void { custom._definition = definition; // Reset callback flags since this is a fresh upgrade diff --git a/src/browser/webapi/Event.zig b/src/browser/webapi/Event.zig index b11a83faf..56461eb5b 100644 --- a/src/browser/webapi/Event.zig +++ b/src/browser/webapi/Event.zig @@ -49,9 +49,10 @@ pub const EventPhase = enum(u8) { pub const Type = union(enum) { generic, - progress_event: *@import("event/ProgressEvent.zig"), error_event: *@import("event/ErrorEvent.zig"), custom_event: *@import("event/CustomEvent.zig"), + message_event: *@import("event/MessageEvent.zig"), + progress_event: *@import("event/ProgressEvent.zig"), }; const Options = struct { diff --git a/src/browser/webapi/Window.zig b/src/browser/webapi/Window.zig index f025d1ed6..ba3683b6c 100644 --- a/src/browser/webapi/Window.zig +++ b/src/browser/webapi/Window.zig @@ -34,6 +34,7 @@ const Location = @import("Location.zig"); const Fetch = @import("net/Fetch.zig"); const EventTarget = @import("EventTarget.zig"); const ErrorEvent = @import("event/ErrorEvent.zig"); +const MessageEvent = @import("event/MessageEvent.zig"); const MediaQueryList = @import("css/MediaQueryList.zig"); const storage = @import("storage/storage.zig"); const Element = @import("Element.zig"); @@ -255,6 +256,28 @@ pub fn getComputedStyle(_: *const Window, _: *Element, page: *Page) !*CSSStyleDe return CSSStyleDeclaration.init(null, page); } +pub fn postMessage(self: *Window, message: js.Object, target_origin: ?[]const u8, page: *Page) !void { + // For now, we ignore targetOrigin checking and just dispatch the message + // In a full implementation, we would validate the origin + _ = target_origin; + + // postMessage queues a task (not a microtask), so use the scheduler + const origin = try self._location.getOrigin(page); + const callback = try page._factory.create(PostMessageCallback{ + .window = self, + .message = try message.persist() , + .origin = try page.arena.dupe(u8, origin), + .page = page, + }); + errdefer page._factory.destroy(callback); + + + try page.scheduler.add(callback, PostMessageCallback.run, 0, .{ + .name = "postMessage", + .low_priority = false, + }); +} + pub fn btoa(_: *const Window, input: []const u8, page: *Page) ![]const u8 { const encoded_len = std.base64.standard.Encoder.calcSize(input.len); const encoded = try page.call_arena.alloc(u8, encoded_len); @@ -268,6 +291,26 @@ pub fn atob(_: *const Window, input: []const u8, page: *Page) ![]const u8 { return decoded; } +pub fn getLength(_: *const Window) u32 { + return 0; +} + +pub fn getInnerWidth(_: *const Window) u32 { + return 1920; +} + +pub fn getInnerHeight(_: *const Window) u32 { + return 1080; +} + +pub fn getScrollX(_: *const Window) u32 { + return 0; +} + +pub fn getScrollY(_: *const Window) u32 { + return 0; +} + const ScheduleOpts = struct { repeat: bool, params: []js.Object, @@ -376,6 +419,35 @@ const ScheduleCallback = struct { } }; +const PostMessageCallback = struct { + window: *Window, + message: js.Object, + origin: []const u8, + page: *Page, + + fn deinit(self: *PostMessageCallback) void { + self.page._factory.destroy(self); + } + + fn run(ctx: *anyopaque) !?u32 { + const self: *PostMessageCallback = @ptrCast(@alignCast(ctx)); + defer self.deinit(); + + const message_event = try MessageEvent.init("message", .{ + .data = self.message, + .origin = self.origin, + .source = self.window, + .bubbles = false, + .cancelable = false, + }, self.page); + + const event = message_event.asEvent(); + try self.page._event_manager.dispatch(self.window.asEventTarget(), event); + + return null; + } +}; + pub const JsApi = struct { pub const bridge = js.Bridge(Window); @@ -415,27 +487,19 @@ pub const JsApi = struct { pub const requestAnimationFrame = bridge.function(Window.requestAnimationFrame, .{}); pub const cancelAnimationFrame = bridge.function(Window.cancelAnimationFrame, .{}); pub const matchMedia = bridge.function(Window.matchMedia, .{}); + pub const postMessage = bridge.function(Window.postMessage, .{}); pub const btoa = bridge.function(Window.btoa, .{}); pub const atob = bridge.function(Window.atob, .{}); pub const reportError = bridge.function(Window.reportError, .{}); pub const frames = bridge.accessor(Window.getWindow, null, .{ .cache = "frames" }); - pub const length = bridge.accessor(struct { - fn wrap(_: *const Window) u32 { - return 0; - } - }.wrap, null, .{ .cache = "length" }); - - pub const innerWidth = bridge.accessor(struct { - fn wrap(_: *const Window) u32 { - return 1920; - } - }.wrap, null, .{ .cache = "innerWidth" }); - pub const innerHeight = bridge.accessor(struct { - fn wrap(_: *const Window) u32 { - return 1080; - } - }.wrap, null, .{ .cache = "innerHeight" }); pub const getComputedStyle = bridge.function(Window.getComputedStyle, .{}); + pub const length = bridge.accessor(Window.getLength, null, .{ .cache = "length" }); + pub const innerWidth = bridge.accessor(Window.getInnerWidth, null, .{ .cache = "innerWidth" }); + pub const innerHeight = bridge.accessor(Window.getInnerHeight, null, .{ .cache = "innerHeight" }); + pub const scrollX = bridge.accessor(Window.getScrollX, null, .{ .cache = "scrollX" }); + pub const scrollY = bridge.accessor(Window.getScrollY, null, .{ .cache = "scrollY" }); + pub const pageXOffset = bridge.accessor(Window.getScrollX, null, .{ .cache = "pageXOffset" }); + pub const pageYOffset = bridge.accessor(Window.getScrollY, null, .{ .cache = "pageYOffset" }); }; const testing = @import("../../testing.zig"); diff --git a/src/browser/webapi/element/html/Custom.zig b/src/browser/webapi/element/html/Custom.zig index a8c95d5c8..3fc9071fa 100644 --- a/src/browser/webapi/element/html/Custom.zig +++ b/src/browser/webapi/element/html/Custom.zig @@ -73,6 +73,16 @@ pub fn invokeAttributeChangedCallback(self: *Custom, name: []const u8, old_value pub fn invokeConnectedCallbackOnElement(comptime from_parser: bool, element: *Element, page: *Page) !void { // Autonomous custom element if (element.is(Custom)) |custom| { + // If the element is undefined, check if a definition now exists and upgrade + if (custom._definition == null) { + const name = custom._tag_name.str(); + if (page.window._custom_elements._definitions.get(name)) |definition| { + const CustomElementRegistry = @import("../../CustomElementRegistry.zig"); + CustomElementRegistry.upgradeCustomElement(custom, definition, page) catch {}; + return; + } + } + if (comptime from_parser) { // From parser, we know the element is brand new custom._connected_callback_invoked = true; diff --git a/src/browser/webapi/event/MessageEvent.zig b/src/browser/webapi/event/MessageEvent.zig new file mode 100644 index 000000000..ed59bb2f4 --- /dev/null +++ b/src/browser/webapi/event/MessageEvent.zig @@ -0,0 +1,90 @@ +// 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"); +const Event = @import("../Event.zig"); +const Window = @import("../Window.zig"); + +const MessageEvent = @This(); + +_proto: *Event, +_data: ?js.Object = null, +_origin: []const u8 = "", +_source: ?*Window = null, + +pub const InitOptions = struct { + data: ?js.Object = null, + origin: ?[]const u8 = null, + source: ?*Window = null, + bubbles: bool = false, + cancelable: bool = false, +}; + +pub fn init(typ: []const u8, opts_: ?InitOptions, page: *Page) !*MessageEvent { + const opts = opts_ orelse InitOptions{}; + + const event = try page._factory.event(typ, MessageEvent{ + ._proto = undefined, + ._data = if (opts.data) |d| try d.persist() else null, + ._origin = if (opts.origin) |str| try page.arena.dupe(u8, str) else "", + ._source = opts.source, + }); + + event._proto._bubbles = opts.bubbles; + event._proto._cancelable = opts.cancelable; + + return event; +} + +pub fn asEvent(self: *MessageEvent) *Event { + return self._proto; +} + +pub fn getData(self: *const MessageEvent) ?js.Object { + return self._data; +} + +pub fn getOrigin(self: *const MessageEvent) []const u8 { + return self._origin; +} + +pub fn getSource(self: *const MessageEvent) ?*Window { + return self._source; +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(MessageEvent); + + pub const Meta = struct { + pub const name = "MessageEvent"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; + + pub const constructor = bridge.constructor(MessageEvent.init, .{}); + pub const data = bridge.accessor(MessageEvent.getData, null, .{}); + pub const origin = bridge.accessor(MessageEvent.getOrigin, null, .{}); + pub const source = bridge.accessor(MessageEvent.getSource, null, .{}); +}; + +const testing = @import("../../../testing.zig"); +test "WebApi: MessageEvent" { + try testing.htmlRunner("event/message.html", .{}); +} From 9f587ab24b0632e3b488d65ee97668cd0c787e97 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Fri, 28 Nov 2025 22:11:55 +0800 Subject: [PATCH 102/219] MessageChannel and MessagePort --- src/browser/EventManager.zig | 2 +- src/browser/Page.zig | 3 + src/browser/Scheduler.zig | 11 +- src/browser/ScriptManager.zig | 4 + src/browser/js/bridge.zig | 2 + src/browser/tests/message_channel.html | 86 +++++++++++++ src/browser/webapi/EventTarget.zig | 2 + src/browser/webapi/MessageChannel.zig | 66 ++++++++++ src/browser/webapi/MessagePort.zig | 169 +++++++++++++++++++++++++ 9 files changed, 343 insertions(+), 2 deletions(-) create mode 100644 src/browser/tests/message_channel.html create mode 100644 src/browser/webapi/MessageChannel.zig create mode 100644 src/browser/webapi/MessagePort.zig diff --git a/src/browser/EventManager.zig b/src/browser/EventManager.zig index 3eb02bae6..a138c44fc 100644 --- a/src/browser/EventManager.zig +++ b/src/browser/EventManager.zig @@ -117,7 +117,7 @@ pub fn dispatch(self: *EventManager, target: *EventTarget, event: *Event) !void switch (target._type) { .node => |node| try self.dispatchNode(node, event, &was_handled), - .xhr, .window, .abort_signal, .media_query_list => { + .xhr, .window, .abort_signal, .media_query_list, .message_port => { const list = self.lookup.getPtr(@intFromPtr(target)) orelse return; try self.dispatchAll(list, target, event, &was_handled); }, diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 37d947c49..f237c315f 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -702,6 +702,9 @@ fn _wait(self: *Page, wait_ms: u32) !Session.WaitResult { } pub fn tick(self: *Page) void { + if (comptime IS_DEBUG) { + log.debug(.page, "tick", .{}); + } _ = self.scheduler.run() catch |err| { log.err(.page, "tick", .{ .err = err }); }; diff --git a/src/browser/Scheduler.zig b/src/browser/Scheduler.zig index 6ad048877..78a7ca1e4 100644 --- a/src/browser/Scheduler.zig +++ b/src/browser/Scheduler.zig @@ -26,17 +26,22 @@ const IS_DEBUG = builtin.mode == .Debug; 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); + const time_order = std.math.order(a.run_at, b.run_at); + if (time_order != .eq) return time_order; + // Break ties with sequence number to maintain FIFO order + return std.math.order(a.sequence, b.sequence); } }.compare); const Scheduler = @This(); +_sequence: u64, low_priority: Queue, high_priority: Queue, pub fn init(allocator: std.mem.Allocator) Scheduler { return .{ + ._sequence = 0, .low_priority = Queue.init(allocator, {}), .high_priority = Queue.init(allocator, {}), }; @@ -59,9 +64,12 @@ pub fn add(self: *Scheduler, ctx: *anyopaque, cb: Callback, run_in_ms: u32, opts 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; + const seq = self._sequence + 1; + self._sequence = seq; return queue.add(.{ .ctx = ctx, .callback = cb, + .sequence = seq, .name = opts.name, .run_at = timestamp(.monotonic) + run_in_ms, }); @@ -105,6 +113,7 @@ fn runQueue(self: *Scheduler, queue: *Queue) !?u64 { const Task = struct { run_at: u64, + sequence: u64, ctx: *anyopaque, name: []const u8, callback: Callback, diff --git a/src/browser/ScriptManager.zig b/src/browser/ScriptManager.zig index a1df242e4..ca098e9f4 100644 --- a/src/browser/ScriptManager.zig +++ b/src/browser/ScriptManager.zig @@ -751,6 +751,10 @@ const Script = struct { break :blk true; }; + if (comptime IS_DEBUG) { + log.info(.browser, "executed script", .{.src = url}); + } + defer page.tick(); if (success) { diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index 68f5e4891..d3d983d4e 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -552,6 +552,8 @@ pub const JsApis = flattenTypes(&.{ @import("../webapi/event/ErrorEvent.zig"), @import("../webapi/event/MessageEvent.zig"), @import("../webapi/event/ProgressEvent.zig"), + @import("../webapi/MessageChannel.zig"), + @import("../webapi/MessagePort.zig"), @import("../webapi/EventTarget.zig"), @import("../webapi/Location.zig"), @import("../webapi/Navigator.zig"), diff --git a/src/browser/tests/message_channel.html b/src/browser/tests/message_channel.html new file mode 100644 index 000000000..0a9848f78 --- /dev/null +++ b/src/browser/tests/message_channel.html @@ -0,0 +1,86 @@ + + + + diff --git a/src/browser/webapi/EventTarget.zig b/src/browser/webapi/EventTarget.zig index 23ecdf985..e0d0acd16 100644 --- a/src/browser/webapi/EventTarget.zig +++ b/src/browser/webapi/EventTarget.zig @@ -34,6 +34,7 @@ pub const Type = union(enum) { xhr: *@import("net/XMLHttpRequestEventTarget.zig"), abort_signal: *@import("AbortSignal.zig"), media_query_list: *@import("css/MediaQueryList.zig"), + message_port: *@import("MessagePort.zig"), }; pub fn dispatchEvent(self: *EventTarget, event: *Event, page: *Page) !bool { @@ -101,6 +102,7 @@ pub fn format(self: *EventTarget, writer: *std.Io.Writer) !void { .xhr => writer.writeAll(""), .abort_signal => writer.writeAll(""), .media_query_list => writer.writeAll(""), + .message_port => writer.writeAll(""), }; } diff --git a/src/browser/webapi/MessageChannel.zig b/src/browser/webapi/MessageChannel.zig new file mode 100644 index 000000000..766310133 --- /dev/null +++ b/src/browser/webapi/MessageChannel.zig @@ -0,0 +1,66 @@ +// 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"); +const MessagePort = @import("MessagePort.zig"); + +const MessageChannel = @This(); + +_port1: *MessagePort, +_port2: *MessagePort, + +pub fn init(page: *Page) !*MessageChannel { + const port1 = try MessagePort.init(page); + const port2 = try MessagePort.init(page); + + MessagePort.entangle(port1, port2); + + return page._factory.create(MessageChannel{ + ._port1 = port1, + ._port2 = port2, + }); +} + + +pub fn getPort1(self: *const MessageChannel) *MessagePort { + return self._port1; +} + +pub fn getPort2(self: *const MessageChannel) *MessagePort { + return self._port2; +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(MessageChannel); + + pub const Meta = struct { + pub const name = "MessageChannel"; + pub var class_id: bridge.ClassId = undefined; + pub const prototype_chain = bridge.prototypeChain(); + }; + + pub const constructor = bridge.constructor(MessageChannel.init, .{}); + pub const port1 = bridge.accessor(MessageChannel.getPort1, null, .{}); + pub const port2 = bridge.accessor(MessageChannel.getPort2, null, .{}); +}; + +const testing = @import("../../testing.zig"); +test "WebApi: MessageChannel" { + try testing.htmlRunner("message_channel.html", .{}); +} diff --git a/src/browser/webapi/MessagePort.zig b/src/browser/webapi/MessagePort.zig new file mode 100644 index 000000000..65a7d36b9 --- /dev/null +++ b/src/browser/webapi/MessagePort.zig @@ -0,0 +1,169 @@ +// 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 log = @import("../../log.zig"); + +const Page = @import("../Page.zig"); +const EventTarget = @import("EventTarget.zig"); +const MessageEvent = @import("event/MessageEvent.zig"); + +const MessagePort = @This(); + +_proto: *EventTarget, +_enabled: bool = false, +_closed: bool = false, +_on_message: ?js.Function = null, +_on_message_error: ?js.Function = null, +_entangled_port: ?*MessagePort = null, + +pub fn init(page: *Page) !*MessagePort { + return page._factory.eventTarget(MessagePort{ + ._proto = undefined, + }); +} + +pub fn asEventTarget(self: *MessagePort) *EventTarget { + return self._proto; +} + +pub fn entangle(port1: *MessagePort, port2: *MessagePort) void { + port1._entangled_port = port2; + port2._entangled_port = port1; +} + +pub fn postMessage(self: *MessagePort, message: js.Object, page: *Page) !void { + if (self._closed) { + return; + } + + const other = self._entangled_port orelse return; + if (other._closed) { + return; + } + + // Create callback to deliver message + const callback = try page._factory.create(PostMessageCallback{ + .page = page, + .port = other, + .message = try message.persist(), + }); + + try page.scheduler.add(callback, PostMessageCallback.run, 0, .{ + .name = "MessagePort.postMessage", + .low_priority = false, + }); +} + +pub fn start(self: *MessagePort) void { + if (self._closed) { + return; + } + self._enabled = true; +} + +pub fn close(self: *MessagePort) void { + self._closed = true; + + // Break entanglement + if (self._entangled_port) |other| { + other._entangled_port = null; + } + self._entangled_port = null; +} + +pub fn getOnMessage(self: *const MessagePort) ?js.Function { + return self._on_message; +} + +pub fn setOnMessage(self: *MessagePort, cb_: ?js.Function) !void { + if (cb_) |cb| { + self._on_message = cb; + } else { + self._on_message = null; + } +} + +pub fn getOnMessageError(self: *const MessagePort) ?js.Function { + return self._on_message_error; +} + +pub fn setOnMessageError(self: *MessagePort, cb_: ?js.Function) !void { + if (cb_) |cb| { + self._on_message_error = cb; + } else { + self._on_message_error = null; + } +} + +const PostMessageCallback = struct { + port: *MessagePort, + message: js.Object, + page: *Page, + + fn deinit(self: *PostMessageCallback) void { + self.page._factory.destroy(self); + } + + fn run(ctx: *anyopaque) !?u32 { + const self: *PostMessageCallback = @ptrCast(@alignCast(ctx)); + defer self.deinit(); + + if (self.port._closed) { + return null; + } + + const event = MessageEvent.init("message", .{ + .data = self.message, + .origin = "", + .source = null, + }, self.page) catch |err| { + log.err(.dom, "MessagePort.postMessage", .{.err = err}); + return null; + }; + + self.page._event_manager.dispatchWithFunction( + self.port.asEventTarget(), + event.asEvent(), + self.port._on_message, + .{ .context = "MessagePort message" }, + ) catch |err| { + log.err(.dom, "MessagePort.postMessage", .{.err = err}); + }; + + return null; + } +}; + +pub const JsApi = struct { + pub const bridge = js.Bridge(MessagePort); + + pub const Meta = struct { + pub const name = "MessagePort"; + pub var class_id: bridge.ClassId = undefined; + pub const prototype_chain = bridge.prototypeChain(); + }; + + pub const postMessage = bridge.function(MessagePort.postMessage, .{}); + pub const start = bridge.function(MessagePort.start, .{}); + pub const close = bridge.function(MessagePort.close, .{}); + + pub const onmessage = bridge.accessor(MessagePort.getOnMessage, MessagePort.setOnMessage, .{}); + pub const onmessageerror = bridge.accessor(MessagePort.getOnMessageError, MessagePort.setOnMessageError, .{}); +}; From 0bc0a38704113b61c68e20212ab704d76df81bc0 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Tue, 25 Nov 2025 12:21:28 +0100 Subject: [PATCH 103/219] ci: update installation workflow --- .github/actions/install/action.yml | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/.github/actions/install/action.yml b/.github/actions/install/action.yml index e9864c01d..d347f11b3 100644 --- a/.github/actions/install/action.yml +++ b/.github/actions/install/action.yml @@ -66,29 +66,3 @@ runs: mkdir -p v8/out/${{ inputs.os }}/release/obj/zig/ ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a v8/out/${{ inputs.os }}/release/obj/zig/libc_v8.a - - - name: Cache libiconv - id: cache-libiconv - uses: actions/cache@v4 - env: - cache-name: cache-libiconv - with: - path: ${{ inputs.cache-dir }}/libiconv - key: vendor/libiconv/libiconv-1.17 - - - name: download libiconv - if: ${{ steps.cache-libiconv.outputs.cache-hit != 'true' }} - shell: bash - run: make download-libiconv - - - name: build libiconv - shell: bash - run: make build-libiconv - - - name: build mimalloc - shell: bash - run: make install-mimalloc - - - name: build netsurf - shell: bash - run: make install-netsurf From dbd500cab913f027194e26e5e67d150fe8cc912f Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Tue, 25 Nov 2025 12:24:28 +0100 Subject: [PATCH 104/219] update docker file --- Dockerfile | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 919a9a658..24936ffbf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -40,10 +40,6 @@ WORKDIR /browser RUN git submodule init && \ git submodule update --recursive -RUN make install-libiconv && \ - make install-netsurf && \ - make install-mimalloc - # download and install v8 RUN case $TARGETPLATFORM in \ "linux/arm64") ARCH="aarch64" ;; \ From a1064a54cc331687ccc69f59ce20772fe8baf311 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Tue, 25 Nov 2025 12:24:41 +0100 Subject: [PATCH 105/219] cleanup README --- README.md | 41 +++++------------------------------------ 1 file changed, 5 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 87c393a52..a51ba2980 100644 --- a/README.md +++ b/README.md @@ -140,13 +140,14 @@ You may still encounter errors or crashes. Please open an issue with specifics i Here are the key features we have implemented: -- [x] HTTP loader (based on Libcurl) -- [x] HTML parser and DOM tree (based on Netsurf libs) -- [x] Javascript support (v8) +- [x] HTTP loader ([Libcurl](https://curl.se/libcurl/)) +- [x] HTML parser ([html5ever](https://github.com/servo/html5ever)) +- [x] DOM tree +- [x] Javascript support ([v8](https://v8.dev/)) - [x] DOM APIs - [x] Ajax - [x] XHR API - - [x] Fetch API (polyfill) + - [x] Fetch API - [x] DOM dump - [x] CDP/websockets server - [x] Click @@ -214,38 +215,6 @@ To init or update the submodules in the `vendor/` directory: make install-submodule ``` -**iconv** - -libiconv is an internationalization library used by Netsurf. - -``` -make install-libiconv -``` - -**Netsurf libs** - -Netsurf libs are used for HTML parsing and DOM tree generation. - -``` -make install-netsurf -``` - -For dev env, use `make install-netsurf-dev`. - -**Mimalloc** - -Mimalloc is used as a C memory allocator. - -``` -make install-mimalloc -``` - -For dev env, use `make install-mimalloc-dev`. - -Note: when Mimalloc is built in dev mode, you can dump memory stats with the -env var `MIMALLOC_SHOW_STATS=1`. See -[https://microsoft.github.io/mimalloc/environment.html](https://microsoft.github.io/mimalloc/environment.html). - **v8** First, get the tools necessary for building V8, as well as the V8 source code: From 1e090f9d30184b96bded56b2c64ea101c027f29e Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Tue, 25 Nov 2025 12:39:40 +0100 Subject: [PATCH 106/219] add html5ever install method --- .github/actions/install/action.yml | 4 ++++ Dockerfile | 2 ++ README.md | 10 ++++++++++ 3 files changed, 16 insertions(+) diff --git a/.github/actions/install/action.yml b/.github/actions/install/action.yml index d347f11b3..2acb71b81 100644 --- a/.github/actions/install/action.yml +++ b/.github/actions/install/action.yml @@ -66,3 +66,7 @@ runs: mkdir -p v8/out/${{ inputs.os }}/release/obj/zig/ ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a v8/out/${{ inputs.os }}/release/obj/zig/libc_v8.a + + - name: build html5ever + shell: bash + run: make install-html5ever diff --git a/Dockerfile b/Dockerfile index 24936ffbf..76531df70 100644 --- a/Dockerfile +++ b/Dockerfile @@ -40,6 +40,8 @@ WORKDIR /browser RUN git submodule init && \ git submodule update --recursive +RUN make install-html5ever + # download and install v8 RUN case $TARGETPLATFORM in \ "linux/arm64") ARCH="aarch64" ;; \ diff --git a/README.md b/README.md index a51ba2980..f40eab57d 100644 --- a/README.md +++ b/README.md @@ -215,6 +215,16 @@ To init or update the submodules in the `vendor/` directory: make install-submodule ``` +**html5ever** + +[html5ver](https://github.com/servo/html5ever) is high-performance browser-grade HTML5 parser. + +``` +make install-html5ever +``` + +For dev env, use `make install-html5ever-dev`. + **v8** First, get the tools necessary for building V8, as well as the V8 source code: From e74a286d7053e2240e2452cde62f17516d929913 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Tue, 25 Nov 2025 12:50:01 +0100 Subject: [PATCH 107/219] ci: add install-html5ever-dev --- .github/actions/install/action.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/actions/install/action.yml b/.github/actions/install/action.yml index 2acb71b81..db615ed45 100644 --- a/.github/actions/install/action.yml +++ b/.github/actions/install/action.yml @@ -70,3 +70,7 @@ runs: - name: build html5ever shell: bash run: make install-html5ever + + - name: build html5ever dev + shell: bash + run: make install-html5ever-dev From bde8b64ba3ebfe803693bd0d452afddc1e73fc76 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Wed, 26 Nov 2025 16:19:16 +0100 Subject: [PATCH 108/219] update html5ever instructions --- .github/actions/install/action.yml | 27 +++++++++++++++++++-------- .github/workflows/build.yml | 4 ++++ .github/workflows/e2e-test.yml | 2 ++ Dockerfile | 2 +- README.md | 4 ++-- 5 files changed, 28 insertions(+), 11 deletions(-) diff --git a/.github/actions/install/action.yml b/.github/actions/install/action.yml index db615ed45..258170c7b 100644 --- a/.github/actions/install/action.yml +++ b/.github/actions/install/action.yml @@ -26,6 +26,10 @@ inputs: description: 'cache dir to use' required: false default: '~/.cache' + mode: + description: 'debug or release' + required: false + default: 'debug' runs: using: "composite" @@ -58,19 +62,26 @@ runs: wget -O ${{ inputs.cache-dir }}/v8/libc_v8.a https://github.com/lightpanda-io/zig-v8-fork/releases/download/${{ inputs.zig-v8 }}/libc_v8_${{ inputs.v8 }}_${{ inputs.os }}_${{ inputs.arch }}.a - - name: install v8 + - name: install v8 release + if: ${{ inputs.mode == 'release' }} shell: bash run: | - mkdir -p v8/out/${{ inputs.os }}/debug/obj/zig/ - ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a v8/out/${{ inputs.os }}/debug/obj/zig/libc_v8.a - mkdir -p v8/out/${{ inputs.os }}/release/obj/zig/ ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a v8/out/${{ inputs.os }}/release/obj/zig/libc_v8.a - - name: build html5ever + - name: install v8 debug + if: ${{ inputs.mode == 'debug' }} + shell: bash + run: | + mkdir -p v8/out/${{ inputs.os }}/debug/obj/zig/ + ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a v8/out/${{ inputs.os }}/debug/obj/zig/libc_v8.a + + - name: hmtl5ever release + if: ${{ inputs.mode == 'release' }} shell: bash - run: make install-html5ever + run: zig -Doptimize=ReleaseSafe build html5ever - - name: build html5ever dev + - name: hmtl5ever debug + if: ${{ inputs.mode == 'debug' }} shell: bash - run: make install-html5ever-dev + run: zig build html5ever diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index df16af4c9..0ab034e84 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -36,6 +36,7 @@ jobs: with: os: ${{env.OS}} arch: ${{env.ARCH}} + mode: 'release' - name: zig build run: zig build --release=safe -Doptimize=ReleaseSafe -Dcpu=x86_64 -Dgit_commit=$(git rev-parse --short ${{ github.sha }}) @@ -74,6 +75,7 @@ jobs: with: os: ${{env.OS}} arch: ${{env.ARCH}} + mode: 'release' - name: zig build run: zig build --release=safe -Doptimize=ReleaseSafe -Dcpu=generic -Dgit_commit=$(git rev-parse --short ${{ github.sha }}) @@ -114,6 +116,7 @@ jobs: with: os: ${{env.OS}} arch: ${{env.ARCH}} + mode: 'release' - name: zig build run: zig build --release=safe -Doptimize=ReleaseSafe -Dgit_commit=$(git rev-parse --short ${{ github.sha }}) @@ -157,6 +160,7 @@ jobs: with: os: ${{env.OS}} arch: ${{env.ARCH}} + mode: 'release' - name: zig build run: zig build --release=safe -Doptimize=ReleaseSafe -Dgit_commit=$(git rev-parse --short ${{ github.sha }}) diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index fb295246c..992c8b2a9 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -56,6 +56,8 @@ jobs: submodules: recursive - uses: ./.github/actions/install + with: + mode: 'release' - name: zig build release run: zig build -Doptimize=ReleaseFast -Dcpu=x86_64 -Dgit_commit=$(git rev-parse --short ${{ github.sha }}) diff --git a/Dockerfile b/Dockerfile index 76531df70..6f0b2936c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -40,7 +40,7 @@ WORKDIR /browser RUN git submodule init && \ git submodule update --recursive -RUN make install-html5ever +RUN zig build -Doptimize=ReleaseFast html5ever # download and install v8 RUN case $TARGETPLATFORM in \ diff --git a/README.md b/README.md index f40eab57d..5e25926ab 100644 --- a/README.md +++ b/README.md @@ -220,10 +220,10 @@ make install-submodule [html5ver](https://github.com/servo/html5ever) is high-performance browser-grade HTML5 parser. ``` -make install-html5ever +zig build html5ever ``` -For dev env, use `make install-html5ever-dev`. +For a release build, use `zig build -Doptimize=ReleaseFast html5ever`. **v8** From 613428c54c0f12485af1b08815f71202530fa008 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Sun, 30 Nov 2025 12:48:15 +0800 Subject: [PATCH 109/219] Execute script.onload/onerror Add object-support for URLSearchParams. Start to treat js.Value as a first class object (instead of js.Object, where appropriate). --- src/browser/ScriptManager.zig | 39 ++++++++--- src/browser/js/Array.zig | 38 ++++++++++ src/browser/js/Context.zig | 25 +++++-- src/browser/js/Object.zig | 29 +++++--- src/browser/js/Value.zig | 74 ++++++++++++++++++++ src/browser/js/js.zig | 54 +------------- src/browser/tests/net/url_search_params.html | 2 +- src/browser/webapi/CustomElementRegistry.zig | 7 +- src/browser/webapi/net/Fetch.zig | 4 ++ src/browser/webapi/net/URLSearchParams.zig | 38 ++++++++-- 10 files changed, 226 insertions(+), 84 deletions(-) create mode 100644 src/browser/js/Array.zig create mode 100644 src/browser/js/Value.zig diff --git a/src/browser/ScriptManager.zig b/src/browser/ScriptManager.zig index ca098e9f4..bc5468bd9 100644 --- a/src/browser/ScriptManager.zig +++ b/src/browser/ScriptManager.zig @@ -726,10 +726,10 @@ const Script = struct { .kind = self.kind, .cacheable = cacheable, }); - self.executeCallback(script_element._on_error, page); + self.executeCallback("error", script_element._on_error, page); return; }; - self.executeCallback(script_element._on_load, page); + self.executeCallback("load", script_element._on_load, page); return; } @@ -752,13 +752,17 @@ const Script = struct { }; if (comptime IS_DEBUG) { - log.info(.browser, "executed script", .{.src = url}); + log.debug(.browser, "executed script", .{ + .src = url, + .success = success, + .on_load = script_element._on_load != null + }); } defer page.tick(); if (success) { - self.executeCallback(script_element._on_load, page); + self.executeCallback("load", script_element._on_load, page); return; } @@ -776,16 +780,31 @@ const Script = struct { .cacheable = cacheable, }); - self.executeCallback(script_element._on_error, page); + self.executeCallback("error", script_element._on_error, page); } - fn executeCallback(self: *const Script, cb_: ?js.Function, page: *Page) void { + fn executeCallback(self: *const Script, comptime typ: []const u8, cb_: ?js.Function, page: *Page) void { const cb = cb_ orelse return; - // @ZIGDOM execute the callback - _ = cb; - _ = self; - _ = page; + const Event = @import("webapi/Event.zig"); + const event = Event.init(typ, .{}, page) catch |err| { + log.warn(.js, "script internal callback", .{ + .url = self.url, + .type = typ, + .err = err, + }); + return; + }; + + var result: js.Function.Result = undefined; + cb.tryCall(void, .{event}, &result) catch { + log.warn(.js, "script callback", .{ + .url = self.url, + .type = typ, + .err = result.exception, + .stack = result.stack, + }); + }; } }; diff --git a/src/browser/js/Array.zig b/src/browser/js/Array.zig new file mode 100644 index 000000000..95bc0e32d --- /dev/null +++ b/src/browser/js/Array.zig @@ -0,0 +1,38 @@ +// 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.zig"); +const v8 = js.v8; + +const Array = @This(); +js_arr: v8.Array, +context: *js.Context, + +pub fn len(self: Array) usize { + return @intCast(self.js_arr.length()); +} + +pub fn get(self: Array, index: usize) !js.Value { + const idx_key = v8.Integer.initU32(self.context.isolate, @intCast(index)); + const js_obj = self.js_arr.castTo(v8.Object); + return .{ + .context = self.context, + .js_val = try js_obj.getValue(self.context.v8_context, idx_key.toValue()), + }; +} diff --git a/src/browser/js/Context.zig b/src/browser/js/Context.zig index a2f358dc4..eea764b21 100644 --- a/src/browser/js/Context.zig +++ b/src/browser/js/Context.zig @@ -392,9 +392,9 @@ pub fn createException(self: *const Context, e: v8.Value) js.Exception { // Wrap a v8.Value, largely so that we can provide a convenient // toString function -pub fn createValue(self: *const Context, value: v8.Value) js.Value { +pub fn createValue(self: *Context, value: v8.Value) js.Value { return .{ - .value = value, + .js_val = value, .context = self, }; } @@ -665,8 +665,7 @@ pub fn mapZigInstanceToJs(self: *Context, js_obj_: ?v8.Object, value: anytype) ! pub fn jsValueToZig(self: *Context, comptime T: type, js_value: v8.Value) !T { switch (@typeInfo(T)) { .optional => |o| { - if (comptime o.child == js.Object) { - // If type type is a ?js.Object, then we want to pass + // If type type is a ?js.Value or a ?js.Object, then we want to pass // a js.Object, not null. Consider a function, // _doSomething(arg: ?Env.JsObjet) void { ... } // @@ -681,6 +680,14 @@ pub fn jsValueToZig(self: *Context, comptime T: type, js_value: v8.Value) !T { // pass in `null` and the the doSomething won't // be able to tell if `null` was explicitly passed // or whether no parameter was passed. + if (comptime o.child == js.Value) { + return js.Value{ + .context = self, + .js_val = js_value, + }; + } + + if (comptime o.child == js.Object) { return js.Object{ .context = self, .js_obj = js_value.castTo(v8.Object), @@ -831,6 +838,16 @@ fn jsValueToStruct(self: *Context, comptime T: type, js_value: v8.Value) !?T { return .{ .string = try self.valueToString(js_value, .{ .allocator = self.arena }) }; } + + if (comptime T == js.Value) { + // Caller wants an opaque js.Object. Probably a parameter + // that it needs to pass back into a callback + return js.Value{ + .context = self, + .js_val = js_value, + }; + } + const js_obj = js_value.castTo(v8.Object); if (comptime T == js.Object) { diff --git a/src/browser/js/Object.zig b/src/browser/js/Object.zig index 53bcafe7a..222f2b752 100644 --- a/src/browser/js/Object.zig +++ b/src/browser/js/Object.zig @@ -135,7 +135,7 @@ pub fn isNullOrUndefined(self: Object) bool { return self.js_obj.toValue().isNullOrUndefined(); } -pub fn nameIterator(self: Object) js.ValueIterator { +pub fn nameIterator(self: Object, allocator: Allocator) NameIterator { const context = self.context; const js_obj = self.js_obj; @@ -145,6 +145,7 @@ pub fn nameIterator(self: Object) js.ValueIterator { return .{ .count = count, .context = context, + .allocator = allocator, .js_obj = array.castTo(v8.Object), }; } @@ -153,10 +154,22 @@ pub fn toZig(self: Object, comptime T: type) !T { return self.context.jsValueToZig(T, self.js_obj.toValue()); } -pub fn TriState(comptime T: type) type { - return union(enum) { - null: void, - undefined: void, - value: T, - }; -} +pub const NameIterator = struct { + count: u32, + idx: u32 = 0, + js_obj: v8.Object, + allocator: Allocator, + context: *const Context, + + pub fn next(self: *NameIterator) !?[]const u8 { + const idx = self.idx; + if (idx == self.count) { + return null; + } + self.idx += 1; + + const context = self.context; + const js_val = try self.js_obj.getAtIndex(context.v8_context, idx); + return try context.valueToString(js_val, .{ .allocator = self.allocator }); + } +}; diff --git a/src/browser/js/Value.zig b/src/browser/js/Value.zig new file mode 100644 index 000000000..143d221a4 --- /dev/null +++ b/src/browser/js/Value.zig @@ -0,0 +1,74 @@ +// 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.zig"); + +const v8 = js.v8; + +const Allocator = std.mem.Allocator; + +const Value = @This(); +js_val: v8.Value, +context: *js.Context, + +pub fn isObject(self: Value) bool { + return self.js_val.isObject(); +} + +pub fn isString(self: Value) bool { + return self.js_val.isString(); +} + +pub fn isArray(self: Value) bool { + return self.js_val.isArray(); +} + +pub fn toString(self: Value, allocator: Allocator) ![]const u8 { + return self.context.valueToString(self.js_val, .{ .allocator = allocator }); +} + +pub fn toObject(self: Value) js.Object { + return .{ + .context = self.context, + .js_obj = self.js_val.castTo(v8.Object), + }; +} + +pub fn toArray(self: Value) js.Array { + return .{ + .context = self.context, + .js_arr = self.js_val.castTo(v8.Array), + }; +} + +// pub const Value = struct { +// value: v8.Value, +// context: *const Context, + +// // the caller needs to deinit the string returned +// pub fn toString(self: Value, allocator: Allocator) ![]const u8 { +// return self.context.valueToString(self.value, .{ .allocator = allocator }); +// } + +// pub fn fromJson(ctx: *Context, json: []const u8) !Value { +// const json_string = v8.String.initUtf8(ctx.isolate, json); +// const value = try v8.Json.parse(ctx.v8_context, json_string); +// return Value{ .context = ctx, .value = value }; +// } +// }; diff --git a/src/browser/js/js.zig b/src/browser/js/js.zig index 71e192865..4f993d8de 100644 --- a/src/browser/js/js.zig +++ b/src/browser/js/js.zig @@ -29,6 +29,8 @@ pub const Inspector = @import("Inspector.zig"); // TODO: Is "This" really necessary? pub const This = @import("This.zig"); +pub const Value = @import("Value.zig"); +pub const Array = @import("Array.zig"); pub const Object = @import("Object.zig"); pub const TryCatch = @import("TryCatch.zig"); pub const Function = @import("Function.zig"); @@ -150,58 +152,6 @@ pub const Exception = struct { } }; -pub const Value = struct { - value: v8.Value, - context: *const Context, - - // the caller needs to deinit the string returned - pub fn toString(self: Value, allocator: Allocator) ![]const u8 { - return self.context.valueToString(self.value, .{ .allocator = allocator }); - } - - pub fn fromJson(ctx: *Context, json: []const u8) !Value { - const json_string = v8.String.initUtf8(ctx.isolate, json); - const value = try v8.Json.parse(ctx.v8_context, json_string); - return Value{ .context = ctx, .value = value }; - } - - pub fn isArray(self: Value) bool { - return self.value.isArray(); - } - - pub fn arrayLength(self: Value) u32 { - std.debug.assert(self.value.isArray()); - return self.value.castTo(v8.Array).length(); - } - - pub fn arrayGet(self: Value, index: u32) !Value { - std.debug.assert(self.value.isArray()); - const array_obj = self.value.castTo(v8.Array).castTo(v8.Object); - const idx_key = v8.Integer.initU32(self.context.isolate, index); - const elem_val = try array_obj.getValue(self.context.v8_context, idx_key.toValue()); - return self.context.createValue(elem_val); - } -}; - -pub const ValueIterator = struct { - count: u32, - idx: u32 = 0, - js_obj: v8.Object, - context: *const Context, - - pub fn next(self: *ValueIterator) !?Value { - const idx = self.idx; - if (idx == self.count) { - return null; - } - self.idx += 1; - - const context = self.context; - const js_val = try self.js_obj.getAtIndex(context.v8_context, idx); - return context.createValue(js_val); - } -}; - pub fn UndefinedOr(comptime T: type) type { return union(enum) { undefined: void, diff --git a/src/browser/tests/net/url_search_params.html b/src/browser/tests/net/url_search_params.html index bece1c646..54b66b3d3 100644 --- a/src/browser/tests/net/url_search_params.html +++ b/src/browser/tests/net/url_search_params.html @@ -21,7 +21,7 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/browser/webapi/MessageChannel.zig b/src/browser/webapi/MessageChannel.zig index 766310133..d43ba7dfc 100644 --- a/src/browser/webapi/MessageChannel.zig +++ b/src/browser/webapi/MessageChannel.zig @@ -37,7 +37,6 @@ pub fn init(page: *Page) !*MessageChannel { }); } - pub fn getPort1(self: *const MessageChannel) *MessagePort { return self._port1; } diff --git a/src/browser/webapi/MessagePort.zig b/src/browser/webapi/MessagePort.zig index 65a7d36b9..4d72a9342 100644 --- a/src/browser/webapi/MessagePort.zig +++ b/src/browser/webapi/MessagePort.zig @@ -134,7 +134,7 @@ const PostMessageCallback = struct { .origin = "", .source = null, }, self.page) catch |err| { - log.err(.dom, "MessagePort.postMessage", .{.err = err}); + log.err(.dom, "MessagePort.postMessage", .{ .err = err }); return null; }; @@ -144,7 +144,7 @@ const PostMessageCallback = struct { self.port._on_message, .{ .context = "MessagePort message" }, ) catch |err| { - log.err(.dom, "MessagePort.postMessage", .{.err = err}); + log.err(.dom, "MessagePort.postMessage", .{ .err = err }); }; return null; diff --git a/src/browser/webapi/Window.zig b/src/browser/webapi/Window.zig index ba3683b6c..84fae9f8a 100644 --- a/src/browser/webapi/Window.zig +++ b/src/browser/webapi/Window.zig @@ -265,13 +265,12 @@ pub fn postMessage(self: *Window, message: js.Object, target_origin: ?[]const u8 const origin = try self._location.getOrigin(page); const callback = try page._factory.create(PostMessageCallback{ .window = self, - .message = try message.persist() , + .message = try message.persist(), .origin = try page.arena.dupe(u8, origin), .page = page, }); errdefer page._factory.destroy(callback); - try page.scheduler.add(callback, PostMessageCallback.run, 0, .{ .name = "postMessage", .low_priority = false, diff --git a/src/browser/webapi/net/Response.zig b/src/browser/webapi/net/Response.zig index bc66fb001..9e79072ba 100644 --- a/src/browser/webapi/net/Response.zig +++ b/src/browser/webapi/net/Response.zig @@ -21,6 +21,7 @@ const js = @import("../../js/js.zig"); const Page = @import("../../Page.zig"); const Headers = @import("Headers.zig"); +const ReadableStream = @import("../streams/ReadableStream.zig"); const Allocator = std.mem.Allocator; const Response = @This(); @@ -28,7 +29,7 @@ const Response = @This(); _status: u16, _arena: Allocator, _headers: *Headers, -_body: []const u8, +_body: ?[]const u8, const InitOpts = struct { status: u16 = 200, @@ -39,10 +40,13 @@ const InitOpts = struct { pub fn init(body_: ?[]const u8, opts_: ?InitOpts, page: *Page) !*Response { const opts = opts_ orelse InitOpts{}; + // Store empty string as empty string, not null + const body = if (body_) |b| try page.arena.dupe(u8, b) else null; + return page._factory.create(Response{ ._arena = page.arena, ._status = opts.status, - ._body = if (body_) |b| try page.arena.dupe(u8, b) else "", + ._body = body, ._headers = opts.headers orelse try Headers.init(page), }); } @@ -55,15 +59,34 @@ pub fn getHeaders(self: *const Response) *Headers { return self._headers; } +pub fn getBody(self: *const Response, page: *Page) !?*ReadableStream { + const body = self._body orelse return null; + + // Empty string should create a closed stream with no data + if (body.len == 0) { + const stream = try ReadableStream.init(page); + try stream._controller.close(); + return stream; + } + + return ReadableStream.initWithData(body, page); +} + pub fn isOK(self: *const Response) bool { return self._status >= 200 and self._status <= 299; } +pub fn getText(self: *const Response, page: *Page) !js.Promise { + const body = self._body orelse ""; + return page.js.resolvePromise(body); +} + pub fn getJson(self: *Response, page: *Page) !js.Promise { + const body = self._body orelse ""; const value = std.json.parseFromSliceLeaky( std.json.Value, page.call_arena, - self._body, + body, .{}, ) catch |err| { return page.js.rejectPromise(.{@errorName(err)}); @@ -83,6 +106,8 @@ pub const JsApi = struct { pub const constructor = bridge.constructor(Response.init, .{}); pub const ok = bridge.accessor(Response.isOK, null, .{}); pub const status = bridge.accessor(Response.getStatus, null, .{}); + pub const text = bridge.function(Response.getText, .{}); pub const json = bridge.function(Response.getJson, .{}); pub const headers = bridge.accessor(Response.getHeaders, null, .{}); + pub const body = bridge.accessor(Response.getBody, null, .{}); }; diff --git a/src/browser/webapi/net/URLSearchParams.zig b/src/browser/webapi/net/URLSearchParams.zig index 8012f23b1..9bdecd2ec 100644 --- a/src/browser/webapi/net/URLSearchParams.zig +++ b/src/browser/webapi/net/URLSearchParams.zig @@ -45,13 +45,13 @@ pub fn init(opts_: ?InitOpts, page: *Page) !*URLSearchParams { .query_string => |qs| break :blk try paramsFromString(arena, qs, &page.buf), .value => |js_val| { if (js_val.isObject()) { - break :blk try paramsFromObject(arena, js_val.toObject()); + break :blk try paramsFromObject(arena, js_val.toObject()); } if (js_val.isString()) { break :blk try paramsFromString(arena, try js_val.toString(arena), &page.buf); } return error.InvalidArgument; - } + }, } }; diff --git a/src/browser/webapi/streams/ReadableStream.zig b/src/browser/webapi/streams/ReadableStream.zig new file mode 100644 index 000000000..eec8cb943 --- /dev/null +++ b/src/browser/webapi/streams/ReadableStream.zig @@ -0,0 +1,140 @@ +// 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 ReadableStreamDefaultReader = @import("ReadableStreamDefaultReader.zig"); +const ReadableStreamDefaultController = @import("ReadableStreamDefaultController.zig"); + +pub fn registerTypes() []const type { + return &.{ + ReadableStream, + AsyncIterator, + }; +} + +const ReadableStream = @This(); + +pub const State = enum { + readable, + closed, + errored, +}; + +_page: *Page, +_state: State, +_reader: ?*ReadableStreamDefaultReader, +_controller: *ReadableStreamDefaultController, +_stored_error: ?[]const u8, + +pub fn init(page: *Page) !*ReadableStream { + const stream = try page._factory.create(ReadableStream{ + ._page = page, + ._state = .readable, + ._reader = null, + ._controller = undefined, + ._stored_error = null, + }); + + stream._controller = try ReadableStreamDefaultController.init(stream, page); + return stream; +} + +pub fn initWithData(data: []const u8, page: *Page) !*ReadableStream { + const stream = try init(page); + + // For Phase 1: immediately enqueue all data and close + try stream._controller.enqueue(data); + try stream._controller.close(); + + return stream; +} + +pub fn getReader(self: *ReadableStream, page: *Page) !*ReadableStreamDefaultReader { + if (self._reader != null) { + return error.ReaderLocked; + } + + const reader = try ReadableStreamDefaultReader.init(self, page); + self._reader = reader; + return reader; +} + +pub fn releaseReader(self: *ReadableStream) void { + self._reader = null; +} + +pub fn getAsyncIterator(self: *ReadableStream, page: *Page) !*AsyncIterator { + return AsyncIterator.init(self, page); +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(ReadableStream); + + pub const Meta = struct { + pub const name = "ReadableStream"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; + + pub const constructor = bridge.constructor(ReadableStream.init, .{}); + pub const getReader = bridge.function(ReadableStream.getReader, .{}); + pub const symbol_async_iterator = bridge.iterator(ReadableStream.getAsyncIterator, .{ .async = true }); +}; + +pub const AsyncIterator = struct { + _stream: *ReadableStream, + _reader: *ReadableStreamDefaultReader, + + pub fn init(stream: *ReadableStream, page: *Page) !*AsyncIterator { + const reader = try stream.getReader(page); + return page._factory.create(AsyncIterator{ + ._reader = reader, + ._stream = stream, + }); + } + + pub fn next(self: *AsyncIterator, page: *Page) !js.Promise { + return self._reader.read(page); + } + + pub fn @"return"(self: *AsyncIterator, page: *Page) !js.Promise { + self._reader.releaseLock(); + return page.js.resolvePromise(.{ .done = true, .value = null }); + } + + pub const JsApi = struct { + pub const bridge = js.Bridge(ReadableStream.AsyncIterator); + + pub const Meta = struct { + pub const name = "ReadableStreamAsyncIterator"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; + + pub const next = bridge.function(ReadableStream.AsyncIterator.next, .{}); + pub const @"return" = bridge.function(ReadableStream.AsyncIterator.@"return", .{}); + }; +}; + +const testing = @import("../../../testing.zig"); +test "WebApi: ReadableStream" { + try testing.htmlRunner("streams/readable_stream.html", .{}); +} diff --git a/src/browser/webapi/streams/ReadableStreamDefaultController.zig b/src/browser/webapi/streams/ReadableStreamDefaultController.zig new file mode 100644 index 000000000..876f546a9 --- /dev/null +++ b/src/browser/webapi/streams/ReadableStreamDefaultController.zig @@ -0,0 +1,100 @@ +// 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 ReadableStream = @import("ReadableStream.zig"); + +const ReadableStreamDefaultController = @This(); + +_page: *Page, +_stream: *ReadableStream, +_arena: std.mem.Allocator, +_queue: std.ArrayList([]const u8), + +pub fn init(stream: *ReadableStream, page: *Page) !*ReadableStreamDefaultController { + return page._factory.create(ReadableStreamDefaultController{ + ._page = page, + ._stream = stream, + ._arena = page.arena, + ._queue = std.ArrayList([]const u8){}, + }); +} + +pub fn enqueue(self: *ReadableStreamDefaultController, chunk: []const u8) !void { + if (self._stream._state != .readable) { + return error.StreamNotReadable; + } + + // Store a copy of the chunk in the page arena + const chunk_copy = try self._page.arena.dupe(u8, chunk); + try self._queue.append(self._arena, chunk_copy); +} + +pub fn close(self: *ReadableStreamDefaultController) !void { + if (self._stream._state != .readable) { + return error.StreamNotReadable; + } + + self._stream._state = .closed; +} + +pub fn doError(self: *ReadableStreamDefaultController, err: []const u8) !void { + if (self._stream._state != .readable) { + return; + } + + self._stream._state = .errored; + self._stream._stored_error = try self._page.arena.dupe(u8, err); +} + +pub fn dequeue(self: *ReadableStreamDefaultController) ?[]const u8 { + if (self._queue.items.len == 0) { + return null; + } + return self._queue.orderedRemove(0); +} + +pub fn getDesiredSize(self: *const ReadableStreamDefaultController) ?i32 { + switch (self._stream._state) { + .errored => return null, + .closed => return 0, + .readable => { + // For now, just report based on queue size + // In a real implementation, this would use highWaterMark + return @as(i32, 1) - @as(i32, @intCast(self._queue.items.len)); + }, + } +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(ReadableStreamDefaultController); + + pub const Meta = struct { + pub const name = "ReadableStreamDefaultController"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; + + pub const enqueue = bridge.function(ReadableStreamDefaultController.enqueue, .{}); + pub const close = bridge.function(ReadableStreamDefaultController.close, .{}); + pub const @"error" = bridge.function(ReadableStreamDefaultController.doError, .{}); + pub const desiredSize = bridge.accessor(ReadableStreamDefaultController.getDesiredSize, null, .{}); +}; diff --git a/src/browser/webapi/streams/ReadableStreamDefaultReader.zig b/src/browser/webapi/streams/ReadableStreamDefaultReader.zig new file mode 100644 index 000000000..7a531a3b8 --- /dev/null +++ b/src/browser/webapi/streams/ReadableStreamDefaultReader.zig @@ -0,0 +1,107 @@ +// 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 ReadableStream = @import("ReadableStream.zig"); + +const ReadableStreamDefaultReader = @This(); + +_page: *Page, +_stream: ?*ReadableStream, + +pub fn init(stream: *ReadableStream, page: *Page) !*ReadableStreamDefaultReader { + return page._factory.create(ReadableStreamDefaultReader{ + ._stream = stream, + ._page = page, + }); +} + +pub const ReadResult = struct { + done: bool, + value: ?js.TypedArray(u8), +}; + +pub fn read(self: *ReadableStreamDefaultReader, page: *Page) !js.Promise { + const stream = self._stream orelse { + return page.js.rejectPromise("Reader has been released"); + }; + + if (stream._state == .errored) { + const err = stream._stored_error orelse "Stream errored"; + return page.js.rejectPromise(err); + } + + if (stream._controller.dequeue()) |chunk| { + const result = ReadResult{ + .done = false, + .value = js.TypedArray(u8){ .values = chunk }, + }; + return page.js.resolvePromise(result); + } + + if (stream._state == .closed) { + const result = ReadResult{ + .value = null, + .done = true, + }; + return page.js.resolvePromise(result); + } + + const result = ReadResult{ + .done = true, + .value = null, + }; + return page.js.resolvePromise(result); +} + +pub fn releaseLock(self: *ReadableStreamDefaultReader) void { + if (self._stream) |stream| { + stream.releaseReader(); + self._stream = null; + } +} + +pub fn cancel(self: *ReadableStreamDefaultReader, reason_: ?[]const u8, page: *Page) !js.Promise { + const stream = self._stream orelse { + return page.js.rejectPromise("Reader has been released"); + }; + + const reason = reason_ orelse "canceled"; + + try stream._controller.doError(reason); + self.releaseLock(); + + return page.js.resolvePromise(.{}); +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(ReadableStreamDefaultReader); + + pub const Meta = struct { + pub const name = "ReadableStreamDefaultReader"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; + + pub const read = bridge.function(ReadableStreamDefaultReader.read, .{}); + pub const cancel = bridge.function(ReadableStreamDefaultReader.cancel, .{}); + pub const releaseLock = bridge.function(ReadableStreamDefaultReader.releaseLock, .{}); +}; From 92572c977be2f10550c31f7d9df51d4f28fd91a6 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Sat, 29 Nov 2025 15:11:15 +0100 Subject: [PATCH 111/219] update zig-v8 version --- .github/actions/install/action.yml | 2 +- Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/install/action.yml b/.github/actions/install/action.yml index 258170c7b..9f73944e7 100644 --- a/.github/actions/install/action.yml +++ b/.github/actions/install/action.yml @@ -17,7 +17,7 @@ inputs: zig-v8: description: 'zig v8 version to install' required: false - default: 'v0.1.33' + default: 'v0.1.35' v8: description: 'v8 version to install' required: false diff --git a/Dockerfile b/Dockerfile index 6f0b2936c..a405a057c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ ARG MINISIG=0.12 ARG ZIG=0.15.2 ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U ARG V8=14.0.365.4 -ARG ZIG_V8=v0.1.33 +ARG ZIG_V8=v0.1.35 ARG TARGETPLATFORM RUN apt-get update -yq && \ From f968db63e9507d1bda84030f7f0ef9685a13c307 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Sat, 29 Nov 2025 15:25:19 +0100 Subject: [PATCH 112/219] ci: use setup-zig v2.0.5 --- .github/actions/install/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/install/action.yml b/.github/actions/install/action.yml index 9f73944e7..1d3006418 100644 --- a/.github/actions/install/action.yml +++ b/.github/actions/install/action.yml @@ -42,7 +42,7 @@ runs: sudo apt-get update sudo apt-get install -y wget xz-utils python3 ca-certificates git pkg-config libglib2.0-dev gperf libexpat1-dev cmake clang - - uses: mlugg/setup-zig@v2 + - uses: mlugg/setup-zig@v2.0.5 with: version: ${{ inputs.zig }} From c9b9ef993411705aa3dbd6979553f0be4163af98 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Mon, 1 Dec 2025 08:38:56 +0100 Subject: [PATCH 113/219] ci: build html5ever typo --- .github/actions/install/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/install/action.yml b/.github/actions/install/action.yml index 1d3006418..e0ca3c02c 100644 --- a/.github/actions/install/action.yml +++ b/.github/actions/install/action.yml @@ -79,7 +79,7 @@ runs: - name: hmtl5ever release if: ${{ inputs.mode == 'release' }} shell: bash - run: zig -Doptimize=ReleaseSafe build html5ever + run: zig build -Doptimize=ReleaseSafe html5ever - name: hmtl5ever debug if: ${{ inputs.mode == 'debug' }} From d18253d50b2c4fe9153bff3aacd037c68a0032f3 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Mon, 1 Dec 2025 08:42:42 +0100 Subject: [PATCH 114/219] fix import for rename CSS.zig insto css.zig --- src/browser/js/bridge.zig | 2 +- src/browser/webapi/Element.zig | 2 +- src/browser/webapi/Window.zig | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index d3d983d4e..748dbc8e4 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -485,7 +485,7 @@ pub const JsApis = flattenTypes(&.{ @import("../webapi/collections.zig"), @import("../webapi/Console.zig"), @import("../webapi/Crypto.zig"), - @import("../webapi/CSS.zig"), + @import("../webapi/css.zig"), @import("../webapi/css/CSSRule.zig"), @import("../webapi/css/CSSRuleList.zig"), @import("../webapi/css/CSSStyleDeclaration.zig"), diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index 36215d479..dddb56553 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -32,7 +32,7 @@ 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"); +const CSS = @import("css.zig"); const ShadowRoot = @import("ShadowRoot.zig"); pub const Svg = @import("element/Svg.zig"); diff --git a/src/browser/webapi/Window.zig b/src/browser/webapi/Window.zig index ba3683b6c..16c553c47 100644 --- a/src/browser/webapi/Window.zig +++ b/src/browser/webapi/Window.zig @@ -25,7 +25,7 @@ const Page = @import("../Page.zig"); const Console = @import("Console.zig"); const History = @import("History.zig"); const Crypto = @import("Crypto.zig"); -const CSS = @import("CSS.zig"); +const CSS = @import("css.zig"); const Navigator = @import("Navigator.zig"); const Screen = @import("Screen.zig"); const Performance = @import("Performance.zig"); From ee7c38045f127ec03d0a392f0fc94c17c84dfb1f Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Mon, 1 Dec 2025 08:43:42 +0100 Subject: [PATCH 115/219] zig fmt --- src/browser/webapi/Window.zig | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/browser/webapi/Window.zig b/src/browser/webapi/Window.zig index 16c553c47..670935790 100644 --- a/src/browser/webapi/Window.zig +++ b/src/browser/webapi/Window.zig @@ -265,13 +265,12 @@ pub fn postMessage(self: *Window, message: js.Object, target_origin: ?[]const u8 const origin = try self._location.getOrigin(page); const callback = try page._factory.create(PostMessageCallback{ .window = self, - .message = try message.persist() , + .message = try message.persist(), .origin = try page.arena.dupe(u8, origin), .page = page, }); errdefer page._factory.destroy(callback); - try page.scheduler.add(callback, PostMessageCallback.run, 0, .{ .name = "postMessage", .low_priority = false, From 4b60f56e5f573443fd9d1f405aa4169739fccc76 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Mon, 1 Dec 2025 09:06:52 +0100 Subject: [PATCH 116/219] ci: use releaseFast for hmtl5ever release mode --- .github/actions/install/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/install/action.yml b/.github/actions/install/action.yml index e0ca3c02c..3b29b7b27 100644 --- a/.github/actions/install/action.yml +++ b/.github/actions/install/action.yml @@ -79,7 +79,7 @@ runs: - name: hmtl5ever release if: ${{ inputs.mode == 'release' }} shell: bash - run: zig build -Doptimize=ReleaseSafe html5ever + run: zig build -Doptimize=ReleaseFast html5ever - name: hmtl5ever debug if: ${{ inputs.mode == 'debug' }} From 129b59a43fad9970d8fae434cd9d82c06de0711d Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Mon, 1 Dec 2025 11:17:59 +0300 Subject: [PATCH 117/219] html5ever: prefer `dev` build only on `Debug` optimization --- build.zig | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/build.zig b/build.zig index 3632f98db..a273d81f9 100644 --- a/build.zig +++ b/build.zig @@ -65,9 +65,9 @@ pub fn build(b: *Build) !void { }; break :blk switch (optimize) { - // Consider these as dev builds. - .Debug, .ReleaseSafe => argv[0 .. argv.len - 1], - .ReleaseFast, .ReleaseSmall => argv, + // Prefer dev build on debug option. + .Debug => argv[0 .. argv.len - 1], + else => argv, }; }; const html5ever_exec_cargo = b.addSystemCommand(html5ever_argv); @@ -94,8 +94,9 @@ pub fn build(b: *Build) !void { }; const html5ever_obj = switch (optimize) { - .Debug, .ReleaseSafe => b.getInstallPath(.prefix, "html5ever/debug/liblitefetch_html5ever.a"), - .ReleaseFast, .ReleaseSmall => b.getInstallPath(.prefix, "html5ever/release/liblitefetch_html5ever.a"), + .Debug => b.getInstallPath(.prefix, "html5ever/debug/liblitefetch_html5ever.a"), + // Release builds. + else => b.getInstallPath(.prefix, "html5ever/release/liblitefetch_html5ever.a"), }; lightpanda_module.addObjectFile(.{ .cwd_relative = html5ever_obj }); From e807c9b6beeb609667fbcbc941a1dde59426e715 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Tue, 2 Dec 2025 00:08:45 +0800 Subject: [PATCH 118/219] Add XmlSerializer, add Response.type, tweak HTMLTemplate to redirect some calls to its Content (DocumentFragment) --- src/browser/Page.zig | 8 +- src/browser/Session.zig | 6 - src/browser/js/Caller.zig | 4 + src/browser/js/Context.zig | 1 - src/browser/js/Function.zig | 2 +- src/browser/js/bridge.zig | 1 + src/browser/js/js.zig | 17 +-- src/browser/tests/element/html/template.html | 40 ++++++ src/browser/tests/xmlserializer.html | 131 +++++++++++++++++++ src/browser/webapi/Window.zig | 4 +- src/browser/webapi/XMLSerializer.zig | 56 ++++++++ src/browser/webapi/element/html/Template.zig | 29 ++++ src/browser/webapi/net/Fetch.zig | 7 +- src/browser/webapi/net/Response.zig | 15 +++ 14 files changed, 295 insertions(+), 26 deletions(-) create mode 100644 src/browser/tests/xmlserializer.html create mode 100644 src/browser/webapi/XMLSerializer.zig diff --git a/src/browser/Page.zig b/src/browser/Page.zig index c2c2b17e9..7158231fb 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -182,7 +182,12 @@ pub fn deinit(self: *Page) void { // stats.print(&stream) catch unreachable; } - self.js.deinit(); + // removeContext() will execute the destructor of any type that + // registered a destructor (e.g. XMLHttpRequest). + // Should be called before we deinit the page, because these objects + // could be referencing it. + self._session.executor.removeContext(); + self._script_manager.shutdown = true; self._session.browser.http_client.abort(); self._script_manager.deinit(); @@ -597,6 +602,7 @@ fn _wait(self: *Page, wait_ms: u32) !Session.WaitResult { // haven't started navigating, I guess. return .done; } + self.js.runMicrotasks(); // Either we have active http connections, or we're in CDP // mode with an extra socket. Either way, we're waiting diff --git a/src/browser/Session.zig b/src/browser/Session.zig index 0f90a82a3..cacd6f0ee 100644 --- a/src/browser/Session.zig +++ b/src/browser/Session.zig @@ -112,12 +112,6 @@ pub fn removePage(self: *Session) void { std.debug.assert(self.page != null); - // RemoveJsContext() will execute the destructor of any type that - // registered a destructor (e.g. XMLHttpRequest). - // Should be called before we deinit the page, because these objects - // could be referencing it. - self.executor.removeContext(); - self.page.?.deinit(); self.page = null; diff --git a/src/browser/js/Caller.zig b/src/browser/js/Caller.zig index efb696ec7..f4b32ddbe 100644 --- a/src/browser/js/Caller.zig +++ b/src/browser/js/Caller.zig @@ -130,6 +130,10 @@ pub fn method(self: *Caller, comptime T: type, func: anytype, info: v8.FunctionC pub fn _method(self: *Caller, comptime T: type, func: anytype, info: v8.FunctionCallbackInfo, comptime opts: CallOpts) !void { const F = @TypeOf(func); + var handle_scope: v8.HandleScope = undefined; + handle_scope.init(self.isolate); + defer handle_scope.deinit(); + var args = try self.getArgs(F, 1, info); @field(args, "0") = try Context.typeTaggedAnyOpaque(*T, info.getThis()); const res = @call(.auto, func, args); diff --git a/src/browser/js/Context.zig b/src/browser/js/Context.zig index 1b0c768fe..ce497e487 100644 --- a/src/browser/js/Context.zig +++ b/src/browser/js/Context.zig @@ -1176,7 +1176,6 @@ pub fn resolvePromise(self: *Context, value: anytype) !js.Promise { return error.FailedToResolvePromise; } self.runMicrotasks(); - return resolver.getPromise(); } diff --git a/src/browser/js/Function.zig b/src/browser/js/Function.zig index 41d8fa2ca..4ab5be8a5 100644 --- a/src/browser/js/Function.zig +++ b/src/browser/js/Function.zig @@ -144,7 +144,7 @@ pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args const result = self.func.castToFunction().call(context.v8_context, js_this, js_args); if (result == null) { - std.debug.print("CB ERR: {s}\n", .{self.src() catch "???"}); + // std.debug.print("CB ERR: {s}\n", .{self.src() catch "???"}); return error.JSExecCallback; } diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index 026ccffac..0feec8a85 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -515,6 +515,7 @@ pub const JsApis = flattenTypes(&.{ @import("../webapi/DOMNodeIterator.zig"), @import("../webapi/DOMRect.zig"), @import("../webapi/DOMParser.zig"), + @import("../webapi/XMLSerializer.zig"), @import("../webapi/NodeFilter.zig"), @import("../webapi/Element.zig"), @import("../webapi/element/DOMStringMap.zig"), diff --git a/src/browser/js/js.zig b/src/browser/js/js.zig index 4f993d8de..e6f736681 100644 --- a/src/browser/js/js.zig +++ b/src/browser/js/js.zig @@ -75,22 +75,20 @@ pub const PromiseResolver = struct { const context = self.context; const js_value = try context.zigValueToJs(value); - // resolver.resolve will return null if the promise isn't pending - const ok = self.resolver.resolve(context.v8_context, js_value) orelse return; - if (!ok) { + if (self.resolver.resolve(context.v8_context, js_value) == null) { return error.FailedToResolvePromise; } + self.runMicrotasks(); } pub fn reject(self: PromiseResolver, value: anytype) !void { const context = self.context; const js_value = try context.zigValueToJs(value); - // resolver.reject will return null if the promise isn't pending - const ok = self.resolver.reject(context.v8_context, js_value) orelse return; - if (!ok) { + if (self.resolver.reject(context.v8_context, js_value) == null) { return error.FailedToRejectPromise; } + self.runMicrotasks(); } }; @@ -111,9 +109,7 @@ pub const PersistentPromiseResolver = struct { const js_value = try context.zigValueToJs(value, .{}); defer context.runMicrotasks(); - // resolver.resolve will return null if the promise isn't pending - const ok = self.resolver.castToPromiseResolver().resolve(context.v8_context, js_value) orelse return; - if (!ok) { + if (self.resolver.castToPromiseResolver().resolve(context.v8_context, js_value) == null) { return error.FailedToResolvePromise; } } @@ -124,8 +120,7 @@ pub const PersistentPromiseResolver = struct { defer context.runMicrotasks(); // resolver.reject will return null if the promise isn't pending - const ok = self.resolver.castToPromiseResolver().reject(context.v8_context, js_value) orelse return; - if (!ok) { + if (self.resolver.castToPromiseResolver().reject(context.v8_context, js_value) == null) { return error.FailedToRejectPromise; } } diff --git a/src/browser/tests/element/html/template.html b/src/browser/tests/element/html/template.html index bc6055846..52db20fdd 100644 --- a/src/browser/tests/element/html/template.html +++ b/src/browser/tests/element/html/template.html @@ -166,3 +166,43 @@

Hello Template

testing.expectEqual('First', inner1.textContent); } + + + + + + + + diff --git a/src/browser/tests/xmlserializer.html b/src/browser/tests/xmlserializer.html new file mode 100644 index 000000000..edbc60c88 --- /dev/null +++ b/src/browser/tests/xmlserializer.html @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/browser/webapi/Window.zig b/src/browser/webapi/Window.zig index 670935790..46a5afbe8 100644 --- a/src/browser/webapi/Window.zig +++ b/src/browser/webapi/Window.zig @@ -157,8 +157,8 @@ pub fn setOnUnhandledRejection(self: *Window, cb_: ?js.Function) !void { } } -pub fn fetch(_: *const Window, input: Fetch.Input, page: *Page) !js.Promise { - return Fetch.init(input, page); +pub fn fetch(_: *const Window, input: Fetch.Input, options: ?Fetch.RequestInit, page: *Page) !js.Promise { + return Fetch.init(input, options, page); } pub fn setTimeout(self: *Window, cb: js.Function, delay_ms: ?u32, params: []js.Object, page: *Page) !u32 { diff --git a/src/browser/webapi/XMLSerializer.zig b/src/browser/webapi/XMLSerializer.zig new file mode 100644 index 000000000..bbd89a800 --- /dev/null +++ b/src/browser/webapi/XMLSerializer.zig @@ -0,0 +1,56 @@ +// 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 Node = @import("Node.zig"); +const dump = @import("../dump.zig"); + +const XMLSerializer = @This(); + +pub fn init() XMLSerializer { + return .{}; +} + +pub fn serializeToString(self: *const XMLSerializer, node: *Node, page: *Page) ![]const u8 { + _ = self; + var buf = std.Io.Writer.Allocating.init(page.call_arena); + try dump.deep(node, .{ .shadow = .skip }, &buf.writer, page); + return buf.written(); +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(XMLSerializer); + + pub const Meta = struct { + pub const name = "XMLSerializer"; + 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(XMLSerializer.init, .{}); + pub const serializeToString = bridge.function(XMLSerializer.serializeToString, .{}); +}; + +const testing = @import("../../testing.zig"); +test "WebApi: XMLSerializer" { + try testing.htmlRunner("xmlserializer.html", .{}); +} diff --git a/src/browser/webapi/element/html/Template.zig b/src/browser/webapi/element/html/Template.zig index 4529230fa..8d8dc0b27 100644 --- a/src/browser/webapi/element/html/Template.zig +++ b/src/browser/webapi/element/html/Template.zig @@ -23,6 +23,21 @@ pub fn getContent(self: *Template) *DocumentFragment { return self._content; } +pub fn setInnerHTML(self: *Template, html: []const u8, page: *Page) !void { + return self._content.setInnerHTML(html, page); +} + +pub fn getOuterHTML(self: *Template, writer: *std.Io.Writer, page: *Page) !void { + const dump = @import("../../../dump.zig"); + const el = self.asElement(); + + try el.format(writer); + try dump.children(self._content.asNode(), .{ .shadow = .skip }, writer, page); + try writer.writeAll("'); +} + pub const JsApi = struct { pub const bridge = js.Bridge(Template); @@ -33,6 +48,20 @@ pub const JsApi = struct { }; pub const content = bridge.accessor(Template.getContent, null, .{}); + pub const innerHTML = bridge.accessor(_getInnerHTML, Template.setInnerHTML, .{}); + pub const outerHTML = bridge.accessor(_getOuterHTML, null, .{}); + + fn _getInnerHTML(self: *Template, page: *Page) ![]const u8 { + var buf = std.Io.Writer.Allocating.init(page.call_arena); + try self._content.getInnerHTML(&buf.writer, page); + return buf.written(); + } + + fn _getOuterHTML(self: *Template, page: *Page) ![]const u8 { + var buf = std.Io.Writer.Allocating.init(page.call_arena); + try self.getOuterHTML(&buf.writer, page); + return buf.written(); + } }; pub const Build = struct { diff --git a/src/browser/webapi/net/Fetch.zig b/src/browser/webapi/net/Fetch.zig index 35056eb1d..b00b3c7c0 100644 --- a/src/browser/webapi/net/Fetch.zig +++ b/src/browser/webapi/net/Fetch.zig @@ -40,10 +40,11 @@ _response: *Response, _resolver: js.PersistentPromiseResolver, pub const Input = Request.Input; +pub const RequestInit = Request.Options; // @ZIGDOM just enough to get campfire demo working -pub fn init(input: Input, page: *Page) !js.Promise { - const request = try Request.init(input, null, page); +pub fn init(input: Input, options: ?RequestInit, page: *Page) !js.Promise { + const request = try Request.init(input, options, page); const fetch = try page.arena.create(Fetch); fetch.* = .{ @@ -60,7 +61,6 @@ pub fn init(input: Input, page: *Page) !js.Promise { if (comptime IS_DEBUG) { log.debug(.http, "fetch", .{ .url = request._url }); } - std.debug.print("fetch: {s}\n", .{request._url}); try http_client.request(.{ .ctx = fetch, @@ -100,7 +100,6 @@ fn httpDataCallback(transfer: *Http.Transfer, data: []const u8) !void { fn httpDoneCallback(ctx: *anyopaque) !void { const self: *Fetch = @ptrCast(@alignCast(ctx)); self._response._body = self._buf.items; - std.debug.print("fetch-resolve: {s}\n", .{self._url}); return self._resolver.resolve(self._response); } diff --git a/src/browser/webapi/net/Response.zig b/src/browser/webapi/net/Response.zig index 9e79072ba..244475668 100644 --- a/src/browser/webapi/net/Response.zig +++ b/src/browser/webapi/net/Response.zig @@ -26,10 +26,19 @@ const Allocator = std.mem.Allocator; const Response = @This(); +pub const Type = enum { + basic, + cors, + @"error", + @"opaque", + opaqueredirect, +}; + _status: u16, _arena: Allocator, _headers: *Headers, _body: ?[]const u8, +_type: Type, const InitOpts = struct { status: u16 = 200, @@ -48,6 +57,7 @@ pub fn init(body_: ?[]const u8, opts_: ?InitOpts, page: *Page) !*Response { ._status = opts.status, ._body = body, ._headers = opts.headers orelse try Headers.init(page), + ._type = .basic, // @ZIGDOM: todo }); } @@ -59,6 +69,10 @@ pub fn getHeaders(self: *const Response) *Headers { return self._headers; } +pub fn getType(self: *const Response) []const u8 { + return @tagName(self._type); +} + pub fn getBody(self: *const Response, page: *Page) !?*ReadableStream { const body = self._body orelse return null; @@ -106,6 +120,7 @@ pub const JsApi = struct { pub const constructor = bridge.constructor(Response.init, .{}); pub const ok = bridge.accessor(Response.isOK, null, .{}); pub const status = bridge.accessor(Response.getStatus, null, .{}); + pub const @"type" = bridge.accessor(Response.getType, null, .{}); pub const text = bridge.function(Response.getText, .{}); pub const json = bridge.function(Response.getJson, .{}); pub const headers = bridge.accessor(Response.getHeaders, null, .{}); From 6a48f6df25f56cde49ce78ed0b70c0308a1ab95a Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Tue, 2 Dec 2025 07:01:14 +0800 Subject: [PATCH 119/219] Element.hasAttributes --- src/browser/tests/element/attributes.html | 36 +++++++++++++++++++++++ src/browser/webapi/Element.zig | 6 ++++ src/browser/webapi/element/Attribute.zig | 3 ++ 3 files changed, 45 insertions(+) diff --git a/src/browser/tests/element/attributes.html b/src/browser/tests/element/attributes.html index 5fb92883c..e33af9ed1 100644 --- a/src/browser/tests/element/attributes.html +++ b/src/browser/tests/element/attributes.html @@ -129,3 +129,39 @@ testing.expectEqual(false, el1.hasAttribute('toggle-test')); } + + diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index 8836770a5..12c4b686c 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -288,6 +288,11 @@ pub fn hasAttribute(self: *const Element, name: []const u8, page: *Page) !bool { return value != null; } +pub fn hasAttributes(self: *const Element) bool { + const attributes = self._attributes orelse return false; + return attributes.isEmpty() == false; +} + pub fn getAttributeNode(self: *Element, name: []const u8, page: *Page) !?*Attribute { const attributes = self._attributes orelse return null; return attributes.getAttribute(name, self, page); @@ -952,6 +957,7 @@ pub const JsApi = struct { 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 hasAttributes = bridge.function(Element.hasAttributes, .{}); pub const getAttribute = bridge.function(Element.getAttribute, .{}); pub const getAttributeNode = bridge.function(Element.getAttributeNode, .{}); pub const setAttribute = bridge.function(Element.setAttribute, .{}); diff --git a/src/browser/webapi/element/Attribute.zig b/src/browser/webapi/element/Attribute.zig index 46e5705ed..b4b9a4ee7 100644 --- a/src/browser/webapi/element/Attribute.zig +++ b/src/browser/webapi/element/Attribute.zig @@ -120,6 +120,9 @@ pub const JsApi = struct { pub const List = struct { _list: std.DoublyLinkedList = .{}, + pub fn isEmpty(self: *const List) bool { + return self._list.first == null; + } pub fn get(self: *const List, name: []const u8, page: *Page) !?[]const u8 { const entry = (try self.getEntry(name, page)) orelse return null; return entry._value.str(); From fd391681068009c202b4185544b71ac853072d09 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Tue, 2 Dec 2025 10:57:20 +0800 Subject: [PATCH 120/219] Range --- src/browser/js/bridge.zig | 1 + src/browser/tests/node/child_nodes.html | 4 + src/browser/tests/range.html | 377 +++++++++++++++ src/browser/webapi/Document.zig | 6 + src/browser/webapi/Node.zig | 55 +++ src/browser/webapi/Range.zig | 493 ++++++++++++++++++++ src/browser/webapi/collections/NodeList.zig | 19 + 7 files changed, 955 insertions(+) create mode 100644 src/browser/tests/range.html create mode 100644 src/browser/webapi/Range.zig diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index 0feec8a85..c6f899d7e 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -516,6 +516,7 @@ pub const JsApis = flattenTypes(&.{ @import("../webapi/DOMRect.zig"), @import("../webapi/DOMParser.zig"), @import("../webapi/XMLSerializer.zig"), + @import("../webapi/Range.zig"), @import("../webapi/NodeFilter.zig"), @import("../webapi/Element.zig"), @import("../webapi/element/DOMStringMap.zig"), diff --git a/src/browser/tests/node/child_nodes.html b/src/browser/tests/node/child_nodes.html index 7534eff44..3a6b67974 100644 --- a/src/browser/tests/node/child_nodes.html +++ b/src/browser/tests/node/child_nodes.html @@ -73,7 +73,11 @@ testing.expectEqual([0], Array.from(one.keys())); testing.expectEqual([p10], Array.from(one.values())); testing.expectEqual([[0, p10]], Array.from(one.entries())); + testing.expectEqual([p10], Array.from(one)); + let foreach = []; + one.forEach((p) => foreach.push(p)); + testing.expectEqual([p10], foreach); + +
+

First paragraph

+

Second paragraph

+ Span content +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/browser/webapi/Document.zig b/src/browser/webapi/Document.zig index 2643e26c0..6cea49870 100644 --- a/src/browser/webapi/Document.zig +++ b/src/browser/webapi/Document.zig @@ -177,6 +177,11 @@ pub fn createTextNode(_: *const Document, data: []const u8, page: *Page) !*Node return page.createTextNode(data); } +const Range = @import("Range.zig"); +pub fn createRange(_: *const Document, page: *Page) !*Range { + return Range.init(page); +} + pub fn createEvent(_: *const Document, event_type: []const u8, page: *Page) !*@import("Event.zig") { const Event = @import("Event.zig"); @@ -290,6 +295,7 @@ pub const JsApi = struct { pub const createDocumentFragment = bridge.function(Document.createDocumentFragment, .{}); pub const createComment = bridge.function(Document.createComment, .{}); pub const createTextNode = bridge.function(Document.createTextNode, .{}); + pub const createRange = bridge.function(Document.createRange, .{}); pub const createEvent = bridge.function(Document.createEvent, .{ .dom_exception = true }); pub const createTreeWalker = bridge.function(Document.createTreeWalker, .{}); pub const createNodeIterator = bridge.function(Document.createNodeIterator, .{}); diff --git a/src/browser/webapi/Node.zig b/src/browser/webapi/Node.zig index 9ae1ec480..ab0c28ec8 100644 --- a/src/browser/webapi/Node.zig +++ b/src/browser/webapi/Node.zig @@ -419,6 +419,61 @@ pub fn childrenIterator(self: *Node) NodeIterator { }; } +pub fn getLength(self: *Node) u32 { + switch (self._type) { + .cdata => |cdata| { + return @intCast(cdata.getData().len); + }, + .element, .document, .document_fragment => { + var count: u32 = 0; + var it = self.childrenIterator(); + while (it.next()) |_| { + count += 1; + } + return count; + }, + .document_type, .attribute => return 0, + } +} + +pub fn getChildIndex(self: *Node, target: *const Node) ?u32 { + var i: u32 = 0; + var it = self.childrenIterator(); + while (it.next()) |child| { + if (child == target) { + return i; + } + i += 1; + } + return null; +} + +pub fn getChildAt(self: *Node, index: u32) ?*Node { + var i: u32 = 0; + var it = self.childrenIterator(); + while (it.next()) |child| { + if (i == index) { + return child; + } + i += 1; + } + return null; +} + +pub fn getData(self: *const Node) []const u8 { + return switch (self._type) { + .cdata => |c| c.getData(), + else => "", + }; +} + +pub fn setData(self: *Node, data: []const u8) void { + switch (self._type) { + .cdata => |c| c._data = data, + else => {}, + } +} + pub fn className(self: *const Node) []const u8 { switch (self._type) { inline else => |c| return c.className(), diff --git a/src/browser/webapi/Range.zig b/src/browser/webapi/Range.zig new file mode 100644 index 000000000..e9af36038 --- /dev/null +++ b/src/browser/webapi/Range.zig @@ -0,0 +1,493 @@ +// 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 Node = @import("Node.zig"); +const DocumentFragment = @import("DocumentFragment.zig"); + +const Range = @This(); + +_end_offset: u32, +_start_offset: u32, +_end_container: *Node, +_start_container: *Node, + +pub fn init(page: *Page) !*Range { + // Per spec, a new range starts collapsed at the document's first position + const doc = page.document.asNode(); + return page._factory.create(Range{ + ._end_offset = 0, + ._start_offset = 0, + ._end_container = doc, + ._start_container = doc, + }); +} + +pub fn getStartContainer(self: *const Range) *Node { + return self._start_container; +} + +pub fn getStartOffset(self: *const Range) u32 { + return self._start_offset; +} + +pub fn getEndContainer(self: *const Range) *Node { + return self._end_container; +} + +pub fn getEndOffset(self: *const Range) u32 { + return self._end_offset; +} + +pub fn getCollapsed(self: *const Range) bool { + return self._start_container == self._end_container and + self._start_offset == self._end_offset; +} + +pub fn setStart(self: *Range, node: *Node, offset: u32) !void { + self._start_container = node; + self._start_offset = offset; + + // If start is now after end, collapse to start + if (self.isStartAfterEnd()) { + self._end_container = self._start_container; + self._end_offset = self._start_offset; + } +} + +pub fn setEnd(self: *Range, node: *Node, offset: u32) !void { + self._end_container = node; + self._end_offset = offset; + + // If end is now before start, collapse to end + if (self.isStartAfterEnd()) { + self._start_container = self._end_container; + self._start_offset = self._end_offset; + } +} + +pub fn setStartBefore(self: *Range, node: *Node) !void { + const parent = node.parentNode() orelse return error.InvalidNodeType; + const offset = parent.getChildIndex(node) orelse return error.NotFound; + try self.setStart(parent, offset); +} + +pub fn setStartAfter(self: *Range, node: *Node) !void { + const parent = node.parentNode() orelse return error.InvalidNodeType; + const offset = parent.getChildIndex(node) orelse return error.NotFound; + try self.setStart(parent, offset + 1); +} + +pub fn setEndBefore(self: *Range, node: *Node) !void { + const parent = node.parentNode() orelse return error.InvalidNodeType; + const offset = parent.getChildIndex(node) orelse return error.NotFound; + try self.setEnd(parent, offset); +} + +pub fn setEndAfter(self: *Range, node: *Node) !void { + const parent = node.parentNode() orelse return error.InvalidNodeType; + const offset = parent.getChildIndex(node) orelse return error.NotFound; + try self.setEnd(parent, offset + 1); +} + +pub fn selectNode(self: *Range, node: *Node) !void { + const parent = node.parentNode() orelse return error.InvalidNodeType; + const offset = parent.getChildIndex(node) orelse return error.NotFound; + try self.setStart(parent, offset); + try self.setEnd(parent, offset + 1); +} + +pub fn selectNodeContents(self: *Range, node: *Node) !void { + const length = node.getLength(); + try self.setStart(node, 0); + try self.setEnd(node, length); +} + +pub fn collapse(self: *Range, to_start: ?bool) void { + if (to_start orelse true) { + self._end_container = self._start_container; + self._end_offset = self._start_offset; + } else { + self._start_container = self._end_container; + self._start_offset = self._end_offset; + } +} + +pub fn cloneRange(self: *const Range, page: *Page) !*Range { + return page._factory.create(Range{ + ._end_offset = self._end_offset, + ._start_offset = self._start_offset, + ._end_container = self._end_container, + ._start_container = self._start_container, + }); +} + +pub fn insertNode(self: *Range, node: *Node, page: *Page) !void { + // Insert node at the start of the range + const container = self._start_container; + const offset = self._start_offset; + + if (container.is(Node.CData)) |_| { + // If container is a text node, we need to split it + const parent = container.parentNode() orelse return error.InvalidNodeType; + + if (offset == 0) { + _ = try parent.insertBefore(node, container, page); + } else { + const text_data = container.getData(); + if (offset >= text_data.len) { + _ = try parent.insertBefore(node, container.nextSibling(), page); + } else { + // Split the text node into before and after parts + const before_text = text_data[0..offset]; + const after_text = text_data[offset..]; + + const before = try page.createTextNode(before_text); + const after = try page.createTextNode(after_text); + + _ = try parent.replaceChild(before, container, page); + _ = try parent.insertBefore(node, before.nextSibling(), page); + _ = try parent.insertBefore(after, node.nextSibling(), page); + } + } + } else { + // Container is an element, insert at offset + const ref_child = container.getChildAt(offset); + _ = try container.insertBefore(node, ref_child, page); + } + + // Update range to be after the inserted node + if (self._start_container == self._end_container) { + self._end_offset += 1; + } +} + +pub fn deleteContents(self: *Range, page: *Page) !void { + if (self.getCollapsed()) { + return; + } + + // Simple case: same container + if (self._start_container == self._end_container) { + if (self._start_container.is(Node.CData)) |_| { + // Delete part of text node + const text_data = self._start_container.getData(); + const new_text = try std.mem.concat( + page.arena, + u8, + &.{ text_data[0..self._start_offset], text_data[self._end_offset..] }, + ); + self._start_container.setData(new_text); + } else { + // Delete child nodes in range + var offset = self._start_offset; + while (offset < self._end_offset) : (offset += 1) { + if (self._start_container.getChildAt(self._start_offset)) |child| { + _ = try self._start_container.removeChild(child, page); + } + } + } + self.collapse(true); + return; + } + + // Complex case: different containers - simplified implementation + // Just collapse the range for now + self.collapse(true); +} + +pub fn cloneContents(self: *const Range, page: *Page) !*DocumentFragment { + const fragment = try DocumentFragment.init(page); + + if (self.getCollapsed()) return fragment; + + // Simple case: same container + if (self._start_container == self._end_container) { + if (self._start_container.is(Node.CData)) |_| { + // Clone part of text node + const text_data = self._start_container.getData(); + if (self._start_offset < text_data.len and self._end_offset <= text_data.len) { + const cloned_text = text_data[self._start_offset..self._end_offset]; + const text_node = try page.createTextNode(cloned_text); + _ = try fragment.asNode().appendChild(text_node, page); + } + } else { + // Clone child nodes in range + var offset = self._start_offset; + while (offset < self._end_offset) : (offset += 1) { + if (self._start_container.getChildAt(offset)) |child| { + const cloned = try child.cloneNode(true, page); + _ = try fragment.asNode().appendChild(cloned, page); + } + } + } + } + + return fragment; +} + +pub fn extractContents(self: *Range, page: *Page) !*DocumentFragment { + const fragment = try self.cloneContents(page); + try self.deleteContents(page); + return fragment; +} + +pub fn surroundContents(self: *Range, new_parent: *Node, page: *Page) !void { + // Extract contents + const contents = try self.extractContents(page); + + // Insert the new parent + try self.insertNode(new_parent, page); + + // Move contents into new parent + _ = try new_parent.appendChild(contents.asNode(), page); + + // Select the new parent's contents + try self.selectNodeContents(new_parent); +} + +pub fn createContextualFragment(self: *const Range, html: []const u8, page: *Page) !*DocumentFragment { + var context_node = self._start_container; + + // If start container is a text node, use its parent as context + if (context_node.is(Node.CData)) |_| { + context_node = context_node.parentNode() orelse context_node; + } + + const fragment = try DocumentFragment.init(page); + + if (html.len == 0) { + return fragment; + } + + // Create a temporary element of the same type as the context for parsing + // This preserves the parsing context without modifying the original node + const temp_node = if (context_node.is(Node.Element)) |el| + try page.createElement(el._namespace.toUri(), el.getTagNameLower(), null) + else + try page.createElement(null, "div", null); + + try page.parseHtmlAsChildren(temp_node, html); + + // Move all parsed children to the fragment + // Keep removing first child until temp element is empty + const fragment_node = fragment.asNode(); + while (temp_node.firstChild()) |child| { + page.removeNode(temp_node, child, .{ .will_be_reconnected = true }); + try page.appendNode(fragment_node, child, .{ .child_already_connected = false }); + } + + return fragment; +} + +pub fn toString(self: *const Range, page: *Page) ![]const u8 { + // Simplified implementation: just extract text content + var buf = std.Io.Writer.Allocating.init(page.call_arena); + try self.writeTextContent(&buf.writer); + return buf.written(); +} + +fn writeTextContent(self: *const Range, writer: *std.Io.Writer) !void { + if (self.getCollapsed()) { + return; + } + + if (self._start_container == self._end_container) { + if (self._start_container.is(Node.CData)) |cdata| { + const data = cdata.getData(); + if (self._start_offset < data.len and self._end_offset <= data.len) { + try writer.writeAll(data[self._start_offset..self._end_offset]); + } + } + // For elements, would need to iterate children + return; + } + + // Complex case: different containers - would need proper tree walking + // For now, just return empty +} + +fn isStartAfterEnd(self: *const Range) bool { + return compareBoundaryPoints( + self._start_container, + self._start_offset, + self._end_container, + self._end_offset, + ) == .after; +} + +const BoundaryComparison = enum { + before, + equal, + after, +}; + +/// Compare two boundary points in tree order +/// Returns whether (nodeA, offsetA) is before/equal/after (nodeB, offsetB) +fn compareBoundaryPoints( + node_a: *Node, + offset_a: u32, + node_b: *Node, + offset_b: u32, +) BoundaryComparison { + // If same container, just compare offsets + if (node_a == node_b) { + if (offset_a < offset_b) return .before; + if (offset_a > offset_b) return .after; + return .equal; + } + + // Check if one contains the other + if (isAncestorOf(node_a, node_b)) { + // A contains B, so A's position comes before B + // But we need to check if the offset in A comes after B + var child = node_b; + var parent = child.parentNode(); + while (parent) |p| { + if (p == node_a) { + const child_index = p.getChildIndex(child) orelse unreachable; + if (offset_a <= child_index) { + return .before; + } + return .after; + } + child = p; + parent = p.parentNode(); + } + unreachable; + } + + if (isAncestorOf(node_b, node_a)) { + // B contains A, so B's position comes before A + var child = node_a; + var parent = child.parentNode(); + while (parent) |p| { + if (p == node_b) { + const child_index = p.getChildIndex(child) orelse unreachable; + if (child_index < offset_b) { + return .before; + } + return .after; + } + child = p; + parent = p.parentNode(); + } + unreachable; + } + + // Neither contains the other, find their relative position in tree order + // Walk up from A to find all ancestors + var current = node_a; + var a_count: usize = 0; + var a_ancestors: [64]*Node = undefined; + while (a_count < 64) { + a_ancestors[a_count] = current; + a_count += 1; + current = current.parentNode() orelse break; + } + + // Walk up from B and find first common ancestor + current = node_b; + while (current.parentNode()) |parent| { + for (a_ancestors[0..a_count]) |ancestor| { + if (ancestor != parent) { + continue; + } + + // Found common ancestor + // Now compare positions of the children in this ancestor + const a_child = blk: { + var node = node_a; + while (node.parentNode()) |p| { + if (p == parent) break :blk node; + node = p; + } + unreachable; + }; + const b_child = current; + + const a_index = parent.getChildIndex(a_child) orelse unreachable; + const b_index = parent.getChildIndex(b_child) orelse unreachable; + + if (a_index < b_index) { + return .before; + } + if (a_index > b_index) { + return .after; + } + return .equal; + } + current = parent; + } + + // Should not reach here if nodes are in the same tree + return .before; +} + +fn isAncestorOf(potential_ancestor: *Node, node: *Node) bool { + var current = node.parentNode(); + while (current) |parent| { + if (parent == potential_ancestor) { + return true; + } + current = parent.parentNode(); + } + return false; +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(Range); + + pub const Meta = struct { + pub const name = "Range"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; + + pub const constructor = bridge.constructor(Range.init, .{}); + pub const startContainer = bridge.accessor(Range.getStartContainer, null, .{}); + pub const startOffset = bridge.accessor(Range.getStartOffset, null, .{}); + pub const endContainer = bridge.accessor(Range.getEndContainer, null, .{}); + pub const endOffset = bridge.accessor(Range.getEndOffset, null, .{}); + pub const collapsed = bridge.accessor(Range.getCollapsed, null, .{}); + pub const setStart = bridge.function(Range.setStart, .{}); + pub const setEnd = bridge.function(Range.setEnd, .{}); + pub const setStartBefore = bridge.function(Range.setStartBefore, .{}); + pub const setStartAfter = bridge.function(Range.setStartAfter, .{}); + pub const setEndBefore = bridge.function(Range.setEndBefore, .{}); + pub const setEndAfter = bridge.function(Range.setEndAfter, .{}); + pub const selectNode = bridge.function(Range.selectNode, .{}); + pub const selectNodeContents = bridge.function(Range.selectNodeContents, .{}); + pub const collapse = bridge.function(Range.collapse, .{}); + pub const cloneRange = bridge.function(Range.cloneRange, .{}); + pub const insertNode = bridge.function(Range.insertNode, .{}); + pub const deleteContents = bridge.function(Range.deleteContents, .{}); + pub const cloneContents = bridge.function(Range.cloneContents, .{}); + pub const extractContents = bridge.function(Range.extractContents, .{}); + pub const surroundContents = bridge.function(Range.surroundContents, .{}); + pub const createContextualFragment = bridge.function(Range.createContextualFragment, .{}); + pub const toString = bridge.function(Range.toString, .{}); +}; + +const testing = @import("../../testing.zig"); +test "WebApi: Range" { + try testing.htmlRunner("range.html", .{}); +} diff --git a/src/browser/webapi/collections/NodeList.zig b/src/browser/webapi/collections/NodeList.zig index b49a29b6b..0e4a3c2e6 100644 --- a/src/browser/webapi/collections/NodeList.zig +++ b/src/browser/webapi/collections/NodeList.zig @@ -18,6 +18,7 @@ const std = @import("std"); +const log = @import("../../..//log.zig"); const js = @import("../../js/js.zig"); const Page = @import("../../Page.zig"); const Node = @import("../Node.zig"); @@ -63,6 +64,23 @@ pub fn entries(self: *NodeList, page: *Page) !*EntryIterator { return .init(.{ .list = self }, page); } +pub fn forEach(self: *NodeList, cb: js.Function, page: *Page) !void { + var i: i32 = 0; + var it = try self.values(page); + while (true) : (i += 1) { + const next = try it.next(page); + if (next.done) { + return; + } + + var result: js.Function.Result = undefined; + cb.tryCall(void, .{ next.value, i, self }, &result) catch { + log.debug(.js, "forEach callback", .{ .err = result.exception, .stack = result.stack }); + return; + }; + } +} + const GenericIterator = @import("iterator.zig").Entry; pub const KeyIterator = GenericIterator(Iterator, "0"); pub const ValueIterator = GenericIterator(Iterator, "1"); @@ -96,5 +114,6 @@ pub const JsApi = struct { pub const keys = bridge.function(NodeList.keys, .{}); pub const values = bridge.function(NodeList.values, .{}); pub const entries = bridge.function(NodeList.entries, .{}); + pub const forEach = bridge.function(NodeList.forEach, .{}); pub const symbol_iterator = bridge.iterator(NodeList.values, .{}); }; From 6a46a9ba47412adb9ca4143b38e34681ec6df7ef Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Tue, 2 Dec 2025 11:08:47 +0800 Subject: [PATCH 121/219] HTMLDataElement --- src/browser/Page.zig | 6 +++ src/browser/js/bridge.zig | 1 + src/browser/webapi/Element.zig | 4 ++ src/browser/webapi/element/Html.zig | 3 ++ src/browser/webapi/element/html/Data.zig | 56 ++++++++++++++++++++++++ 5 files changed, 70 insertions(+) create mode 100644 src/browser/webapi/element/html/Data.zig diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 7158231fb..0b4a4149b 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -1094,6 +1094,12 @@ pub fn createElement(self: *Page, ns_: ?[]const u8, name: []const u8, attribute_ attribute_iterator, .{ ._proto = undefined, ._tag_name = String.init(undefined, "main", .{}) catch unreachable, ._tag = .main }, ), + asUint("data") => return self.createHtmlElementT( + Element.Html.Data, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), else => {}, }, 5 => switch (@as(u40, @bitCast(name[0..5].*))) { diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index c6f899d7e..a45693558 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -528,6 +528,7 @@ pub const JsApis = flattenTypes(&.{ @import("../webapi/element/html/BR.zig"), @import("../webapi/element/html/Button.zig"), @import("../webapi/element/html/Custom.zig"), + @import("../webapi/element/html/Data.zig"), @import("../webapi/element/html/Dialog.zig"), @import("../webapi/element/html/Div.zig"), @import("../webapi/element/html/Form.zig"), diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index 12c4b686c..e37687e9d 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -129,6 +129,7 @@ pub fn getTagNameLower(self: *const Element) []const u8 { .br => "br", .button => "button", .custom => |e| e._tag_name.str(), + .data => "data", .dialog => "dialog", .div => "div", .form => "form", @@ -174,6 +175,7 @@ pub fn getTagNameSpec(self: *const Element, buf: []u8) []const u8 { .br => "BR", .button => "BUTTON", .custom => |e| upperTagName(&e._tag_name, buf), + .data => "DATA", .dialog => "DIALOG", .div => "DIV", .form => "FORM", @@ -793,6 +795,7 @@ pub fn getTag(self: *const Element) Tag { .form => .form, .p => .p, .custom => .custom, + .data => .data, .dialog => .dialog, .iframe => .iframe, .img => .img, @@ -835,6 +838,7 @@ pub const Tag = enum { button, circle, custom, + data, dialog, div, ellipse, diff --git a/src/browser/webapi/element/Html.zig b/src/browser/webapi/element/Html.zig index e6d748c8f..cefdaf659 100644 --- a/src/browser/webapi/element/Html.zig +++ b/src/browser/webapi/element/Html.zig @@ -42,6 +42,7 @@ pub const Custom = @import("html/Custom.zig"); pub const Script = @import("html/Script.zig"); pub const Anchor = @import("html/Anchor.zig"); pub const Button = @import("html/Button.zig"); +pub const Data = @import("html/Data.zig"); pub const Dialog = @import("html/Dialog.zig"); pub const Form = @import("html/Form.zig"); pub const Heading = @import("html/Heading.zig"); @@ -72,6 +73,7 @@ pub const Type = union(enum) { br: *BR, button: *Button, custom: *Custom, + data: *Data, dialog: *Dialog, div: *Div, form: *Form, @@ -121,6 +123,7 @@ pub fn className(self: *const HtmlElement) []const u8 { .form => "[object HTMLFormElement]", .p => "[object HtmlParagraphElement]", .custom => "[object CUSTOM-TODO]", + .data => "[object HTMLDataElement]", .dialog => "[object HTMLDialogElement]", .img => "[object HTMLImageElement]", .iframe => "[object HTMLIFrameElement]", diff --git a/src/browser/webapi/element/html/Data.zig b/src/browser/webapi/element/html/Data.zig new file mode 100644 index 000000000..08e779f8e --- /dev/null +++ b/src/browser/webapi/element/html/Data.zig @@ -0,0 +1,56 @@ +// 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"); + +const Node = @import("../../Node.zig"); +const Element = @import("../../Element.zig"); +const HtmlElement = @import("../Html.zig"); + +const Data = @This(); + +_proto: *HtmlElement, + +pub fn asElement(self: *Data) *Element { + return self._proto._proto; +} + +pub fn asNode(self: *Data) *Node { + return self.asElement().asNode(); +} + +pub fn getValue(self: *Data) []const u8 { + return self.asElement().getAttributeSafe("value") orelse ""; +} + +pub fn setValue(self: *Data, value: []const u8, page: *Page) !void { + try self.asElement().setAttributeSafe("value", value, page); +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(Data); + + pub const Meta = struct { + pub const name = "HTMLDataElement"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; + + pub const value = bridge.accessor(Data.getValue, Data.setValue, .{}); +}; From 3dd61aeb7104e2bc43e1b836fc48a970128bc544 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Tue, 2 Dec 2025 11:14:06 +0800 Subject: [PATCH 122/219] css.zig -> CSS.zig --- src/browser/js/bridge.zig | 2 +- src/browser/webapi/{css.zig => CSS.zig} | 0 src/browser/webapi/Element.zig | 2 +- src/browser/webapi/Window.zig | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename src/browser/webapi/{css.zig => CSS.zig} (100%) diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index a45693558..1522c9d37 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -493,7 +493,7 @@ pub const JsApis = flattenTypes(&.{ @import("../webapi/collections.zig"), @import("../webapi/Console.zig"), @import("../webapi/Crypto.zig"), - @import("../webapi/css.zig"), + @import("../webapi/CSS.zig"), @import("../webapi/css/CSSRule.zig"), @import("../webapi/css/CSSRuleList.zig"), @import("../webapi/css/CSSStyleDeclaration.zig"), diff --git a/src/browser/webapi/css.zig b/src/browser/webapi/CSS.zig similarity index 100% rename from src/browser/webapi/css.zig rename to src/browser/webapi/CSS.zig diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index e37687e9d..9bf36ab3f 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -32,7 +32,7 @@ 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"); +const CSS = @import("CSS.zig"); const ShadowRoot = @import("ShadowRoot.zig"); pub const Svg = @import("element/Svg.zig"); diff --git a/src/browser/webapi/Window.zig b/src/browser/webapi/Window.zig index 46a5afbe8..ad755b441 100644 --- a/src/browser/webapi/Window.zig +++ b/src/browser/webapi/Window.zig @@ -25,7 +25,7 @@ const Page = @import("../Page.zig"); const Console = @import("Console.zig"); const History = @import("History.zig"); const Crypto = @import("Crypto.zig"); -const CSS = @import("css.zig"); +const CSS = @import("CSS.zig"); const Navigator = @import("Navigator.zig"); const Screen = @import("Screen.zig"); const Performance = @import("Performance.zig"); From abd3ee9c5d59b42848c00dc7c544908ad13e9696 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Tue, 2 Dec 2025 11:24:26 +0800 Subject: [PATCH 123/219] Add ignore list for unkown global property This is for often-seen globals which we _know_ come from client-side libraries, e.g. litNonce. --- src/browser/polyfill/polyfill.zig | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/browser/polyfill/polyfill.zig b/src/browser/polyfill/polyfill.zig index cfb502a73..c14c75a2c 100644 --- a/src/browser/polyfill/polyfill.zig +++ b/src/browser/polyfill/polyfill.zig @@ -54,6 +54,28 @@ pub const Loader = struct { } if (comptime builtin.mode == .Debug) { + const ignored = std.StaticStringMap(void).initComptime(.{ + .{ "process", {} }, + .{ "ShadyDOM", {} }, + .{ "ShadyCSS", {} }, + + .{ "litNonce", {} }, + .{ "litHtmlVersions", {} }, + .{ "litHtmlPolyfillSupport", {} }, + .{ "litElementHydrateSupport", {} }, + + .{ "recaptcha", {} }, + .{ "grecaptcha", {} }, + .{ "___grecaptcha_cfg", {} }, + .{ "__recaptcha_api", {} }, + .{ "__google_recaptcha_client", {} }, + + .{ "CLOSURE_FLAGS", {} }, + }); + if (ignored.has(name)) { + return false; + } + log.debug(.unknown_prop, "unkown global property", .{ .info = "but the property can exist in pure JS", .stack = js_context.stackTrace() catch "???", From a61e87c5ddde3fb95c0287bc344aa9456a66e651 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Tue, 2 Dec 2025 13:25:48 +0800 Subject: [PATCH 124/219] Don't break wait on scheduler callback error Allow recursive parsing --- src/browser/Page.zig | 7 +++---- src/browser/webapi/net/Fetch.zig | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 0b4a4149b..7f8ad2bcf 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -629,8 +629,7 @@ fn _wait(self: *Page, wait_ms: u32) !Session.WaitResult { if (try_catch.hasCaught()) { const msg = (try try_catch.err(self.arena)) orelse "unknown"; - log.warn(.js, "page wait", .{ .err = msg, .src = "scheduler" }); - return error.JsError; + log.info(.js, "page wait", .{ .err = msg, .src = "scheduler" }); } const http_active = http_client.active; @@ -1648,9 +1647,9 @@ pub fn childListChange( // TODO: optimize and cleanup, this is called a lot (e.g., innerHTML = '') pub fn parseHtmlAsChildren(self: *Page, node: *Node, html: []const u8) !void { - std.debug.assert(self._parse_mode == .document); + const previous_parse_mode = self._parse_mode; self._parse_mode = .fragment; - defer self._parse_mode = .document; + defer self._parse_mode = previous_parse_mode; var parser = Parser.init(self.call_arena, node, self); parser.parseFragment(html); diff --git a/src/browser/webapi/net/Fetch.zig b/src/browser/webapi/net/Fetch.zig index b00b3c7c0..cea5a2132 100644 --- a/src/browser/webapi/net/Fetch.zig +++ b/src/browser/webapi/net/Fetch.zig @@ -67,8 +67,8 @@ pub fn init(input: Input, options: ?RequestInit, page: *Page) !js.Promise { .url = request._url, .method = .GET, .headers = headers, - .cookie_jar = &page._session.cookie_jar, .resource_type = .fetch, + .cookie_jar = &page._session.cookie_jar, .header_callback = httpHeaderDoneCallback, .data_callback = httpDataCallback, .done_callback = httpDoneCallback, From c90e9c165b4caada8f056273bbaa90e66ea98af1 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Tue, 2 Dec 2025 15:13:55 +0800 Subject: [PATCH 125/219] add Performance.Mark --- src/browser/tests/performance.html | 88 ++++++++++++++++++ src/browser/webapi/Performance.zig | 142 +++++++++++++++++++++++++++-- 2 files changed, 222 insertions(+), 8 deletions(-) diff --git a/src/browser/tests/performance.html b/src/browser/tests/performance.html index 5aed2cc12..a26477920 100644 --- a/src/browser/tests/performance.html +++ b/src/browser/tests/performance.html @@ -43,3 +43,91 @@ } } + + + + + + + + + + diff --git a/src/browser/webapi/Performance.zig b/src/browser/webapi/Performance.zig index c659a7f89..e272f71a9 100644 --- a/src/browser/webapi/Performance.zig +++ b/src/browser/webapi/Performance.zig @@ -1,20 +1,22 @@ const js = @import("../js/js.zig"); +const Page = @import("../Page.zig"); const datetime = @import("../../datetime.zig"); pub fn registerTypes() []const type { - return &.{ - Performance, - Entry, - }; + return &.{ Performance, Entry, Mark }; } +const std = @import("std"); + const Performance = @This(); _time_origin: u64, +_entries: std.ArrayListUnmanaged(*Entry) = .{}, pub fn init() Performance { return .{ ._time_origin = datetime.milliTimestamp(.monotonic), + ._entries = .{}, }; } @@ -28,6 +30,75 @@ pub fn getTimeOrigin(self: *const Performance) f64 { return @floatFromInt(self._time_origin); } +pub fn mark(self: *Performance, name: []const u8, _options: ?Mark.Options, page: *Page) !*Mark { + const m = try Mark.init(name, _options, page); + try self._entries.append(page.arena, m._proto); + return m; +} + +pub fn clearMarks(self: *Performance, mark_name: ?[]const u8) void { + if (mark_name) |name| { + // Remove specific mark by name + var i: usize = 0; + while (i < self._entries.items.len) { + const entry = self._entries.items[i]; + if (entry._type == .mark and std.mem.eql(u8, entry._name, name)) { + _ = self._entries.orderedRemove(i); + } else { + i += 1; + } + } + } else { + // Remove all marks + var i: usize = 0; + while (i < self._entries.items.len) { + const entry = self._entries.items[i]; + if (entry._type == .mark) { + _ = self._entries.orderedRemove(i); + } else { + i += 1; + } + } + } +} + +pub fn getEntries(self: *const Performance) []*Entry { + return self._entries.items; +} + +pub fn getEntriesByType(self: *const Performance, entry_type: []const u8, page: *Page) ![]const *Entry { + var result: std.ArrayList(*Entry) = .empty; + + for (self._entries.items) |entry| { + if (std.mem.eql(u8, entry.getEntryType(), entry_type)) { + try result.append(page.call_arena, entry); + } + } + + return result.items; +} + +pub fn getEntriesByName(self: *const Performance, name: []const u8, entry_type: ?[]const u8, page: *Page) ![]const *Entry { + var result: std.ArrayList(*Entry) = .empty; + + for (self._entries.items) |entry| { + if (!std.mem.eql(u8, entry._name, name)) { + continue; + } + + const et = entry_type orelse { + try result.append(page.call_arena, entry); + continue; + }; + + if (std.mem.eql(u8, entry.getEntryType(), et)) { + try result.append(page.call_arena, entry); + } + } + + return result.items; +} + pub const JsApi = struct { pub const bridge = js.Bridge(Performance); @@ -38,16 +109,21 @@ pub const JsApi = struct { }; pub const now = bridge.function(Performance.now, .{}); + pub const mark = bridge.function(Performance.mark, .{}); + pub const clearMarks = bridge.function(Performance.clearMarks, .{}); + pub const getEntries = bridge.function(Performance.getEntries, .{}); + pub const getEntriesByType = bridge.function(Performance.getEntriesByType, .{}); + pub const getEntriesByName = bridge.function(Performance.getEntriesByName, .{}); pub const timeOrigin = bridge.accessor(Performance.getTimeOrigin, null, .{}); }; pub const Entry = struct { _duration: f64 = 0.0, - _entry_type: Type, + _type: Type, _name: []const u8, _start_time: f64 = 0.0, - const Type = enum { + const Type = union(enum) { element, event, first_input, @@ -55,13 +131,13 @@ pub const Entry = struct { layout_shift, long_animation_frame, longtask, - mark, measure, navigation, paint, resource, taskattribution, visibility_state, + mark: *Mark, }; pub fn getDuration(self: *const Entry) f64 { @@ -69,7 +145,7 @@ pub const Entry = struct { } pub fn getEntryType(self: *const Entry) []const u8 { - return switch (self._entry_type) { + return switch (self._type) { .first_input => "first-input", .largest_contentful_paint => "largest-contentful-paint", .layout_shift => "layout-shift", @@ -95,8 +171,58 @@ pub const Entry = struct { pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; }; + pub const name = bridge.accessor(Entry.getName, null, .{}); pub const duration = bridge.accessor(Entry.getDuration, null, .{}); pub const entryType = bridge.accessor(Entry.getEntryType, null, .{}); + pub const startTime = bridge.accessor(Entry.getStartTime, null, .{}); + }; +}; + +pub const Mark = struct { + _proto: *Entry, + _detail: ?js.Object, + + const Options = struct { + detail: ?js.Object = null, + startTime: ?f64 = null, + }; + + pub fn init(name: []const u8, _opts: ?Options, page: *Page) !*Mark { + const opts = _opts orelse Options{}; + const start_time = opts.startTime orelse page.window._performance.now(); + + if (start_time < 0.0) { + return error.TypeError; + } + + const detail = if (opts.detail) |d| try d.persist() else null; + const m = try page._factory.create(Mark{ + ._proto = undefined, + ._detail = detail, + }); + + const entry = try page._factory.create(Entry{ + ._start_time = start_time, + ._name = try page.dupeString(name), + ._type = .{ .mark = m }, + }); + m._proto = entry; + return m; + } + + pub fn getDetail(self: *const Mark) ?js.Object { + return self._detail; + } + + pub const JsApi = struct { + pub const bridge = js.Bridge(Mark); + + pub const Meta = struct { + pub const name = "PerformanceMark"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; + pub const detail = bridge.accessor(Mark.getDetail, null, .{}); }; }; From b5eceb52fbb052b274f7d2ce39d0be4ad35f9662 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Tue, 2 Dec 2025 16:05:57 +0800 Subject: [PATCH 126/219] try safer http cleanup on page deinit --- src/browser/webapi/net/Fetch.zig | 7 +++++++ src/browser/webapi/net/XMLHttpRequest.zig | 7 +++++++ src/http/Client.zig | 11 ++++++++++- 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/browser/webapi/net/Fetch.zig b/src/browser/webapi/net/Fetch.zig index cea5a2132..0f6c37f32 100644 --- a/src/browser/webapi/net/Fetch.zig +++ b/src/browser/webapi/net/Fetch.zig @@ -77,6 +77,13 @@ pub fn init(input: Input, options: ?RequestInit, page: *Page) !js.Promise { return fetch._resolver.promise(); } +pub fn deinit(self: *Fetch) void { + if (self.transfer) |transfer| { + transfer.abort(); + self.transfer = null; + } +} + fn httpHeaderDoneCallback(transfer: *Http.Transfer) !void { const self: *Fetch = @ptrCast(@alignCast(transfer.ctx)); diff --git a/src/browser/webapi/net/XMLHttpRequest.zig b/src/browser/webapi/net/XMLHttpRequest.zig index 3bb219e2f..4b0cdb9f9 100644 --- a/src/browser/webapi/net/XMLHttpRequest.zig +++ b/src/browser/webapi/net/XMLHttpRequest.zig @@ -74,6 +74,13 @@ pub fn init(page: *Page) !*XMLHttpRequest { }); } +pub fn deinit(self: *XMLHttpRequest) void { + if (self.transfer) |transfer| { + transfer.abort(); + self.transfer = null; + } +} + fn asEventTarget(self: *XMLHttpRequest) *EventTarget { return self._proto._proto; } diff --git a/src/http/Client.zig b/src/http/Client.zig index 65f310667..1a646ea00 100644 --- a/src/http/Client.zig +++ b/src/http/Client.zig @@ -153,7 +153,7 @@ pub fn abort(self: *Client) void { log.err(.http, "get private info", .{ .err = err, .source = "abort" }); continue; }; - transfer.abort(); + transfer.kill(); } std.debug.assert(self.active == 0); @@ -812,6 +812,15 @@ pub const Transfer = struct { self.deinit(); } + // internal, when the page is shutting down. Doesn't have the same ceremony + // as abort (doesn't send a notification, doesn't invoke an error callback) + fn kill(self: *Transfer) void { + if (self._handle != null) { + self.client.endTransfer(self); + } + self.deinit(); + } + // abortAuthChallenge is called when an auth chanllenge interception is // abort. We don't call self.client.endTransfer here b/c it has been done // before interception process. From 568a4428baac8681cb6edea089cceafa38774c17 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Tue, 2 Dec 2025 22:19:58 +0800 Subject: [PATCH 127/219] custom element registry 'whenDefine' function --- src/browser/polyfill/polyfill.zig | 3 ++ .../tests/custom_elements/registry.html | 38 +++++++++++++++++++ src/browser/webapi/CustomElementRegistry.zig | 25 ++++++++++++ 3 files changed, 66 insertions(+) diff --git a/src/browser/polyfill/polyfill.zig b/src/browser/polyfill/polyfill.zig index c14c75a2c..bf6f92274 100644 --- a/src/browser/polyfill/polyfill.zig +++ b/src/browser/polyfill/polyfill.zig @@ -61,8 +61,11 @@ pub const Loader = struct { .{ "litNonce", {} }, .{ "litHtmlVersions", {} }, + .{ "litElementVersions", {} }, .{ "litHtmlPolyfillSupport", {} }, .{ "litElementHydrateSupport", {} }, + .{ "litElementPolyfillSupport", {} }, + .{ "reactiveElementVersions", {} }, .{ "recaptcha", {} }, .{ "grecaptcha", {} }, diff --git a/src/browser/tests/custom_elements/registry.html b/src/browser/tests/custom_elements/registry.html index 8ead6ae2a..064aa9f9e 100644 --- a/src/browser/tests/custom_elements/registry.html +++ b/src/browser/tests/custom_elements/registry.html @@ -81,3 +81,41 @@ testing.expectEqual('NO-HYPHEN-INVALID', el.tagName); } + + + + + + + diff --git a/src/browser/webapi/CustomElementRegistry.zig b/src/browser/webapi/CustomElementRegistry.zig index 2f8a912cd..727e74edb 100644 --- a/src/browser/webapi/CustomElementRegistry.zig +++ b/src/browser/webapi/CustomElementRegistry.zig @@ -30,6 +30,7 @@ const CustomElementDefinition = @import("CustomElementDefinition.zig"); const CustomElementRegistry = @This(); _definitions: std.StringHashMapUnmanaged(*CustomElementDefinition) = .{}, +_when_defined: std.StringHashMapUnmanaged(js.PersistentPromiseResolver) = .{}, const DefineOptions = struct { extends: ?[]const u8 = null, @@ -103,6 +104,10 @@ pub fn define(self: *CustomElementRegistry, name: []const u8, constructor: js.Fu _ = page._undefined_custom_elements.swapRemove(idx); } + + if (self._when_defined.fetchRemove(name)) |entry| { + try entry.value.resolve(constructor); + } } pub fn get(self: *CustomElementRegistry, name: []const u8) ?js.Function { @@ -114,6 +119,25 @@ pub fn upgrade(self: *CustomElementRegistry, root: *Node, page: *Page) !void { try upgradeNode(self, root, page); } +pub fn whenDefined(self: *CustomElementRegistry, name: []const u8, page: *Page) !js.Promise { + if (self._definitions.get(name)) |definition| { + return page.js.resolvePromise(definition.constructor); + } + + const gop = try self._when_defined.getOrPut(page.arena, name); + if (gop.found_existing) { + return gop.value_ptr.promise(); + } + errdefer _ = self._when_defined.remove(name); + const owned_name = try page.dupeString(name); + + const resolver = try page.js.createPromiseResolver(.page); + gop.key_ptr.* = owned_name; + gop.value_ptr.* = resolver; + + return resolver.promise(); +} + fn upgradeNode(self: *CustomElementRegistry, node: *Node, page: *Page) !void { if (node.is(Element)) |element| { try upgradeElement(self, element, page); @@ -222,6 +246,7 @@ pub const JsApi = struct { pub const define = bridge.function(CustomElementRegistry.define, .{ .dom_exception = true }); pub const get = bridge.function(CustomElementRegistry.get, .{ .null_as_undefined = true }); pub const upgrade = bridge.function(CustomElementRegistry.upgrade, .{}); + pub const whenDefined = bridge.function(CustomElementRegistry.whenDefined, .{}); }; const testing = @import("../../testing.zig"); From c0da6994dab1a8615e211168d56aa479b559631d Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Wed, 3 Dec 2025 08:52:51 +0800 Subject: [PATCH 128/219] Element.setInnerText --- src/browser/dump.zig | 34 +++++++++++++++++++++++++++- src/browser/tests/element/inner.html | 33 +++++++++++++++++++++++++++ src/browser/webapi/Element.zig | 22 +++++++++++++++++- 3 files changed, 87 insertions(+), 2 deletions(-) diff --git a/src/browser/dump.zig b/src/browser/dump.zig index 73ebe42b9..e1feb57f8 100644 --- a/src/browser/dump.zig +++ b/src/browser/dump.zig @@ -64,7 +64,7 @@ pub fn root(opts: RootOpts, writer: *std.Io.Writer, page: *Page) !void { pub fn deep(node: *Node, opts: Opts, writer: *std.Io.Writer, page: *Page) error{WriteFailed}!void { switch (node._type) { - .cdata => |cd| try writer.writeAll(cd.getData()), + .cdata => |cd| try writeEscapedText(cd.getData(), writer), .element => |el| { if (shouldStripElement(el, opts)) { return; @@ -211,3 +211,35 @@ fn shouldStripElement(el: *const Node.Element, opts: Opts) bool { return false; } + +fn writeEscapedText(text: []const u8, writer: *std.Io.Writer) !void { + // Fast path: if no special characters, write directly + const first_special = std.mem.indexOfAny(u8, text, "&<>") orelse { + return writer.writeAll(text); + }; + + try writer.writeAll(text[0..first_special]); + try writer.writeAll(switch (text[first_special]) { + '&' => "&", + '<' => "<", + '>' => ">", + else => unreachable, + }); + + // Process remaining text + var remaining = text[first_special + 1 ..]; + while (std.mem.indexOfAny(u8, remaining, "&<>")) |offset| { + try writer.writeAll(remaining[0..offset]); + try writer.writeAll(switch (remaining[offset]) { + '&' => "&", + '<' => "<", + '>' => ">", + else => unreachable, + }); + remaining = remaining[offset + 1 ..]; + } + + if (remaining.len > 0) { + try writer.writeAll(remaining); + } +} diff --git a/src/browser/tests/element/inner.html b/src/browser/tests/element/inner.html index da2aa5c62..b80231224 100644 --- a/src/browser/tests/element/inner.html +++ b/src/browser/tests/element/inner.html @@ -129,3 +129,36 @@ d1.innerHTML = '


'; testing.expectEqual('


', d1.innerHTML); + + diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index 9bf36ab3f..402409539 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -229,6 +229,26 @@ pub fn getInnerText(self: *Element, writer: *std.Io.Writer) !void { } } +pub fn setInnerText(self: *Element, text: []const u8, page: *Page) !void { + const parent = self.asNode(); + + // Remove all existing children + page.domChanged(); + var it = parent.childrenIterator(); + while (it.next()) |child| { + page.removeNode(parent, child, .{ .will_be_reconnected = false }); + } + + // Fast path: skip if text is empty + if (text.len == 0) { + return; + } + + // Create and append text node + const text_node = try page.createTextNode(text); + try page.appendNode(parent, text_node, .{ .child_already_connected = false }); +} + pub fn getOuterHTML(self: *Element, writer: *std.Io.Writer, page: *Page) !void { const dump = @import("../dump.zig"); return dump.deep(self.asNode(), .{ .shadow = .skip }, writer, page); @@ -913,7 +933,7 @@ pub const JsApi = struct { } pub const namespaceURI = bridge.accessor(Element.getNamespaceURI, null, .{}); - pub const innerText = bridge.accessor(_innerText, null, .{}); + pub const innerText = bridge.accessor(_innerText, Element.setInnerText, .{}); fn _innerText(self: *Element, page: *const Page) ![]const u8 { var buf = std.Io.Writer.Allocating.init(page.call_arena); try self.getInnerText(&buf.writer); From 2de0d4bc484a3c793a200dd836c23978c052661d Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Wed, 3 Dec 2025 09:59:55 +0800 Subject: [PATCH 129/219] Header case insensitive --- src/browser/tests/element/inner.html | 1 - src/browser/tests/net/headers.html | 138 +++++++++++++++++++- src/browser/webapi/collections/NodeList.zig | 2 +- src/browser/webapi/net/Headers.zig | 59 +++++++-- 4 files changed, 182 insertions(+), 18 deletions(-) diff --git a/src/browser/tests/element/inner.html b/src/browser/tests/element/inner.html index b80231224..c9bb08946 100644 --- a/src/browser/tests/element/inner.html +++ b/src/browser/tests/element/inner.html @@ -143,7 +143,6 @@ // innerText does NOT parse HTML (unlike innerHTML) d1.innerText = 'hello
world
!!'; testing.expectEqual('hello
world
!!', d1.innerText); - console.warn(d1.innerHTML); testing.expectEqual('hello <div>world</div><b>!!</b>', d1.innerHTML); // Setting empty string clears children diff --git a/src/browser/tests/net/headers.html b/src/browser/tests/net/headers.html index d0d1c35ea..07e967256 100644 --- a/src/browser/tests/net/headers.html +++ b/src/browser/tests/net/headers.html @@ -17,15 +17,145 @@ testing.expectEqual(null, headers.get('Content-Type')); testing.expectEqual(false, headers.has('Content-Type')); } + + + + diff --git a/src/browser/webapi/collections/NodeList.zig b/src/browser/webapi/collections/NodeList.zig index 0e4a3c2e6..dae615098 100644 --- a/src/browser/webapi/collections/NodeList.zig +++ b/src/browser/webapi/collections/NodeList.zig @@ -75,7 +75,7 @@ pub fn forEach(self: *NodeList, cb: js.Function, page: *Page) !void { var result: js.Function.Result = undefined; cb.tryCall(void, .{ next.value, i, self }, &result) catch { - log.debug(.js, "forEach callback", .{ .err = result.exception, .stack = result.stack }); + log.debug(.js, "forEach callback", .{ .err = result.exception, .stack = result.stack, .source = "nodelist" }); return; }; } diff --git a/src/browser/webapi/net/Headers.zig b/src/browser/webapi/net/Headers.zig index 9dea0b958..136207bd9 100644 --- a/src/browser/webapi/net/Headers.zig +++ b/src/browser/webapi/net/Headers.zig @@ -1,5 +1,6 @@ const std = @import("std"); const js = @import("../../js/js.zig"); +const log = @import("../../../log.zig"); const Page = @import("../../Page.zig"); const KeyValueList = @import("../KeyValueList.zig"); @@ -15,27 +16,58 @@ pub fn init(page: *Page) !*Headers { } pub fn append(self: *Headers, name: []const u8, value: []const u8, page: *Page) !void { - try self._list.append(page.arena, name, value); + const normalized_name = normalizeHeaderName(name, page); + try self._list.append(page.arena, normalized_name, value); } -pub fn delete(self: *Headers, name: []const u8) void { - self._list.delete(name, null); +pub fn delete(self: *Headers, name: []const u8, page: *Page) void { + const normalized_name = normalizeHeaderName(name, page); + self._list.delete(normalized_name, null); } -pub fn get(self: *const Headers, name: []const u8) ?[]const u8 { - return self._list.get(name); +pub fn get(self: *const Headers, name: []const u8, page: *Page) ?[]const u8 { + const normalized_name = normalizeHeaderName(name, page); + return self._list.get(normalized_name); } -pub fn getAll(self: *const Headers, name: []const u8, page: *Page) ![]const []const u8 { - return self._list.getAll(name, page); +pub fn has(self: *const Headers, name: []const u8, page: *Page) bool { + const normalized_name = normalizeHeaderName(name, page); + return self._list.has(normalized_name); } -pub fn has(self: *const Headers, name: []const u8) bool { - return self._list.has(name); +pub fn set(self: *Headers, name: []const u8, value: []const u8, page: *Page) !void { + const normalized_name = normalizeHeaderName(name, page); + try self._list.set(page.arena, normalized_name, value); } -pub fn set(self: *Headers, name: []const u8, value: []const u8, page: *Page) !void { - try self._list.set(page.arena, name, value); +pub fn keys(self: *Headers, page: *Page) !*KeyValueList.KeyIterator { + return KeyValueList.KeyIterator.init(.{ .list = self, .kv = &self._list }, page); +} + +pub fn values(self: *Headers, page: *Page) !*KeyValueList.ValueIterator { + return KeyValueList.ValueIterator.init(.{ .list = self, .kv = &self._list }, page); +} + +pub fn entries(self: *Headers, page: *Page) !*KeyValueList.EntryIterator { + return KeyValueList.EntryIterator.init(.{ .list = self, .kv = &self._list }, page); +} + +pub fn forEach(self: *Headers, cb_: js.Function, js_this_: ?js.Object) !void { + const cb = if (js_this_) |js_this| try cb_.withThis(js_this) else cb_; + + for (self._list._entries.items) |entry| { + var result: js.Function.Result = undefined; + cb.tryCall(void, .{ entry.value.str(), entry.name.str(), self }, &result) catch { + log.debug(.js, "forEach callback", .{ .err = result.exception, .stack = result.stack, .source = "headers" }); + }; + } +} + +fn normalizeHeaderName(name: []const u8, page: *Page) []const u8 { + if (name.len > page.buf.len) { + return name; + } + return std.ascii.lowerString(&page.buf, name); } pub const JsApi = struct { @@ -51,9 +83,12 @@ pub const JsApi = struct { pub const append = bridge.function(Headers.append, .{}); pub const delete = bridge.function(Headers.delete, .{}); pub const get = bridge.function(Headers.get, .{}); - pub const getAll = bridge.function(Headers.getAll, .{}); pub const has = bridge.function(Headers.has, .{}); pub const set = bridge.function(Headers.set, .{}); + pub const keys = bridge.function(Headers.keys, .{}); + pub const values = bridge.function(Headers.values, .{}); + pub const entries = bridge.function(Headers.entries, .{}); + pub const forEach = bridge.function(Headers.forEach, .{}); }; const testing = @import("../../../testing.zig"); From 63eeadad1d11d6cac03d85944604ddfaff376423 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Wed, 3 Dec 2025 16:10:11 +0800 Subject: [PATCH 130/219] Fix comment dump, improve dump of shadowroot and slots --- src/browser/dump.zig | 45 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/src/browser/dump.zig b/src/browser/dump.zig index e1feb57f8..03c9bd28c 100644 --- a/src/browser/dump.zig +++ b/src/browser/dump.zig @@ -63,25 +63,58 @@ pub fn root(opts: RootOpts, writer: *std.Io.Writer, page: *Page) !void { } pub fn deep(node: *Node, opts: Opts, writer: *std.Io.Writer, page: *Page) error{WriteFailed}!void { + return _deep(node, opts, false, writer, page); +} + +fn _deep(node: *Node, opts: Opts, comptime force_slot: bool, writer: *std.Io.Writer, page: *Page) error{WriteFailed}!void { switch (node._type) { - .cdata => |cd| try writeEscapedText(cd.getData(), writer), + .cdata => |cd| { + if (node.is(Node.CData.Comment)) |_| { + try writer.writeAll(""); + } else { + try writeEscapedText(cd.getData(), writer); + } + }, .element => |el| { if (shouldStripElement(el, opts)) { return; } - // Handle elements in rendered mode - if (opts.shadow == .rendered) { - if (el.is(Slot)) |slot| { - return dumpSlotContent(slot, opts, writer, page); + // When opts.shadow == .rendered, we normally skip any element with + // a slot attribute. Only the "active" element will get rendered into + // the . However, the `deep` function is itself used + // to render that "active" content, so when we're trying to render + // it, we don't want to skip it. + if ((comptime force_slot == false) and opts.shadow == .rendered) { + if (el.getAttributeSafe("slot")) |_| { + // Skip - will be rendered by the Slot if it's the active container + return; } } try el.format(writer); + if (opts.shadow == .rendered) { + if (el.is(Slot)) |slot| { + try dumpSlotContent(slot, opts, writer, page); + return writer.writeAll(""); + } + } if (opts.shadow != .skip) { if (page._element_shadow_roots.get(el)) |shadow| { try children(shadow.asNode(), opts, writer, page); + // In rendered mode, light DOM is only shown through slots, not directly + if (opts.shadow == .rendered) { + // Skip rendering light DOM children + if (!isVoidElement(el)) { + try writer.writeAll("'); + } + return; + } } } @@ -151,7 +184,7 @@ fn dumpSlotContent(slot: *Slot, opts: Opts, writer: *std.Io.Writer, page: *Page) if (assigned.len > 0) { for (assigned) |assigned_node| { - try deep(assigned_node, opts, writer, page); + try _deep(assigned_node, opts, true, writer, page); } } else { try children(slot.asNode(), opts, writer, page); From 2a4cbbe56943b9f82a19ff7e4781bd8af12bf7b6 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Wed, 3 Dec 2025 18:24:28 +0800 Subject: [PATCH 131/219] Performance.measure --- src/browser/tests/performance.html | 149 +++++++++++++++++++++++++++++ src/browser/webapi/Performance.zig | 122 ++++++++++++++++++----- 2 files changed, 249 insertions(+), 22 deletions(-) diff --git a/src/browser/tests/performance.html b/src/browser/tests/performance.html index a26477920..5928bba93 100644 --- a/src/browser/tests/performance.html +++ b/src/browser/tests/performance.html @@ -131,3 +131,152 @@ testing.expectEqual(0, marks.length); } + + + + + + + + + + + + + + + + diff --git a/src/browser/webapi/Performance.zig b/src/browser/webapi/Performance.zig index e272f71a9..ce0707417 100644 --- a/src/browser/webapi/Performance.zig +++ b/src/browser/webapi/Performance.zig @@ -3,7 +3,7 @@ const Page = @import("../Page.zig"); const datetime = @import("../../datetime.zig"); pub fn registerTypes() []const type { - return &.{ Performance, Entry, Mark }; + return &.{ Performance, Entry, Mark, Measure }; } const std = @import("std"); @@ -36,28 +36,32 @@ pub fn mark(self: *Performance, name: []const u8, _options: ?Mark.Options, page: return m; } +pub fn measure(self: *Performance, name: []const u8, _options: ?Measure.Options, page: *Page) !*Measure { + const m = try Measure.init(name, _options, page); + try self._entries.append(page.arena, m._proto); + return m; +} + pub fn clearMarks(self: *Performance, mark_name: ?[]const u8) void { - if (mark_name) |name| { - // Remove specific mark by name - var i: usize = 0; - while (i < self._entries.items.len) { - const entry = self._entries.items[i]; - if (entry._type == .mark and std.mem.eql(u8, entry._name, name)) { - _ = self._entries.orderedRemove(i); - } else { - i += 1; - } + var i: usize = 0; + while (i < self._entries.items.len) { + const entry = self._entries.items[i]; + if (entry._type == .mark and (mark_name == null or std.mem.eql(u8, entry._name, mark_name.?))) { + _ = self._entries.orderedRemove(i); + } else { + i += 1; } - } else { - // Remove all marks - var i: usize = 0; - while (i < self._entries.items.len) { - const entry = self._entries.items[i]; - if (entry._type == .mark) { - _ = self._entries.orderedRemove(i); - } else { - i += 1; - } + } +} + +pub fn clearMeasures(self: *Performance, measure_name: ?[]const u8) void { + var i: usize = 0; + while (i < self._entries.items.len) { + const entry = self._entries.items[i]; + if (entry._type == .measure and (measure_name == null or std.mem.eql(u8, entry._name, measure_name.?))) { + _ = self._entries.orderedRemove(i); + } else { + i += 1; } } } @@ -99,6 +103,15 @@ pub fn getEntriesByName(self: *const Performance, name: []const u8, entry_type: return result.items; } +fn getMarkTime(self: *const Performance, mark_name: []const u8) !f64 { + for (self._entries.items) |entry| { + if (entry._type == .mark and std.mem.eql(u8, entry._name, mark_name)) { + return entry._start_time; + } + } + return error.SyntaxError; // Mark not found +} + pub const JsApi = struct { pub const bridge = js.Bridge(Performance); @@ -110,7 +123,9 @@ pub const JsApi = struct { pub const now = bridge.function(Performance.now, .{}); pub const mark = bridge.function(Performance.mark, .{}); + pub const measure = bridge.function(Performance.measure, .{}); pub const clearMarks = bridge.function(Performance.clearMarks, .{}); + pub const clearMeasures = bridge.function(Performance.clearMeasures, .{}); pub const getEntries = bridge.function(Performance.getEntries, .{}); pub const getEntriesByType = bridge.function(Performance.getEntriesByType, .{}); pub const getEntriesByName = bridge.function(Performance.getEntriesByName, .{}); @@ -131,7 +146,7 @@ pub const Entry = struct { layout_shift, long_animation_frame, longtask, - measure, + measure: *Measure, navigation, paint, resource, @@ -226,6 +241,69 @@ pub const Mark = struct { }; }; +pub const Measure = struct { + _proto: *Entry, + _detail: ?js.Object, + + const Options = struct { + detail: ?js.Object = null, + start: ?[]const u8 = null, + end: ?[]const u8 = null, + duration: ?f64 = null, + }; + + pub fn init(name: []const u8, _opts: ?Options, page: *Page) !*Measure { + const opts = _opts orelse Options{}; + const perf = &page.window._performance; + + const start_time = if (opts.start) |start_mark| + try perf.getMarkTime(start_mark) + else + 0.0; + + const end_time = if (opts.end) |end_mark| + try perf.getMarkTime(end_mark) + else + perf.now(); + + const duration = opts.duration orelse (end_time - start_time); + + if (duration < 0.0) { + return error.TypeError; + } + + const detail = if (opts.detail) |d| try d.persist() else null; + const m = try page._factory.create(Measure{ + ._proto = undefined, + ._detail = detail, + }); + + const entry = try page._factory.create(Entry{ + ._start_time = start_time, + ._duration = duration, + ._name = try page.dupeString(name), + ._type = .{ .measure = m }, + }); + m._proto = entry; + return m; + } + + pub fn getDetail(self: *const Measure) ?js.Object { + return self._detail; + } + + pub const JsApi = struct { + pub const bridge = js.Bridge(Measure); + + pub const Meta = struct { + pub const name = "PerformanceMeasure"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; + pub const detail = bridge.accessor(Measure.getDetail, null, .{}); + }; +}; + const testing = @import("../../testing.zig"); test "WebApi: Performance" { try testing.htmlRunner("performance.html", .{}); From 74ffc273eff21947233c471769354b78241e2f4e Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Wed, 3 Dec 2025 18:53:25 +0800 Subject: [PATCH 132/219] Add stack & line number to script eval failure --- src/browser/ScriptManager.zig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/browser/ScriptManager.zig b/src/browser/ScriptManager.zig index d2c19150f..b3b2c0ef3 100644 --- a/src/browser/ScriptManager.zig +++ b/src/browser/ScriptManager.zig @@ -773,6 +773,8 @@ const Script = struct { log.warn(.js, "eval script", .{ .url = url, .err = msg, + .stack = try_catch.stack(page.call_arena) catch null, + .line = try_catch.sourceLineNumber() orelse 0, .cacheable = cacheable, }); From 9071d98cbebfa9ff1247f698692c34fba8c79019 Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Mon, 1 Dec 2025 13:11:20 +0300 Subject: [PATCH 133/219] port `insertAdjacentHTML` --- src/browser/webapi/Element.zig | 84 ++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index 402409539..c9c9c31f2 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -360,6 +360,89 @@ pub fn attachShadow(self: *Element, mode_str: []const u8, page: *Page) !*ShadowR return shadow_root; } +pub fn insertAdjacentHTML( + self: *Element, + position: []const u8, + /// TODO: Add support for XML parsing. + html_or_xml: []const u8, + page: *Page, +) !void { + // Create a new HTMLDocument. + const doc = try page._factory.document(@import("HTMLDocument.zig"){ + ._proto = undefined, + }); + const doc_node = doc.asNode(); + + const Parser = @import("../parser/Parser.zig"); + var parser = Parser.init(page.call_arena, doc_node, page); + parser.parseFragment(html_or_xml); + // Check if there's parsing error. + if (parser.err) |_| return error.Invalid; + + // We always get it wrapped like so: + // { ... } + // None of the following can be null. + const maybe_html_node = doc_node.firstChild(); + std.debug.assert(maybe_html_node != null); + const html_node = maybe_html_node orelse return; + + const maybe_body_node = html_node.lastChild(); + std.debug.assert(maybe_body_node != null); + const body = maybe_body_node orelse return; + + const self_node = self.asNode(); + // * `target_node` is `*Node` (where we actually insert), + // * `prev_node` is `?*Node`. + const target_node, const prev_node = blk: { + // Prefer case-sensitive match. + // "beforeend" was the most common case in my tests; we might adjust the order + // depending on which ones websites prefer most. + if (std.mem.eql(u8, position, "beforeend")) { + break :blk .{ self_node, null }; + } + + if (std.mem.eql(u8, position, "afterbegin")) { + // Get the first child; null indicates there are no children. + break :blk .{ self_node, self_node.firstChild() }; + } + + if (std.mem.eql(u8, position, "beforebegin")) { + // The node must have a parent node in order to use this variant. + const parent_node = self_node.parentNode() orelse return error.NoModificationAllowed; + // Parent cannot be Document. + switch (parent_node._type) { + .document, .document_fragment => return error.NoModificationAllowed, + else => {}, + } + + break :blk .{ parent_node, self_node }; + } + + if (std.mem.eql(u8, position, "afterend")) { + // The node must have a parent node in order to use this variant. + const parent_node = self_node.parentNode() orelse return error.NoModificationAllowed; + // Parent cannot be Document. + switch (parent_node._type) { + .document, .document_fragment => return error.NoModificationAllowed, + else => {}, + } + + // Get the next sibling or null; null indicates our node is the only one. + break :blk .{ parent_node, self_node.nextSibling() }; + } + + // Returned if: + // * position is not one of the four listed values. + // * The input is XML that is not well-formed. + return error.Syntax; + }; + + var iter = body.childrenIterator(); + while (iter.next()) |child_node| { + _ = try target_node.insertBefore(child_node, prev_node, page); + } +} + pub fn setAttributeNode(self: *Element, attr: *Attribute, page: *Page) !?*Attribute { if (attr._element) |el| { if (el == self) { @@ -992,6 +1075,7 @@ pub const JsApi = struct { pub const removeAttributeNode = bridge.function(Element.removeAttributeNode, .{ .dom_exception = true }); pub const shadowRoot = bridge.accessor(Element.getShadowRoot, null, .{}); pub const attachShadow = bridge.function(_attachShadow, .{ .dom_exception = true }); + pub const insertAdjacentHTML = bridge.function(Element.insertAdjacentHTML, .{ .dom_exception = true }); const ShadowRootInit = struct { mode: []const u8, From dc040dfc3767c9c23b9ad607a62bba9379626527 Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Mon, 1 Dec 2025 15:37:08 +0300 Subject: [PATCH 134/219] add `insertAdjacentElement` and `insertAdjacentText` --- src/browser/webapi/Element.zig | 72 ++++++++++++---------------------- src/browser/webapi/Node.zig | 48 +++++++++++++++++++++++ 2 files changed, 73 insertions(+), 47 deletions(-) diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index c9c9c31f2..4eb08af70 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -375,7 +375,7 @@ pub fn insertAdjacentHTML( const Parser = @import("../parser/Parser.zig"); var parser = Parser.init(page.call_arena, doc_node, page); - parser.parseFragment(html_or_xml); + parser.parse(html_or_xml); // Check if there's parsing error. if (parser.err) |_| return error.Invalid; @@ -390,52 +390,7 @@ pub fn insertAdjacentHTML( std.debug.assert(maybe_body_node != null); const body = maybe_body_node orelse return; - const self_node = self.asNode(); - // * `target_node` is `*Node` (where we actually insert), - // * `prev_node` is `?*Node`. - const target_node, const prev_node = blk: { - // Prefer case-sensitive match. - // "beforeend" was the most common case in my tests; we might adjust the order - // depending on which ones websites prefer most. - if (std.mem.eql(u8, position, "beforeend")) { - break :blk .{ self_node, null }; - } - - if (std.mem.eql(u8, position, "afterbegin")) { - // Get the first child; null indicates there are no children. - break :blk .{ self_node, self_node.firstChild() }; - } - - if (std.mem.eql(u8, position, "beforebegin")) { - // The node must have a parent node in order to use this variant. - const parent_node = self_node.parentNode() orelse return error.NoModificationAllowed; - // Parent cannot be Document. - switch (parent_node._type) { - .document, .document_fragment => return error.NoModificationAllowed, - else => {}, - } - - break :blk .{ parent_node, self_node }; - } - - if (std.mem.eql(u8, position, "afterend")) { - // The node must have a parent node in order to use this variant. - const parent_node = self_node.parentNode() orelse return error.NoModificationAllowed; - // Parent cannot be Document. - switch (parent_node._type) { - .document, .document_fragment => return error.NoModificationAllowed, - else => {}, - } - - // Get the next sibling or null; null indicates our node is the only one. - break :blk .{ parent_node, self_node.nextSibling() }; - } - - // Returned if: - // * position is not one of the four listed values. - // * The input is XML that is not well-formed. - return error.Syntax; - }; + const target_node, const prev_node = try self.asNode().findAdjacentNodes(position); var iter = body.childrenIterator(); while (iter.next()) |child_node| { @@ -443,6 +398,27 @@ pub fn insertAdjacentHTML( } } +pub fn insertAdjacentElement( + self: *Element, + position: []const u8, + element: *Element, + page: *Page, +) !void { + const target_node, const prev_node = try self.asNode().findAdjacentNodes(position); + _ = try target_node.insertBefore(element.asNode(), prev_node, page); +} + +pub fn insertAdjacentText( + self: *Element, + where: []const u8, + data: []const u8, + page: *Page, +) !void { + const text_node = try page.createTextNode(data); + const target_node, const prev_node = try self.asNode().findAdjacentNodes(where); + _ = try target_node.insertBefore(text_node, prev_node, page); +} + pub fn setAttributeNode(self: *Element, attr: *Attribute, page: *Page) !?*Attribute { if (attr._element) |el| { if (el == self) { @@ -1076,6 +1052,8 @@ pub const JsApi = struct { pub const shadowRoot = bridge.accessor(Element.getShadowRoot, null, .{}); pub const attachShadow = bridge.function(_attachShadow, .{ .dom_exception = true }); pub const insertAdjacentHTML = bridge.function(Element.insertAdjacentHTML, .{ .dom_exception = true }); + pub const insertAdjacentElement = bridge.function(Element.insertAdjacentElement, .{ .dom_exception = true }); + pub const insertAdjacentText = bridge.function(Element.insertAdjacentText, .{ .dom_exception = true }); const ShadowRootInit = struct { mode: []const u8, diff --git a/src/browser/webapi/Node.zig b/src/browser/webapi/Node.zig index ab0c28ec8..1b686c869 100644 --- a/src/browser/webapi/Node.zig +++ b/src/browser/webapi/Node.zig @@ -115,6 +115,54 @@ pub fn is(self: *Node, comptime T: type) ?*T { return null; } +/// Given a position, returns target and previous nodes required for +/// insertAdjacentHTML, insertAdjacentElement and insertAdjacentText. +/// * `target_node` is `*Node` (where we actually insert), +/// * `previous_node` is `?*Node`. +pub fn findAdjacentNodes(self: *Node, position: []const u8) !struct { *Node, ?*Node } { + // Prefer case-sensitive match. + // "beforeend" was the most common case in my tests; we might adjust the order + // depending on which ones websites prefer most. + if (std.mem.eql(u8, position, "beforeend")) { + return .{ self, null }; + } + + if (std.mem.eql(u8, position, "afterbegin")) { + // Get the first child; null indicates there are no children. + return .{ self, self.firstChild() }; + } + + if (std.mem.eql(u8, position, "beforebegin")) { + // The node must have a parent node in order to use this variant. + const parent_node = self.parentNode() orelse return error.NoModificationAllowed; + // Parent cannot be Document. + switch (parent_node._type) { + .document, .document_fragment => return error.NoModificationAllowed, + else => {}, + } + + return .{ parent_node, self }; + } + + if (std.mem.eql(u8, position, "afterend")) { + // The node must have a parent node in order to use this variant. + const parent_node = self.parentNode() orelse return error.NoModificationAllowed; + // Parent cannot be Document. + switch (parent_node._type) { + .document, .document_fragment => return error.NoModificationAllowed, + else => {}, + } + + // Get the next sibling or null; null indicates our node is the only one. + return .{ parent_node, self.nextSibling() }; + } + + // Returned if: + // * position is not one of the four listed values. + // * The input is XML that is not well-formed. + return error.Syntax; +} + pub fn firstChild(self: *const Node) ?*Node { const children = self._children orelse return null; return children.first(); From 45e74d3336d89dcbebc7ce5b5db572da98a114f4 Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Mon, 1 Dec 2025 15:37:36 +0300 Subject: [PATCH 135/219] add `insertAdjacentElement` and `insertAdjacentHTML` tests --- .../document/insert_adjacent_element.html | 54 +++++++++++++++++++ .../tests/document/insert_adjacent_html.html | 44 +++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 src/browser/tests/document/insert_adjacent_element.html create mode 100644 src/browser/tests/document/insert_adjacent_html.html diff --git a/src/browser/tests/document/insert_adjacent_element.html b/src/browser/tests/document/insert_adjacent_element.html new file mode 100644 index 000000000..7f897cb1f --- /dev/null +++ b/src/browser/tests/document/insert_adjacent_element.html @@ -0,0 +1,54 @@ + + + Test Document Title + + + + + +
+
+ +

content

+
+
+ + + diff --git a/src/browser/tests/document/insert_adjacent_html.html b/src/browser/tests/document/insert_adjacent_html.html new file mode 100644 index 000000000..cd8d1b19d --- /dev/null +++ b/src/browser/tests/document/insert_adjacent_html.html @@ -0,0 +1,44 @@ + + + Test Document Title + + + + + +
+
+ +

content

+
+
+ + + From b6420f75e29432730768ff09124f3a9c60596df2 Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Mon, 1 Dec 2025 16:03:28 +0300 Subject: [PATCH 136/219] add `insertAdjacentText` test --- .../tests/document/insert_adjacent_text.html | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 src/browser/tests/document/insert_adjacent_text.html diff --git a/src/browser/tests/document/insert_adjacent_text.html b/src/browser/tests/document/insert_adjacent_text.html new file mode 100644 index 000000000..c8f9f3371 --- /dev/null +++ b/src/browser/tests/document/insert_adjacent_text.html @@ -0,0 +1,49 @@ + + + Test Document Title + + + + + +
+
+ +

content

+
+
+ + + From 60c1f19581989570a3544aa1cff3e9d61f773a9e Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Wed, 3 Dec 2025 20:04:07 +0800 Subject: [PATCH 137/219] add TextTrackCue and VTTCue (for reddit) --- src/browser/EventManager.zig | 2 +- src/browser/Factory.zig | 9 ++ src/browser/js/bridge.zig | 2 + src/browser/tests/media/vttcue.html | 71 +++++++++ src/browser/webapi/EventTarget.zig | 2 + src/browser/webapi/media/TextTrackCue.zig | 118 ++++++++++++++ src/browser/webapi/media/VTTCue.zig | 182 ++++++++++++++++++++++ 7 files changed, 385 insertions(+), 1 deletion(-) create mode 100644 src/browser/tests/media/vttcue.html create mode 100644 src/browser/webapi/media/TextTrackCue.zig create mode 100644 src/browser/webapi/media/VTTCue.zig diff --git a/src/browser/EventManager.zig b/src/browser/EventManager.zig index a138c44fc..408d67446 100644 --- a/src/browser/EventManager.zig +++ b/src/browser/EventManager.zig @@ -117,7 +117,7 @@ pub fn dispatch(self: *EventManager, target: *EventTarget, event: *Event) !void switch (target._type) { .node => |node| try self.dispatchNode(node, event, &was_handled), - .xhr, .window, .abort_signal, .media_query_list, .message_port => { + .xhr, .window, .abort_signal, .media_query_list, .message_port, .text_track_cue => { const list = self.lookup.getPtr(@intFromPtr(target)) orelse return; try self.dispatchAll(list, target, event, &was_handled); }, diff --git a/src/browser/Factory.zig b/src/browser/Factory.zig index 2a4a06276..91e011abb 100644 --- a/src/browser/Factory.zig +++ b/src/browser/Factory.zig @@ -277,6 +277,15 @@ pub fn xhrEventTarget(self: *Factory, child: anytype) !*@TypeOf(child) { ).create(allocator, child); } +pub fn textTrackCue(self: *Factory, child: anytype) !*@TypeOf(child) { + const allocator = self._slab.allocator(); + const TextTrackCue = @import("webapi/media/TextTrackCue.zig"); + + return try AutoPrototypeChain( + &.{ EventTarget, TextTrackCue, @TypeOf(child) }, + ).create(allocator, child); +} + fn hasChainRoot(comptime T: type) bool { // Check if this is a root if (@hasDecl(T, "_prototype_root")) { diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index 1522c9d37..c3cb095d2 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -565,6 +565,8 @@ pub const JsApis = flattenTypes(&.{ @import("../webapi/event/ProgressEvent.zig"), @import("../webapi/MessageChannel.zig"), @import("../webapi/MessagePort.zig"), + @import("../webapi/media/TextTrackCue.zig"), + @import("../webapi/media/VTTCue.zig"), @import("../webapi/EventTarget.zig"), @import("../webapi/Location.zig"), @import("../webapi/Navigator.zig"), diff --git a/src/browser/tests/media/vttcue.html b/src/browser/tests/media/vttcue.html new file mode 100644 index 000000000..ad1d2cd40 --- /dev/null +++ b/src/browser/tests/media/vttcue.html @@ -0,0 +1,71 @@ + + + + + + + + diff --git a/src/browser/webapi/EventTarget.zig b/src/browser/webapi/EventTarget.zig index 70aacb833..9792f2b39 100644 --- a/src/browser/webapi/EventTarget.zig +++ b/src/browser/webapi/EventTarget.zig @@ -36,6 +36,7 @@ pub const Type = union(enum) { abort_signal: *@import("AbortSignal.zig"), media_query_list: *@import("css/MediaQueryList.zig"), message_port: *@import("MessagePort.zig"), + text_track_cue: *@import("media/TextTrackCue.zig"), }; pub fn dispatchEvent(self: *EventTarget, event: *Event, page: *Page) !bool { @@ -104,6 +105,7 @@ pub fn format(self: *EventTarget, writer: *std.Io.Writer) !void { .abort_signal => writer.writeAll(""), .media_query_list => writer.writeAll(""), .message_port => writer.writeAll(""), + .text_track_cue => writer.writeAll(""), }; } diff --git a/src/browser/webapi/media/TextTrackCue.zig b/src/browser/webapi/media/TextTrackCue.zig new file mode 100644 index 000000000..e590fa7f5 --- /dev/null +++ b/src/browser/webapi/media/TextTrackCue.zig @@ -0,0 +1,118 @@ +// 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 EventTarget = @import("../EventTarget.zig"); + +const TextTrackCue = @This(); + +_type: Type, +_proto: *EventTarget, +_id: []const u8 = "", +_start_time: f64 = 0, +_end_time: f64 = 0, +_pause_on_exit: bool = false, +_on_enter: ?js.Function = null, +_on_exit: ?js.Function = null, + +pub const Type = union(enum) { + vtt: *@import("VTTCue.zig"), +}; + +pub fn asEventTarget(self: *TextTrackCue) *EventTarget { + return self._proto; +} + +pub fn getId(self: *const TextTrackCue) []const u8 { + return self._id; +} + +pub fn setId(self: *TextTrackCue, value: []const u8, page: *Page) !void { + self._id = try page.dupeString(value); +} + +pub fn getStartTime(self: *const TextTrackCue) f64 { + return self._start_time; +} + +pub fn setStartTime(self: *TextTrackCue, value: f64) void { + self._start_time = value; +} + +pub fn getEndTime(self: *const TextTrackCue) f64 { + return self._end_time; +} + +pub fn setEndTime(self: *TextTrackCue, value: f64) void { + self._end_time = value; +} + +pub fn getPauseOnExit(self: *const TextTrackCue) bool { + return self._pause_on_exit; +} + +pub fn setPauseOnExit(self: *TextTrackCue, value: bool) void { + self._pause_on_exit = value; +} + +pub fn getOnEnter(self: *const TextTrackCue) ?js.Function { + return self._on_enter; +} + +pub fn setOnEnter(self: *TextTrackCue, cb_: ?js.Function) !void { + if (cb_) |cb| { + self._on_enter = try cb.withThis(self); + } else { + self._on_enter = null; + } +} + +pub fn getOnExit(self: *const TextTrackCue) ?js.Function { + return self._on_exit; +} + +pub fn setOnExit(self: *TextTrackCue, cb_: ?js.Function) !void { + if (cb_) |cb| { + self._on_exit = try cb.withThis(self); + } else { + self._on_exit = null; + } +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(TextTrackCue); + + pub const Meta = struct { + pub const name = "TextTrackCue"; + + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; + + pub const Prototype = EventTarget; + + pub const id = bridge.accessor(TextTrackCue.getId, TextTrackCue.setId, .{}); + pub const startTime = bridge.accessor(TextTrackCue.getStartTime, TextTrackCue.setStartTime, .{}); + pub const endTime = bridge.accessor(TextTrackCue.getEndTime, TextTrackCue.setEndTime, .{}); + pub const pauseOnExit = bridge.accessor(TextTrackCue.getPauseOnExit, TextTrackCue.setPauseOnExit, .{}); + pub const onenter = bridge.accessor(TextTrackCue.getOnEnter, TextTrackCue.setOnEnter, .{}); + pub const onexit = bridge.accessor(TextTrackCue.getOnExit, TextTrackCue.setOnExit, .{}); +}; diff --git a/src/browser/webapi/media/VTTCue.zig b/src/browser/webapi/media/VTTCue.zig new file mode 100644 index 000000000..de796a27b --- /dev/null +++ b/src/browser/webapi/media/VTTCue.zig @@ -0,0 +1,182 @@ +// 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 TextTrackCue = @import("TextTrackCue.zig"); + +const VTTCue = @This(); + +_proto: *TextTrackCue, +_text: []const u8 = "", +_region: ?js.Object = null, +_vertical: []const u8 = "", +_snap_to_lines: bool = true, +_line: ?f64 = null, // null represents "auto" +_position: ?f64 = null, // null represents "auto" +_size: f64 = 100, +_align: []const u8 = "center", + +pub fn constructor(start_time: f64, end_time: f64, text: []const u8, page: *Page) !*VTTCue { + const cue = try page._factory.textTrackCue(VTTCue{ + ._proto = undefined, + ._text = try page.dupeString(text), + ._region = null, + ._vertical = "", + ._snap_to_lines = true, + ._line = null, // "auto" + ._position = null, // "auto" + ._size = 100, + ._align = "center", + }); + + cue._proto._start_time = start_time; + cue._proto._end_time = end_time; + + return cue; +} + +pub fn asTextTrackCue(self: *VTTCue) *TextTrackCue { + return self._proto; +} + +pub fn getText(self: *const VTTCue) []const u8 { + return self._text; +} + +pub fn setText(self: *VTTCue, value: []const u8, page: *Page) !void { + self._text = try page.dupeString(value); +} + +pub fn getRegion(self: *const VTTCue) ?js.Object { + return self._region; +} + +pub fn setRegion(self: *VTTCue, value: ?js.Object) !void { + if (value) |v| { + self._region = try v.persist(); + } else { + self._region = null; + } +} + +pub fn getVertical(self: *const VTTCue) []const u8 { + return self._vertical; +} + +pub fn setVertical(self: *VTTCue, value: []const u8, page: *Page) !void { + // Valid values: "", "rl", "lr" + self._vertical = try page.dupeString(value); +} + +pub fn getSnapToLines(self: *const VTTCue) bool { + return self._snap_to_lines; +} + +pub fn setSnapToLines(self: *VTTCue, value: bool) void { + self._snap_to_lines = value; +} + +pub const LineAndPositionSetting = union(enum) { + number: f64, + auto: []const u8, +}; + +pub fn getLine(self: *const VTTCue) LineAndPositionSetting { + if (self._line) |num| { + return .{ .number = num }; + } + return .{ .auto = "auto" }; +} + +pub fn setLine(self: *VTTCue, value: LineAndPositionSetting) void { + switch (value) { + .number => |num| self._line = num, + .auto => self._line = null, + } +} + +pub fn getPosition(self: *const VTTCue) LineAndPositionSetting { + if (self._position) |num| { + return .{ .number = num }; + } + return .{ .auto = "auto" }; +} + +pub fn setPosition(self: *VTTCue, value: LineAndPositionSetting) void { + switch (value) { + .number => |num| self._position = num, + .auto => self._position = null, + } +} + +pub fn getSize(self: *const VTTCue) f64 { + return self._size; +} + +pub fn setSize(self: *VTTCue, value: f64) void { + self._size = value; +} + +pub fn getAlign(self: *const VTTCue) []const u8 { + return self._align; +} + +pub fn setAlign(self: *VTTCue, value: []const u8, page: *Page) !void { + // Valid values: "start", "center", "end", "left", "right" + self._align = try page.dupeString(value); +} + +pub fn getCueAsHTML(self: *const VTTCue, page: *Page) !js.Object { + // Minimal implementation: return a document fragment + // In a full implementation, this would parse the VTT text into HTML nodes + _ = self; + _ = page; + return error.NotImplemented; +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(VTTCue); + + pub const Meta = struct { + pub const name = "VTTCue"; + + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; + + pub const Prototype = TextTrackCue; + + pub const constructor = bridge.constructor(VTTCue.constructor, .{}); + pub const text = bridge.accessor(VTTCue.getText, VTTCue.setText, .{}); + pub const region = bridge.accessor(VTTCue.getRegion, VTTCue.setRegion, .{}); + pub const vertical = bridge.accessor(VTTCue.getVertical, VTTCue.setVertical, .{}); + pub const snapToLines = bridge.accessor(VTTCue.getSnapToLines, VTTCue.setSnapToLines, .{}); + pub const line = bridge.accessor(VTTCue.getLine, VTTCue.setLine, .{}); + pub const position = bridge.accessor(VTTCue.getPosition, VTTCue.setPosition, .{}); + pub const size = bridge.accessor(VTTCue.getSize, VTTCue.setSize, .{}); + pub const @"align" = bridge.accessor(VTTCue.getAlign, VTTCue.setAlign, .{}); + pub const getCueAsHTML = bridge.function(VTTCue.getCueAsHTML, .{}); +}; + +const testing = @import("../../../testing.zig"); +test "WebApi: VTTCue" { + try testing.htmlRunner("media/vttcue.html", .{}); +} From 7cb06f3e5840d6ffa1d61638157676b600df41fd Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Wed, 3 Dec 2025 22:29:45 +0800 Subject: [PATCH 138/219] MediaError and :scope pseudoclass --- src/browser/js/bridge.zig | 1 + .../tests/element/query_selector_scope.html | 127 ++++++++++++++++++ src/browser/tests/media/mediaerror.html | 12 ++ src/browser/webapi/media/MediaError.zig | 64 +++++++++ src/browser/webapi/selector/List.zig | 77 ++++++----- src/browser/webapi/selector/Parser.zig | 1 + src/browser/webapi/selector/Selector.zig | 3 +- 7 files changed, 248 insertions(+), 37 deletions(-) create mode 100644 src/browser/tests/element/query_selector_scope.html create mode 100644 src/browser/tests/media/mediaerror.html create mode 100644 src/browser/webapi/media/MediaError.zig diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index c3cb095d2..e20d9b7fb 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -565,6 +565,7 @@ pub const JsApis = flattenTypes(&.{ @import("../webapi/event/ProgressEvent.zig"), @import("../webapi/MessageChannel.zig"), @import("../webapi/MessagePort.zig"), + @import("../webapi/media/MediaError.zig"), @import("../webapi/media/TextTrackCue.zig"), @import("../webapi/media/VTTCue.zig"), @import("../webapi/EventTarget.zig"), diff --git a/src/browser/tests/element/query_selector_scope.html b/src/browser/tests/element/query_selector_scope.html new file mode 100644 index 000000000..ba18615a9 --- /dev/null +++ b/src/browser/tests/element/query_selector_scope.html @@ -0,0 +1,127 @@ + + + +
+
+ Grandchild 1 + Grandchild 2 +
+
+ Grandchild 3 +
+
+ + + + + +
+
+
+ Inner text +
+
+ Other text +
+
+
+ + + +
+
Box 1
+
Box 2
+ Box 3 +
+ + + +
+
+
Child 1
+
Child 2
+
+
+
Child 3
+
+
+ + diff --git a/src/browser/tests/media/mediaerror.html b/src/browser/tests/media/mediaerror.html new file mode 100644 index 000000000..928860fb4 --- /dev/null +++ b/src/browser/tests/media/mediaerror.html @@ -0,0 +1,12 @@ + + + + diff --git a/src/browser/webapi/media/MediaError.zig b/src/browser/webapi/media/MediaError.zig new file mode 100644 index 000000000..5e1f15b40 --- /dev/null +++ b/src/browser/webapi/media/MediaError.zig @@ -0,0 +1,64 @@ +// 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"); + +const MediaError = @This(); + +_code: u16, +_message: []const u8 = "", + +pub fn init(code: u16, message: []const u8, page: *Page) !*MediaError { + return page.arena.create(MediaError{ + ._code = code, + ._message = try page.dupeString(message), + }); +} + +pub fn getCode(self: *const MediaError) u16 { + return self._code; +} + +pub fn getMessage(self: *const MediaError) []const u8 { + return self._message; +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(MediaError); + + pub const Meta = struct { + pub const name = "MediaError"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; + + // Error code constants + pub const MEDIA_ERR_ABORTED = bridge.property(1); + pub const MEDIA_ERR_NETWORK = bridge.property(2); + pub const MEDIA_ERR_DECODE = bridge.property(3); + pub const MEDIA_ERR_SRC_NOT_SUPPORTED = bridge.property(4); + + pub const code = bridge.accessor(MediaError.getCode, null, .{}); + pub const message = bridge.accessor(MediaError.getMessage, null, .{}); +}; + +const testing = @import("../../../testing.zig"); +test "WebApi: MediaError" { + try testing.htmlRunner("media/mediaerror.html", .{}); +} diff --git a/src/browser/webapi/selector/List.zig b/src/browser/webapi/selector/List.zig index 8ce759a84..06d2045ed 100644 --- a/src/browser/webapi/selector/List.zig +++ b/src/browser/webapi/selector/List.zig @@ -54,7 +54,7 @@ pub fn collect( } while (tw.next()) |node| { - if (matches(node, result.selector, page)) { + if (matches(node, result.selector, root, page)) { try nodes.put(allocator, node, {}); } } @@ -66,12 +66,11 @@ pub fn initOne(root: *Node, selector: Selector.Selector, page: *Page) ?*Node { const result = optimizeSelector(root, &selector, page) orelse return null; var tw = TreeWalker.init(result.root, .{}); - const optimized_selector = result.selector; if (result.exclude_root) { _ = tw.next(); } while (tw.next()) |node| { - if (matches(node, optimized_selector, page)) { + if (matches(node, result.selector, root, page)) { return node; } } @@ -89,10 +88,12 @@ const OptimizeResult = struct { exclude_root: bool, selector: Selector.Selector, }; + fn optimizeSelector(root: *Node, selector: *const Selector.Selector, page: *Page) ?OptimizeResult { const anchor = findIdSelector(selector) orelse return .{ .root = root, .selector = selector.*, + // Always exclude root - querySelector only returns descendants .exclude_root = true, }; @@ -173,7 +174,7 @@ fn optimizeSelector(root: *Node, selector: *const Selector.Selector, page: *Page .segments = selector.segments[0 .. seg_idx + 1], }; - if (!matches(id_node, prefix_selector, page)) { + if (!matches(id_node, prefix_selector, id_node, page)) { return null; } @@ -248,23 +249,23 @@ fn findIdSelector(selector: *const Selector.Selector) ?IdAnchor { return null; } -pub fn matches(node: *Node, selector: Selector.Selector, page: *Page) bool { +pub fn matches(node: *Node, selector: Selector.Selector, scope: *Node, page: *Page) bool { const el = node.is(Node.Element) orelse return false; if (selector.segments.len == 0) { - return matchesCompound(el, selector.first, page); + return matchesCompound(el, selector.first, scope, page); } const last_segment = selector.segments[selector.segments.len - 1]; - if (!matchesCompound(el, last_segment.compound, page)) { + if (!matchesCompound(el, last_segment.compound, scope, page)) { return false; } - return matchSegments(node, selector, selector.segments.len - 1, null, page); + return matchSegments(node, selector, selector.segments.len - 1, null, scope, page); } // Match segments backward, with support for backtracking on subsequent_sibling -fn matchSegments(node: *Node, selector: Selector.Selector, segment_index: usize, root: ?*Node, page: *Page) bool { +fn matchSegments(node: *Node, selector: Selector.Selector, segment_index: usize, root: ?*Node, scope: *Node, page: *Page) bool { const segment = selector.segments[segment_index]; const target_compound = if (segment_index == 0) selector.first @@ -272,9 +273,9 @@ fn matchSegments(node: *Node, selector: Selector.Selector, segment_index: usize, selector.segments[segment_index - 1].compound; const matched: ?*Node = switch (segment.combinator) { - .descendant => matchDescendant(node, target_compound, root, page), - .child => matchChild(node, target_compound, root, page), - .next_sibling => matchNextSibling(node, target_compound, page), + .descendant => matchDescendant(node, target_compound, root, scope, page), + .child => matchChild(node, target_compound, root, scope, page), + .next_sibling => matchNextSibling(node, target_compound, scope, page), .subsequent_sibling => { // For subsequent_sibling, try all matching siblings with backtracking var sibling = node.previousSibling(); @@ -284,13 +285,13 @@ fn matchSegments(node: *Node, selector: Selector.Selector, segment_index: usize, continue; }; - if (matchesCompound(sibling_el, target_compound, page)) { + if (matchesCompound(sibling_el, target_compound, scope, page)) { // If we're at the first segment, we found a match if (segment_index == 0) { return true; } // Try to match remaining segments from this sibling - if (matchSegments(s, selector, segment_index - 1, root, page)) { + if (matchSegments(s, selector, segment_index - 1, root, scope, page)) { return true; } // This sibling didn't work, try the next one @@ -307,7 +308,7 @@ fn matchSegments(node: *Node, selector: Selector.Selector, segment_index: usize, if (segment_index == 0) { return true; } - return matchSegments(current, selector, segment_index - 1, root, page); + return matchSegments(current, selector, segment_index - 1, root, scope, page); } // subsequent_sibling already handled its recursion above @@ -315,12 +316,12 @@ fn matchSegments(node: *Node, selector: Selector.Selector, segment_index: usize, } // Find an ancestor that matches the compound (any distance up the tree) -fn matchDescendant(node: *Node, compound: Selector.Compound, root: ?*Node, page: *Page) ?*Node { +fn matchDescendant(node: *Node, compound: Selector.Compound, root: ?*Node, scope: *Node, page: *Page) ?*Node { var current = node._parent; while (current) |ancestor| { if (ancestor.is(Node.Element)) |ancestor_el| { - if (matchesCompound(ancestor_el, compound, page)) { + if (matchesCompound(ancestor_el, compound, scope, page)) { return ancestor; } } @@ -339,7 +340,7 @@ fn matchDescendant(node: *Node, compound: Selector.Compound, root: ?*Node, page: } // Find the direct parent if it matches the compound -fn matchChild(node: *Node, compound: Selector.Compound, root: ?*Node, page: *Page) ?*Node { +fn matchChild(node: *Node, compound: Selector.Compound, root: ?*Node, scope: *Node, page: *Page) ?*Node { const parent = node._parent orelse return null; // Don't match beyond the root boundary @@ -352,7 +353,7 @@ fn matchChild(node: *Node, compound: Selector.Compound, root: ?*Node, page: *Pag const parent_el = parent.is(Node.Element) orelse return null; - if (matchesCompound(parent_el, compound, page)) { + if (matchesCompound(parent_el, compound, scope, page)) { return parent; } @@ -360,7 +361,7 @@ fn matchChild(node: *Node, compound: Selector.Compound, root: ?*Node, page: *Pag } // Find the immediately preceding sibling if it matches the compound -fn matchNextSibling(node: *Node, compound: Selector.Compound, page: *Page) ?*Node { +fn matchNextSibling(node: *Node, compound: Selector.Compound, scope: *Node, page: *Page) ?*Node { var sibling = node.previousSibling(); // For next_sibling (+), we need the immediately preceding element sibling @@ -372,7 +373,7 @@ fn matchNextSibling(node: *Node, compound: Selector.Compound, page: *Page) ?*Nod }; // Found an element - check if it matches - if (matchesCompound(sibling_el, compound, page)) { + if (matchesCompound(sibling_el, compound, scope, page)) { return s; } // we found an element, it wasn't a match, we're done @@ -383,7 +384,7 @@ fn matchNextSibling(node: *Node, compound: Selector.Compound, page: *Page) ?*Nod } // Find any preceding sibling that matches the compound -fn matchSubsequentSibling(node: *Node, compound: Selector.Compound, page: *Page) ?*Node { +fn matchSubsequentSibling(node: *Node, compound: Selector.Compound, scope: *Node, page: *Page) ?*Node { var sibling = node.previousSibling(); // For subsequent_sibling (~), check all preceding element siblings @@ -394,7 +395,7 @@ fn matchSubsequentSibling(node: *Node, compound: Selector.Compound, page: *Page) continue; }; - if (matchesCompound(sibling_el, compound, page)) { + if (matchesCompound(sibling_el, compound, scope, page)) { return s; } @@ -404,17 +405,17 @@ fn matchSubsequentSibling(node: *Node, compound: Selector.Compound, page: *Page) return null; } -fn matchesCompound(el: *Node.Element, compound: Selector.Compound, page: *Page) bool { +fn matchesCompound(el: *Node.Element, compound: Selector.Compound, scope: *Node, page: *Page) bool { // For compound selectors, ALL parts must match for (compound.parts) |part| { - if (!matchesPart(el, part, page)) { + if (!matchesPart(el, part, scope, page)) { return false; } } return true; } -fn matchesPart(el: *Node.Element, part: Part, page: *Page) bool { +fn matchesPart(el: *Node.Element, part: Part, scope: *Node, page: *Page) bool { switch (part) { .id => |id| { const element_id = el.getAttributeSafe("id") orelse return false; @@ -435,7 +436,7 @@ fn matchesPart(el: *Node.Element, part: Part, page: *Page) bool { return std.mem.eql(u8, element_tag, tag_name); }, .universal => return true, - .pseudo_class => |pseudo| return matchesPseudoClass(el, pseudo, page), + .pseudo_class => |pseudo| return matchesPseudoClass(el, pseudo, scope, page), .attribute => |attr| return matchesAttribute(el, attr), } } @@ -495,7 +496,7 @@ fn attributeContainsWord(value: []const u8, word: []const u8) bool { return false; } -fn matchesPseudoClass(el: *Node.Element, pseudo: Selector.PseudoClass, page: *Page) bool { +fn matchesPseudoClass(el: *Node.Element, pseudo: Selector.PseudoClass, scope: *Node, page: *Page) bool { const node = el.asNode(); switch (pseudo) { // State pseudo-classes @@ -565,6 +566,10 @@ fn matchesPseudoClass(el: *Node.Element, pseudo: Selector.PseudoClass, page: *Pa const parent = node.parentNode() orelse return false; return parent._type == .document; }, + .scope => { + // :scope matches the reference element (querySelector root) + return node == scope; + }, .empty => { return node.firstChild() == null; }, @@ -591,7 +596,7 @@ fn matchesPseudoClass(el: *Node.Element, pseudo: Selector.PseudoClass, page: *Pa .lang => return false, .not => |selectors| { for (selectors) |selector| { - if (matches(node, selector, page)) { + if (matches(node, selector, scope, page)) { return false; } } @@ -599,7 +604,7 @@ fn matchesPseudoClass(el: *Node.Element, pseudo: Selector.PseudoClass, page: *Pa }, .is => |selectors| { for (selectors) |selector| { - if (matches(node, selector, page)) { + if (matches(node, selector, scope, page)) { return true; } } @@ -607,7 +612,7 @@ fn matchesPseudoClass(el: *Node.Element, pseudo: Selector.PseudoClass, page: *Pa }, .where => |selectors| { for (selectors) |selector| { - if (matches(node, selector, page)) { + if (matches(node, selector, scope, page)) { return true; } } @@ -622,11 +627,11 @@ fn matchesPseudoClass(el: *Node.Element, pseudo: Selector.PseudoClass, page: *Pa continue; }; - if (matches(child_el.asNode(), selector, page)) { + if (matches(child_el.asNode(), selector, scope, page)) { return true; } - if (matchesHasDescendant(child_el, selector, page)) { + if (matchesHasDescendant(child_el, selector, scope, page)) { return true; } @@ -638,7 +643,7 @@ fn matchesPseudoClass(el: *Node.Element, pseudo: Selector.PseudoClass, page: *Pa } } -fn matchesHasDescendant(el: *Node.Element, selector: Selector.Selector, page: *Page) bool { +fn matchesHasDescendant(el: *Node.Element, selector: Selector.Selector, scope: *Node, page: *Page) bool { var child = el.asNode().firstChild(); while (child) |c| { const child_el = c.is(Node.Element) orelse { @@ -646,11 +651,11 @@ fn matchesHasDescendant(el: *Node.Element, selector: Selector.Selector, page: *P continue; }; - if (matches(child_el.asNode(), selector, page)) { + if (matches(child_el.asNode(), selector, scope, page)) { return true; } - if (matchesHasDescendant(child_el, selector, page)) { + if (matchesHasDescendant(child_el, selector, scope, page)) { return true; } diff --git a/src/browser/webapi/selector/Parser.zig b/src/browser/webapi/selector/Parser.zig index a793e7c82..02d9e1c74 100644 --- a/src/browser/webapi/selector/Parser.zig +++ b/src/browser/webapi/selector/Parser.zig @@ -504,6 +504,7 @@ fn pseudoClass(self: *Parser, arena: Allocator, page: *Page) !Selector.PseudoCla if (fastEql(name, "modal")) return .modal; if (fastEql(name, "hover")) return .hover; if (fastEql(name, "focus")) return .focus; + if (fastEql(name, "scope")) return .scope; if (fastEql(name, "empty")) return .empty; if (fastEql(name, "valid")) return .valid; }, diff --git a/src/browser/webapi/selector/Selector.zig b/src/browser/webapi/selector/Selector.zig index 5360cd3fe..44d7c4387 100644 --- a/src/browser/webapi/selector/Selector.zig +++ b/src/browser/webapi/selector/Selector.zig @@ -82,7 +82,7 @@ pub fn matches(el: *Node.Element, input: []const u8, page: *Page) !bool { const selectors = try Parser.parseList(arena, input, page); for (selectors) |selector| { - if (List.matches(el.asNode(), selector, page)) { + if (List.matches(el.asNode(), selector, el.asNode(), page)) { return true; } } @@ -165,6 +165,7 @@ pub const PseudoClass = union(enum) { // Tree structural root, + scope, empty, first_child, last_child, From c9882e10a49de77b58e50789244dd9913d6648e1 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 4 Dec 2025 14:39:15 +0800 Subject: [PATCH 139/219] Properly handle insertion of DocumentFragment Add various CData methods XHR and Fetch request headers Animation mocks --- src/browser/Page.zig | 18 + src/browser/js/Object.zig | 6 +- src/browser/js/bridge.zig | 1 + src/browser/tests/cdata/character_data.html | 730 ++++++++++++++++++ .../tests/document_fragment/insertion.html | 238 ++++++ src/browser/tests/net/request.html | 6 + src/browser/webapi/CData.zig | 133 ++++ src/browser/webapi/DOMException.zig | 4 + src/browser/webapi/Element.zig | 21 +- src/browser/webapi/KeyValueList.zig | 31 + src/browser/webapi/Node.zig | 6 +- src/browser/webapi/Window.zig | 2 +- src/browser/webapi/animation/Animation.zig | 49 ++ src/browser/webapi/net/Fetch.zig | 10 +- src/browser/webapi/net/Headers.zig | 31 +- src/browser/webapi/net/Request.zig | 14 +- src/browser/webapi/net/Response.zig | 2 +- src/browser/webapi/net/URLSearchParams.zig | 21 +- src/browser/webapi/net/XMLHttpRequest.zig | 13 +- 19 files changed, 1288 insertions(+), 48 deletions(-) create mode 100644 src/browser/tests/cdata/character_data.html create mode 100644 src/browser/tests/document_fragment/insertion.html create mode 100644 src/browser/webapi/animation/Animation.zig diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 7f8ad2bcf..6c94e4530 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -1431,6 +1431,24 @@ pub fn appendAllChildren(self: *Page, parent: *Node, target: *Node) !void { } } +pub fn insertAllChildrenBefore(self: *Page, fragment: *Node, target: *Node, ref_node: *Node) !void { + self.domChanged(); + const dest_connected = target.isConnected(); + + var it = fragment.childrenIterator(); + while (it.next()) |child| { + // Check if child was connected BEFORE removing it from fragment + const child_was_connected = child.isConnected(); + self.removeNode(fragment, child, .{ .will_be_reconnected = dest_connected }); + try self.insertNodeRelative( + target, + child, + .{ .before = ref_node }, + .{ .child_already_connected = child_was_connected }, + ); + } +} + fn _appendNode(self: *Page, comptime from_parser: bool, parent: *Node, child: *Node, opts: InsertNodeOpts) !void { self._insertNodeRelative(from_parser, parent, child, .append, opts); } diff --git a/src/browser/js/Object.zig b/src/browser/js/Object.zig index 222f2b752..9ab35fe12 100644 --- a/src/browser/js/Object.zig +++ b/src/browser/js/Object.zig @@ -135,7 +135,7 @@ pub fn isNullOrUndefined(self: Object) bool { return self.js_obj.toValue().isNullOrUndefined(); } -pub fn nameIterator(self: Object, allocator: Allocator) NameIterator { +pub fn nameIterator(self: Object) NameIterator { const context = self.context; const js_obj = self.js_obj; @@ -145,7 +145,6 @@ pub fn nameIterator(self: Object, allocator: Allocator) NameIterator { return .{ .count = count, .context = context, - .allocator = allocator, .js_obj = array.castTo(v8.Object), }; } @@ -158,7 +157,6 @@ pub const NameIterator = struct { count: u32, idx: u32 = 0, js_obj: v8.Object, - allocator: Allocator, context: *const Context, pub fn next(self: *NameIterator) !?[]const u8 { @@ -170,6 +168,6 @@ pub const NameIterator = struct { const context = self.context; const js_val = try self.js_obj.getAtIndex(context.v8_context, idx); - return try context.valueToString(js_val, .{ .allocator = self.allocator }); + return try context.valueToString(js_val, .{}); } }; diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index e20d9b7fb..372d260aa 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -568,6 +568,7 @@ pub const JsApis = flattenTypes(&.{ @import("../webapi/media/MediaError.zig"), @import("../webapi/media/TextTrackCue.zig"), @import("../webapi/media/VTTCue.zig"), + @import("../webapi/animation/Animation.zig"), @import("../webapi/EventTarget.zig"), @import("../webapi/Location.zig"), @import("../webapi/Navigator.zig"), diff --git a/src/browser/tests/cdata/character_data.html b/src/browser/tests/cdata/character_data.html new file mode 100644 index 000000000..85513b00f --- /dev/null +++ b/src/browser/tests/cdata/character_data.html @@ -0,0 +1,730 @@ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/browser/tests/document_fragment/insertion.html b/src/browser/tests/document_fragment/insertion.html new file mode 100644 index 000000000..f110766c5 --- /dev/null +++ b/src/browser/tests/document_fragment/insertion.html @@ -0,0 +1,238 @@ + + + +
+ + + + + + + + + + + + + + + + + + diff --git a/src/browser/tests/net/request.html b/src/browser/tests/net/request.html index c0028cf83..437c26301 100644 --- a/src/browser/tests/net/request.html +++ b/src/browser/tests/net/request.html @@ -45,6 +45,11 @@ const req = new Request('https://example.com/api', { headers }); testing.expectEqual('value', req.headers.get('X-Custom')); } + +{ + const req = new Request('https://example.com/api', {headers: {over: '9000!'}}); + testing.expectEqual('9000!', req.headers.get('over')); +} + diff --git a/src/browser/webapi/CData.zig b/src/browser/webapi/CData.zig index cb39a70ba..f02d51cad 100644 --- a/src/browser/webapi/CData.zig +++ b/src/browser/webapi/CData.zig @@ -79,6 +79,119 @@ pub fn format(self: *const CData, writer: *std.io.Writer) !void { }; } +pub fn getLength(self: *const CData) usize { + return self._data.len; +} + +pub fn appendData(self: *CData, data: []const u8, page: *Page) !void { + const new_data = try std.mem.concat(page.arena, u8, &.{ self._data, data }); + try self.setData(new_data, page); +} + +pub fn deleteData(self: *CData, offset: usize, count: usize, page: *Page) !void { + if (offset > self._data.len) return error.IndexSizeError; + const end = @min(offset + count, self._data.len); + + // Just slice - original data stays in arena + const old_value = self._data; + if (offset == 0) { + self._data = self._data[end..]; + } else if (end >= self._data.len) { + self._data = self._data[0..offset]; + } else { + self._data = try std.mem.concat(page.arena, u8, &.{ + self._data[0..offset], + self._data[end..], + }); + } + page.characterDataChange(self.asNode(), old_value); +} + +pub fn insertData(self: *CData, offset: usize, data: []const u8, page: *Page) !void { + if (offset > self._data.len) return error.IndexSizeError; + const new_data = try std.mem.concat(page.arena, u8, &.{ + self._data[0..offset], + data, + self._data[offset..], + }); + try self.setData(new_data, page); +} + +pub fn replaceData(self: *CData, offset: usize, count: usize, data: []const u8, page: *Page) !void { + if (offset > self._data.len) return error.IndexSizeError; + const end = @min(offset + count, self._data.len); + const new_data = try std.mem.concat(page.arena, u8, &.{ + self._data[0..offset], + data, + self._data[end..], + }); + try self.setData(new_data, page); +} + +pub fn substringData(self: *const CData, offset: usize, count: usize) ![]const u8 { + if (offset > self._data.len) return error.IndexSizeError; + const end = @min(offset + count, self._data.len); + return self._data[offset..end]; +} + +pub fn remove(self: *CData, page: *Page) !void { + const node = self.asNode(); + const parent = node.parentNode() orelse return; + _ = try parent.removeChild(node, page); +} + +pub fn before(self: *CData, nodes: []const Node.NodeOrText, page: *Page) !void { + const node = self.asNode(); + const parent = node.parentNode() orelse return; + + for (nodes) |node_or_text| { + const child = try node_or_text.toNode(page); + _ = try parent.insertBefore(child, node, page); + } +} + +pub fn after(self: *CData, nodes: []const Node.NodeOrText, page: *Page) !void { + const node = self.asNode(); + const parent = node.parentNode() orelse return; + const next = node.nextSibling(); + + for (nodes) |node_or_text| { + const child = try node_or_text.toNode(page); + _ = try parent.insertBefore(child, next, page); + } +} + +pub fn replaceWith(self: *CData, nodes: []const Node.NodeOrText, page: *Page) !void { + const node = self.asNode(); + const parent = node.parentNode() orelse return; + const next = node.nextSibling(); + + _ = try parent.removeChild(node, page); + + for (nodes) |node_or_text| { + const child = try node_or_text.toNode(page); + _ = try parent.insertBefore(child, next, page); + } +} + +pub fn nextElementSibling(self: *CData) ?*Node.Element { + var maybe_sibling = self.asNode().nextSibling(); + while (maybe_sibling) |sibling| { + if (sibling.is(Node.Element)) |el| return el; + maybe_sibling = sibling.nextSibling(); + } + return null; +} + +pub fn previousElementSibling(self: *CData) ?*Node.Element { + var maybe_sibling = self.asNode().previousSibling(); + while (maybe_sibling) |sibling| { + if (sibling.is(Node.Element)) |el| return el; + maybe_sibling = sibling.previousSibling(); + } + return null; +} + pub const JsApi = struct { pub const bridge = js.Bridge(CData); @@ -89,4 +202,24 @@ pub const JsApi = struct { }; pub const data = bridge.accessor(CData.getData, CData.setData, .{}); + pub const length = bridge.accessor(CData.getLength, null, .{}); + + pub const appendData = bridge.function(CData.appendData, .{}); + pub const deleteData = bridge.function(CData.deleteData, .{ .dom_exception = true }); + pub const insertData = bridge.function(CData.insertData, .{ .dom_exception = true }); + pub const replaceData = bridge.function(CData.replaceData, .{ .dom_exception = true }); + pub const substringData = bridge.function(CData.substringData, .{ .dom_exception = true }); + + pub const remove = bridge.function(CData.remove, .{}); + pub const before = bridge.function(CData.before, .{}); + pub const after = bridge.function(CData.after, .{}); + pub const replaceWith = bridge.function(CData.replaceWith, .{}); + + pub const nextElementSibling = bridge.accessor(CData.nextElementSibling, null, .{}); + pub const previousElementSibling = bridge.accessor(CData.previousElementSibling, null, .{}); }; + +const testing = @import("../../testing.zig"); +test "WebApi: CData" { + try testing.htmlRunner("cdata", .{}); +} diff --git a/src/browser/webapi/DOMException.zig b/src/browser/webapi/DOMException.zig index 2f1cc789f..72d795595 100644 --- a/src/browser/webapi/DOMException.zig +++ b/src/browser/webapi/DOMException.zig @@ -33,6 +33,7 @@ pub fn fromError(err: anyerror) ?DOMException { error.NotFound => .{ ._code = .not_found }, error.NotSupported => .{ ._code = .not_supported }, error.HierarchyError => .{ ._code = .hierarchy_error }, + error.IndexSizeError => .{ ._code = .index_size_error }, else => null, }; } @@ -45,6 +46,7 @@ pub fn getName(self: *const DOMException) []const u8 { return switch (self._code) { .none => "Error", .invalid_character_error => "InvalidCharacterError", + .index_size_error => "IndexSizeErorr", .syntax_error => "SyntaxError", .not_found => "NotFoundError", .not_supported => "NotSupportedError", @@ -56,6 +58,7 @@ pub fn getMessage(self: *const DOMException) []const u8 { return switch (self._code) { .none => "", .invalid_character_error => "Invalid Character", + .index_size_error => "IndexSizeError: Index or size is negative or greater than the allowed amount", .syntax_error => "Syntax Error", .not_supported => "Not Supported", .not_found => "Not Found", @@ -65,6 +68,7 @@ pub fn getMessage(self: *const DOMException) []const u8 { const Code = enum(u8) { none = 0, + index_size_error = 1, hierarchy_error = 3, invalid_character_error = 5, not_found = 8, diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index 402409539..b3847bc16 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -26,17 +26,18 @@ const Page = @import("../Page.zig"); const reflect = @import("../reflect.zig"); const Node = @import("Node.zig"); +const CSS = @import("CSS.zig"); +const DOMRect = @import("DOMRect.zig"); +const ShadowRoot = @import("ShadowRoot.zig"); const collections = @import("collections.zig"); const Selector = @import("selector/Selector.zig"); -pub const Attribute = @import("element/Attribute.zig"); +const Animation = @import("animation/Animation.zig"); +const DOMStringMap = @import("element/DOMStringMap.zig"); const CSSStyleProperties = @import("css/CSSStyleProperties.zig"); -pub const DOMStringMap = @import("element/DOMStringMap.zig"); -const DOMRect = @import("DOMRect.zig"); -const CSS = @import("CSS.zig"); -const ShadowRoot = @import("ShadowRoot.zig"); pub const Svg = @import("element/Svg.zig"); pub const Html = @import("element/Html.zig"); +pub const Attribute = @import("element/Attribute.zig"); const Element = @This(); @@ -587,6 +588,14 @@ pub fn querySelectorAll(self: *Element, input: []const u8, page: *Page) !*Select return Selector.querySelectorAll(self.asNode(), input, page); } +pub fn getAnimations(_: *const Element) []*Animation { + return &.{}; +} + +pub fn animate(_: *Element, _: js.Object, _: js.Object) !Animation { + return Animation.init(); +} + pub fn closest(self: *Element, selector: []const u8, page: *Page) !?*Element { if (selector.len == 0) { return error.SyntaxError; @@ -1012,6 +1021,8 @@ pub const JsApi = struct { pub const querySelector = bridge.function(Element.querySelector, .{ .dom_exception = true }); pub const querySelectorAll = bridge.function(Element.querySelectorAll, .{ .dom_exception = true }); pub const closest = bridge.function(Element.closest, .{ .dom_exception = true }); + pub const getAnimations = bridge.function(Element.getAnimations, .{}); + pub const animate = bridge.function(Element.animate, .{}); pub const checkVisibility = bridge.function(Element.checkVisibility, .{}); pub const getBoundingClientRect = bridge.function(Element.getBoundingClientRect, .{}); pub const getElementsByTagName = bridge.function(Element.getElementsByTagName, .{}); diff --git a/src/browser/webapi/KeyValueList.zig b/src/browser/webapi/KeyValueList.zig index c9eb70c8d..4ec85f203 100644 --- a/src/browser/webapi/KeyValueList.zig +++ b/src/browser/webapi/KeyValueList.zig @@ -41,9 +41,40 @@ pub const empty: KeyValueList = .{ ._entries = .empty, }; +pub fn copy(arena: Allocator, original: KeyValueList) !KeyValueList { + var list = KeyValueList.init(); + try list.ensureTotalCapacity(arena, original.len()); + for (original._entries.items) |entry| { + try list.appendAssumeCapacity(arena, entry.name.str(), entry.value.str()); + } + return list; +} + +pub fn fromJsObject(arena: Allocator, js_obj: js.Object) !KeyValueList { + var it = js_obj.nameIterator(); + var list = KeyValueList.init(); + try list.ensureTotalCapacity(arena, it.count); + + while (try it.next()) |name| { + const js_value = try js_obj.get(name); + const value = try js_value.toString(arena); + + try list._entries.append(arena, .{ + .name = try String.init(arena, name, .{}), + .value = try String.init(arena, value, .{}), + }); + } + + return list; +} + pub const Entry = struct { name: String, value: String, + + pub fn format(self: Entry, writer: *std.Io.Writer) !void { + return writer.print("{f}: {f}", .{ self.name, self.value }); + } }; pub fn init() KeyValueList { diff --git a/src/browser/webapi/Node.zig b/src/browser/webapi/Node.zig index ab0c28ec8..8e65a5e9a 100644 --- a/src/browser/webapi/Node.zig +++ b/src/browser/webapi/Node.zig @@ -143,7 +143,6 @@ pub fn parentElement(self: *const Node) ?*Element { } pub fn appendChild(self: *Node, child: *Node, page: *Page) !*Node { - // Special case: DocumentFragment - append all its children instead if (child.is(DocumentFragment)) |_| { try page.appendAllChildren(child, self); return child; @@ -338,6 +337,11 @@ pub fn insertBefore(self: *Node, new_node: *Node, ref_node_: ?*Node, page: *Page return error.NotFound; } + if (new_node.is(DocumentFragment)) |_| { + try page.insertAllChildrenBefore(new_node, self, ref_node); + return new_node; + } + const child_already_connected = new_node.isConnected(); page.domChanged(); diff --git a/src/browser/webapi/Window.zig b/src/browser/webapi/Window.zig index ad755b441..250abb268 100644 --- a/src/browser/webapi/Window.zig +++ b/src/browser/webapi/Window.zig @@ -157,7 +157,7 @@ pub fn setOnUnhandledRejection(self: *Window, cb_: ?js.Function) !void { } } -pub fn fetch(_: *const Window, input: Fetch.Input, options: ?Fetch.RequestInit, page: *Page) !js.Promise { +pub fn fetch(_: *const Window, input: Fetch.Input, options: ?Fetch.InitOpts, page: *Page) !js.Promise { return Fetch.init(input, options, page); } diff --git a/src/browser/webapi/animation/Animation.zig b/src/browser/webapi/animation/Animation.zig new file mode 100644 index 000000000..2fecfa954 --- /dev/null +++ b/src/browser/webapi/animation/Animation.zig @@ -0,0 +1,49 @@ +// 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"); + +const Animation = @This(); + +pub fn init() !Animation { + return .{}; +} + +pub fn play(_: *Animation) void {} +pub fn pause(_: *Animation) void {} +pub fn cancel(_: *Animation) void {} +pub fn finish(_: *Animation) void {} +pub fn reverse(_: *Animation) void {} + +pub const JsApi = struct { + pub const bridge = js.Bridge(Animation); + + pub const Meta = struct { + pub const name = "Animation"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + pub const empty_with_no_proto = true; + }; + + pub const play = bridge.function(Animation.play, .{}); + pub const pause = bridge.function(Animation.pause, .{}); + pub const cancel = bridge.function(Animation.cancel, .{}); + pub const finish = bridge.function(Animation.finish, .{}); + pub const reverse = bridge.function(Animation.reverse, .{}); +}; diff --git a/src/browser/webapi/net/Fetch.zig b/src/browser/webapi/net/Fetch.zig index 0f6c37f32..50d44270e 100644 --- a/src/browser/webapi/net/Fetch.zig +++ b/src/browser/webapi/net/Fetch.zig @@ -40,10 +40,10 @@ _response: *Response, _resolver: js.PersistentPromiseResolver, pub const Input = Request.Input; -pub const RequestInit = Request.Options; +pub const InitOpts = Request.InitOpts; // @ZIGDOM just enough to get campfire demo working -pub fn init(input: Input, options: ?RequestInit, page: *Page) !js.Promise { +pub fn init(input: Input, options: ?InitOpts, page: *Page) !js.Promise { const request = try Request.init(input, options, page); const fetch = try page.arena.create(Fetch); @@ -56,7 +56,11 @@ pub fn init(input: Input, options: ?RequestInit, page: *Page) !js.Promise { }; const http_client = page._session.browser.http_client; - const headers = try http_client.newHeaders(); + var headers = try http_client.newHeaders(); + if (request._headers) |h| { + try h.populateHttpHeader(page.call_arena, &headers); + } + try page.requestCookie(.{}).headersForRequest(page.arena, request._url, &headers); if (comptime IS_DEBUG) { log.debug(.http, "fetch", .{ .url = request._url }); diff --git a/src/browser/webapi/net/Headers.zig b/src/browser/webapi/net/Headers.zig index 136207bd9..633771791 100644 --- a/src/browser/webapi/net/Headers.zig +++ b/src/browser/webapi/net/Headers.zig @@ -5,16 +5,34 @@ const log = @import("../../../log.zig"); const Page = @import("../../Page.zig"); const KeyValueList = @import("../KeyValueList.zig"); +const Allocator = std.mem.Allocator; + const Headers = @This(); _list: KeyValueList, -pub fn init(page: *Page) !*Headers { +pub const InitOpts = union(enum) { + obj: *Headers, + js_obj: js.Object, +}; + +pub fn init(opts_: ?InitOpts, page: *Page) !*Headers { + const list = if (opts_) |opts| switch (opts) { + .obj => |obj| try KeyValueList.copy(page.arena, obj._list), + .js_obj => |js_obj| try KeyValueList.fromJsObject(page.arena, js_obj), + } else KeyValueList.init(); + return page._factory.create(Headers{ - ._list = KeyValueList.init(), + ._list = list, }); } +// pub fn fromJsObject(js_obj: js.Object, page: *Page) !*Headers { +// return page._factory.create(Headers{ +// ._list = try KeyValueList.fromJsObject(page.arena, js_obj), +// }); +// } + pub fn append(self: *Headers, name: []const u8, value: []const u8, page: *Page) !void { const normalized_name = normalizeHeaderName(name, page); try self._list.append(page.arena, normalized_name, value); @@ -63,6 +81,15 @@ pub fn forEach(self: *Headers, cb_: js.Function, js_this_: ?js.Object) !void { } } +// TODO: do we really need 2 different header structs?? +const Http = @import("../../../http/Http.zig"); +pub fn populateHttpHeader(self: *Headers, allocator: Allocator, http_headers: *Http.Headers) !void { + for (self._list._entries.items) |entry| { + const merged = try std.mem.concatWithSentinel(allocator, u8, &.{ entry.name.str(), ": ", entry.value.str() }, 0); + try http_headers.add(merged); + } +} + fn normalizeHeaderName(name: []const u8, page: *Page) []const u8 { if (name.len > page.buf.len) { return name; diff --git a/src/browser/webapi/net/Request.zig b/src/browser/webapi/net/Request.zig index d1524afe1..9ca84c418 100644 --- a/src/browser/webapi/net/Request.zig +++ b/src/browser/webapi/net/Request.zig @@ -37,19 +37,19 @@ pub const Input = union(enum) { url: [:0]const u8, }; -pub const Options = struct { +pub const InitOpts = struct { method: ?[]const u8 = null, - headers: ?*Headers = null, + headers: ?Headers.InitOpts = null, }; -pub fn init(input: Input, opts_: ?Options, page: *Page) !*Request { +pub fn init(input: Input, opts_: ?InitOpts, page: *Page) !*Request { const arena = page.arena; const url = switch (input) { .url => |u| try URL.resolve(arena, page.url, u, .{ .always_dupe = true }), .request => |r| try arena.dupeZ(u8, r._url), }; - const opts = opts_ orelse Options{}; + const opts = opts_ orelse InitOpts{}; const method = if (opts.method) |m| try parseMethod(m, page) else switch (input) { @@ -57,8 +57,8 @@ pub fn init(input: Input, opts_: ?Options, page: *Page) !*Request { .request => |r| r._method, }; - const headers = if (opts.headers) |h| - h + const headers = if (opts.headers) |header_init| + try Headers.init(header_init, page) else switch (input) { .url => null, .request => |r| r._headers, @@ -103,7 +103,7 @@ pub fn getHeaders(self: *Request, page: *Page) !*Headers { return headers; } - const headers = try Headers.init(page); + const headers = try Headers.init(null, page); self._headers = headers; return headers; } diff --git a/src/browser/webapi/net/Response.zig b/src/browser/webapi/net/Response.zig index 244475668..fe4643209 100644 --- a/src/browser/webapi/net/Response.zig +++ b/src/browser/webapi/net/Response.zig @@ -56,7 +56,7 @@ pub fn init(body_: ?[]const u8, opts_: ?InitOpts, page: *Page) !*Response { ._arena = page.arena, ._status = opts.status, ._body = body, - ._headers = opts.headers orelse try Headers.init(page), + ._headers = opts.headers orelse try Headers.init(null, page), ._type = .basic, // @ZIGDOM: todo }); } diff --git a/src/browser/webapi/net/URLSearchParams.zig b/src/browser/webapi/net/URLSearchParams.zig index 9bdecd2ec..73e5e1101 100644 --- a/src/browser/webapi/net/URLSearchParams.zig +++ b/src/browser/webapi/net/URLSearchParams.zig @@ -45,7 +45,7 @@ pub fn init(opts_: ?InitOpts, page: *Page) !*URLSearchParams { .query_string => |qs| break :blk try paramsFromString(arena, qs, &page.buf), .value => |js_val| { if (js_val.isObject()) { - break :blk try paramsFromObject(arena, js_val.toObject()); + break :blk try KeyValueList.fromJsObject(arena, js_val.toObject()); } if (js_val.isString()) { break :blk try paramsFromString(arena, try js_val.toString(arena), &page.buf); @@ -187,25 +187,6 @@ fn paramsFromString(allocator: Allocator, input_: []const u8, buf: []u8) !KeyVal return params; } -fn paramsFromObject(arena: Allocator, js_obj: js.Object) !KeyValueList { - var it = js_obj.nameIterator(arena); - - var params = KeyValueList.init(); - try params.ensureTotalCapacity(arena, it.count); - - while (try it.next()) |name| { - const js_value = try js_obj.get(name); - const value = try js_value.toString(arena); - - try params._entries.append(arena, .{ - .name = try String.init(arena, name, .{}), - .value = try String.init(arena, value, .{}), - }); - } - - return params; -} - fn unescape(arena: Allocator, value: []const u8, buf: []u8) !String { if (value.len == 0) { return String.init(undefined, "", .{}); diff --git a/src/browser/webapi/net/XMLHttpRequest.zig b/src/browser/webapi/net/XMLHttpRequest.zig index 4b0cdb9f9..4959d5638 100644 --- a/src/browser/webapi/net/XMLHttpRequest.zig +++ b/src/browser/webapi/net/XMLHttpRequest.zig @@ -26,6 +26,7 @@ const URL = @import("../../URL.zig"); const Mime = @import("../../Mime.zig"); const Page = @import("../../Page.zig"); const Event = @import("../Event.zig"); +const Headers = @import("Headers.zig"); const EventTarget = @import("../EventTarget.zig"); const XMLHttpRequestEventTarget = @import("XMLHttpRequestEventTarget.zig"); @@ -40,6 +41,7 @@ _transfer: ?*Http.Transfer = null, _url: [:0]const u8 = "", _method: Http.Method = .GET, +_request_headers: *Headers, _request_body: ?[]const u8 = null, _response: std.ArrayList(u8) = .empty, @@ -71,6 +73,7 @@ pub fn init(page: *Page) !*XMLHttpRequest { ._page = page, ._proto = undefined, ._arena = page.arena, + ._request_headers = try Headers.init(null, page), }); } @@ -129,6 +132,10 @@ pub fn open(self: *XMLHttpRequest, method_: []const u8, url: [:0]const u8) !void try self.stateChanged(.opened, self._page); } +pub fn setRequestHeader(self: *XMLHttpRequest, name: []const u8, value: []const u8, page: *Page) !void { + return self._request_headers.append(name, value, page); +} + pub fn send(self: *XMLHttpRequest, body_: ?[]const u8) !void { if (comptime IS_DEBUG) { log.debug(.http, "XMLHttpRequest.send", .{ .url = self._url }); @@ -143,10 +150,7 @@ pub fn send(self: *XMLHttpRequest, body_: ?[]const u8) !void { const page = self._page; const http_client = page._session.browser.http_client; var headers = try http_client.newHeaders(); - // @ZIGDOM - // for (self._headers.items) |hdr| { - // try headers.add(hdr); - // } + try self._request_headers.populateHttpHeader(page.call_arena, &headers); try page.requestCookie(.{}).headersForRequest(self._arena, self._url, &headers); try http_client.request(.{ @@ -351,6 +355,7 @@ pub const JsApi = struct { pub const responseType = bridge.accessor(XMLHttpRequest.getResponseType, XMLHttpRequest.setResponseType, .{}); pub const status = bridge.accessor(XMLHttpRequest.getStatus, null, .{}); pub const response = bridge.accessor(XMLHttpRequest.getResponse, null, .{}); + pub const setRequestHeader = bridge.function(XMLHttpRequest.setRequestHeader, .{}); }; const testing = @import("../../../testing.zig"); From aa3a402f70a4901a7d8f57885a37c15c7edc74f8 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 4 Dec 2025 15:38:47 +0800 Subject: [PATCH 140/219] Link get/set rel Include stack trace on console.error Don't unnecessarily copy request header on fetch --- src/browser/webapi/Console.zig | 18 ++++++++++++------ src/browser/webapi/element/html/Link.zig | 9 +++++++++ src/browser/webapi/net/Request.zig | 7 ++++--- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/src/browser/webapi/Console.zig b/src/browser/webapi/Console.zig index 3563f1c57..81fdc54b5 100644 --- a/src/browser/webapi/Console.zig +++ b/src/browser/webapi/Console.zig @@ -19,6 +19,7 @@ const std = @import("std"); const js = @import("../js/js.zig"); +const Page = @import("../Page.zig"); const logger = @import("../../log.zig"); const Console = @This(); @@ -26,25 +27,30 @@ _pad: bool = false, pub const init: Console = .{}; -pub fn log(_: *const Console, values: []js.Object) void { - logger.info(.js, "console.log", .{ValueWriter{ .values = values }}); +pub fn log(_: *const Console, values: []js.Object, page: *Page) void { + logger.info(.js, "console.log", .{ValueWriter{ .page = page, .values = values }}); } -pub fn warn(_: *const Console, values: []js.Object) void { - logger.warn(.js, "console.warn", .{ValueWriter{ .values = values }}); +pub fn warn(_: *const Console, values: []js.Object, page: *Page) void { + logger.warn(.js, "console.warn", .{ValueWriter{ .page = page, .values = values }}); } -pub fn @"error"(_: *const Console, values: []js.Object) void { - logger.warn(.js, "console.error", .{ValueWriter{ .values = values }}); +pub fn @"error"(_: *const Console, values: []js.Object, page: *Page) void { + logger.warn(.js, "console.error", .{ValueWriter{ .page = page, .values = values, .include_stack = true }}); } const ValueWriter = struct { + page: *Page, values: []js.Object, + include_stack: bool = false, pub fn format(self: ValueWriter, writer: *std.io.Writer) !void { for (self.values, 1..) |value, i| { try writer.print("\n arg({d}): {f}", .{ i, value }); } + if (self.include_stack) { + try writer.print("\n stack: {s}", .{self.page.js.stackTrace() catch |err| @errorName(err) orelse "???"}); + } } pub fn jsonStringify(self: ValueWriter, writer: *std.json.Stringify) !void { try writer.beginArray(); diff --git a/src/browser/webapi/element/html/Link.zig b/src/browser/webapi/element/html/Link.zig index 65e879179..b9db1e53c 100644 --- a/src/browser/webapi/element/html/Link.zig +++ b/src/browser/webapi/element/html/Link.zig @@ -43,6 +43,14 @@ pub fn setHref(self: *Link, value: []const u8, page: *Page) !void { try self.asElement().setAttributeSafe("href", value, page); } +pub fn getRel(self: *Link) []const u8 { + return self.asElement().getAttributeSafe("rel") orelse return ""; +} + +pub fn setRel(self: *Link, value: []const u8, page: *Page) !void { + try self.asElement().setAttributeSafe("rel", value, page); +} + pub const JsApi = struct { pub const bridge = js.Bridge(Link); @@ -52,5 +60,6 @@ pub const JsApi = struct { pub var class_id: bridge.ClassId = undefined; }; + pub const rel = bridge.accessor(Link.getRel, Link.setRel, .{}); pub const href = bridge.accessor(Link.getHref, Link.setHref, .{}); }; diff --git a/src/browser/webapi/net/Request.zig b/src/browser/webapi/net/Request.zig index 9ca84c418..8dea853f9 100644 --- a/src/browser/webapi/net/Request.zig +++ b/src/browser/webapi/net/Request.zig @@ -57,9 +57,10 @@ pub fn init(input: Input, opts_: ?InitOpts, page: *Page) !*Request { .request => |r| r._method, }; - const headers = if (opts.headers) |header_init| - try Headers.init(header_init, page) - else switch (input) { + const headers = if (opts.headers) |headers_init| switch (headers_init) { + .obj => |h| h, + else => try Headers.init(headers_init, page), + } else switch (input) { .url => null, .request => |r| r._headers, }; From ff9f9bae1db3c98a963b014c2d61507cdf9a1d50 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 4 Dec 2025 16:10:56 +0800 Subject: [PATCH 141/219] fetch with body --- src/browser/webapi/net/Fetch.zig | 3 ++- src/browser/webapi/net/Request.zig | 34 ++++++++++++++++++++---------- src/http/Http.zig | 2 ++ 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/src/browser/webapi/net/Fetch.zig b/src/browser/webapi/net/Fetch.zig index 50d44270e..88c961d73 100644 --- a/src/browser/webapi/net/Fetch.zig +++ b/src/browser/webapi/net/Fetch.zig @@ -69,7 +69,8 @@ pub fn init(input: Input, options: ?InitOpts, page: *Page) !js.Promise { try http_client.request(.{ .ctx = fetch, .url = request._url, - .method = .GET, + .method = request._method, + .body = request._body, .headers = headers, .resource_type = .fetch, .cookie_jar = &page._session.cookie_jar, diff --git a/src/browser/webapi/net/Request.zig b/src/browser/webapi/net/Request.zig index 8dea853f9..d053b4347 100644 --- a/src/browser/webapi/net/Request.zig +++ b/src/browser/webapi/net/Request.zig @@ -19,6 +19,7 @@ const std = @import("std"); const js = @import("../../js/js.zig"); +const Http = @import("../../../http/Http.zig"); const URL = @import("../URL.zig"); const Page = @import("../../Page.zig"); @@ -28,8 +29,9 @@ const Allocator = std.mem.Allocator; const Request = @This(); _url: [:0]const u8, -_method: std.http.Method, +_method: Http.Method, _headers: ?*Headers, +_body: ?[]const u8, _arena: Allocator, pub const Input = union(enum) { @@ -40,6 +42,7 @@ pub const Input = union(enum) { pub const InitOpts = struct { method: ?[]const u8 = null, headers: ?Headers.InitOpts = null, + body: ?[]const u8 = null, }; pub fn init(input: Input, opts_: ?InitOpts, page: *Page) !*Request { @@ -65,30 +68,39 @@ pub fn init(input: Input, opts_: ?InitOpts, page: *Page) !*Request { .request => |r| r._headers, }; + const body = if (opts.body) |b| + try arena.dupe(u8, b) + else switch (input) { + .url => null, + .request => |r| r._body, + }; + return page._factory.create(Request{ ._url = url, ._arena = arena, ._method = method, ._headers = headers, + ._body = body, }); } -fn parseMethod(method: []const u8, page: *Page) !std.http.Method { +fn parseMethod(method: []const u8, page: *Page) !Http.Method { if (method.len > "options".len) { return error.InvalidMethod; } const lower = std.ascii.lowerString(&page.buf, method); - if (std.mem.eql(u8, lower, "get")) return .GET; - if (std.mem.eql(u8, lower, "post")) return .POST; - if (std.mem.eql(u8, lower, "delete")) return .DELETE; - if (std.mem.eql(u8, lower, "put")) return .PUT; - if (std.mem.eql(u8, lower, "patch")) return .PATCH; - if (std.mem.eql(u8, lower, "head")) return .HEAD; - if (std.mem.eql(u8, lower, "options")) return .OPTIONS; - - return error.InvalidMethod; + const method_lookup = std.StaticStringMap(Http.Method).initComptime(.{ + .{ "get", .GET }, + .{ "post", .POST }, + .{ "delete", .DELETE }, + .{ "put", .PUT }, + .{ "patch", .PATCH }, + .{ "head", .HEAD }, + .{ "options", .OPTIONS }, + }); + return method_lookup.get(lower) orelse return error.InvalidMethod; } pub fn getUrl(self: *const Request) []const u8 { diff --git a/src/http/Http.zig b/src/http/Http.zig index e5be87ee2..1a5580293 100644 --- a/src/http/Http.zig +++ b/src/http/Http.zig @@ -222,6 +222,7 @@ pub const Connection = struct { .DELETE => "DELETE", .HEAD => "HEAD", .OPTIONS => "OPTIONS", + .PATCH => "PATCH", }; try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_CUSTOMREQUEST, m.ptr)); } @@ -360,6 +361,7 @@ pub const Method = enum(u8) { DELETE = 3, HEAD = 4, OPTIONS = 5, + PATCH = 6, }; // TODO: on BSD / Linux, we could just read the PEM file directly. From dd3781a1ea43064ef91b76a33c039abe5f68335f Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Fri, 5 Dec 2025 16:09:00 +0800 Subject: [PATCH 142/219] Higher performance.now() precision (closer to FFs behavior) Much better v8 object debugging/printing in debug mode Window.requestIdleCallback and cancelIdleCallback Don't prematurely close stream on empty read - queue promises. --- src/browser/js/Caller.zig | 22 ++--- src/browser/js/Context.zig | 99 +++++++++++-------- src/browser/js/Object.zig | 10 +- src/browser/js/js.zig | 29 +++++- .../tests/streams/readable_stream.html | 12 --- src/browser/webapi/CustomElementRegistry.zig | 2 +- src/browser/webapi/Performance.zig | 20 +++- src/browser/webapi/Window.zig | 20 ++++ src/browser/webapi/net/Fetch.zig | 6 +- .../ReadableStreamDefaultController.zig | 43 +++++++- .../streams/ReadableStreamDefaultReader.zig | 9 +- src/datetime.zig | 2 +- 12 files changed, 177 insertions(+), 97 deletions(-) diff --git a/src/browser/js/Caller.zig b/src/browser/js/Caller.zig index f4b32ddbe..d705e01f0 100644 --- a/src/browser/js/Caller.zig +++ b/src/browser/js/Caller.zig @@ -475,23 +475,15 @@ fn logFunctionCallError(self: *Caller, type_name: []const u8, func: []const u8, } fn serializeFunctionArgs(self: *Caller, info: v8.FunctionCallbackInfo) ![]const u8 { - const separator = log.separator(); - const js_parameter_count = info.length(); - const context = self.context; - var arr: std.ArrayListUnmanaged(u8) = .{}; - for (0..js_parameter_count) |i| { - const js_value = info.getArg(@intCast(i)); - const value_string = try context.valueToDetailString(js_value); - const value_type = try context.jsStringToZig(try js_value.typeOf(self.isolate), .{}); - try std.fmt.format(arr.writer(context.call_arena), "{s}{d}: {s} ({s})", .{ - separator, - i + 1, - value_string, - value_type, - }); + var buf = std.Io.Writer.Allocating.init(context.call_arena); + + const separator = log.separator(); + for (0..info.length()) |i| { + try buf.writer.print("{s}{d} - ", .{ separator, i + 1 }); + try context.debugValue(info.getArg(@intCast(i)), &buf.writer); } - return arr.items; + return buf.written(); } // Takes a function, and returns a tuple for its argument. Used when we diff --git a/src/browser/js/Context.zig b/src/browser/js/Context.zig index ce497e487..24f52decc 100644 --- a/src/browser/js/Context.zig +++ b/src/browser/js/Context.zig @@ -1062,67 +1062,84 @@ pub fn jsStringToZigZ(self: *const Context, str: v8.String, opts: JsStringToZigO return buf; } -pub fn valueToDetailString(self: *const Context, value: v8.Value) ![]u8 { - var str: ?v8.String = null; - const v8_context = self.v8_context; - - if (value.isObject() and !value.isFunction()) blk: { - str = v8.Json.stringify(v8_context, value, null) catch break :blk; +pub fn debugValue(self: *const Context, js_val: v8.Value, writer: *std.Io.Writer) !void { + var seen: std.AutoHashMapUnmanaged(u32, void) = .empty; + return _debugValue(self, js_val, &seen, 0, writer) catch error.WriteFailed; +} - if (str.?.lenUtf8(self.isolate) == 2) { - // {} isn't useful, null this so that we can get the toDetailString - // (which might also be useless, but maybe not) - str = null; - } +fn _debugValue(self: *const Context, js_val: v8.Value, seen: *std.AutoHashMapUnmanaged(u32, void), depth: usize, writer: *std.Io.Writer) !void { + if (js_val.isNull()) { + // I think null can sometimes appear as an object, so check this and + // handle it first. + return writer.writeAll("null"); } - if (str == null) { - str = try value.toDetailString(v8_context); + if (!js_val.isObject()) { + // handle these explicitly, so we don't include the type (we only want to include + // it when there's some ambiguity, e.g. the string "true") + if (js_val.isUndefined()) { + return writer.writeAll("undefined"); + } + if (js_val.isTrue()) { + return writer.writeAll("true"); + } + if (js_val.isFalse()) { + return writer.writeAll("false"); + } + // TODO: KARL wait for v8 build to work again, this works with + // the latest version of zig-v8-fork, I just can't build it right now + // APPLY THIS change to valueToString and valueToStringz + // if (js_val.isSymbol()) { + // const js_sym = v8.Symbol{.handle = js_val.handle}; + // const js_sym_desc = js_sym.getDescription(self.isolate); + // const js_sym_str = try self.valueToString(js_sym_desc, .{}); + // return writer.print("{s} (symbol)", .{js_sym_str}); + // } + const js_type = try self.jsStringToZig(try js_val.typeOf(self.isolate), .{}); + const js_val_str = try self.valueToString(js_val, .{}); + if (js_val_str.len > 2000) { + try writer.writeAll(js_val_str[0..2000]); + try writer.writeAll(" ... (truncated)"); + } else { + try writer.writeAll(js_val_str); + } + return writer.print(" ({s})", .{js_type}); } - const s = try self.jsStringToZig(str.?, .{}); - if (comptime builtin.mode == .Debug) { - if (std.mem.eql(u8, s, "[object Object]")) { - if (self.debugValueToString(value.castTo(v8.Object))) |ds| { - return ds; - } else |err| { - log.err(.js, "debug serialize value", .{ .err = err }); - } + const js_obj = js_val.castTo(v8.Object); + { + // explicit scope because gop will become invalid in recursive call + const gop = try seen.getOrPut(self.call_arena, js_obj.getIdentityHash()); + if (gop.found_existing) { + return writer.writeAll("\n"); } + gop.value_ptr.* = {}; } - return s; -} -fn debugValueToString(self: *const Context, js_obj: v8.Object) ![]u8 { - if (comptime builtin.mode != .Debug) { - @compileError("debugValue can only be called in debug mode"); - } const v8_context = self.v8_context; - const names_arr = js_obj.getOwnPropertyNames(v8_context); const names_obj = names_arr.castTo(v8.Object); const len = names_arr.length(); - var arr: std.ArrayListUnmanaged(u8) = .empty; - var writer = arr.writer(self.call_arena); - try writer.writeAll("(JSON.stringify failed, dumping top-level fields)\n"); + if (depth > 20) { + return writer.writeAll("...deeply nested object..."); + } + + try writer.print("({d}/{d})", .{ js_obj.getOwnPropertyNames(v8_context).length(), js_obj.getPropertyNames(v8_context).length() }); for (0..len) |i| { + if (i == 0) { + try writer.writeByte('\n'); + } const field_name = try names_obj.getAtIndex(v8_context, @intCast(i)); - const field_value = try js_obj.getValue(v8_context, field_name); const name = try self.valueToString(field_name, .{}); - const value = try self.valueToString(field_value, .{}); + try writer.splatByteAll(' ', depth); try writer.writeAll(name); try writer.writeAll(": "); - if (std.mem.indexOfAny(u8, value, &std.ascii.whitespace) == null) { - try writer.writeAll(value); - } else { - try writer.writeByte('"'); - try writer.writeAll(value); - try writer.writeByte('"'); + try self._debugValue(try js_obj.getValue(v8_context, field_name), seen, depth + 1, writer); + if (i != len - 1) { + try writer.writeByte('\n'); } - try writer.writeByte(' '); } - return arr.items; } pub fn stackTrace(self: *const Context) !?[]const u8 { diff --git a/src/browser/js/Object.zig b/src/browser/js/Object.zig index 9ab35fe12..2e77a54af 100644 --- a/src/browser/js/Object.zig +++ b/src/browser/js/Object.zig @@ -20,6 +20,8 @@ const std = @import("std"); const js = @import("js.zig"); const v8 = js.v8; +const IS_DEBUG = @import("builtin").mode == .Debug; + const Caller = @import("Caller.zig"); const Context = @import("Context.zig"); const PersistentObject = v8.Persistent(v8.Object); @@ -74,12 +76,10 @@ pub fn toString(self: Object) ![]const u8 { return self.context.valueToString(js_value, .{}); } -pub fn toDetailString(self: Object) ![]const u8 { - const js_value = self.js_obj.toValue(); - return self.context.valueToDetailString(js_value); -} - pub fn format(self: Object, writer: *std.Io.Writer) !void { + if (comptime IS_DEBUG) { + return self.context.debugValue(self.js_obj.toValue(), writer); + } const str = self.toString() catch return error.WriteFailed; return writer.writeAll(str); } diff --git a/src/browser/js/js.zig b/src/browser/js/js.zig index e6f736681..99828f53c 100644 --- a/src/browser/js/js.zig +++ b/src/browser/js/js.zig @@ -71,7 +71,12 @@ pub const PromiseResolver = struct { return self.resolver.getPromise(); } - pub fn resolve(self: PromiseResolver, value: anytype) !void { + pub fn resolve(self: PromiseResolver, comptime source: []const u8, value: anytype) void { + self._resolve(value) catch |err| { + log.err(.bug, "resolve", .{ .source = source, .err = err, .persistent = false }); + }; + } + fn _resolve(self: PromiseResolver, value: anytype) !void { const context = self.context; const js_value = try context.zigValueToJs(value); @@ -81,7 +86,12 @@ pub const PromiseResolver = struct { self.runMicrotasks(); } - pub fn reject(self: PromiseResolver, value: anytype) !void { + pub fn reject(self: PromiseResolver, comptime source: []const u8, value: anytype) void { + self._reject(value) catch |err| { + log.err(.bug, "reject", .{ .source = source, .err = err, .persistent = false }); + }; + } + fn _reject(self: PromiseResolver, value: anytype) !void { const context = self.context; const js_value = try context.zigValueToJs(value); @@ -104,7 +114,12 @@ pub const PersistentPromiseResolver = struct { return self.resolver.castToPromiseResolver().getPromise(); } - pub fn resolve(self: PersistentPromiseResolver, value: anytype) !void { + pub fn resolve(self: PersistentPromiseResolver, comptime source: []const u8, value: anytype) void { + self._resolve(value) catch |err| { + log.err(.bug, "resolve", .{ .source = source, .err = err, .persistent = true }); + }; + } + fn _resolve(self: PersistentPromiseResolver, value: anytype) !void { const context = self.context; const js_value = try context.zigValueToJs(value, .{}); defer context.runMicrotasks(); @@ -114,7 +129,13 @@ pub const PersistentPromiseResolver = struct { } } - pub fn reject(self: PersistentPromiseResolver, value: anytype) !void { + pub fn reject(self: PersistentPromiseResolver, comptime source: []const u8, value: anytype) void { + self._reject(value) catch |err| { + log.err(.bug, "reject", .{ .source = source, .err = err, .persistent = true }); + }; + } + + fn _reject(self: PersistentPromiseResolver, value: anytype) !void { const context = self.context; const js_value = try context.zigValueToJs(value, .{}); defer context.runMicrotasks(); diff --git a/src/browser/tests/streams/readable_stream.html b/src/browser/tests/streams/readable_stream.html index 2c74697b2..3e0744bbe 100644 --- a/src/browser/tests/streams/readable_stream.html +++ b/src/browser/tests/streams/readable_stream.html @@ -21,18 +21,6 @@ } - - + + diff --git a/src/browser/tests/element/svg/svg.html b/src/browser/tests/element/svg/svg.html index 77f29c316..b981089ca 100644 --- a/src/browser/tests/element/svg/svg.html +++ b/src/browser/tests/element/svg/svg.html @@ -6,23 +6,61 @@ + + + + OVER 9000!! + + + + + OVER 9000!!! + + + diff --git a/src/browser/tests/node/normalize.html b/src/browser/tests/node/normalize.html index 45c2a0bb5..ead599a6b 100644 --- a/src/browser/tests/node/normalize.html +++ b/src/browser/tests/node/normalize.html @@ -28,3 +28,22 @@ testing.expectEqual('a

b', container.innerHTML); testing.expectEqual(3, container.childNodes.length); + +"puppeteer " +

Leto + + + Atreides

+ diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index eadcff3d7..758bb1f24 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -343,7 +343,7 @@ pub fn getOrCreateAttributeList(self: *Element, page: *Page) !*Attribute.List { pub fn createAttributeList(self: *Element, page: *Page) !*Attribute.List { std.debug.assert(self._attributes == null); const a = try page.arena.create(Attribute.List); - a.* = .{.normalize = self._namespace == .html}; + a.* = .{ .normalize = self._namespace == .html }; self._attributes = a; return a; } diff --git a/src/browser/webapi/element/Attribute.zig b/src/browser/webapi/element/Attribute.zig index b4b9a4ee7..1919a24e6 100644 --- a/src/browser/webapi/element/Attribute.zig +++ b/src/browser/webapi/element/Attribute.zig @@ -118,6 +118,7 @@ pub const JsApi = struct { // in our Entry? Because that would require an extra 8 bytes for every single // attribute in the DOM, and, again, we expect that to almost always be null. pub const List = struct { + normalize: bool, _list: std.DoublyLinkedList = .{}, pub fn isEmpty(self: *const List) bool { @@ -273,7 +274,9 @@ pub const List = struct { entry: ?*Entry, }; fn getEntryAndNormalizedName(self: *const List, name: []const u8, page: *Page) !NormalizeAndEntry { - const normalized = try normalizeNameForLookup(name, page); + const normalized = + if (self.normalize) try normalizeNameForLookup(name, page) else name; + return .{ .normalized = normalized, .entry = self.getEntryWithNormalizedName(normalized), From 8e16c587c820390e8446667db1e9f7eb4c2d2010 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Fri, 5 Dec 2025 18:02:27 +0800 Subject: [PATCH 145/219] encode property as u32 whenever possible --- src/browser/js/js.zig | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/browser/js/js.zig b/src/browser/js/js.zig index 99828f53c..203b3b248 100644 --- a/src/browser/js/js.zig +++ b/src/browser/js/js.zig @@ -256,6 +256,9 @@ pub fn simpleZigValueToJs(isolate: v8.Isolate, value: anytype, comptime fail: bo .bool => return v8.getValue(if (value) v8.initTrue(isolate) else v8.initFalse(isolate)), .int => |n| switch (n.signedness) { .signed => { + if (value > 0 and value <= 4_294_967_295) { + return v8.Integer.initU32(isolate, @intCast(value)).toValue(); + } if (value >= -2_147_483_648 and value <= 2_147_483_647) { return v8.Integer.initI32(isolate, @intCast(value)).toValue(); } From 637a105e5d860eb713faff9b3307eb8784adbd40 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Fri, 5 Dec 2025 18:11:45 +0800 Subject: [PATCH 146/219] getRootNode composed support --- src/browser/tests/node/node.html | 21 +++++++++++++++++++++ src/browser/webapi/Node.zig | 15 ++++++++++++--- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/src/browser/tests/node/node.html b/src/browser/tests/node/node.html index 9305bf875..72c51748e 100644 --- a/src/browser/tests/node/node.html +++ b/src/browser/tests/node/node.html @@ -189,3 +189,24 @@ testing.expectEqual(8, Node.COMMENT_NODE); testing.expectEqual(11, Node.DOCUMENT_FRAGMENT_NODE); + +
+ diff --git a/src/browser/webapi/Node.zig b/src/browser/webapi/Node.zig index 8e65a5e9a..596dc5549 100644 --- a/src/browser/webapi/Node.zig +++ b/src/browser/webapi/Node.zig @@ -259,14 +259,23 @@ const GetRootNodeOpts = struct { }; pub fn getRootNode(self: *const Node, opts_: ?GetRootNodeOpts) *const Node { const opts = opts_ orelse GetRootNodeOpts{}; - if (opts.composed) { - log.warn(.not_implemented, "Node.getRootNode", .{ .feature = "composed" }); - } var root = self; while (root._parent) |parent| { root = parent; } + + // If composed is true, traverse through shadow boundaries + if (opts.composed) { + while (true) { + const shadow_root = @constCast(root).is(ShadowRoot) orelse break; + root = shadow_root.getHost().asNode(); + while (root._parent) |parent| { + root = parent; + } + } + } + return root; } From e41d53019f22a2ae9035fea362f6d62027ccb7fc Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Fri, 5 Dec 2025 18:18:46 +0800 Subject: [PATCH 147/219] CompositionEvent --- src/browser/js/bridge.zig | 1 + src/browser/tests/event/composition.html | 36 +++++++++ src/browser/webapi/Event.zig | 1 + src/browser/webapi/event/CompositionEvent.zig | 73 +++++++++++++++++++ 4 files changed, 111 insertions(+) create mode 100644 src/browser/tests/event/composition.html create mode 100644 src/browser/webapi/event/CompositionEvent.zig diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index 372d260aa..fa6229759 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -559,6 +559,7 @@ pub const JsApis = flattenTypes(&.{ @import("../webapi/encoding/TextDecoder.zig"), @import("../webapi/encoding/TextEncoder.zig"), @import("../webapi/Event.zig"), + @import("../webapi/event/CompositionEvent.zig"), @import("../webapi/event/CustomEvent.zig"), @import("../webapi/event/ErrorEvent.zig"), @import("../webapi/event/MessageEvent.zig"), diff --git a/src/browser/tests/event/composition.html b/src/browser/tests/event/composition.html new file mode 100644 index 000000000..b5a6a7100 --- /dev/null +++ b/src/browser/tests/event/composition.html @@ -0,0 +1,36 @@ + + + + + + + + + diff --git a/src/browser/webapi/Event.zig b/src/browser/webapi/Event.zig index 579ad418a..dafb0204c 100644 --- a/src/browser/webapi/Event.zig +++ b/src/browser/webapi/Event.zig @@ -55,6 +55,7 @@ pub const Type = union(enum) { custom_event: *@import("event/CustomEvent.zig"), message_event: *@import("event/MessageEvent.zig"), progress_event: *@import("event/ProgressEvent.zig"), + composition_event: *@import("event/CompositionEvent.zig"), }; const Options = struct { diff --git a/src/browser/webapi/event/CompositionEvent.zig b/src/browser/webapi/event/CompositionEvent.zig new file mode 100644 index 000000000..7fa701bd0 --- /dev/null +++ b/src/browser/webapi/event/CompositionEvent.zig @@ -0,0 +1,73 @@ +// 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"); +const Event = @import("../Event.zig"); + +const CompositionEvent = @This(); + +_proto: *Event, +_data: []const u8 = "", + +pub const InitOptions = struct { + data: ?[]const u8 = null, + bubbles: bool = false, + cancelable: bool = false, +}; + +pub fn init(typ: []const u8, opts_: ?InitOptions, page: *Page) !*CompositionEvent { + const opts = opts_ orelse InitOptions{}; + + const event = try page._factory.event(typ, CompositionEvent{ + ._proto = undefined, + ._data = if (opts.data) |str| try page.dupeString(str) else "", + }); + + event._proto._bubbles = opts.bubbles; + event._proto._cancelable = opts.cancelable; + + return event; +} + +pub fn asEvent(self: *CompositionEvent) *Event { + return self._proto; +} + +pub fn getData(self: *const CompositionEvent) []const u8 { + return self._data; +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(CompositionEvent); + + pub const Meta = struct { + pub const name = "CompositionEvent"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; + + pub const constructor = bridge.constructor(CompositionEvent.init, .{}); + pub const data = bridge.accessor(CompositionEvent.getData, null, .{}); +}; + +const testing = @import("../../../testing.zig"); +test "WebApi: CompositionEvent" { + try testing.htmlRunner("event/composition.html", .{}); +} From f5d3dede6b6aab06a5758ec847f9b29cda95f652 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Sat, 6 Dec 2025 18:19:44 +0100 Subject: [PATCH 148/219] node: textContent must ignore comments for elements --- src/browser/tests/node/text_content.html | 17 +++++++++++++---- src/browser/webapi/Node.zig | 17 +++++++++++++++-- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/browser/tests/node/text_content.html b/src/browser/tests/node/text_content.html index d6250320e..fc6a0de9a 100644 --- a/src/browser/tests/node/text_content.html +++ b/src/browser/tests/node/text_content.html @@ -1,6 +1,12 @@
d1

hello

+
+ + + This is a
+ text +
diff --git a/src/browser/webapi/Node.zig b/src/browser/webapi/Node.zig index 596dc5549..2cb0d323d 100644 --- a/src/browser/webapi/Node.zig +++ b/src/browser/webapi/Node.zig @@ -171,7 +171,20 @@ pub fn childNodes(self: *const Node, page: *Page) !*collections.ChildNodes { pub fn getTextContent(self: *Node, writer: *std.Io.Writer) error{WriteFailed}!void { switch (self._type) { - .element => |el| return el.getInnerText(writer), + .element => { + var it = self.childrenIterator(); + while (it.next()) |child| { + // ignore comments and TODO processing instructions. + switch (child._type) { + .cdata => |c| switch (c._type) { + .comment => continue, + .text => {}, + }, + else => {}, + } + try child.getTextContent(writer); + } + }, .cdata => |c| try writer.writeAll(c.getData()), .document => {}, .document_type => {}, @@ -719,7 +732,7 @@ pub const JsApi = struct { switch (self._type) { .element => |el| { var buf = std.Io.Writer.Allocating.init(page.call_arena); - try el.getInnerText(&buf.writer); + try el.asNode().getTextContent(&buf.writer); return buf.written(); }, .cdata => |cdata| return cdata.getData(), From a673eb89b658ca7071c01e74bec868c7debf2dd4 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Sat, 6 Dec 2025 19:09:20 +0100 Subject: [PATCH 149/219] element: innerText which must return rendered text --- src/browser/tests/element/inner.html | 13 ++++++++++ src/browser/webapi/Element.zig | 36 +++++++++++++++++++++++++++- 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/src/browser/tests/element/inner.html b/src/browser/tests/element/inner.html index c9bb08946..2c93a7178 100644 --- a/src/browser/tests/element/inner.html +++ b/src/browser/tests/element/inner.html @@ -1,6 +1,12 @@
hello world
+
+ + + This is a
+ text +
diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index 758bb1f24..83f971a6d 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -223,10 +223,44 @@ pub fn getNamespaceURI(self: *const Element) []const u8 { return self._namespace.toUri(); } +// innerText represents the **rendered** text content of a node and its +// descendants. pub fn getInnerText(self: *Element, writer: *std.Io.Writer) !void { var it = self.asNode().childrenIterator(); while (it.next()) |child| { - try child.getTextContent(writer); + switch (child._type) { + .element => |e| switch (e._type) { + .html => |he| switch (he._type) { + .br => try writer.writeByte('\n'), + .script, .style, .template => continue, + else => try e.getInnerText(writer), // TODO check if elt is hidden. + }, + .svg => {}, + }, + .cdata => |c| switch (c._type) { + .comment => continue, + .text => { + const data = c.getData(); + if (std.mem.trim(u8, data, &std.ascii.whitespace).len != 0) { + // Trim all whitespaces except spaces. + // TODO this is not the correct way to render text, this is + // a temp approximation. + const text = std.mem.trim(u8, data, &[_]u8{ + '\t', + '\n', + '\r', + std.ascii.control_code.vt, + std.ascii.control_code.ff, + }); + try writer.writeAll(text); + } + }, + }, + .document => {}, + .document_type => {}, + .document_fragment => {}, + .attribute => |attr| try writer.writeAll(attr._value), + } } } From 08d7f544ddfe668ed86470e23bfc0c08a8d75b49 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Sat, 6 Dec 2025 19:15:24 +0100 Subject: [PATCH 150/219] fix comment formatting --- src/browser/webapi/CData.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browser/webapi/CData.zig b/src/browser/webapi/CData.zig index f02d51cad..8bcb673ae 100644 --- a/src/browser/webapi/CData.zig +++ b/src/browser/webapi/CData.zig @@ -75,7 +75,7 @@ pub fn setData(self: *CData, value: ?[]const u8, page: *Page) !void { pub fn format(self: *const CData, writer: *std.io.Writer) !void { return switch (self._type) { .text => writer.print("{s}", .{self._data}), - .comment => writer.print("{s}", .{self._data}), + .comment => writer.print("", .{self._data}), }; } From 240e8b35022570926fd004815b5eebe1ca07a39d Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Sun, 7 Dec 2025 09:52:59 +0100 Subject: [PATCH 151/219] use a better comparison to detect comment --- src/browser/webapi/Node.zig | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/browser/webapi/Node.zig b/src/browser/webapi/Node.zig index 2cb0d323d..ef6de4a7d 100644 --- a/src/browser/webapi/Node.zig +++ b/src/browser/webapi/Node.zig @@ -175,12 +175,8 @@ pub fn getTextContent(self: *Node, writer: *std.Io.Writer) error{WriteFailed}!vo var it = self.childrenIterator(); while (it.next()) |child| { // ignore comments and TODO processing instructions. - switch (child._type) { - .cdata => |c| switch (c._type) { - .comment => continue, - .text => {}, - }, - else => {}, + if (child.is(CData.Comment) != null) { + continue; } try child.getTextContent(writer); } From 9370e298d2286991cff83b7306870d37f84b19f5 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Mon, 8 Dec 2025 09:07:56 +0800 Subject: [PATCH 152/219] improve HTMLOption and HTMLOptionCollection --- src/browser/tests/element/html/select.html | 23 ++++++++++++++ .../collections/HTMLOptionsCollection.zig | 24 +++++++++++---- src/browser/webapi/element/html/Option.zig | 30 ++++++++----------- src/browser/webapi/element/html/Select.zig | 21 +++++++++---- 4 files changed, 69 insertions(+), 29 deletions(-) diff --git a/src/browser/tests/element/html/select.html b/src/browser/tests/element/html/select.html index ceb46c16b..4c4e6ccde 100644 --- a/src/browser/tests/element/html/select.html +++ b/src/browser/tests/element/html/select.html @@ -158,6 +158,7 @@ { const sel = $('#select1') const opts = sel.options + testing.expectEqual(3, sel.length) testing.expectEqual(3, opts.length) testing.expectEqual('HTMLOptionsCollection', opts.constructor.name) @@ -165,6 +166,9 @@ testing.expectEqual('val1', opts[0].value) testing.expectEqual('val2', opts[1].value) testing.expectEqual('val3', opts[2].value) + testing.expectEqual('val1', opts.item(0).value); + testing.expectEqual('val2', opts.item(1).value); + testing.expectEqual('val3', opts.item(2).value); } @@ -224,6 +228,12 @@ testing.expectEqual(2, opts.length) testing.expectEqual('zero', opts[0].value) testing.expectEqual('b', opts[1].value) + + opts.add(opt1, 0) + testing.expectEqual(3, opts.length) + testing.expectEqual('a', opts[0].value) + testing.expectEqual('zero', opts[1].value) + testing.expectEqual('b', opts[2].value) } @@ -364,3 +374,16 @@ testing.expectTrue(select.outerHTML.includes('size="7"')) } + + + diff --git a/src/browser/webapi/collections/HTMLOptionsCollection.zig b/src/browser/webapi/collections/HTMLOptionsCollection.zig index 4c9d59c44..6a0cadc95 100644 --- a/src/browser/webapi/collections/HTMLOptionsCollection.zig +++ b/src/browser/webapi/collections/HTMLOptionsCollection.zig @@ -20,6 +20,7 @@ const std = @import("std"); const js = @import("../../js/js.zig"); const Page = @import("../../Page.zig"); +const Node = @import("../Node.zig"); const Element = @import("../Element.zig"); const HTMLCollection = @import("HTMLCollection.zig"); const NodeLive = @import("node_live.zig").NodeLive; @@ -59,17 +60,28 @@ pub fn setSelectedIndex(self: *HTMLOptionsCollection, index: i32) !void { const Option = @import("../element/html/Option.zig"); +const AddBeforeOption = union(enum) { + option: *Option, + index: u32, +}; + // Add a new option element -pub fn add(self: *HTMLOptionsCollection, element: *Option, before: ?*Option, page: *Page) !void { +pub fn add(self: *HTMLOptionsCollection, element: *Option, before_: ?AddBeforeOption, 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); + var before_node: ?*Node = null; + if (before_) |before| { + switch (before) { + .index => |idx| { + if (self.getAtIndex(idx, page)) |el| { + before_node = el.asNode(); + } + }, + .option => |before_option| before_node = before_option.asNode(), + } } + _ = try select_node.insertBefore(element_node, before_node, page); } // Remove an option element by index diff --git a/src/browser/webapi/element/html/Option.zig b/src/browser/webapi/element/html/Option.zig index b5718a1ec..207f4aaf4 100644 --- a/src/browser/webapi/element/html/Option.zig +++ b/src/browser/webapi/element/html/Option.zig @@ -28,7 +28,6 @@ const Option = @This(); _proto: *HtmlElement, _value: ?[]const u8 = null, -_text: ?[]const u8 = null, _selected: bool = false, _default_selected: bool = false, _disabled: bool = false, @@ -43,9 +42,15 @@ pub fn asNode(self: *Option) *Node { return self.asElement().asNode(); } -pub fn getValue(self: *const Option) []const u8 { - // If value attribute exists, use that; otherwise use text content - return self._value orelse self._text orelse ""; +pub fn getValue(self: *Option, page: *Page) []const u8 { + // If value attribute exists, use that; otherwise use text content (stripped) + if (self._value) |v| { + return v; + } + + const node = self.asNode(); + const text = node.getTextContentAlloc(page.call_arena) catch return ""; + return std.mem.trim(u8, text, &std.ascii.whitespace); } pub fn setValue(self: *Option, value: []const u8, page: *Page) !void { @@ -55,7 +60,9 @@ pub fn setValue(self: *Option, value: []const u8, page: *Page) !void { } pub fn getText(self: *const Option) []const u8 { - return self._text orelse ""; + const node: *Node = @constCast(self.asConstElement().asConstNode()); + const allocator = std.heap.page_allocator; // TODO: use proper allocator + return node.getTextContentAlloc(allocator) catch ""; } pub fn getSelected(self: *const Option) bool { @@ -112,8 +119,6 @@ pub const JsApi = struct { }; pub const Build = struct { - const CData = @import("../../CData.zig"); - pub fn created(node: *Node, _: *Page) !void { var self = node.as(Option); const element = self.asElement(); @@ -129,17 +134,6 @@ pub const Build = struct { self._disabled = element.getAttributeSafe("disabled") != null; } - pub fn complete(node: *Node, _: *const Page) !void { - var self = node.as(Option); - - // Get text content - if (node.firstChild()) |child| { - if (child.is(CData.Text)) |txt| { - self._text = txt.getWholeText(); - } - } - } - pub fn attributeChange(element: *Element, name: []const u8, value: []const u8, _: *Page) !void { const attribute = std.meta.stringToEnum(enum { value, selected }, name) orelse return; const self = element.as(Option); diff --git a/src/browser/webapi/element/html/Select.zig b/src/browser/webapi/element/html/Select.zig index beb00ee5c..5b3c0b9ec 100644 --- a/src/browser/webapi/element/html/Select.zig +++ b/src/browser/webapi/element/html/Select.zig @@ -44,7 +44,7 @@ pub fn asConstNode(self: *const Select) *const Node { return self.asConstElement().asConstNode(); } -pub fn getValue(self: *Select) []const u8 { +pub fn getValue(self: *Select, page: *Page) []const u8 { // Return value of first selected option, or first option if none selected var first_option: ?*Option = null; var iter = self.asNode().childrenIterator(); @@ -54,25 +54,24 @@ pub fn getValue(self: *Select) []const u8 { first_option = option; } if (option.getSelected()) { - return option.getValue(); + return option.getValue(page); } } // No explicitly selected option, return first option's value if (first_option) |opt| { - return opt.getValue(); + return opt.getValue(page); } return ""; } pub fn setValue(self: *Select, value: []const u8, page: *Page) !void { - _ = page; // Find option with matching value and select it // Note: This updates the current state (_selected), not the default state (attribute) // Setting value always deselects all others, even for multiple selects var iter = self.asNode().childrenIterator(); while (iter.next()) |child| { const option = child.is(Option) orelse continue; - option._selected = std.mem.eql(u8, option.getValue(), value); + option._selected = std.mem.eql(u8, option.getValue(page), value); } } @@ -196,6 +195,17 @@ pub fn getOptions(self: *Select, page: *Page) !*collections.HTMLOptionsCollectio }); } +pub fn getLength(self: *Select) u32 { + var i: u32 = 0; + var it = self.asNode().childrenIterator(); + while (it.next()) |child| { + if (child.is(Option) != null) { + i += 1; + } + } + return i; +} + pub fn getSelectedOptions(self: *Select, page: *Page) !collections.NodeLive(.selected_options) { return collections.NodeLive(.selected_options).init(null, self.asNode(), {}, page); } @@ -243,6 +253,7 @@ pub const JsApi = struct { pub const selectedOptions = bridge.accessor(Select.getSelectedOptions, null, .{}); pub const form = bridge.accessor(Select.getForm, null, .{}); pub const size = bridge.accessor(Select.getSize, Select.setSize, .{}); + pub const length = bridge.accessor(Select.getLength, null, .{}); }; pub const Build = struct { From 57ce4e16a95783cba08000037c972165bd169bbc Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Sat, 6 Dec 2025 17:03:34 +1100 Subject: [PATCH 153/219] feat: support listening on ipv6 --- src/main.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.zig b/src/main.zig index 1f7bd57e3..b43ea92cf 100644 --- a/src/main.zig +++ b/src/main.zig @@ -101,7 +101,7 @@ fn run(allocator: Allocator, main_arena: Allocator) !void { switch (args.mode) { .serve => |opts| { log.debug(.app, "startup", .{ .mode = "serve" }); - const address = std.net.Address.parseIp4(opts.host, opts.port) catch |err| { + const address = std.net.Address.parseIp(opts.host, opts.port) catch |err| { log.fatal(.app, "invalid server address", .{ .err = err, .host = opts.host, .port = opts.port }); return args.printUsageAndExit(false); }; From 0beae3b1a67d5f19491763edc6727b4247860286 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Mon, 8 Dec 2025 14:22:24 +0800 Subject: [PATCH 154/219] Various legacy document tests document.embeds, document.plugins, document.anchor, document.getElementsByName getElementsByClassName support for multiple class names various document getters --- src/browser/Page.zig | 6 + src/browser/js/bridge.zig | 1 + src/browser/tests/document/children.html | 28 ++++ src/browser/tests/document/collections.html | 3 +- .../tests/document/document-title.html | 34 +++++ src/browser/tests/document/document.html | 129 ++++++++++++++++++ .../get_elements_by_class_name-multiple.html | 48 +++++++ .../document/get_elements_by_class_name.html | 4 +- .../tests/document/get_elements_by_name.html | 60 ++++++++ .../get_elements_by_tag_name-wildcard.html | 39 ++++++ src/browser/tests/legacy/dom/document.html | 33 +++-- src/browser/tests/legacy/html/document.html | 2 +- src/browser/webapi/Document.zig | 67 ++++++++- src/browser/webapi/DocumentFragment.zig | 2 +- src/browser/webapi/Element.zig | 36 +++-- src/browser/webapi/HTMLDocument.zig | 33 ++++- .../webapi/collections/HTMLCollection.zig | 20 +++ src/browser/webapi/collections/node_live.zig | 54 ++++++-- src/browser/webapi/element/Attribute.zig | 7 + src/browser/webapi/element/Html.zig | 49 +++---- src/browser/webapi/element/html/Anchor.zig | 12 ++ src/browser/webapi/element/html/Embed.zig | 42 ++++++ src/browser/webapi/element/html/Select.zig | 4 +- 23 files changed, 638 insertions(+), 75 deletions(-) create mode 100644 src/browser/tests/document/children.html create mode 100644 src/browser/tests/document/document-title.html create mode 100644 src/browser/tests/document/get_elements_by_class_name-multiple.html create mode 100644 src/browser/tests/document/get_elements_by_name.html create mode 100644 src/browser/tests/document/get_elements_by_tag_name-wildcard.html create mode 100644 src/browser/webapi/element/html/Embed.zig diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 0845b72b1..17f25adef 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -1120,6 +1120,12 @@ pub fn createElement(self: *Page, ns_: ?[]const u8, name: []const u8, attribute_ attribute_iterator, .{ ._proto = undefined }, ), + asUint("embed") => return self.createHtmlElementT( + Element.Html.Embed, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), else => {}, }, 6 => switch (@as(u48, @bitCast(name[0..6].*))) { diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index fa6229759..faa61a2a3 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -531,6 +531,7 @@ pub const JsApis = flattenTypes(&.{ @import("../webapi/element/html/Data.zig"), @import("../webapi/element/html/Dialog.zig"), @import("../webapi/element/html/Div.zig"), + @import("../webapi/element/html/Embed.zig"), @import("../webapi/element/html/Form.zig"), @import("../webapi/element/html/Generic.zig"), @import("../webapi/element/html/Head.zig"), diff --git a/src/browser/tests/document/children.html b/src/browser/tests/document/children.html new file mode 100644 index 000000000..b8a45d35c --- /dev/null +++ b/src/browser/tests/document/children.html @@ -0,0 +1,28 @@ + + + + + Test + + +
Content
+ + + + + diff --git a/src/browser/tests/document/collections.html b/src/browser/tests/document/collections.html index c9d4c14c1..b1c575858 100644 --- a/src/browser/tests/document/collections.html +++ b/src/browser/tests/document/collections.html @@ -5,7 +5,7 @@ - + + + + + + + + + + + + + diff --git a/src/browser/tests/document/document.html b/src/browser/tests/document/document.html index 5b2e40006..cc5ee34e3 100644 --- a/src/browser/tests/document/document.html +++ b/src/browser/tests/document/document.html @@ -40,3 +40,132 @@ const emptyText = document.createTextNode(''); testing.expectEqual('', emptyText.nodeValue); + + + + + + +Link 1 +Link 2 +Anchor 1 +Anchor 2 +Both href and name +No attributes + + + + + + + + diff --git a/src/browser/tests/document/get_elements_by_class_name-multiple.html b/src/browser/tests/document/get_elements_by_class_name-multiple.html new file mode 100644 index 000000000..9f1b0f5b9 --- /dev/null +++ b/src/browser/tests/document/get_elements_by_class_name-multiple.html @@ -0,0 +1,48 @@ + + + +
Div 1
+
Div 2
+
Div 3
+
Div 4
+
Div 5
+
Div 6
+ + + + + + + + diff --git a/src/browser/tests/document/get_elements_by_class_name.html b/src/browser/tests/document/get_elements_by_class_name.html index 1175fa554..3e291dce4 100644 --- a/src/browser/tests/document/get_elements_by_class_name.html +++ b/src/browser/tests/document/get_elements_by_class_name.html @@ -20,8 +20,8 @@ + + + + + +Section 1 +User Link + + + + + + diff --git a/src/browser/tests/document/get_elements_by_tag_name-wildcard.html b/src/browser/tests/document/get_elements_by_tag_name-wildcard.html new file mode 100644 index 000000000..685278df3 --- /dev/null +++ b/src/browser/tests/document/get_elements_by_tag_name-wildcard.html @@ -0,0 +1,39 @@ + + + + + Test + + +
+ Text +
+

Paragraph

+ + + diff --git a/src/browser/tests/legacy/dom/document.html b/src/browser/tests/legacy/dom/document.html index 950daaab6..822134d00 100644 --- a/src/browser/tests/legacy/dom/document.html +++ b/src/browser/tests/legacy/dom/document.html @@ -25,7 +25,6 @@ testing.expectEqual(true, newdoc.compatMode === document.compatMode); testing.expectEqual(true, newdoc.characterSet === document.characterSet); testing.expectEqual(true, newdoc.charset === document.charset); - testing.expectEqual(true, newdoc.contentType === document.contentType); testing.expectEqual('HTML', document.documentElement.tagName); @@ -35,8 +34,8 @@ testing.expectEqual('CSS1Compat', document.compatMode); testing.expectEqual('text/html', document.contentType); - testing.expectEqual('http://localhost:9582/src/tests/dom/document.html', document.documentURI); - testing.expectEqual('http://localhost:9582/src/tests/dom/document.html', document.URL); + testing.expectEqual('http://localhost:9589/dom/document.html', document.documentURI); + testing.expectEqual('http://localhost:9589/dom/document.html', document.URL); testing.expectEqual(document.body, document.activeElement); @@ -61,7 +60,7 @@ let byTagNameAll = document.getElementsByTagName('*'); // If you add a script block (or change the HTML in any other way on this // page), this test will break. Adjust it accordingly. - testing.expectEqual(21, byTagNameAll.length); + testing.expectEqual(12, byTagNameAll.length); testing.expectEqual('html', byTagNameAll.item(0).localName); testing.expectEqual('SCRIPT', byTagNameAll.item(11).tagName); @@ -170,21 +169,21 @@ diff --git a/src/browser/tests/legacy/html/document.html b/src/browser/tests/legacy/html/document.html index cc02f7c64..003ee8205 100644 --- a/src/browser/tests/legacy/html/document.html +++ b/src/browser/tests/legacy/html/document.html @@ -12,7 +12,7 @@ testing.expectEqual('Document', document.__proto__.__proto__.constructor.name); testing.expectEqual('body', document.body.localName); - testing.expectEqual('localhost:9582', document.domain); + testing.expectEqual('localhost', document.domain); testing.expectEqual('', document.referrer); testing.expectEqual('', document.title); testing.expectEqual('body', document.body.localName); diff --git a/src/browser/webapi/Document.zig b/src/browser/webapi/Document.zig index 6cea49870..928b97c0a 100644 --- a/src/browser/webapi/Document.zig +++ b/src/browser/webapi/Document.zig @@ -21,6 +21,7 @@ const String = @import("../../string.zig").String; const js = @import("../js/js.zig"); const Page = @import("../Page.zig"); +const URL = @import("../URL.zig"); const Node = @import("Node.zig"); const Element = @import("Element.zig"); @@ -79,6 +80,29 @@ pub fn getURL(_: *const Document, page: *const Page) [:0]const u8 { return page.url; } +pub fn getContentType(self: *const Document) []const u8 { + return switch (self._type) { + .html => "text/html", + .generic => "application/xml", + }; +} + +pub fn getCharacterSet(_: *const Document) []const u8 { + return "UTF-8"; +} + +pub fn getCompatMode(_: *const Document) []const u8 { + return "CSS1Compat"; +} + +pub fn getReferrer(_: *const Document) []const u8 { + return ""; +} + +pub fn getDomain(_: *const Document, page: *const Page) []const u8 { + return URL.getHostname(page.url); +} + const CreateElementOptions = struct { is: ?[]const u8 = null, }; @@ -109,6 +133,7 @@ pub fn getElementById(self: *const Document, id_: ?[]const u8) ?*Element { const GetElementsByTagNameResult = union(enum) { tag: collections.NodeLive(.tag), tag_name: collections.NodeLive(.tag_name), + all_elements: collections.NodeLive(.all_elements), }; pub fn getElementsByTagName(self: *Document, tag_name: []const u8, page: *Page) !GetElementsByTagNameResult { if (tag_name.len > 256) { @@ -116,23 +141,47 @@ pub fn getElementsByTagName(self: *Document, tag_name: []const u8, page: *Page) return error.InvalidTagName; } + // Handle wildcard '*' - return all elements + if (std.mem.eql(u8, tag_name, "*")) { + return .{ + .all_elements = collections.NodeLive(.all_elements).init(self.asNode(), {}, page), + }; + } + const lower = std.ascii.lowerString(&page.buf, tag_name); if (Node.Element.Tag.parseForMatch(lower)) |known| { // optimized for known tag names, comparis return .{ - .tag = collections.NodeLive(.tag).init(null, self.asNode(), known, page), + .tag = collections.NodeLive(.tag).init(self.asNode(), known, page), }; } const arena = page.arena; const filter = try String.init(arena, lower, .{}); - return .{ .tag_name = collections.NodeLive(.tag_name).init(arena, self.asNode(), filter, page) }; + return .{ .tag_name = collections.NodeLive(.tag_name).init(self.asNode(), filter, page) }; } pub fn getElementsByClassName(self: *Document, class_name: []const u8, page: *Page) !collections.NodeLive(.class_name) { const arena = page.arena; - const filter = try arena.dupe(u8, class_name); - return collections.NodeLive(.class_name).init(arena, self.asNode(), filter, page); + + // Parse space-separated class names + var class_names: std.ArrayList([]const u8) = .empty; + var it = std.mem.tokenizeAny(u8, class_name, &std.ascii.whitespace); + while (it.next()) |name| { + try class_names.append(arena, try page.dupeString(name)); + } + + return collections.NodeLive(.class_name).init(self.asNode(), class_names.items, page); +} + +pub fn getElementsByName(self: *Document, name: []const u8, page: *Page) !collections.NodeLive(.name) { + const arena = page.arena; + const filter = try arena.dupe(u8, name); + return collections.NodeLive(.name).init(self.asNode(), filter, page); +} + +pub fn getChildren(self: *Document, page: *Page) !collections.NodeLive(.child_elements) { + return collections.NodeLive(.child_elements).init(self.asNode(), {}, page); } pub fn getDocumentElement(self: *Document) ?*Element { @@ -285,11 +334,20 @@ pub const JsApi = struct { } pub const URL = bridge.accessor(Document.getURL, null, .{}); + pub const documentURI = bridge.accessor(Document.getURL, null, .{}); pub const documentElement = bridge.accessor(Document.getDocumentElement, null, .{}); + pub const children = bridge.accessor(Document.getChildren, 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 styleSheets = bridge.accessor(Document.getStyleSheets, null, .{}); + pub const contentType = bridge.accessor(Document.getContentType, null, .{}); + pub const characterSet = bridge.accessor(Document.getCharacterSet, null, .{}); + pub const charset = bridge.accessor(Document.getCharacterSet, null, .{}); + pub const inputEncoding = bridge.accessor(Document.getCharacterSet, null, .{}); + pub const compatMode = bridge.accessor(Document.getCompatMode, null, .{}); + pub const referrer = bridge.accessor(Document.getReferrer, null, .{}); + pub const domain = bridge.accessor(Document.getDomain, null, .{}); pub const createElement = bridge.function(Document.createElement, .{}); pub const createElementNS = bridge.function(Document.createElementNS, .{}); pub const createDocumentFragment = bridge.function(Document.createDocumentFragment, .{}); @@ -304,6 +362,7 @@ 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 getElementsByName = bridge.function(Document.getElementsByName, .{}); pub const adoptNode = bridge.function(Document.adoptNode, .{ .dom_exception = true }); pub const importNode = bridge.function(Document.importNode, .{ .dom_exception = true }); diff --git a/src/browser/webapi/DocumentFragment.zig b/src/browser/webapi/DocumentFragment.zig index 6c712f556..5e94f3abe 100644 --- a/src/browser/webapi/DocumentFragment.zig +++ b/src/browser/webapi/DocumentFragment.zig @@ -94,7 +94,7 @@ pub fn querySelectorAll(self: *DocumentFragment, input: []const u8, page: *Page) } pub fn getChildren(self: *DocumentFragment, page: *Page) !collections.NodeLive(.child_elements) { - return collections.NodeLive(.child_elements).init(null, self.asNode(), {}, page); + return collections.NodeLive(.child_elements).init(self.asNode(), {}, page); } pub fn firstElementChild(self: *DocumentFragment) ?*Element { diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index 758bb1f24..b958f552f 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -133,6 +133,7 @@ pub fn getTagNameLower(self: *const Element) []const u8 { .data => "data", .dialog => "dialog", .div => "div", + .embed => "embed", .form => "form", .generic => |e| e._tag_name.str(), .heading => |e| e._tag_name.str(), @@ -179,6 +180,7 @@ pub fn getTagNameSpec(self: *const Element, buf: []u8) []const u8 { .data => "DATA", .dialog => "DIALOG", .div => "DIV", + .embed => "EMBED", .form => "FORM", .generic => |e| upperTagName(&e._tag_name, buf), .heading => |e| upperTagName(&e._tag_name, buf), @@ -305,12 +307,22 @@ pub fn getAttribute(self: *const Element, name: []const u8, page: *Page) !?[]con return attributes.get(name, page); } +pub fn getAttributeSafe(self: *const Element, name: []const u8) ?[]const u8 { + const attributes = self._attributes orelse return null; + return attributes.getSafe(name); +} + 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 hasAttributeSafe(self: *const Element, name: []const u8) bool { + const attributes = self._attributes orelse return false; + return attributes.hasSafe(name); +} + pub fn hasAttributes(self: *const Element) bool { const attributes = self._attributes orelse return false; return attributes.isEmpty() == false; @@ -321,11 +333,6 @@ pub fn getAttributeNode(self: *Element, name: []const u8, page: *Page) !?*Attrib return attributes.getAttribute(name, self, page); } -pub fn getAttributeSafe(self: *const Element, name: []const u8) ?[]const u8 { - const attributes = self._attributes orelse return null; - return attributes.getSafe(name); -} - pub fn setAttribute(self: *Element, name: []const u8, value: []const u8, page: *Page) !void { const attributes = try self.getOrCreateAttributeList(page); _ = try attributes.put(name, value, self, page); @@ -506,7 +513,7 @@ pub fn blur(self: *Element, page: *Page) !void { } pub fn getChildren(self: *Element, page: *Page) !collections.NodeLive(.child_elements) { - return collections.NodeLive(.child_elements).init(null, self.asNode(), {}, page); + return collections.NodeLive(.child_elements).init(self.asNode(), {}, page); } pub fn append(self: *Element, nodes: []const Node.NodeOrText, page: *Page) !void { @@ -750,19 +757,26 @@ pub fn getElementsByTagName(self: *Element, tag_name: []const u8, page: *Page) ! if (Tag.parseForMatch(lower)) |known| { // optimized for known tag names return .{ - .tag = collections.NodeLive(.tag).init(null, self.asNode(), known, page), + .tag = collections.NodeLive(.tag).init(self.asNode(), known, page), }; } const arena = page.arena; const filter = try String.init(arena, lower, .{}); - return .{ .tag_name = collections.NodeLive(.tag_name).init(arena, self.asNode(), filter, page) }; + return .{ .tag_name = collections.NodeLive(.tag_name).init(self.asNode(), filter, page) }; } pub fn getElementsByClassName(self: *Element, class_name: []const u8, page: *Page) !collections.NodeLive(.class_name) { const arena = page.arena; - const filter = try arena.dupe(u8, class_name); - return collections.NodeLive(.class_name).init(arena, self.asNode(), filter, page); + + // Parse space-separated class names + var class_names: std.ArrayList([]const u8) = .empty; + var it = std.mem.tokenizeAny(u8, class_name, &std.ascii.whitespace); + while (it.next()) |name| { + try class_names.append(arena, name); + } + + return collections.NodeLive(.class_name).init(self.asNode(), class_names.items, page); } pub fn cloneElement(self: *Element, deep: bool, page: *Page) !*Node { @@ -819,6 +833,7 @@ pub fn getTag(self: *const Element) Tag { .html => |he| switch (he._type) { .anchor => .anchor, .div => .div, + .embed => .embed, .form => .form, .p => .p, .custom => .custom, @@ -868,6 +883,7 @@ pub const Tag = enum { data, dialog, div, + embed, ellipse, em, form, diff --git a/src/browser/webapi/HTMLDocument.zig b/src/browser/webapi/HTMLDocument.zig index 5e22ecff1..16914b7bb 100644 --- a/src/browser/webapi/HTMLDocument.zig +++ b/src/browser/webapi/HTMLDocument.zig @@ -18,6 +18,7 @@ const std = @import("std"); const js = @import("../js/js.zig"); +const String = @import("../../string.zig").String; const Page = @import("../Page.zig"); const Node = @import("Node.zig"); @@ -91,22 +92,40 @@ pub fn setTitle(self: *HTMLDocument, title: []const u8, page: *Page) !void { return title_element.asElement().replaceChildren(&.{.{ .text = title }}, page); } } + + const title_node = try page.createElement(null, "title", null); + const title_element = title_node.as(Element); + try title_element.replaceChildren(&.{.{ .text = title }}, page); + _ = try head.asNode().appendChild(title_node, page); } pub fn getImages(self: *HTMLDocument, page: *Page) !collections.NodeLive(.tag) { - return collections.NodeLive(.tag).init(null, self.asNode(), .img, page); + return collections.NodeLive(.tag).init(self.asNode(), .img, page); } pub fn getScripts(self: *HTMLDocument, page: *Page) !collections.NodeLive(.tag) { - return collections.NodeLive(.tag).init(null, self.asNode(), .script, page); + return collections.NodeLive(.tag).init(self.asNode(), .script, page); +} + +pub fn getLinks(self: *HTMLDocument, page: *Page) !collections.NodeLive(.links) { + return collections.NodeLive(.links).init(self.asNode(), {}, page); } -pub fn getLinks(self: *HTMLDocument, page: *Page) !collections.NodeLive(.tag) { - return collections.NodeLive(.tag).init(null, self.asNode(), .anchor, page); +pub fn getAnchors(self: *HTMLDocument, page: *Page) !collections.NodeLive(.anchors) { + return collections.NodeLive(.anchors).init(self.asNode(), {}, page); } pub fn getForms(self: *HTMLDocument, page: *Page) !collections.NodeLive(.tag) { - return collections.NodeLive(.tag).init(null, self.asNode(), .form, page); + return collections.NodeLive(.tag).init(self.asNode(), .form, page); +} + +pub fn getEmbeds(self: *HTMLDocument, page: *Page) !collections.NodeLive(.tag) { + return collections.NodeLive(.tag).init(self.asNode(), .embed, page); +} + +const applet_string = String.init(undefined, "applet", .{}) catch unreachable; +pub fn getApplets(self: *HTMLDocument, page: *Page) !collections.NodeLive(.tag_name) { + return collections.NodeLive(.tag_name).init(self.asNode(), applet_string, page); } pub fn getCurrentScript(self: *const HTMLDocument) ?*Element.Html.Script { @@ -143,7 +162,11 @@ pub const JsApi = struct { pub const images = bridge.accessor(HTMLDocument.getImages, null, .{}); pub const scripts = bridge.accessor(HTMLDocument.getScripts, null, .{}); pub const links = bridge.accessor(HTMLDocument.getLinks, null, .{}); + pub const anchors = bridge.accessor(HTMLDocument.getAnchors, null, .{}); pub const forms = bridge.accessor(HTMLDocument.getForms, null, .{}); + pub const embeds = bridge.accessor(HTMLDocument.getEmbeds, null, .{}); + pub const applets = bridge.accessor(HTMLDocument.getApplets, null, .{}); + pub const plugins = bridge.accessor(HTMLDocument.getEmbeds, null, .{}); pub const currentScript = bridge.accessor(HTMLDocument.getCurrentScript, null, .{}); pub const location = bridge.accessor(HTMLDocument.getLocation, null, .{ .cache = "location" }); pub const all = bridge.accessor(HTMLDocument.getAll, null, .{}); diff --git a/src/browser/webapi/collections/HTMLCollection.zig b/src/browser/webapi/collections/HTMLCollection.zig index 54c99bffa..3160524da 100644 --- a/src/browser/webapi/collections/HTMLCollection.zig +++ b/src/browser/webapi/collections/HTMLCollection.zig @@ -28,9 +28,13 @@ const Mode = enum { tag, tag_name, class_name, + name, + all_elements, child_elements, child_tag, selected_options, + links, + anchors, }; const HTMLCollection = @This(); @@ -39,9 +43,13 @@ data: union(Mode) { tag: NodeLive(.tag), tag_name: NodeLive(.tag_name), class_name: NodeLive(.class_name), + name: NodeLive(.name), + all_elements: NodeLive(.all_elements), child_elements: NodeLive(.child_elements), child_tag: NodeLive(.child_tag), selected_options: NodeLive(.selected_options), + links: NodeLive(.links), + anchors: NodeLive(.anchors), }, pub fn length(self: *HTMLCollection, page: *const Page) u32 { @@ -69,9 +77,13 @@ pub fn iterator(self: *HTMLCollection, page: *Page) !*Iterator { .tag => |*impl| .{ .tag = impl._tw.clone() }, .tag_name => |*impl| .{ .tag_name = impl._tw.clone() }, .class_name => |*impl| .{ .class_name = impl._tw.clone() }, + .name => |*impl| .{ .name = impl._tw.clone() }, + .all_elements => |*impl| .{ .all_elements = 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() }, + .links => |*impl| .{ .links = impl._tw.clone() }, + .anchors => |*impl| .{ .anchors = impl._tw.clone() }, }, }, page); } @@ -83,9 +95,13 @@ pub const Iterator = GenericIterator(struct { tag: TreeWalker.FullExcludeSelf, tag_name: TreeWalker.FullExcludeSelf, class_name: TreeWalker.FullExcludeSelf, + name: TreeWalker.FullExcludeSelf, + all_elements: TreeWalker.FullExcludeSelf, child_elements: TreeWalker.Children, child_tag: TreeWalker.Children, selected_options: TreeWalker.Children, + links: TreeWalker.FullExcludeSelf, + anchors: TreeWalker.FullExcludeSelf, }, pub fn next(self: *@This(), _: *Page) ?*Element { @@ -93,9 +109,13 @@ pub const Iterator = GenericIterator(struct { .tag => |*impl| impl.nextTw(&self.tw.tag), .tag_name => |*impl| impl.nextTw(&self.tw.tag_name), .class_name => |*impl| impl.nextTw(&self.tw.class_name), + .name => |*impl| impl.nextTw(&self.tw.name), + .all_elements => |*impl| impl.nextTw(&self.tw.all_elements), .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), + .links => |*impl| impl.nextTw(&self.tw.links), + .anchors => |*impl| impl.nextTw(&self.tw.anchors), }; } }, null); diff --git a/src/browser/webapi/collections/node_live.zig b/src/browser/webapi/collections/node_live.zig index ee123b4c4..f3f4dd1a1 100644 --- a/src/browser/webapi/collections/node_live.zig +++ b/src/browser/webapi/collections/node_live.zig @@ -35,18 +35,26 @@ const Mode = enum { tag, tag_name, class_name, + name, + all_elements, child_elements, child_tag, selected_options, + links, + anchors, }; const Filters = union(Mode) { tag: Element.Tag, tag_name: String, - class_name: []const u8, + class_name: [][]const u8, + name: []const u8, + all_elements, child_elements, child_tag: Element.Tag, selected_options, + links, + anchors, fn TypeOf(comptime mode: Mode) type { @setEvalBranchQuota(2000); @@ -74,7 +82,7 @@ const Filters = union(Mode) { pub fn NodeLive(comptime mode: Mode) type { const Filter = Filters.TypeOf(mode); const TW = switch (mode) { - .tag, .tag_name, .class_name => TreeWalker.FullExcludeSelf, + .tag, .tag_name, .class_name, .name, .all_elements, .links, .anchors => TreeWalker.FullExcludeSelf, .child_elements, .child_tag, .selected_options => TreeWalker.Children, }; return struct { @@ -83,16 +91,11 @@ pub fn NodeLive(comptime mode: Mode) type { _last_index: usize, _last_length: ?u32, _cached_version: usize, - // NodeLive doesn't use an arena directly, but the filter might have - // used it (to own the string). So we take ownership of the arena so that - // we can free it when we're freed.s - _arena: ?Allocator, const Self = @This(); - pub fn init(arena: ?Allocator, root: *Node, filter: Filter, page: *Page) Self { + pub fn init(root: *Node, filter: Filter, page: *Page) Self { return .{ - ._arena = arena, ._last_index = 0, ._last_length = null, ._filter = filter, @@ -212,10 +215,25 @@ pub fn NodeLive(comptime mode: Mode) type { return std.mem.eql(u8, element_tag, self._filter.str()); }, .class_name => { + if (self._filter.len == 0) { + return false; + } + const el = node.is(Element) orelse return false; const class_attr = el.getAttributeSafe("class") orelse return false; - return Selector.classAttributeContains(class_attr, self._filter); + for (self._filter) |class_name| { + if (!Selector.classAttributeContains(class_attr, class_name)) { + return false; + } + } + return true; + }, + .name => { + const el = node.is(Element) orelse return false; + const name_attr = el.getAttributeSafe("name") orelse return false; + return std.mem.eql(u8, name_attr, self._filter); }, + .all_elements => return node._type == .element, .child_elements => return node._type == .element, .child_tag => { const el = node.is(Element) orelse return false; @@ -227,6 +245,20 @@ pub fn NodeLive(comptime mode: Mode) type { const opt = el.is(Option) orelse return false; return opt.getSelected(); }, + .links => { + // Links are elements with href attribute (TODO: also when implemented) + const el = node.is(Element) orelse return false; + const Anchor = Element.Html.Anchor; + if (el.is(Anchor) == null) return false; + return el.hasAttributeSafe("href"); + }, + .anchors => { + // Anchors are elements with name attribute + const el = node.is(Element) orelse return false; + const Anchor = Element.Html.Anchor; + if (el.is(Anchor) == null) return false; + return el.hasAttributeSafe("name"); + }, } } @@ -249,9 +281,13 @@ pub fn NodeLive(comptime mode: Mode) type { .tag => HTMLCollection{ .data = .{ .tag = self } }, .tag_name => HTMLCollection{ .data = .{ .tag_name = self } }, .class_name => HTMLCollection{ .data = .{ .class_name = self } }, + .name => HTMLCollection{ .data = .{ .name = self } }, + .all_elements => HTMLCollection{ .data = .{ .all_elements = self } }, .child_elements => HTMLCollection{ .data = .{ .child_elements = self } }, .child_tag => HTMLCollection{ .data = .{ .child_tag = self } }, .selected_options => HTMLCollection{ .data = .{ .selected_options = self } }, + .links => HTMLCollection{ .data = .{ .links = self } }, + .anchors => HTMLCollection{ .data = .{ .anchors = self } }, }; return page._factory.create(collection); } diff --git a/src/browser/webapi/element/Attribute.zig b/src/browser/webapi/element/Attribute.zig index 1919a24e6..b5d45a612 100644 --- a/src/browser/webapi/element/Attribute.zig +++ b/src/browser/webapi/element/Attribute.zig @@ -135,6 +135,11 @@ pub const List = struct { return entry._value.str(); } + // meant for internal usage, where the name is known to be properly cased + pub fn hasSafe(self: *const List, name: []const u8) bool { + return self.getEntryWithNormalizedName(name) != null; + } + pub fn getAttribute(self: *const List, name: []const u8, element: ?*Element, page: *Page) !?*Attribute { const entry = (try self.getEntry(name, page)) orelse return null; const gop = try page._attribute_lookup.getOrPut(page.arena, @intFromPtr(entry)); @@ -184,6 +189,7 @@ pub const List = struct { }; try page.addElementId(parent, element, entry._value.str()); } + page.domChanged(); page.attributeChange(element, result.normalized, entry._value.str(), old_value); return entry; } @@ -242,6 +248,7 @@ pub const List = struct { page.removeElementId(element, entry._value.str()); } + page.domChanged(); page.attributeRemove(element, result.normalized, old_value); _ = page._attribute_lookup.remove(@intFromPtr(entry)); self._list.remove(&entry._node); diff --git a/src/browser/webapi/element/Html.zig b/src/browser/webapi/element/Html.zig index cefdaf659..341fbc949 100644 --- a/src/browser/webapi/element/Html.zig +++ b/src/browser/webapi/element/Html.zig @@ -23,38 +23,39 @@ const Page = @import("../../Page.zig"); const Node = @import("../Node.zig"); const Element = @import("../Element.zig"); -pub const BR = @import("html/BR.zig"); -pub const HR = @import("html/HR.zig"); -pub const LI = @import("html/LI.zig"); -pub const OL = @import("html/OL.zig"); -pub const UL = @import("html/UL.zig"); -pub const Div = @import("html/Div.zig"); -pub const Html = @import("html/Html.zig"); -pub const Head = @import("html/Head.zig"); -pub const Meta = @import("html/Meta.zig"); -pub const Body = @import("html/Body.zig"); -pub const Link = @import("html/Link.zig"); -pub const Image = @import("html/Image.zig"); -pub const Input = @import("html/Input.zig"); -pub const Title = @import("html/Title.zig"); -pub const Style = @import("html/Style.zig"); -pub const Custom = @import("html/Custom.zig"); -pub const Script = @import("html/Script.zig"); pub const Anchor = @import("html/Anchor.zig"); +pub const Body = @import("html/Body.zig"); +pub const BR = @import("html/BR.zig"); pub const Button = @import("html/Button.zig"); +pub const Custom = @import("html/Custom.zig"); pub const Data = @import("html/Data.zig"); pub const Dialog = @import("html/Dialog.zig"); +pub const Div = @import("html/Div.zig"); +pub const Embed = @import("html/Embed.zig"); pub const Form = @import("html/Form.zig"); -pub const Heading = @import("html/Heading.zig"); -pub const Unknown = @import("html/Unknown.zig"); pub const Generic = @import("html/Generic.zig"); -pub const Template = @import("html/Template.zig"); -pub const TextArea = @import("html/TextArea.zig"); +pub const Head = @import("html/Head.zig"); +pub const Heading = @import("html/Heading.zig"); +pub const HR = @import("html/HR.zig"); +pub const Html = @import("html/Html.zig"); +pub const IFrame = @import("html/IFrame.zig"); +pub const Image = @import("html/Image.zig"); +pub const Input = @import("html/Input.zig"); +pub const LI = @import("html/LI.zig"); +pub const Link = @import("html/Link.zig"); +pub const Meta = @import("html/Meta.zig"); +pub const OL = @import("html/OL.zig"); +pub const Option = @import("html/Option.zig"); pub const Paragraph = @import("html/Paragraph.zig"); +pub const Script = @import("html/Script.zig"); pub const Select = @import("html/Select.zig"); pub const Slot = @import("html/Slot.zig"); -pub const Option = @import("html/Option.zig"); -pub const IFrame = @import("html/IFrame.zig"); +pub const Style = @import("html/Style.zig"); +pub const Template = @import("html/Template.zig"); +pub const TextArea = @import("html/TextArea.zig"); +pub const Title = @import("html/Title.zig"); +pub const UL = @import("html/UL.zig"); +pub const Unknown = @import("html/Unknown.zig"); const HtmlElement = @This(); @@ -76,6 +77,7 @@ pub const Type = union(enum) { data: *Data, dialog: *Dialog, div: *Div, + embed: *Embed, form: *Form, generic: *Generic, heading: *Heading, @@ -120,6 +122,7 @@ pub fn className(self: *const HtmlElement) []const u8 { return switch (self._type) { .anchor => "[object HtmlAnchorElement]", .div => "[object HtmlDivElement]", + .embed => "[object HtmlEmbedElement]", .form => "[object HTMLFormElement]", .p => "[object HtmlParagraphElement]", .custom => "[object CUSTOM-TODO]", diff --git a/src/browser/webapi/element/html/Anchor.zig b/src/browser/webapi/element/html/Anchor.zig index 47cbe9d4a..d6b85c46f 100644 --- a/src/browser/webapi/element/html/Anchor.zig +++ b/src/browser/webapi/element/html/Anchor.zig @@ -31,6 +31,9 @@ _proto: *HtmlElement, pub fn asElement(self: *Anchor) *Element { return self._proto._proto; } +pub fn asConstElement(self: *const Anchor) *const Element { + return self._proto._proto; +} pub fn asNode(self: *Anchor) *Node { return self.asElement().asNode(); } @@ -193,6 +196,14 @@ pub fn setType(self: *Anchor, value: []const u8, page: *Page) !void { try self.asElement().setAttributeSafe("type", value, page); } +pub fn getName(self: *const Anchor) []const u8 { + return self.asConstElement().getAttributeSafe("name") orelse ""; +} + +pub fn setName(self: *Anchor, value: []const u8, page: *Page) !void { + try self.asElement().setAttributeSafe("name", value, page); +} + pub fn getText(self: *Anchor, page: *Page) ![:0]const u8 { return self.asNode().getTextContentAlloc(page.call_arena); } @@ -235,6 +246,7 @@ pub const JsApi = struct { pub const href = bridge.accessor(Anchor.getHref, Anchor.setHref, .{}); pub const target = bridge.accessor(Anchor.getTarget, Anchor.setTarget, .{}); + pub const name = bridge.accessor(Anchor.getName, Anchor.setName, .{}); pub const origin = bridge.accessor(Anchor.getOrigin, null, .{}); pub const host = bridge.accessor(Anchor.getHost, Anchor.setHost, .{}); pub const hostname = bridge.accessor(Anchor.getHostname, Anchor.setHostname, .{}); diff --git a/src/browser/webapi/element/html/Embed.zig b/src/browser/webapi/element/html/Embed.zig new file mode 100644 index 000000000..a3292cb10 --- /dev/null +++ b/src/browser/webapi/element/html/Embed.zig @@ -0,0 +1,42 @@ +// 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"); +const HtmlElement = @import("../Html.zig"); + +const Embed = @This(); +_proto: *HtmlElement, + +pub fn asElement(self: *Embed) *Element { + return self._proto._proto; +} +pub fn asNode(self: *Embed) *Node { + return self.asElement().asNode(); +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(Embed); + + pub const Meta = struct { + pub const name = "HTMLEmbedElement"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; +}; diff --git a/src/browser/webapi/element/html/Select.zig b/src/browser/webapi/element/html/Select.zig index 5b3c0b9ec..8a5ef4d25 100644 --- a/src/browser/webapi/element/html/Select.zig +++ b/src/browser/webapi/element/html/Select.zig @@ -185,7 +185,7 @@ pub fn setRequired(self: *Select, required: bool, page: *Page) !void { pub fn getOptions(self: *Select, page: *Page) !*collections.HTMLOptionsCollection { // For options, we use the child_tag mode to filter only
+

Hello World

+

Hello + World

diff --git a/src/browser/webapi/CData.zig b/src/browser/webapi/CData.zig index a22a0d6cd..cb17aaad5 100644 --- a/src/browser/webapi/CData.zig +++ b/src/browser/webapi/CData.zig @@ -71,7 +71,8 @@ pub const RenderOpts = struct { }; // Replace successives whitespaces with one withespace. // Trims left and right according to the options. -pub fn render(self: *const CData, writer: *std.io.Writer, opts: RenderOpts) !void { +// Returns true if the string ends with a trimmed whitespace. +pub fn render(self: *const CData, writer: *std.io.Writer, opts: RenderOpts) !bool { var start: usize = 0; var prev_w: ?bool = null; var is_w: bool = undefined; @@ -110,11 +111,15 @@ pub fn render(self: *const CData, writer: *std.io.Writer, opts: RenderOpts) !voi // If the string contains only whitespaces, don't write it. if (start > 0 and opts.trim_right == false) { try writer.writeByte(' '); + } else { + return true; } } else { // last chunk is non whitespaces. try writer.writeAll(s[start..]); } + + return false; } pub fn setData(self: *CData, value: ?[]const u8, page: *Page) !void { @@ -288,19 +293,20 @@ test "WebApi: CData.render" { const TestCase = struct { value: []const u8, expected: []const u8, + result: bool = false, opts: RenderOpts = .{}, }; const test_cases = [_]TestCase{ - .{ .value = " ", .expected = "" }, - .{ .value = " ", .expected = "", .opts = .{ .trim_left = false, .trim_right = false } }, + .{ .value = " ", .expected = "", .result = true }, + .{ .value = " ", .expected = "", .opts = .{ .trim_left = false, .trim_right = false }, .result = true }, .{ .value = "foo bar", .expected = "foo bar" }, .{ .value = "foo bar", .expected = "foo bar" }, .{ .value = " foo bar", .expected = "foo bar" }, - .{ .value = "foo bar ", .expected = "foo bar" }, - .{ .value = " foo bar ", .expected = "foo bar" }, + .{ .value = "foo bar ", .expected = "foo bar", .result = true }, + .{ .value = " foo bar ", .expected = "foo bar", .result = true }, .{ .value = "foo\n\tbar", .expected = "foo bar" }, - .{ .value = "\tfoo bar baz \t\n yeah\r\n", .expected = "foo bar baz yeah" }, + .{ .value = "\tfoo bar baz \t\n yeah\r\n", .expected = "foo bar baz yeah", .result = true }, .{ .value = " foo bar", .expected = " foo bar", .opts = .{ .trim_left = false } }, .{ .value = "foo bar ", .expected = "foo bar ", .opts = .{ .trim_right = false } }, .{ .value = " foo bar ", .expected = " foo bar ", .opts = .{ .trim_left = false, .trim_right = false } }, @@ -317,8 +323,9 @@ test "WebApi: CData.render" { ._data = test_case.value, }; - try cdata.render(&buffer.writer, test_case.opts); + const result = try cdata.render(&buffer.writer, test_case.opts); try std.testing.expectEqualStrings(test_case.expected, buffer.written()); + try std.testing.expect(result == test_case.result); } } diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index 915600107..bf37dfd8b 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -228,21 +228,43 @@ pub fn getNamespaceURI(self: *const Element) []const u8 { // innerText represents the **rendered** text content of a node and its // descendants. pub fn getInnerText(self: *Element, writer: *std.Io.Writer) !void { + var state = innerTextState{}; + return try self._getInnerText(writer, &state); +} +const innerTextState = struct { + pre_w: bool = false, + trim_left: bool = true, +}; +fn _getInnerText(self: *Element, writer: *std.Io.Writer, state: *innerTextState) !void { var it = self.asNode().childrenIterator(); while (it.next()) |child| { switch (child._type) { .element => |e| switch (e._type) { .html => |he| switch (he._type) { - .br => try writer.writeByte('\n'), - .script, .style, .template => continue, - else => try e.getInnerText(writer), // TODO check if elt is hidden. + .br => { + try writer.writeByte('\n'); + state.pre_w = false; // prevent a next pre space. + state.trim_left = true; + }, + .script, .style, .template => { + state.pre_w = false; // prevent a next pre space. + state.trim_left = true; + }, + else => try e._getInnerText(writer, state), // TODO check if elt is hidden. }, .svg => {}, }, .cdata => |c| switch (c._type) { - .comment => continue, - .text => try c.render(writer, .{ .trim_right = false, .trim_left = false }), - .cdata_section => try writer.writeAll(c._data), + .comment => { + state.pre_w = false; // prevent a next pre space. + state.trim_left = true; + }, + .text => { + if (state.pre_w) try writer.writeByte(' '); + state.pre_w = try c.render(writer, .{ .trim_left = state.trim_left }); + // if we had a pre space, trim left next one. + state.trim_left = state.pre_w; + }, }, .document => {}, .document_type => {}, From 3538c77b7851d3d47867de4721ffb6565f628166 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Mon, 8 Dec 2025 17:45:53 +0100 Subject: [PATCH 166/219] innerText: ignore CDATA section --- src/browser/webapi/Element.zig | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index bf37dfd8b..f8ab2501d 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -265,6 +265,9 @@ fn _getInnerText(self: *Element, writer: *std.Io.Writer, state: *innerTextState) // if we had a pre space, trim left next one. state.trim_left = state.pre_w; }, + // CDATA sections should not be used within HTML. They are + // considered comments and are not displayed. + .cdata_section => {}, }, .document => {}, .document_type => {}, From 9132bc23757512e774aa6185ccdcc68fee208c18 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Tue, 9 Dec 2025 11:50:33 +0800 Subject: [PATCH 167/219] re-enable CDP node registry --- src/browser/Page.zig | 2 +- src/browser/tests/cdp/registry1.html | 1 + src/browser/tests/cdp/registry2.html | 1 + src/browser/tests/cdp/registry3.html | 1 + src/browser/webapi/Element.zig | 20 +- src/browser/webapi/Navigator.zig | 1 - src/browser/webapi/Node.zig | 21 +- src/browser/webapi/NodeFilter.zig | 2 +- src/browser/webapi/net/Fetch.zig | 1 - src/cdp/Node.zig | 1153 +++++++++++++------------- src/cdp/cdp.zig | 49 +- src/cdp/testing.zig | 3 +- src/testing.zig | 22 + 13 files changed, 653 insertions(+), 624 deletions(-) create mode 100644 src/browser/tests/cdp/registry1.html create mode 100644 src/browser/tests/cdp/registry2.html create mode 100644 src/browser/tests/cdp/registry3.html diff --git a/src/browser/Page.zig b/src/browser/Page.zig index fb70f9d8c..9bfd17cfb 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -1276,7 +1276,7 @@ fn createHtmlElementT(self: *Page, comptime E: type, namespace: Element.Namespac const node = element.asNode(); if (@hasDecl(E, "Build") and @hasDecl(E.Build, "created")) { @call(.auto, @field(E.Build, "created"), .{ node, self }) catch |err| { - log.err(.page, "build.created", .{ .tag = node.getNodeName(self), .err = err }); + log.err(.page, "build.created", .{ .tag = node.getNodeName(&self.buf), .err = err }); return err; }; } diff --git a/src/browser/tests/cdp/registry1.html b/src/browser/tests/cdp/registry1.html new file mode 100644 index 000000000..b603ad249 --- /dev/null +++ b/src/browser/tests/cdp/registry1.html @@ -0,0 +1 @@ +
link1

other

diff --git a/src/browser/tests/cdp/registry2.html b/src/browser/tests/cdp/registry2.html new file mode 100644 index 000000000..136680aa7 --- /dev/null +++ b/src/browser/tests/cdp/registry2.html @@ -0,0 +1 @@ + diff --git a/src/browser/tests/cdp/registry3.html b/src/browser/tests/cdp/registry3.html new file mode 100644 index 000000000..a5b16fc5e --- /dev/null +++ b/src/browser/tests/cdp/registry3.html @@ -0,0 +1 @@ +
diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index ecaf35b95..cd6b003ea 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -225,6 +225,15 @@ pub fn getNamespaceURI(self: *const Element) []const u8 { return self._namespace.toUri(); } +pub fn getLocalName(self: *Element) []const u8 { + const name = self.getTagNameLower(); + if (std.mem.indexOfPos(u8, name, 0, ":")) |pos| { + return name[pos + 1 ..]; + } + + return name; +} + // innerText represents the **rendered** text content of a node and its // descendants. pub fn getInnerText(self: *Element, writer: *std.Io.Writer) !void { @@ -1091,16 +1100,7 @@ pub const JsApi = struct { return null; } - pub const localName = bridge.accessor(_localName, null, .{}); - fn _localName(self: *Element) []const u8 { - const name = self.getTagNameLower(); - if (std.mem.indexOfPos(u8, name, 0, ":")) |pos| { - return name[pos + 1 ..]; - } - - return name; - } - + pub const localName = bridge.accessor(Element.getLocalName, null, .{}); 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, .{}); diff --git a/src/browser/webapi/Navigator.zig b/src/browser/webapi/Navigator.zig index 3fa8154f1..23efd49f6 100644 --- a/src/browser/webapi/Navigator.zig +++ b/src/browser/webapi/Navigator.zig @@ -98,7 +98,6 @@ pub const JsApi = struct { pub const name = "Navigator"; pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; - // ZIGDOM (currently no optimization for empty types) pub const empty_with_no_proto = true; }; diff --git a/src/browser/webapi/Node.zig b/src/browser/webapi/Node.zig index 67245cb34..619518f74 100644 --- a/src/browser/webapi/Node.zig +++ b/src/browser/webapi/Node.zig @@ -256,9 +256,9 @@ pub fn setTextContent(self: *Node, data: []const u8, page: *Page) !void { } } -pub fn getNodeName(self: *const Node, page: *Page) []const u8 { +pub fn getNodeName(self: *const Node, buf: []u8) []const u8 { return switch (self._type) { - .element => |el| el.getTagNameSpec(&page.buf), + .element => |el| el.getTagNameSpec(buf), .cdata => |cd| switch (cd._type) { .text => "#text", .cdata_section => "#cdata-section", @@ -271,7 +271,7 @@ pub fn getNodeName(self: *const Node, page: *Page) []const u8 { }; } -pub fn nodeType(self: *const Node) u8 { +pub fn getNodeType(self: *const Node) u8 { return switch (self._type) { .element => 1, .attribute => 2, @@ -491,6 +491,13 @@ pub fn childrenIterator(self: *Node) NodeIterator { }; } +pub fn getChildrenCount(self: *Node) usize { + return switch (self._type) { + .element, .document, .document_fragment => self.getLength(), + .document_type, .attribute, .cdata => return 0, + }; +} + pub fn getLength(self: *Node) u32 { switch (self._type) { .cdata => |cdata| { @@ -770,8 +777,12 @@ pub const JsApi = struct { pub const DOCUMENT_POSITION_CONTAINED_BY = bridge.property(0x10); pub const DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC = bridge.property(0x20); - pub const nodeName = bridge.accessor(Node.getNodeName, null, .{}); - pub const nodeType = bridge.accessor(Node.nodeType, null, .{}); + pub const nodeName = bridge.accessor(struct{ + fn wrap(self: *const Node, page: *Page) []const u8 { + return self.getNodeName(&page.buf); + } + }.wrap, null, .{}); + pub const nodeType = bridge.accessor(Node.getNodeType, null, .{}); pub const textContent = bridge.accessor(_textContext, Node.setTextContent, .{}); fn _textContext(self: *Node, page: *const Page) !?[]const u8 { diff --git a/src/browser/webapi/NodeFilter.zig b/src/browser/webapi/NodeFilter.zig index 232355dc5..bdb523a80 100644 --- a/src/browser/webapi/NodeFilter.zig +++ b/src/browser/webapi/NodeFilter.zig @@ -72,7 +72,7 @@ pub fn shouldShow(node: *const Node, what_to_show: u32) bool { // TODO: Test this mapping thoroughly! // nodeType values (1=ELEMENT, 3=TEXT, 9=DOCUMENT, etc.) need to map to // SHOW_* bitmask positions (0x1, 0x4, 0x100, etc.) - const node_type_value = node.nodeType(); + const node_type_value = node.getNodeType(); const bit_position = node_type_value - 1; const node_type_bit: u32 = @as(u32, 1) << @intCast(bit_position); return (what_to_show & node_type_bit) != 0; diff --git a/src/browser/webapi/net/Fetch.zig b/src/browser/webapi/net/Fetch.zig index 7f85741fa..9f708e0a2 100644 --- a/src/browser/webapi/net/Fetch.zig +++ b/src/browser/webapi/net/Fetch.zig @@ -42,7 +42,6 @@ _resolver: js.PersistentPromiseResolver, pub const Input = Request.Input; pub const InitOpts = Request.InitOpts; -// @ZIGDOM just enough to get campfire demo working pub fn init(input: Input, options: ?InitOpts, page: *Page) !js.Promise { const request = try Request.init(input, options, page); diff --git a/src/cdp/Node.zig b/src/cdp/Node.zig index c51093128..ca7c43ca3 100644 --- a/src/cdp/Node.zig +++ b/src/cdp/Node.zig @@ -1,587 +1,592 @@ -// Copyright (C) 2023-2024 Lightpanda (Selecy SAS) -// +// 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 . -// @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); -// } -// } +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const log = @import("../log.zig"); +const Page = @import("../browser/Page.zig"); +const DOMNode = @import("../browser/webapi/Node.zig"); + +pub const Id = u32; + +const Node = @This(); + +id: Id, +dom: *DOMNode, +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(*DOMNode, *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, dom_node: *DOMNode) !*Node { + const node_lookup_gop = try self.lookup_by_node.getOrPut(self.allocator, dom_node); + 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(dom_node); + + const node = try self.node_pool.create(); + errdefer self.node_pool.destroy(node); + + const id = self.node_id; + self.node_id = id + 1; + + node.* = .{ + .id = id, + .dom = dom_node, + .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, dom_node: *DOMNode) u64 { + return std.hash.Wyhash.hash(0, std.mem.asBytes(&@intFromPtr(dom_node))); + } + + pub fn eql(_: NodeContext, a: *DOMNode, b: *DOMNode) 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 { + search_id: u16 = 0, + registry: *Registry, + 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 *DOMNode) !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 count: usize = 0; + var it = node.dom.childrenIterator(); + + var registry = self.registry; + const full_child = self.depth < 0 or self.depth < depth; + + try w.beginArray(); + while (it.next()) |dom_child| { + const child_node = try registry.register(dom_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(); + } + count += 1; + } + try w.endArray(); + + return count; + } + + 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 dom_node = node.dom; + + if (dom_node._parent) |dom_parent| { + const parent_node = try self.registry.register(dom_parent); + try w.objectField("parentId"); + try w.write(parent_node.id); + } + + if (dom_node.is(DOMNode.Element)) |element| { + if (element.hasAttributes()) { + try w.objectField("attributes"); + try w.beginArray(); + var it = element.attributeIterator(); + while (it.next()) |attr| { + try w.write(attr._name.str()); + try w.write(attr._value.str()); + } + try w.endArray(); + } + + try w.objectField("localName"); + try w.write(element.getLocalName()); + } else { + try w.objectField("localName"); + try w.write(""); + } + + try w.objectField("nodeType"); + try w.write(dom_node.getNodeType()); + + try w.objectField("nodeName"); + var name_buf: [Page.BUF_SIZE]u8 = undefined; + try w.write(dom_node.getNodeName(&name_buf)); + + try w.objectField("nodeValue"); + try w.write(dom_node.getNodeValue() orelse ""); + + if (include_child_count) { + try w.objectField("childNodeCount"); + try w.write(dom_node.getChildrenCount()); + } + + 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" { + 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 page = try testing.pageTest("cdp/registry1.html"); + defer page._session.removePage(); + var doc = page.window._document; + + { + const dom_node = (try doc.querySelector("#a1", page)).?.asNode(); + const node = try registry.register(dom_node); + const n1b = registry.lookup_by_id.get(1).?; + const n1c = registry.lookup_by_node.get(node.dom).?; + try testing.expectEqual(node, n1b); + try testing.expectEqual(node, n1c); + + try testing.expectEqual(1, node.id); + try testing.expectEqual(dom_node, node.dom); + } + + { + const dom_node = (try doc.querySelector("p", page)).?.asNode(); + const node = try registry.register(dom_node); + const n1b = registry.lookup_by_id.get(2).?; + const n1c = registry.lookup_by_node.get(node.dom).?; + try testing.expectEqual(node, n1b); + try testing.expectEqual(node, n1c); + + try testing.expectEqual(2, node.id); + try testing.expectEqual(dom_node, node.dom); + } +} + +test "cdp Node: search list" { + 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 page = try testing.pageTest("cdp/registry2.html"); + defer page._session.removePage(); + var doc = page.window._document; + + const s1 = try search_list.create((try doc.querySelectorAll("a", page))._nodes); + 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", page))._nodes); + try testing.expectEqual("2", s2.name); + try testing.expectEqualSlices(u32, &.{1}, s2.node_ids); + + const s3 = try search_list.create((try doc.querySelectorAll("#a2", page))._nodes); + 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" { + var registry = Registry.init(testing.allocator); + defer registry.deinit(); + + var page = try testing.pageTest("cdp/registry3.html"); + defer page._session.removePage(); + var doc = page.window._document; + + { + 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 = 3, + .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 = 3, + .documentURL = null, + .baseURL = null, + .xmlVersion = "", + .compatibilityMode = "NoQuirksMode", + .isScrollable = false, + .children = &.{ .{ + .nodeId = 5, + .localName = "a", + .childNodeCount = 0, + .attributes = &.{"id", "a1"}, + .parentId = 4, + }, .{ + .nodeId = 6, + .localName = "div", + .childNodeCount = 1, + .parentId = 4, + .children = &.{.{ + .nodeId = 7, + .localName = "a", + .childNodeCount = 0, + .parentId = 6, + .attributes = &.{"id", "a2"}, + }}, + }, .{ + .nodeId = 8, + .backendNodeId = 8, + .nodeName = "#text", + .localName = "", + .childNodeCount = 0, + .parentId = 4, + .nodeValue = "\n", + } }, + } }, json); + } +} diff --git a/src/cdp/cdp.zig b/src/cdp/cdp.zig index a9b3c9198..a2ce7159e 100644 --- a/src/cdp/cdp.zig +++ b/src/cdp/cdp.zig @@ -287,8 +287,7 @@ pub fn CDPT(comptime TypeProvider: type) type { } pub fn BrowserContext(comptime CDP_T: type) type { - // @ZIGMOD - // const Node = @import("Node.zig"); + const Node = @import("Node.zig"); return struct { id: []const u8, @@ -328,9 +327,8 @@ pub fn BrowserContext(comptime CDP_T: type) type { security_origin: []const u8, page_life_cycle_events: bool, secure_context_type: []const u8, - // @ZIGDOM - // node_registry: Node.Registry, - // node_search_list: Node.Search.List, + node_registry: Node.Registry, + node_search_list: Node.Search.List, inspector: js.Inspector, isolated_worlds: std.ArrayListUnmanaged(IsolatedWorld), @@ -363,9 +361,8 @@ pub fn BrowserContext(comptime CDP_T: type) type { const inspector = try cdp.browser.env.newInspector(arena, self); - // @ZIGDOM - // var registry = Node.Registry.init(allocator); - // errdefer registry.deinit(); + var registry = Node.Registry.init(allocator); + errdefer registry.deinit(); self.* = .{ .id = id, @@ -378,9 +375,8 @@ 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 - // @ZIGDOM - // .node_registry = registry, - // .node_search_list = undefined, + .node_registry = registry, + .node_search_list = undefined, .isolated_worlds = .empty, .inspector = inspector, .notification_arena = cdp.notification_arena.allocator(), @@ -388,8 +384,7 @@ pub fn BrowserContext(comptime CDP_T: type) type { .captured_responses = .empty, .log_interceptor = LogInterceptor(Self).init(allocator, self), }; - // ZIGDOM - // self.node_search_list = Node.Search.List.init(allocator, &self.node_registry); + self.node_search_list = Node.Search.List.init(allocator, &self.node_registry); errdefer self.deinit(); try cdp.browser.notification.register(.page_remove, self, onPageRemove); @@ -424,9 +419,8 @@ pub fn BrowserContext(comptime CDP_T: type) type { world.deinit(); } self.isolated_worlds.clearRetainingCapacity(); - // @ZIGDOM - // self.node_registry.deinit(); - // self.node_search_list.deinit(); + self.node_registry.deinit(); + self.node_search_list.deinit(); self.cdp.browser.notification.unregisterAll(self); if (self.http_proxy_changed) { @@ -440,10 +434,8 @@ pub fn BrowserContext(comptime CDP_T: type) type { } pub fn reset(self: *Self) void { - // @ZIGDOM - _ = self; - // self.node_registry.reset(); - // self.node_search_list.reset(); + self.node_registry.reset(); + self.node_search_list.reset(); } pub fn createIsolatedWorld(self: *Self, world_name: []const u8, grant_universal_access: bool) !*IsolatedWorld { @@ -462,15 +454,14 @@ pub fn BrowserContext(comptime CDP_T: type) type { return world; } - // @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 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) ?[:0]const u8 { const page = self.session.currentPage() orelse return null; diff --git a/src/cdp/testing.zig b/src/cdp/testing.zig index 3912b842f..cefc88232 100644 --- a/src/cdp/testing.zig +++ b/src/cdp/testing.zig @@ -32,8 +32,7 @@ pub const expect = std.testing.expect; pub const expectEqual = base.expectEqual; pub const expectError = base.expectError; pub const expectEqualSlices = base.expectEqualSlices; - -pub const Document = @import("../testing.zig").Document; +pub const pageTest = base.pageTest; const Client = struct { allocator: Allocator, diff --git a/src/testing.zig b/src/testing.zig index 77cc1f00a..b250dc207 100644 --- a/src/testing.zig +++ b/src/testing.zig @@ -40,6 +40,7 @@ const App = @import("App.zig"); const js = @import("browser/js/js.zig"); const Browser = @import("browser/Browser.zig"); const Session = @import("browser/Session.zig"); +const Page = @import("browser/Page.zig"); // Merged std.testing.expectEqual and std.testing.expectString // can be useful when testing fields of an anytype an you don't know @@ -415,6 +416,27 @@ fn runWebApiTest(test_file: [:0]const u8) !void { }; } +// Used by a few CDP tests - wouldn't be sad to see this go. +pub fn pageTest(comptime test_file: []const u8) !*Page { + const page = try test_session.createPage(); + errdefer test_session.removePage(); + + const url = try std.fmt.allocPrintSentinel( + arena_allocator, + "http://127.0.0.1:9582/{s}{s}", + .{ WEB_API_TEST_ROOT, test_file }, + 0, + ); + + try page.navigate(url, .{}); + test_session.fetchWait(2000); + + page._session.browser.runMicrotasks(); + page._session.browser.runMessageLoop(); + + return page; +} + test { std.testing.refAllDecls(@This()); } From 0e1b966dcef4e2c57e07871230ff28d9bdf6f8b5 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Tue, 9 Dec 2025 13:04:01 +0800 Subject: [PATCH 168/219] re-enable CDP dom domain --- src/browser/js/Inspector.zig | 8 +- src/browser/tests/cdp/dom1.html | 1 + src/browser/tests/cdp/dom2.html | 1 + src/browser/webapi/Element.zig | 2 +- src/cdp/domains/dom.zig | 1269 +++++++++++++++---------------- src/cdp/testing.zig | 25 +- src/testing.zig | 4 - 7 files changed, 653 insertions(+), 657 deletions(-) create mode 100644 src/browser/tests/cdp/dom1.html create mode 100644 src/browser/tests/cdp/dom2.html diff --git a/src/browser/js/Inspector.zig b/src/browser/js/Inspector.zig index f04409e87..04a8c5c8d 100644 --- a/src/browser/js/Inspector.zig +++ b/src/browser/js/Inspector.zig @@ -109,7 +109,7 @@ pub fn getRemoteObject( group: []const u8, value: anytype, ) !RemoteObject { - const js_value = try context.zigValueToJs(value); + const js_value = try context.zigValueToJs(value, .{}); // We do not want to expose this as a parameter for now const generate_preview = false; @@ -127,8 +127,10 @@ pub fn getNodePtr(self: *const Inspector, allocator: Allocator, object_id: []con const unwrapped = try self.session.unwrapObject(allocator, object_id); // The values context and groupId are not used here const toa = getTaggedAnyOpaque(unwrapped.value) orelse return null; - if (toa.subtype == null or toa.subtype != .node) return error.ObjectIdIsNotANode; - return toa.ptr; + if (toa.subtype == null or toa.subtype != .node) { + return error.ObjectIdIsNotANode; + } + return toa.value; } const NoopInspector = struct { diff --git a/src/browser/tests/cdp/dom1.html b/src/browser/tests/cdp/dom1.html new file mode 100644 index 000000000..db532cc51 --- /dev/null +++ b/src/browser/tests/cdp/dom1.html @@ -0,0 +1 @@ +

1

2

diff --git a/src/browser/tests/cdp/dom2.html b/src/browser/tests/cdp/dom2.html new file mode 100644 index 000000000..c9b3acb3a --- /dev/null +++ b/src/browser/tests/cdp/dom2.html @@ -0,0 +1 @@ +

2

diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index cd6b003ea..8022cea8e 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -27,7 +27,6 @@ const reflect = @import("../reflect.zig"); const Node = @import("Node.zig"); const CSS = @import("CSS.zig"); -const DOMRect = @import("DOMRect.zig"); const ShadowRoot = @import("ShadowRoot.zig"); const collections = @import("collections.zig"); const Selector = @import("selector/Selector.zig"); @@ -35,6 +34,7 @@ const Animation = @import("animation/Animation.zig"); const DOMStringMap = @import("element/DOMStringMap.zig"); const CSSStyleProperties = @import("css/CSSStyleProperties.zig"); +pub const DOMRect = @import("DOMRect.zig"); pub const Svg = @import("element/Svg.zig"); pub const Html = @import("element/Html.zig"); pub const Attribute = @import("element/Attribute.zig"); diff --git a/src/cdp/domains/dom.zig b/src/cdp/domains/dom.zig index e99fd6b65..791f94585 100644 --- a/src/cdp/domains/dom.zig +++ b/src/cdp/domains/dom.zig @@ -18,657 +18,642 @@ const std = @import("std"); const log = @import("../../log.zig"); +const Node = @import("../Node.zig"); +const DOMNode = @import("../../browser/webapi/Node.zig"); +const Selector = @import("../../browser/webapi/selector/Selector.zig"); + const Allocator = std.mem.Allocator; -// 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, - // ZIGDOM - // getDocument, - // performSearch, - // getSearchResults, - // discardSearchResults, - // querySelector, - // querySelectorAll, - // resolveNode, - // describeNode, - // scrollIntoViewIfNeeded, - // getContentQuads, - // getBoxModel, - // requestChildNodes, - // getFrameOwner, + 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, .{}), - // @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), + .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 node = try bc.node_registry.register(page.window._document.asNode()); + 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 list = try Selector.querySelectorAll(page.window._document.asNode(), params.query, page); + const search = try bc.node_search_list.create(list._nodes); + + // dispatch setChildNodesEvents to inform the client of the subpart of node + // tree covering the results. + try dispatchSetChildNodes(cmd, list._nodes); + + 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, dom_nodes: []const *DOMNode) !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.ArrayList(*Node) = .empty; + for (dom_nodes) |dom_node| { + var current = dom_node; + while (true) { + const parent_node = current._parent orelse break; + + const node = try bc.node_registry.register(parent_node); + if (node.set_child_nodes_event) { + break; + } + try parents.append(arena, node); + current = parent_node; + } + } + + 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 dom_parent = node.dom._parent orelse continue; + + // Retrieve the parent from the registry. + const parent_node = try bc.node_registry.register(dom_parent); + + 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 page = bc.session.currentPage() orelse return error.PageNotLoaded; + + const node = bc.node_registry.lookup_by_id.get(params.nodeId) orelse { + return cmd.sendError(-32000, "Could not find node with given id", .{}); + }; + + const element = try Selector.querySelector(node.dom, params.selector, page) orelse return error.NodeNotFoundForGivenId; + const dom_node = element.asNode(); + const registered_node = try bc.node_registry.register(dom_node); + + // Dispatch setChildNodesEvents to inform the client of the subpart of node tree covering the results. + var array = [1]*DOMNode{dom_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 page = bc.session.currentPage() orelse return error.PageNotLoaded; + + 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_nodes = try Selector.querySelectorAll(node.dom, params.selector, page); + const nodes = selected_nodes._nodes; + + const node_ids = try cmd.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 *DOMNode 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 "", + node.dom, + ); + 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: *const DOMNode.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: ?DOMNode.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); + + switch (node.dom._type) { + .element => {}, + .document => {}, + .cdata => {}, + 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 + + const element = node.dom.is(DOMNode.Element) orelse 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 rect = try element.getBoundingClientRect(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 + const element = node.dom.is(DOMNode.Element) orelse return error.NodeIsNotAnElement; + + const rect = try element.getBoundingClientRect(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 node = try bc.node_registry.register(page.window._document.asNode()); + return cmd.sendResult(.{ .nodeId = node.id, .backendNodeId = node.id }, .{}); } -// 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 }); -// } +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", .url = "cdp/dom1.html" }); + + 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", .url = "cdp/dom1.html" }); + + 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", .url = "cdp/dom1.html" }); + + 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", .url = "cdp/dom2.html" }); + + 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 = 7 }, .{ .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 = &.{7} }, .{ .id = 5 }); +} + +test "cdp.dom: getBoxModel" { + var ctx = testing.context(); + defer ctx.deinit(); + + _ = try ctx.loadBrowserContext(.{ .id = "BID-A", .url = "cdp/dom2.html" }); + + 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/testing.zig b/src/cdp/testing.zig index cefc88232..8d459a9f4 100644 --- a/src/cdp/testing.zig +++ b/src/cdp/testing.zig @@ -93,7 +93,7 @@ const TestContext = struct { id: ?[]const u8 = null, target_id: ?[]const u8 = null, session_id: ?[]const u8 = null, - html: ?[]const u8 = null, + url: ?[:0]const u8 = null, }; pub fn loadBrowserContext(self: *TestContext, opts: BrowserContextOpts) !*main.BrowserContext(TestCDP) { var c = self.cdp(); @@ -116,12 +116,23 @@ const TestContext = struct { bc.session_id = sid; } - // @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; - // } + if (opts.url) |url| { + if (bc.session_id == null) { + bc.session_id = "SID-X"; + } + if (bc.target_id == null) { + bc.target_id = "TID-X"; + } + const page = try bc.session.createPage(); + const full_url = try std.fmt.allocPrintSentinel( + self.arena.allocator(), + "http://127.0.0.1:9582/src/browser/tests/{s}", + .{ url }, + 0, + ); + try page.navigate(full_url, .{}); + bc.session.fetchWait(2000); + } return bc; } diff --git a/src/testing.zig b/src/testing.zig index b250dc207..452ce812d 100644 --- a/src/testing.zig +++ b/src/testing.zig @@ -430,10 +430,6 @@ pub fn pageTest(comptime test_file: []const u8) !*Page { try page.navigate(url, .{}); test_session.fetchWait(2000); - - page._session.browser.runMicrotasks(); - page._session.browser.runMessageLoop(); - return page; } From 53ccefc15c4de9a254e095b70fdac8c48badab72 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Tue, 9 Dec 2025 08:50:58 +0100 Subject: [PATCH 169/219] cdp: implement Security.setIgnoreCertificateErrors ensure no inflight conns is running before set TLS verify --- src/cdp/domains/security.zig | 39 +++++++++++++++++++++++++++++++++ src/http/Client.zig | 42 ++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+) diff --git a/src/cdp/domains/security.zig b/src/cdp/domains/security.zig index dad0cebd4..830cd5910 100644 --- a/src/cdp/domains/security.zig +++ b/src/cdp/domains/security.zig @@ -21,9 +21,48 @@ const std = @import("std"); pub fn processMessage(cmd: anytype) !void { const action = std.meta.stringToEnum(enum { enable, + setIgnoreCertificateErrors, }, cmd.input.action) orelse return error.UnknownMethod; switch (action) { .enable => return cmd.sendResult(null, .{}), + .setIgnoreCertificateErrors => return setIgnoreCertificateErrors(cmd), } } + +fn setIgnoreCertificateErrors(cmd: anytype) !void { + const params = (try cmd.params(struct { + ignore: bool, + })) orelse return error.InvalidParams; + + if (params.ignore) { + try cmd.cdp.browser.http_client.disableTlsVerify(); + } else { + try cmd.cdp.browser.http_client.enableTlsVerify(); + } + + return cmd.sendResult(null, .{}); +} + +const testing = @import("../testing.zig"); + +test "cdp.Security: setIgnoreCertificateErrors" { + var ctx = testing.context(); + defer ctx.deinit(); + + _ = try ctx.loadBrowserContext(.{ .id = "BID-9" }); + + try ctx.processMessage(.{ + .id = 8, + .method = "Security.setIgnoreCertificateErrors", + .params = .{ .ignore = true }, + }); + try ctx.expectSentResult(null, .{ .id = 8 }); + + try ctx.processMessage(.{ + .id = 9, + .method = "Security.setIgnoreCertificateErrors", + .params = .{ .ignore = false }, + }); + try ctx.expectSentResult(null, .{ .id = 9 }); +} diff --git a/src/http/Client.zig b/src/http/Client.zig index 69e6d9e13..0e2e08159 100644 --- a/src/http/Client.zig +++ b/src/http/Client.zig @@ -92,6 +92,11 @@ notification: ?*Notification = null, // restoring, this originally-configured value is what it goes to. http_proxy: ?[:0]const u8 = null, +// track if the client use a proxy for connections. +// We can't use http_proxy because we want also to track proxy configured via +// CDP. +use_proxy: bool, + // The complete user-agent header line user_agent: [:0]const u8, @@ -125,6 +130,7 @@ pub fn init(allocator: Allocator, ca_blob: ?c.curl_blob, opts: Http.Opts) !*Clie .handles = handles, .allocator = allocator, .http_proxy = opts.http_proxy, + .use_proxy = opts.http_proxy != null, .user_agent = opts.user_agent, .transfer_pool = transfer_pool, }; @@ -308,6 +314,7 @@ pub fn changeProxy(self: *Client, proxy: [:0]const u8) !void { for (self.handles.handles) |*h| { try errorCheck(c.curl_easy_setopt(h.conn.easy, c.CURLOPT_PROXY, proxy.ptr)); } + self.use_proxy = true; } // Same restriction as changeProxy. Should be ok since this is only called on @@ -319,6 +326,41 @@ pub fn restoreOriginalProxy(self: *Client) !void { for (self.handles.handles) |*h| { try errorCheck(c.curl_easy_setopt(h.conn.easy, c.CURLOPT_PROXY, proxy)); } + self.use_proxy = proxy != null; +} + +// Enable TLS verification on all connections. +pub fn enableTlsVerify(self: *const Client) !void { + try self.ensureNoActiveConnection(); + + for (self.handles.handles) |*h| { + const easy = h.conn.easy; + + try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_SSL_VERIFYHOST, @as(c_long, 2))); + try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_SSL_VERIFYPEER, @as(c_long, 1))); + + if (self.use_proxy) { + try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_PROXY_SSL_VERIFYHOST, @as(c_long, 2))); + try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_PROXY_SSL_VERIFYPEER, @as(c_long, 1))); + } + } +} + +// Disable TLS verification on all connections. +pub fn disableTlsVerify(self: *const Client) !void { + try self.ensureNoActiveConnection(); + + for (self.handles.handles) |*h| { + const easy = h.conn.easy; + + try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_SSL_VERIFYHOST, @as(c_long, 0))); + try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_SSL_VERIFYPEER, @as(c_long, 0))); + + if (self.use_proxy) { + try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_PROXY_SSL_VERIFYHOST, @as(c_long, 0))); + try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_PROXY_SSL_VERIFYPEER, @as(c_long, 0))); + } + } } fn makeRequest(self: *Client, handle: *Handle, transfer: *Transfer) !void { From 47b4b68e60143fc1ca29bc023d51815172fc914b Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Tue, 9 Dec 2025 16:38:56 +0800 Subject: [PATCH 170/219] add parsed DocType to document (and handle dumping it) --- src/browser/Page.zig | 1 - src/browser/dump.zig | 31 +++++++++++++++++++++--- src/browser/parser/Parser.zig | 22 +++++++++++------ src/browser/tests/document/document.html | 1 + src/browser/webapi/XMLSerializer.zig | 6 ++++- src/lightpanda.zig | 2 +- 6 files changed, 49 insertions(+), 14 deletions(-) diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 9bfd17cfb..af235a5bd 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -868,7 +868,6 @@ fn notifyNetworkAlmostIdle(self: *Page) void { // called from the parser pub fn appendNew(self: *Page, parent: *Node, child: Node.NodeOrText) !void { - // TODO: should some of this be pushed into appendNode... ? const node = switch (child) { .node => |n| n, .text => |txt| blk: { diff --git a/src/browser/dump.zig b/src/browser/dump.zig index 628073974..adcef5861 100644 --- a/src/browser/dump.zig +++ b/src/browser/dump.zig @@ -24,6 +24,7 @@ const Slot = @import("webapi/element/html/Slot.zig"); pub const RootOpts = struct { with_base: bool = false, strip: Opts.Strip = .{}, + shadow: Opts.Shadow = .rendered, }; pub const Opts = struct { @@ -48,10 +49,10 @@ pub const Opts = struct { }; }; -pub fn root(opts: RootOpts, writer: *std.Io.Writer, page: *Page) !void { - const doc = page.document; +pub fn root(doc: *Node.Document, opts: RootOpts, writer: *std.Io.Writer, page: *Page) !void { if (opts.with_base) { if (doc.is(Node.Document.HTMLDocument)) |html_doc| { + try writer.writeAll(""); const parent = if (html_doc.getHead()) |head| head.asNode() else doc.asNode(); const base = try doc.createElement("base", null, page); try base.setAttributeSafe("base", page.url, page); @@ -59,7 +60,7 @@ pub fn root(opts: RootOpts, writer: *std.Io.Writer, page: *Page) !void { } } - return deep(doc.asNode(), .{ .strip = opts.strip }, writer, page); + return deep(doc.asNode(), .{ .strip = opts.strip, .shadow = opts.shadow }, writer, page); } pub fn deep(node: *Node, opts: Opts, writer: *std.Io.Writer, page: *Page) error{WriteFailed}!void { @@ -130,7 +131,29 @@ fn _deep(node: *Node, opts: Opts, comptime force_slot: bool, writer: *std.Io.Wri } }, .document => try children(node, opts, writer, page), - .document_type => {}, + .document_type => |dt| { + try writer.writeAll("\n"); + }, .document_fragment => try children(node, opts, writer, page), .attribute => unreachable, } diff --git a/src/browser/parser/Parser.zig b/src/browser/parser/Parser.zig index 65e25ecd0..24991caf7 100644 --- a/src/browser/parser/Parser.zig +++ b/src/browser/parser/Parser.zig @@ -219,17 +219,25 @@ fn _createCommentCallback(self: *Parser, str: []const u8) !*anyopaque { } fn appendDoctypeToDocument(ctx: *anyopaque, name: h5e.StringSlice, public_id: h5e.StringSlice, system_id: h5e.StringSlice) callconv(.c) void { - _ = public_id; - _ = system_id; - const self: *Parser = @ptrCast(@alignCast(ctx)); - self._appendDoctypeToDocument(name.slice()) catch |err| { + self._appendDoctypeToDocument(name.slice(), public_id.slice(), system_id.slice()) catch |err| { self.err = .{ .err = err, .source = .append_doctype_to_document }; }; } -fn _appendDoctypeToDocument(self: *Parser, name: []const u8) !void { - _ = self; - _ = name; +fn _appendDoctypeToDocument(self: *Parser, name: []const u8, public_id: []const u8, system_id: []const u8) !void { + const page = self.page; + + // Create the DocumentType node + const DocumentType = @import("../webapi/DocumentType.zig"); + const doctype = try page._factory.node(DocumentType{ + ._proto = undefined, + ._name = try page.dupeString(name), + ._public_id = try page.dupeString(public_id), + ._system_id = try page.dupeString(system_id), + }); + + // Append it to the document + try page.appendNew(self.container.node, .{ .node = doctype.asNode() }); } fn addAttrsIfMissingCallback(ctx: *anyopaque, target_ref: *anyopaque, attributes: h5e.AttributeIterator) callconv(.c) void { diff --git a/src/browser/tests/document/document.html b/src/browser/tests/document/document.html index 8658b9b64..1138c132a 100644 --- a/src/browser/tests/document/document.html +++ b/src/browser/tests/document/document.html @@ -8,6 +8,7 @@ + + diff --git a/src/browser/webapi/Location.zig b/src/browser/webapi/Location.zig index e7191a138..205d1732e 100644 --- a/src/browser/webapi/Location.zig +++ b/src/browser/webapi/Location.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 URL = @import("URL.zig"); @@ -64,6 +65,25 @@ pub fn getHash(self: *const Location) []const u8 { return self._url.getHash(); } +pub fn setHash(_: *const Location, hash: []const u8, page: *Page) !void { + const normalized_hash = blk: { + if (hash.len == 0) { + const old_url = page.url; + + break :blk if (std.mem.indexOfScalar(u8, old_url, '#')) |index| + old_url[0..index] + else + old_url; + } else if (hash[0] == '#') + break :blk hash + else + break :blk try std.fmt.allocPrint(page.arena, "#{s}", .{hash}); + }; + + const duped_hash = try page.arena.dupeZ(u8, normalized_hash); + return page.navigate(duped_hash, .{ .reason = .script }); +} + pub fn toString(self: *const Location, page: *const Page) ![:0]const u8 { return self._url.toString(page); } @@ -80,7 +100,7 @@ pub const JsApi = struct { pub const toString = bridge.function(Location.toString, .{}); pub const href = bridge.accessor(Location.toString, null, .{}); pub const search = bridge.accessor(Location.getSearch, null, .{}); - pub const hash = bridge.accessor(Location.getHash, null, .{}); + pub const hash = bridge.accessor(Location.getHash, Location.setHash, .{}); pub const pathname = bridge.accessor(Location.getPathname, null, .{}); pub const hostname = bridge.accessor(Location.getHostname, null, .{}); pub const host = bridge.accessor(Location.getHost, null, .{}); From ee7852665e1fd50648696c09551d9674233c1d56 Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Mon, 8 Dec 2025 05:16:47 -0800 Subject: [PATCH 184/219] fix GPL headers --- .../NavigationCurrentEntryChangeEvent.zig | 18 ++++++++++++++++++ .../navigation/NavigationEventTarget.zig | 18 ++++++++++++++++++ .../navigation/NavigationHistoryEntry.zig | 18 ++++++++++++++++++ 3 files changed, 54 insertions(+) diff --git a/src/browser/webapi/event/NavigationCurrentEntryChangeEvent.zig b/src/browser/webapi/event/NavigationCurrentEntryChangeEvent.zig index 4d0166cfa..ead742e31 100644 --- a/src/browser/webapi/event/NavigationCurrentEntryChangeEvent.zig +++ b/src/browser/webapi/event/NavigationCurrentEntryChangeEvent.zig @@ -1,3 +1,21 @@ +// Copyright (C) 2023-2024 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 Event = @import("../Event.zig"); const Page = @import("../../Page.zig"); diff --git a/src/browser/webapi/navigation/NavigationEventTarget.zig b/src/browser/webapi/navigation/NavigationEventTarget.zig index d262d3b49..1e9b0bd48 100644 --- a/src/browser/webapi/navigation/NavigationEventTarget.zig +++ b/src/browser/webapi/navigation/NavigationEventTarget.zig @@ -1,3 +1,21 @@ +// Copyright (C) 2023-2024 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 EventTarget = @import("../EventTarget.zig"); const js = @import("../../js/js.zig"); diff --git a/src/browser/webapi/navigation/NavigationHistoryEntry.zig b/src/browser/webapi/navigation/NavigationHistoryEntry.zig index 3ef16bdce..86dc295c7 100644 --- a/src/browser/webapi/navigation/NavigationHistoryEntry.zig +++ b/src/browser/webapi/navigation/NavigationHistoryEntry.zig @@ -1,3 +1,21 @@ +// Copyright (C) 2023-2024 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 URL = @import("../URL.zig"); const EventTarget = @import("../EventTarget.zig"); From 01d71323fcaf375e090d894499bb15b8cb73a7ea Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Mon, 8 Dec 2025 05:16:48 -0800 Subject: [PATCH 185/219] complete History impl backed by Navigation --- src/browser/Page.zig | 8 +- src/browser/Session.zig | 3 + src/browser/js/bridge.zig | 1 + src/browser/webapi/Event.zig | 1 + src/browser/webapi/History.zig | 92 ++++++++++++---------- src/browser/webapi/Window.zig | 21 ++++- src/browser/webapi/event/PopStateEvent.zig | 72 +++++++++++++++++ 7 files changed, 150 insertions(+), 48 deletions(-) create mode 100644 src/browser/webapi/event/PopStateEvent.zig diff --git a/src/browser/Page.zig b/src/browser/Page.zig index ce3620fa0..75b644135 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -34,7 +34,6 @@ const Mime = @import("Mime.zig"); const Factory = @import("Factory.zig"); const Session = @import("Session.zig"); const Scheduler = @import("Scheduler.zig"); -const History = @import("webapi/History.zig"); const EventManager = @import("EventManager.zig"); const ScriptManager = @import("ScriptManager.zig"); @@ -211,7 +210,6 @@ fn reset(self: *Page, comptime initializing: bool) !void { self.window = try self._factory.eventTarget(Window{ ._document = self.document, ._storage_bucket = storage_bucket, - ._history = History.init(self), ._performance = Performance.init(), ._proto = undefined, ._location = &default_location, @@ -1903,6 +1901,12 @@ const IdleNotification = union(enum) { } }; +pub fn isSameOrigin(self: *const Page, url: [:0]const u8) !bool { + const URLRaw = @import("URL.zig"); + const current_origin = (try URLRaw.getOrigin(self.arena, self.url)) orelse return false; + return std.mem.startsWith(u8, url, current_origin); +} + pub const NavigateReason = enum { anchor, address_bar, diff --git a/src/browser/Session.zig b/src/browser/Session.zig index d8b4f0d1b..5f25ae857 100644 --- a/src/browser/Session.zig +++ b/src/browser/Session.zig @@ -23,6 +23,7 @@ const log = @import("../log.zig"); const js = @import("js/js.zig"); const storage = @import("webapi/storage/storage.zig"); const Navigation = @import("webapi/navigation/Navigation.zig"); +const History = @import("webapi/History.zig"); const Page = @import("Page.zig"); const Browser = @import("Browser.zig"); @@ -55,6 +56,7 @@ executor: js.ExecutionWorld, cookie_jar: storage.Cookie.Jar, storage_shed: storage.Shed, +history: History, navigation: Navigation, page: ?*Page = null, @@ -80,6 +82,7 @@ pub fn init(self: *Session, browser: *Browser) !void { .arena = session_allocator, .cookie_jar = storage.Cookie.Jar.init(allocator), .navigation = Navigation.init(session_allocator), + .history = .{}, .transfer_arena = browser.transfer_arena.allocator(), }; } diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index 5fccd42c7..3358cc535 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -569,6 +569,7 @@ pub const JsApis = flattenTypes(&.{ @import("../webapi/event/ProgressEvent.zig"), @import("../webapi/event/NavigationCurrentEntryChangeEvent.zig"), @import("../webapi/event/PageTransitionEvent.zig"), + @import("../webapi/event/PopStateEvent.zig"), @import("../webapi/MessageChannel.zig"), @import("../webapi/MessagePort.zig"), @import("../webapi/media/MediaError.zig"), diff --git a/src/browser/webapi/Event.zig b/src/browser/webapi/Event.zig index 11da60fdb..0a49655b3 100644 --- a/src/browser/webapi/Event.zig +++ b/src/browser/webapi/Event.zig @@ -58,6 +58,7 @@ pub const Type = union(enum) { composition_event: *@import("event/CompositionEvent.zig"), navigation_current_entry_change_event: *@import("event/NavigationCurrentEntryChangeEvent.zig"), page_transition_event: *@import("event/PageTransitionEvent.zig"), + pop_state_event: *@import("event/PopStateEvent.zig"), }; const Options = struct { diff --git a/src/browser/webapi/History.zig b/src/browser/webapi/History.zig index d80fe3ba7..214f7230e 100644 --- a/src/browser/webapi/History.zig +++ b/src/browser/webapi/History.zig @@ -20,67 +20,75 @@ const std = @import("std"); const js = @import("../js/js.zig"); const Page = @import("../Page.zig"); +const PopStateEvent = @import("event/PopStateEvent.zig"); const History = @This(); -_page: *Page, -_length: u32 = 1, -_state: ?js.Object = null, - -pub fn init(page: *Page) History { - return .{ - ._page = page, - }; +pub fn getLength(_: *const History, page: *Page) u32 { + return @intCast(page._session.navigation._entries.items.len); } -pub fn deinit(self: *History) void { - if (self._state) |state| { - js.q.JS_FreeValue(self._page.js.ctx, state.value); - } +pub fn getState(_: *const History, page: *Page) !?js.Value { + if (page._session.navigation.getCurrentEntry()._state.value) |state| { + const value = try js.Value.fromJson(page.js, state); + return value; + } else return null; } -pub fn getLength(self: *const History) u32 { - return self._length; -} +pub fn pushState(_: *History, state: js.Object, _: []const u8, _url: ?[]const u8, page: *Page) !void { + const arena = page._session.arena; + const url = if (_url) |u| try arena.dupeZ(u8, u) else try arena.dupeZ(u8, page.url); -pub fn getState(self: *const History) ?js.Object { - return self._state; + const json = state.toJson(arena) catch return error.DateClone; + _ = try page._session.navigation.pushEntry(url, .{ .source = .history, .value = json }, page, true); } -pub fn pushState(self: *History, state: js.Object, _title: []const u8, url: ?[]const u8, page: *Page) !void { - _ = _title; // title is ignored in modern browsers - _ = url; // For minimal implementation, we don't actually navigate - _ = page; +pub fn replaceState(_: *History, state: js.Object, _: []const u8, _url: ?[]const u8, page: *Page) !void { + const arena = page._session.arena; + const url = if (_url) |u| try arena.dupeZ(u8, u) else try arena.dupeZ(u8, page.url); - self._state = try state.persist(); - self._length += 1; + const json = state.toJson(arena) catch return error.DateClone; + _ = try page._session.navigation.replaceEntry(url, .{ .source = .history, .value = json }, page, true); } -pub fn replaceState(self: *History, state: js.Object, _title: []const u8, url: ?[]const u8, page: *Page) !void { - _ = _title; - _ = url; - _ = page; - self._state = try state.persist(); - // Note: replaceState doesn't change length +fn goInner(delta: i32, page: *Page) !void { + // 0 behaves the same as no argument, both reloadig the page. + + const current = page._session.navigation._index; + const index_s: i64 = @intCast(@as(i64, @intCast(current)) + @as(i64, @intCast(delta))); + if (index_s < 0 or index_s > page._session.navigation._entries.items.len - 1) { + return; + } + + const index = @as(usize, @intCast(index_s)); + const entry = page._session.navigation._entries.items[index]; + + if (entry._url) |url| { + if (try page.isSameOrigin(url)) { + const event = try PopStateEvent.init("popstate", .{ .state = entry._state.value }, page); + + try page._event_manager.dispatchWithFunction( + page.window.asEventTarget(), + event.asEvent(), + page.window._on_popstate, + .{ .context = "Pop State" }, + ); + } + } + + _ = try page._session.navigation.navigateInner(entry._url, .{ .traverse = index }, page); } -pub fn back(self: *History, page: *Page) void { - _ = self; - _ = page; - // Minimal implementation: no-op +pub fn back(_: *History, page: *Page) !void { + try goInner(-1, page); } -pub fn forward(self: *History, page: *Page) void { - _ = self; - _ = page; - // Minimal implementation: no-op +pub fn forward(_: *History, page: *Page) !void { + try goInner(1, page); } -pub fn go(self: *History, delta: i32, page: *Page) void { - _ = self; - _ = delta; - _ = page; - // Minimal implementation: no-op +pub fn go(_: *History, delta: ?i32, page: *Page) !void { + try goInner(delta orelse 0, page); } pub const JsApi = struct { diff --git a/src/browser/webapi/Window.zig b/src/browser/webapi/Window.zig index d6b83bb40..72db12651 100644 --- a/src/browser/webapi/Window.zig +++ b/src/browser/webapi/Window.zig @@ -52,10 +52,10 @@ _console: Console = .init, _navigator: Navigator = .init, _screen: Screen = .init, _performance: Performance, -_history: History, _storage_bucket: *storage.Bucket, _on_load: ?js.Function = null, _on_pageshow: ?js.Function = null, +_on_popstate: ?js.Function = null, _on_error: ?js.Function = null, // TODO: invoke on error? _on_unhandled_rejection: ?js.Function = null, // TODO: invoke on error _location: *Location, @@ -115,8 +115,8 @@ pub fn getLocation(self: *const Window) *Location { return self._location; } -pub fn getHistory(self: *Window) *History { - return &self._history; +pub fn getHistory(_: *Window, page: *Page) *History { + return &page._session.history; } pub fn getNavigation(_: *Window, page: *Page) *Navigation { @@ -151,6 +151,18 @@ pub fn setOnPageShow(self: *Window, cb_: ?js.Function) !void { } } +pub fn getOnPopState(self: *const Window) ?js.Function { + return self._on_popstate; +} + +pub fn setOnPopState(self: *Window, cb_: ?js.Function) !void { + if (cb_) |cb| { + self._on_popstate = cb; + } else { + self._on_popstate = null; + } +} + pub fn getOnError(self: *const Window) ?js.Function { return self._on_error; } @@ -504,13 +516,14 @@ pub const JsApi = struct { pub const sessionStorage = bridge.accessor(Window.getSessionStorage, null, .{ .cache = "sessionStorage" }); pub const document = bridge.accessor(Window.getDocument, null, .{ .cache = "document" }); pub const location = bridge.accessor(Window.getLocation, null, .{ .cache = "location" }); - pub const history = bridge.accessor(Window.getHistory, null, .{ .cache = "history" }); + pub const history = bridge.accessor(Window.getHistory, null, .{}); pub const navigation = bridge.accessor(Window.getNavigation, null, .{}); pub const crypto = bridge.accessor(Window.getCrypto, null, .{ .cache = "crypto" }); pub const CSS = bridge.accessor(Window.getCSS, null, .{ .cache = "CSS" }); pub const customElements = bridge.accessor(Window.getCustomElements, null, .{ .cache = "customElements" }); pub const onload = bridge.accessor(Window.getOnLoad, Window.setOnLoad, .{}); pub const onpageshow = bridge.accessor(Window.getOnPageShow, Window.setOnPageShow, .{}); + pub const onpopstate = bridge.accessor(Window.getOnPopState, Window.setOnPopState, .{}); pub const onerror = bridge.accessor(Window.getOnError, Window.getOnError, .{}); pub const onunhandledrejection = bridge.accessor(Window.getOnUnhandledRejection, Window.setOnUnhandledRejection, .{}); pub const fetch = bridge.function(Window.fetch, .{}); diff --git a/src/browser/webapi/event/PopStateEvent.zig b/src/browser/webapi/event/PopStateEvent.zig new file mode 100644 index 000000000..f6d7ce0f6 --- /dev/null +++ b/src/browser/webapi/event/PopStateEvent.zig @@ -0,0 +1,72 @@ +// Copyright (C) 2023-2024 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 Window = @import("../html/window.zig").Window; +const Event = @import("../Event.zig"); +const js = @import("../../js/js.zig"); +const Page = @import("../../Page.zig"); + +// https://developer.mozilla.org/en-US/docs/Web/API/PopStateEvent +const PopStateEvent = @This(); + +const EventOptions = struct { + state: ?[]const u8 = null, +}; + +_proto: *Event, +_state: ?[]const u8, + +pub fn init(typ: []const u8, _options: ?EventOptions, page: *Page) !*PopStateEvent { + const options = _options orelse EventOptions{}; + + return page._factory.event(typ, PopStateEvent{ + ._proto = undefined, + ._state = options.state, + }); +} + +pub fn asEvent(self: *PopStateEvent) *Event { + return self._proto; +} + +pub fn getState(self: *PopStateEvent, page: *Page) !?js.Value { + if (self._state == null) return null; + + const value = try js.Value.fromJson(page.js, self._state.?); + return value; +} + +pub fn getUAVisualTransition(_: *PopStateEvent) bool { + // Not currently supported so we always return false; + return false; +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(PopStateEvent); + + pub const Meta = struct { + pub const name = "PopStateEvent"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; + + pub const constructor = bridge.constructor(PopStateEvent.init, .{}); + pub const state = bridge.accessor(PopStateEvent.getState, null, .{}); + pub const hasUAVisualTransition = bridge.accessor(PopStateEvent.getUAVisualTransition, null, .{}); +}; From ac85341cab9301d4260fd8246862976e1dcf0292 Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Mon, 8 Dec 2025 05:16:48 -0800 Subject: [PATCH 186/219] add NavigationKind to navigate --- src/browser/Page.zig | 7 +++-- src/browser/Session.zig | 6 +++- src/browser/webapi/Location.zig | 2 +- src/browser/webapi/navigation/Navigation.zig | 29 ++++++++++++-------- src/cdp/domains/page.zig | 2 +- src/cdp/domains/target.zig | 8 ++++-- src/cdp/testing.zig | 4 +-- src/lightpanda.zig | 2 +- src/main_legacy_test.zig | 2 +- src/main_wpt.zig | 2 +- src/testing.zig | 4 +-- 11 files changed, 42 insertions(+), 26 deletions(-) diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 75b644135..0fd2c7508 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -58,6 +58,7 @@ const IntersectionObserver = @import("webapi/IntersectionObserver.zig"); const CustomElementDefinition = @import("webapi/CustomElementDefinition.zig"); const storage = @import("webapi/storage/storage.zig"); const PageTransitionEvent = @import("webapi/event/PageTransitionEvent.zig"); +const NavigationKind = @import("webapi/navigation/root.zig").NavigationKind; const timestamp = @import("../datetime.zig").timestamp; const milliTimestamp = @import("../datetime.zig").milliTimestamp; @@ -270,7 +271,7 @@ fn registerBackgroundTasks(self: *Page) !void { }.runMessageLoop, 250, .{ .name = "page.messageLoop" }); } -pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !void { +pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts, kind: NavigationKind) !void { const session = self._session; const resolved_url = try URL.resolve( @@ -292,7 +293,7 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi self.window._location = try Location.init(self.url, self); self.document._location = self.window._location; - try session.navigation.updateEntries("", .{ .push = null }, self, true); + try session.navigation.updateEntries(resolved_url, kind, self, true); return; } } @@ -353,6 +354,8 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi .timestamp = timestamp(.monotonic), }); + session.navigation._current_navigation_kind = kind; + http_client.request(.{ .ctx = self, .url = self.url, diff --git a/src/browser/Session.zig b/src/browser/Session.zig index 5f25ae857..3c599c393 100644 --- a/src/browser/Session.zig +++ b/src/browser/Session.zig @@ -191,7 +191,11 @@ fn processQueuedNavigation(self: *Session) !bool { return err; }; - page.navigate(qn.url, qn.opts) catch |err| { + page.navigate( + qn.url, + qn.opts, + self.navigation._current_navigation_kind orelse .{ .push = null }, + ) catch |err| { log.err(.browser, "queued navigation error", .{ .err = err, .url = qn.url }); return err; }; diff --git a/src/browser/webapi/Location.zig b/src/browser/webapi/Location.zig index 205d1732e..c2c2b1a8f 100644 --- a/src/browser/webapi/Location.zig +++ b/src/browser/webapi/Location.zig @@ -81,7 +81,7 @@ pub fn setHash(_: *const Location, hash: []const u8, page: *Page) !void { }; const duped_hash = try page.arena.dupeZ(u8, normalized_hash); - return page.navigate(duped_hash, .{ .reason = .script }); + return page.navigate(duped_hash, .{ .reason = .script }, .{ .replace = null }); } pub fn toString(self: *const Location, page: *const Page) ![:0]const u8 { diff --git a/src/browser/webapi/navigation/Navigation.zig b/src/browser/webapi/navigation/Navigation.zig index e19e8b03f..2f2b284fc 100644 --- a/src/browser/webapi/navigation/Navigation.zig +++ b/src/browser/webapi/navigation/Navigation.zig @@ -92,7 +92,6 @@ pub fn back(self: *Navigation, page: *Page) !NavigationReturn { const new_index = self._index - 1; const next_entry = self._entries.items[new_index]; - self._index = new_index; return self.navigateInner(next_entry._url, .{ .traverse = new_index }, page); } @@ -108,7 +107,6 @@ pub fn forward(self: *Navigation, page: *Page) !NavigationReturn { const new_index = self._index + 1; const next_entry = self._entries.items[new_index]; - self._index = new_index; return self.navigateInner(next_entry._url, .{ .traverse = new_index }, page); } @@ -132,7 +130,10 @@ pub fn updateEntries(self: *Navigation, url: [:0]const u8, kind: NavigationKind, // This is only really safe to run in the `pageDoneCallback` where we can guarantee that the URL and NavigationKind are correct. pub fn processNavigation(self: *Navigation, page: *Page) !void { const url = page.url; + const kind: NavigationKind = self._current_navigation_kind orelse .{ .push = null }; + defer self._current_navigation_kind = null; + try self.updateEntries(url, kind, page, false); } @@ -247,9 +248,11 @@ pub fn navigateInner( const committed = try page.js.createPromiseResolver(.page); const finished = try page.js.createPromiseResolver(.page); - const new_url = try URL.resolve(arena, url, page.url, .{}); + const new_url = try URL.resolve(arena, page.url, url, .{}); const is_same_document = URL.eqlDocument(new_url, page.url); + const previous = self.getCurrentEntry(); + switch (kind) { .push => |state| { if (is_same_document) { @@ -261,8 +264,7 @@ pub fn navigateInner( _ = try self.pushEntry(url, .{ .source = .navigation, .value = state }, page, true); } else { - // try page.navigate(url, .{ .reason = .navigation }, kind); - try page.navigate(url, .{ .reason = .navigation }); + try page.navigate(url, .{ .reason = .navigation }, kind); } }, .replace => |state| { @@ -275,8 +277,7 @@ pub fn navigateInner( _ = try self.replaceEntry(url, .{ .source = .navigation, .value = state }, page, true); } else { - // try page.navigate(url, .{ .reason = .navigation }, kind); - try page.navigate(url, .{ .reason = .navigation }); + try page.navigate(url, .{ .reason = .navigation }, kind); } }, .traverse => |index| { @@ -289,16 +290,22 @@ pub fn navigateInner( // todo: Fire navigate event finished.resolve("navigation traverse", {}); } else { - // try page.navigate(url, .{ .reason = .navigation }, kind); - try page.navigate(url, .{ .reason = .navigation }); + try page.navigate(url, .{ .reason = .navigation }, kind); } }, .reload => { - // try page.navigate(url, .{ .reason = .navigation }, kind); - try page.navigate(url, .{ .reason = .navigation }); + try page.navigate(url, .{ .reason = .navigation }, kind); }, } + // If we haven't navigated off, let us fire off an a currententrychange. + const event = try NavigationCurrentEntryChangeEvent.init( + "currententrychange", + .{ .from = previous, .navigationType = @tagName(kind) }, + page, + ); + try self._proto.dispatch(.{ .currententrychange = event }, page); + return .{ .committed = committed.promise(), .finished = finished.promise(), diff --git a/src/cdp/domains/page.zig b/src/cdp/domains/page.zig index bc961fe16..c27d3de78 100644 --- a/src/cdp/domains/page.zig +++ b/src/cdp/domains/page.zig @@ -183,7 +183,7 @@ fn navigate(cmd: anytype) !void { try page.navigate(params.url, .{ .reason = .address_bar, .cdp_id = cmd.input.id, - }); + }, .{ .push = null }); } pub fn pageNavigate(arena: Allocator, bc: anytype, event: *const Notification.PageNavigate) !void { diff --git a/src/cdp/domains/target.zig b/src/cdp/domains/target.zig index b59285afb..d6f672e29 100644 --- a/src/cdp/domains/target.zig +++ b/src/cdp/domains/target.zig @@ -179,9 +179,11 @@ fn createTarget(cmd: anytype) !void { try doAttachtoTarget(cmd, target_id); } - try page.navigate(params.url, .{ - .reason = .address_bar, - }); + try page.navigate( + params.url, + .{ .reason = .address_bar }, + .{ .push = null }, + ); try cmd.sendResult(.{ .targetId = target_id, diff --git a/src/cdp/testing.zig b/src/cdp/testing.zig index 8d459a9f4..b575d4f66 100644 --- a/src/cdp/testing.zig +++ b/src/cdp/testing.zig @@ -127,10 +127,10 @@ const TestContext = struct { const full_url = try std.fmt.allocPrintSentinel( self.arena.allocator(), "http://127.0.0.1:9582/src/browser/tests/{s}", - .{ url }, + .{url}, 0, ); - try page.navigate(full_url, .{}); + try page.navigate(full_url, .{}, .{ .push = null }); bc.session.fetchWait(2000); } return bc; diff --git a/src/lightpanda.zig b/src/lightpanda.zig index c7e714b1e..55ec5df7f 100644 --- a/src/lightpanda.zig +++ b/src/lightpanda.zig @@ -60,7 +60,7 @@ pub fn fetch(app: *App, url: [:0]const u8, opts: FetchOpts) !void { // } // } - _ = try page.navigate(url, .{}); + _ = try page.navigate(url, .{}, .{ .push = null }); _ = session.fetchWait(opts.wait_ms); const writer = opts.writer orelse return; diff --git a/src/main_legacy_test.zig b/src/main_legacy_test.zig index 0c409c357..5b1cf8628 100644 --- a/src/main_legacy_test.zig +++ b/src/main_legacy_test.zig @@ -85,7 +85,7 @@ pub fn run(allocator: Allocator, file: []const u8, session: *lp.Session) !void { try_catch.init(js_context); defer try_catch.deinit(); - try page.navigate(url, .{}); + try page.navigate(url, .{}, .{ .push = null }); session.fetchWait(2000); page._session.browser.runMicrotasks(); diff --git a/src/main_wpt.zig b/src/main_wpt.zig index ff512e408..5e334a224 100644 --- a/src/main_wpt.zig +++ b/src/main_wpt.zig @@ -114,7 +114,7 @@ fn run( defer session.removePage(); const url = try std.fmt.allocPrintSentinel(arena, "http://localhost:9582/{s}", .{test_file}, 0); - try page.navigate(url, .{}); + try page.navigate(url, .{}, .{ .push = null }); _ = page.wait(2000); diff --git a/src/testing.zig b/src/testing.zig index 452ce812d..f3c1aeac3 100644 --- a/src/testing.zig +++ b/src/testing.zig @@ -403,7 +403,7 @@ fn runWebApiTest(test_file: [:0]const u8) !void { try_catch.init(js_context); defer try_catch.deinit(); - try page.navigate(url, .{}); + try page.navigate(url, .{}, .{ .push = null }); test_session.fetchWait(2000); page._session.browser.runMicrotasks(); @@ -428,7 +428,7 @@ pub fn pageTest(comptime test_file: []const u8) !*Page { 0, ); - try page.navigate(url, .{}); + try page.navigate(url, .{}, .{ .push = null }); test_session.fetchWait(2000); return page; } From 395f93240d0d878ff12c472cf4783f750880b2ae Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Tue, 9 Dec 2025 15:10:18 -0800 Subject: [PATCH 187/219] minor Navigation style changes --- src/browser/webapi/Window.zig | 2 +- .../webapi/event/NavigationCurrentEntryChangeEvent.zig | 2 +- src/browser/webapi/event/PageTransitionEvent.zig | 4 +--- src/browser/webapi/event/PopStateEvent.zig | 6 +++--- src/browser/webapi/navigation/Navigation.zig | 4 ++-- src/browser/webapi/navigation/NavigationEventTarget.zig | 2 +- src/browser/webapi/navigation/NavigationHistoryEntry.zig | 2 +- src/browser/webapi/navigation/root.zig | 2 +- 8 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/browser/webapi/Window.zig b/src/browser/webapi/Window.zig index 72db12651..792ca8ddf 100644 --- a/src/browser/webapi/Window.zig +++ b/src/browser/webapi/Window.zig @@ -24,7 +24,7 @@ const log = @import("../../log.zig"); const Page = @import("../Page.zig"); const Console = @import("Console.zig"); const History = @import("History.zig"); -const Navigation = @import("../webapi/navigation/Navigation.zig"); +const Navigation = @import("navigation/Navigation.zig"); const Crypto = @import("Crypto.zig"); const CSS = @import("CSS.zig"); const Navigator = @import("Navigator.zig"); diff --git a/src/browser/webapi/event/NavigationCurrentEntryChangeEvent.zig b/src/browser/webapi/event/NavigationCurrentEntryChangeEvent.zig index ead742e31..f5d2a9358 100644 --- a/src/browser/webapi/event/NavigationCurrentEntryChangeEvent.zig +++ b/src/browser/webapi/event/NavigationCurrentEntryChangeEvent.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/browser/webapi/event/PageTransitionEvent.zig b/src/browser/webapi/event/PageTransitionEvent.zig index f4cebf549..44b2c9e71 100644 --- a/src/browser/webapi/event/PageTransitionEvent.zig +++ b/src/browser/webapi/event/PageTransitionEvent.zig @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2024 Lightpanda (Selecy SAS) +// Copyright (C) 2023-2025 Lightpanda (Selecy SAS) // // Francis Bouvier // Pierre Tachoire @@ -47,8 +47,6 @@ pub fn getPersisted(self: *PageTransitionEvent) bool { return self._persisted; } -const PageTransitionKind = enum { show, hide }; - pub const JsApi = struct { pub const bridge = js.Bridge(PageTransitionEvent); diff --git a/src/browser/webapi/event/PopStateEvent.zig b/src/browser/webapi/event/PopStateEvent.zig index f6d7ce0f6..3ecffb997 100644 --- a/src/browser/webapi/event/PopStateEvent.zig +++ b/src/browser/webapi/event/PopStateEvent.zig @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2024 Lightpanda (Selecy SAS) +// Copyright (C) 2023-2025 Lightpanda (Selecy SAS) // // Francis Bouvier // Pierre Tachoire @@ -52,7 +52,7 @@ pub fn getState(self: *PopStateEvent, page: *Page) !?js.Value { return value; } -pub fn getUAVisualTransition(_: *PopStateEvent) bool { +pub fn hasUAVisualTransition(_: *PopStateEvent) bool { // Not currently supported so we always return false; return false; } @@ -68,5 +68,5 @@ pub const JsApi = struct { pub const constructor = bridge.constructor(PopStateEvent.init, .{}); pub const state = bridge.accessor(PopStateEvent.getState, null, .{}); - pub const hasUAVisualTransition = bridge.accessor(PopStateEvent.getUAVisualTransition, null, .{}); + pub const hasUAVisualTransition = bridge.accessor(PopStateEvent.hasUAVisualTransition, null, .{}); }; diff --git a/src/browser/webapi/navigation/Navigation.zig b/src/browser/webapi/navigation/Navigation.zig index 2f2b284fc..28016bd38 100644 --- a/src/browser/webapi/navigation/Navigation.zig +++ b/src/browser/webapi/navigation/Navigation.zig @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2024 Lightpanda (Selecy SAS) +// Copyright (C) 2023-2025 Lightpanda (Selecy SAS) // // Francis Bouvier // Pierre Tachoire @@ -42,7 +42,7 @@ _current_navigation_kind: ?NavigationKind = null, _index: usize = 0, // Need to be stable pointers, because Events can reference entries. -_entries: std.ArrayListUnmanaged(*NavigationHistoryEntry) = .empty, +_entries: std.ArrayList(*NavigationHistoryEntry) = .empty, _next_entry_id: usize = 0, pub fn init(arena: std.mem.Allocator) Navigation { diff --git a/src/browser/webapi/navigation/NavigationEventTarget.zig b/src/browser/webapi/navigation/NavigationEventTarget.zig index 1e9b0bd48..0872d7fb5 100644 --- a/src/browser/webapi/navigation/NavigationEventTarget.zig +++ b/src/browser/webapi/navigation/NavigationEventTarget.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/browser/webapi/navigation/NavigationHistoryEntry.zig b/src/browser/webapi/navigation/NavigationHistoryEntry.zig index 86dc295c7..0f3289b27 100644 --- a/src/browser/webapi/navigation/NavigationHistoryEntry.zig +++ b/src/browser/webapi/navigation/NavigationHistoryEntry.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/browser/webapi/navigation/root.zig b/src/browser/webapi/navigation/root.zig index e2a5c1733..d611cf9fc 100644 --- a/src/browser/webapi/navigation/root.zig +++ b/src/browser/webapi/navigation/root.zig @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2024 Lightpanda (Selecy SAS) +// Copyright (C) 2023-2025 Lightpanda (Selecy SAS) // // Francis Bouvier // Pierre Tachoire From 6534dc4c4ff4a7ec2600f5bdaf8f97c0834b3473 Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Tue, 9 Dec 2025 15:11:15 -0800 Subject: [PATCH 188/219] use Navigation ptr instead of fat copy --- src/browser/webapi/navigation/NavigationHistoryEntry.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browser/webapi/navigation/NavigationHistoryEntry.zig b/src/browser/webapi/navigation/NavigationHistoryEntry.zig index 0f3289b27..2411a7418 100644 --- a/src/browser/webapi/navigation/NavigationHistoryEntry.zig +++ b/src/browser/webapi/navigation/NavigationHistoryEntry.zig @@ -52,7 +52,7 @@ pub fn id(self: *const NavigationHistoryEntry) []const u8 { } pub fn index(self: *const NavigationHistoryEntry, page: *Page) i32 { - const navigation = page._session.navigation; + const navigation = &page._session.navigation; for (navigation._entries.items, 0..) |entry, i| { if (std.mem.eql(u8, entry._id, self._id)) { From 3662d1681ec2da68841df159bf1ca7474d76121a Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Tue, 9 Dec 2025 15:13:17 -0800 Subject: [PATCH 189/219] no need to run microtasks before onload --- src/browser/Page.zig | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 0fd2c7508..904f03b70 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -429,9 +429,6 @@ pub fn documentIsComplete(self: *Page) void { fn _documentIsComplete(self: *Page) !void { self.document._ready_state = .complete; - self._session.browser.runMicrotasks(); - self._session.browser.runMessageLoop(); - // dispatch window.load event const event = try Event.init("load", .{}, self); // this event is weird, it's dispatched directly on the window, but From ddb83cf9c52fc60168c63a61710a8668a644fefe Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Tue, 9 Dec 2025 15:17:54 -0800 Subject: [PATCH 190/219] add assert and note on getCurrentEntry --- src/browser/webapi/navigation/Navigation.zig | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/browser/webapi/navigation/Navigation.zig b/src/browser/webapi/navigation/Navigation.zig index 28016bd38..49de232e8 100644 --- a/src/browser/webapi/navigation/Navigation.zig +++ b/src/browser/webapi/navigation/Navigation.zig @@ -72,6 +72,10 @@ pub fn getCanGoForward(self: *const Navigation) bool { } pub fn getCurrentEntry(self: *Navigation) *NavigationHistoryEntry { + // This should never fail. An entry should always be created before + // we run the scripts on the page we are loading. + std.debug.assert(self._entries.items.len > 0); + return self._entries.items[self._index]; } From 7c9d7259e6cf6089eac1dfd4b181e7f86d09c64a Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Tue, 9 Dec 2025 15:47:09 -0800 Subject: [PATCH 191/219] add NavigationActivation --- src/browser/Page.zig | 5 +- src/browser/js/bridge.zig | 1 + src/browser/webapi/navigation/Navigation.zig | 34 +++++++++-- .../navigation/NavigationActivation.zig | 56 +++++++++++++++++++ src/browser/webapi/navigation/root.zig | 23 ++------ 5 files changed, 90 insertions(+), 29 deletions(-) create mode 100644 src/browser/webapi/navigation/NavigationActivation.zig diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 904f03b70..2b81cf970 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -530,7 +530,7 @@ fn pageDoneCallback(ctx: *anyopaque) !void { self.clearTransferArena(); //We need to handle different navigation types differently. - try self._session.navigation.processNavigation(self); + try self._session.navigation.commitNavigation(self); defer if (comptime IS_DEBUG) { log.debug(.page, "page.load.complete", .{ .url = self.url }); @@ -567,9 +567,6 @@ fn pageDoneCallback(ctx: *anyopaque) !void { }, else => unreachable, } - // We need to handle different navigation types differently. - // @ZIGDOM - // try self._session.navigation.processNavigation(self); } fn pageErrorCallback(ctx: *anyopaque, err: anyerror) void { diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index 3358cc535..b79622d06 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -605,4 +605,5 @@ pub const JsApis = flattenTypes(&.{ @import("../webapi/navigation/Navigation.zig"), @import("../webapi/navigation/NavigationEventTarget.zig"), @import("../webapi/navigation/NavigationHistoryEntry.zig"), + @import("../webapi/navigation/NavigationActivation.zig"), }); diff --git a/src/browser/webapi/navigation/Navigation.zig b/src/browser/webapi/navigation/Navigation.zig index 49de232e8..424c8d784 100644 --- a/src/browser/webapi/navigation/Navigation.zig +++ b/src/browser/webapi/navigation/Navigation.zig @@ -29,10 +29,11 @@ const EventTarget = @import("../EventTarget.zig"); const Navigation = @This(); const NavigationKind = @import("root.zig").NavigationKind; -const NavigationHistoryEntry = @import("NavigationHistoryEntry.zig"); +const NavigationActivation = @import("NavigationActivation.zig"); const NavigationTransition = @import("root.zig").NavigationTransition; const NavigationState = @import("root.zig").NavigationState; +const NavigationHistoryEntry = @import("NavigationHistoryEntry.zig"); const NavigationCurrentEntryChangeEvent = @import("../event/NavigationCurrentEntryChangeEvent.zig"); const NavigationEventTarget = @import("NavigationEventTarget.zig"); @@ -44,6 +45,7 @@ _index: usize = 0, // Need to be stable pointers, because Events can reference entries. _entries: std.ArrayList(*NavigationHistoryEntry) = .empty, _next_entry_id: usize = 0, +_activation: ?NavigationActivation = null, pub fn init(arena: std.mem.Allocator) Navigation { return Navigation{ ._arena = arena }; @@ -63,6 +65,10 @@ pub fn onNewPage(self: *Navigation, page: *Page) !void { ); } +pub fn getActivation(self: *const Navigation) ?NavigationActivation { + return self._activation; +} + pub fn getCanGoBack(self: *const Navigation) bool { return self._index > 0; } @@ -71,12 +77,18 @@ pub fn getCanGoForward(self: *const Navigation) bool { return self._entries.items.len > self._index + 1; } +pub fn getCurrentEntryOrNull(self: *Navigation) ?*NavigationHistoryEntry { + if (self._entries.items.len > self._index) { + return self._entries.items[self._index]; + } else return null; +} + pub fn getCurrentEntry(self: *Navigation) *NavigationHistoryEntry { // This should never fail. An entry should always be created before // we run the scripts on the page we are loading. std.debug.assert(self._entries.items.len > 0); - return self._entries.items[self._index]; + return self.getCurrentEntryOrNull().?; } pub fn getTransition(_: *const Navigation) ?NavigationTransition { @@ -117,8 +129,8 @@ pub fn forward(self: *Navigation, page: *Page) !NavigationReturn { pub fn updateEntries(self: *Navigation, url: [:0]const u8, kind: NavigationKind, page: *Page, dispatch: bool) !void { switch (kind) { - .replace => { - _ = try self.replaceEntry(url, .{ .source = .navigation, .value = null }, page, dispatch); + .replace => |state| { + _ = try self.replaceEntry(url, .{ .source = .navigation, .value = state }, page, dispatch); }, .push => |state| { _ = try self.pushEntry(url, .{ .source = .navigation, .value = state }, page, dispatch); @@ -131,14 +143,23 @@ pub fn updateEntries(self: *Navigation, url: [:0]const u8, kind: NavigationKind, } // This is for after true navigation processing, where we need to ensure that our entries are up to date. -// This is only really safe to run in the `pageDoneCallback` where we can guarantee that the URL and NavigationKind are correct. -pub fn processNavigation(self: *Navigation, page: *Page) !void { +// +// This is only really safe to run in the `pageDoneCallback` +// where we can guarantee that the URL and NavigationKind are correct. +pub fn commitNavigation(self: *Navigation, page: *Page) !void { const url = page.url; const kind: NavigationKind = self._current_navigation_kind orelse .{ .push = null }; defer self._current_navigation_kind = null; + const from_entry = self.getCurrentEntryOrNull(); try self.updateEntries(url, kind, page, false); + + self._activation = NavigationActivation{ + ._from = from_entry, + ._entry = self.getCurrentEntry(), + ._type = kind.toNavigationType(), + }; } /// Pushes an entry into the Navigation stack WITHOUT actually navigating to it. @@ -401,6 +422,7 @@ pub const JsApi = struct { pub var class_id: bridge.ClassId = undefined; }; + pub const activation = bridge.accessor(Navigation.getActivation, null, .{}); pub const canGoBack = bridge.accessor(Navigation.getCanGoBack, null, .{}); pub const canGoForward = bridge.accessor(Navigation.getCanGoForward, null, .{}); pub const currentEntry = bridge.accessor(Navigation.getCurrentEntry, null, .{}); diff --git a/src/browser/webapi/navigation/NavigationActivation.zig b/src/browser/webapi/navigation/NavigationActivation.zig new file mode 100644 index 000000000..3c161f9bf --- /dev/null +++ b/src/browser/webapi/navigation/NavigationActivation.zig @@ -0,0 +1,56 @@ +// 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 NavigationType = @import("root.zig").NavigationType; +const NavigationHistoryEntry = @import("NavigationHistoryEntry.zig"); + +// https://developer.mozilla.org/en-US/docs/Web/API/NavigationActivation +const NavigationActivation = @This(); + +_entry: *NavigationHistoryEntry, +_from: ?*NavigationHistoryEntry = null, +_type: NavigationType, + +pub fn getEntry(self: *const NavigationActivation) *NavigationHistoryEntry { + return self._entry; +} + +pub fn getFrom(self: *const NavigationActivation) ?*NavigationHistoryEntry { + return self._from; +} + +pub fn getNavigationType(self: *const NavigationActivation) []const u8 { + return @tagName(self._type); +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(NavigationActivation); + + pub const Meta = struct { + pub const name = "NavigationActivation"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; + + pub const entry = bridge.accessor(NavigationActivation.getEntry, null, .{}); + pub const from = bridge.accessor(NavigationActivation.getFrom, null, .{}); + pub const navigationType = bridge.accessor(NavigationActivation.getNavigationType, null, .{}); +}; diff --git a/src/browser/webapi/navigation/root.zig b/src/browser/webapi/navigation/root.zig index d611cf9fc..ef8b20e2c 100644 --- a/src/browser/webapi/navigation/root.zig +++ b/src/browser/webapi/navigation/root.zig @@ -38,6 +38,10 @@ pub const NavigationKind = union(NavigationType) { replace: ?[]const u8, traverse: usize, reload, + + pub fn toNavigationType(self: NavigationKind) NavigationType { + return std.meta.activeTag(self); + } }; pub const NavigationState = struct { @@ -45,25 +49,6 @@ pub const NavigationState = struct { value: ?[]const u8, }; -// https://developer.mozilla.org/en-US/docs/Web/API/NavigationActivation -pub const NavigationActivation = struct { - entry: NavigationHistoryEntry, - from: ?NavigationHistoryEntry = null, - type: NavigationType, - - pub fn get_entry(self: *const NavigationActivation) NavigationHistoryEntry { - return self.entry; - } - - pub fn get_from(self: *const NavigationActivation) ?NavigationHistoryEntry { - return self.from; - } - - pub fn get_navigationType(self: *const NavigationActivation) NavigationType { - return self.type; - } -}; - // https://developer.mozilla.org/en-US/docs/Web/API/NavigationTransition pub const NavigationTransition = struct { finished: js.Promise, From 02a0727870cfece2c75c294bc16d2fa28cd63922 Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Tue, 9 Dec 2025 15:51:06 -0800 Subject: [PATCH 192/219] eqlDocument slicing at hash --- src/browser/URL.zig | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/browser/URL.zig b/src/browser/URL.zig index 11739b070..4a399a3b1 100644 --- a/src/browser/URL.zig +++ b/src/browser/URL.zig @@ -272,14 +272,10 @@ pub fn getHost(raw: [:0]const u8) []const u8 { // Returns true if these two URLs point to the same document. pub fn eqlDocument(first: [:0]const u8, second: [:0]const u8) bool { - if (!std.mem.eql(u8, getProtocol(first), getProtocol(second))) return false; - if (!std.mem.eql(u8, getHost(first), getHost(second))) return false; - if (!std.mem.eql(u8, getPort(first), getPort(second))) return false; - if (!std.mem.eql(u8, getPathname(first), getPathname(second))) return false; - if (!std.mem.eql(u8, getSearch(first), getSearch(second))) return false; - // hashes are allowed to be different. - - return true; + // First '#' signifies the start of the fragment. + const first_hash_index = std.mem.indexOfScalar(u8, first, '#') orelse first.len; + const second_hash_index = std.mem.indexOfScalar(u8, second, '#') orelse second.len; + return std.mem.eql(u8, first[0..first_hash_index], second[0..second_hash_index]); } const KnownProtocol = enum { From 27e58181fba2e0132b010048715fd0c018105328 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Wed, 10 Dec 2025 15:28:24 +0800 Subject: [PATCH 193/219] Properly resolve inspector ObjectId back to a DOM Node Tweak element boundingRect and "renderer" based on what puppeteer needs. --- src/browser/Renderer.zig | 109 -------------------- src/browser/js/Inspector.zig | 18 +++- src/browser/webapi/Element.zig | 99 ++++++++++++------ src/browser/webapi/IntersectionObserver.zig | 45 ++------ src/cdp/domains/dom.zig | 19 ++-- 5 files changed, 101 insertions(+), 189 deletions(-) delete mode 100644 src/browser/Renderer.zig diff --git a/src/browser/Renderer.zig b/src/browser/Renderer.zig deleted file mode 100644 index 9a11dd32f..000000000 --- a/src/browser/Renderer.zig +++ /dev/null @@ -1,109 +0,0 @@ -// 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("netsurf.zig"); - -const Allocator = std.mem.Allocator; - -const Renderer = @This(); - -allocator: Allocator, - -// key is a @ptrFromInt of the element -// value is the index position -positions: std.AutoHashMapUnmanaged(u64, u32), - -// given an index, get the element -elements: std.ArrayListUnmanaged(u64), - -const Element = @import("dom/element.zig").Element; - -// we expect allocator to be an arena -pub fn init(allocator: Allocator) Renderer { - return .{ - .elements = .{}, - .positions = .{}, - .allocator = allocator, - }; -} - -// The DOMRect is always relative to the viewport, not the document the element belongs to. -// Element that are not part of the main document, either detached or in a shadow DOM should not call this function. -pub fn getRect(self: *Renderer, e: *parser.Element) !Element.DOMRect { - var elements = &self.elements; - const gop = try self.positions.getOrPut(self.allocator, @intFromPtr(e)); - var x: u32 = gop.value_ptr.*; - if (gop.found_existing == false) { - x = @intCast(elements.items.len); - try elements.append(self.allocator, @intFromPtr(e)); - gop.value_ptr.* = x; - } - - const _x: f64 = @floatFromInt(x); - const y: f64 = 0.0; - const w: f64 = 1.0; - const h: f64 = 1.0; - - return .{ - .x = _x, - .y = y, - .width = w, - .height = h, - .left = _x, - .top = y, - .right = _x + w, - .bottom = y + h, - }; -} - -pub fn boundingRect(self: *const Renderer) Element.DOMRect { - const x: f64 = 0.0; - const y: f64 = 0.0; - const w: f64 = @floatFromInt(self.width()); - const h: f64 = @floatFromInt(self.width()); - - return .{ - .x = x, - .y = y, - .width = w, - .height = h, - .left = x, - .top = y, - .right = x + w, - .bottom = y + h, - }; -} - -pub fn width(self: *const Renderer) u32 { - return @max(@as(u32, @intCast(self.elements.items.len)), 1); // At least 1 pixel even if empty -} - -pub fn height(_: *const Renderer) u32 { - return 1; -} - -pub fn getElementAtPosition(self: *const Renderer, x: i32, y: i32) ?*parser.Element { - if (y != 0 or x < 0) { - return null; - } - - const elements = self.elements.items; - return if (x < elements.len) @ptrFromInt(elements[@intCast(x)]) else null; -} diff --git a/src/browser/js/Inspector.zig b/src/browser/js/Inspector.zig index 04a8c5c8d..d53820d84 100644 --- a/src/browser/js/Inspector.zig +++ b/src/browser/js/Inspector.zig @@ -123,14 +123,24 @@ pub fn getRemoteObject( } // Gets a value by object ID regardless of which context it is in. -pub fn getNodePtr(self: *const Inspector, allocator: Allocator, object_id: []const u8) !?*anyopaque { +// Our TaggedAnyOpaque stores the "resolved" ptr value (the most specific _type, +// e.g. we store the ptr to the Div not the EventTarget). But, this is asking for +// the pointer to the Node, so we need to use the same resolution mechanism which +// is used when we're calling a function to turn the Div into a Node, which is +// what Context.typeTaggedAnyOpaque does. +pub fn getNodePtr(self: *const Inspector, allocator: Allocator, object_id: []const u8) !*anyopaque { const unwrapped = try self.session.unwrapObject(allocator, object_id); // The values context and groupId are not used here - const toa = getTaggedAnyOpaque(unwrapped.value) orelse return null; - if (toa.subtype == null or toa.subtype != .node) { + const js_val = unwrapped.value; + if (js_val.isObject() == false) { + std.debug.print("XX-0\n", .{}); return error.ObjectIdIsNotANode; } - return toa.value; + const Node = @import("../webapi/Node.zig"); + return Context.typeTaggedAnyOpaque(*Node, js_val.castTo(v8.Object)) catch { + std.debug.print("XX-1\n", .{}); + return error.ObjectIdIsNotANode; + }; } const NoopInspector = struct { diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index 8022cea8e..f38d21a00 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -755,9 +755,53 @@ pub fn checkVisibility(self: *Element, page: *Page) !bool { return true; } +fn getElementDimensions(self: *Element, page: *Page) !struct { width: f64, height: f64 } { + const style = try self.getStyle(page); + const decl = style.asCSSStyleDeclaration(); + var width = CSS.parseDimension(decl.getPropertyValue("width", page)) orelse 5.0; + var height = CSS.parseDimension(decl.getPropertyValue("height", page)) orelse 5.0; + + if (width == 5.0 or height == 5.0) { + const tag = self.getTag(); + + // Root containers get large default size to contain descendant positions. + // With calculateDocumentPosition using 10x multipliers per level, deep trees + // can position elements at y=millions, so we need a large container height. + // 100M pixels is plausible for very long documents. + if (tag == .html or tag == .body) { + if (width == 5.0) width = 1920.0; + if (height == 5.0) height = 100_000_000.0; + } else 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; + } + } + } + + return .{ .width = width, .height = height }; +} + +pub fn getClientWidth(self: *Element, page: *Page) !f64 { + if (!try self.checkVisibility(page)) { + return 0.0; + } + const dims = try self.getElementDimensions(page); + return dims.width; +} + +pub fn getClientHeight(self: *Element, page: *Page) !f64 { + if (!try self.checkVisibility(page)) { + return 0.0; + } + const dims = try self.getElementDimensions(page); + return dims.height; +} + pub fn getBoundingClientRect(self: *Element, page: *Page) !*DOMRect { - const is_visible = try self.checkVisibility(page); - if (!is_visible) { + if (!try self.checkVisibility(page)) { return page._factory.create(DOMRect{ ._x = 0.0, ._y = 0.0, @@ -771,38 +815,19 @@ pub fn getBoundingClientRect(self: *Element, page: *Page) !*DOMRect { } 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 dims = try self.getElementDimensions(page); const x: f64 = 0.0; const top = y; const left = x; - const right = x + width; - const bottom = y + height; + const right = x + dims.width; + const bottom = y + dims.height; return page._factory.create(DOMRect{ ._x = x, ._y = y, - ._width = width, - ._height = height, + ._width = dims.width, + ._height = dims.height, ._top = top, ._right = right, ._bottom = bottom, @@ -810,11 +835,19 @@ pub fn getBoundingClientRect(self: *Element, page: *Page) !*DOMRect { }); } +pub fn getClientRects(self: *Element, page: *Page) ![]DOMRect { + if (!try self.checkVisibility(page)) { + return &.{}; + } + const ptr = try self.getBoundingClientRect(page); + return ptr[0..1]; +} + // 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. +// uses exponential weighting (10x 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. @@ -825,15 +858,16 @@ pub fn getBoundingClientRect(self: *Element, page: *Page) !*DOMRect { // → 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) +//
→ position 10 (1 sibling at level 1, weighted by 10) +//

→ position 10 (0 siblings at level 2, parent has 10) //
// // // 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 +// - Keeps positions within reasonable bounds (10-level deep tree → ~10M pixels) +// - Not pixel-accurate, but sufficient for layout heuristics fn calculateDocumentPosition(node: *Node) f64 { var position: f64 = 0.0; var multiplier: f64 = 1.0; @@ -849,7 +883,7 @@ fn calculateDocumentPosition(node: *Node) f64 { } position += count * multiplier; - multiplier *= 1000.0; + multiplier *= 10.0; current = parent; } @@ -1145,6 +1179,9 @@ pub const JsApi = struct { pub const getAnimations = bridge.function(Element.getAnimations, .{}); pub const animate = bridge.function(Element.animate, .{}); pub const checkVisibility = bridge.function(Element.checkVisibility, .{}); + pub const clientWidth = bridge.accessor(Element.getClientWidth, null, .{}); + pub const clientHeight = bridge.accessor(Element.getClientHeight, null, .{}); + pub const getClientRects = bridge.function(Element.getClientRects, .{}); pub const getBoundingClientRect = bridge.function(Element.getBoundingClientRect, .{}); pub const getElementsByTagName = bridge.function(Element.getElementsByTagName, .{}); pub const getElementsByClassName = bridge.function(Element.getElementsByClassName, .{}); diff --git a/src/browser/webapi/IntersectionObserver.zig b/src/browser/webapi/IntersectionObserver.zig index c6940899e..4666e5266 100644 --- a/src/browser/webapi/IntersectionObserver.zig +++ b/src/browser/webapi/IntersectionObserver.zig @@ -142,7 +142,7 @@ fn calculateIntersection( ) !IntersectionData { const target_rect = try target.getBoundingClientRect(page); - // Use root element's rect or viewport (simplified: assume infinite viewport) + // Use root element's rect or viewport (simplified: assume 1920x1080) const root_rect = if (self._root) |root| try root.getBoundingClientRect(page) else @@ -158,46 +158,19 @@ fn calculateIntersection( ._left = 0.0, }); - // Calculate intersection rectangle - const left = @max(target_rect._left, root_rect._left); - const top = @max(target_rect._top, root_rect._top); - const right = @min(target_rect._right, root_rect._right); - const bottom = @min(target_rect._bottom, root_rect._bottom); + // For a headless browser without real layout, we treat all elements as fully visible. + // This avoids fingerprinting issues (massive viewports) and matches the behavior + // scripts expect when querying element visibility. + const is_intersecting = true; + const intersection_ratio: f64 = 1.0; - const is_intersecting = left < right and top < bottom; - - var intersection_rect: ?*DOMRect = null; - var intersection_ratio: f64 = 0.0; - - if (is_intersecting) { - const width = right - left; - const height = bottom - top; - const intersection_area = width * height; - const target_area = target_rect._width * target_rect._height; - - if (target_area > 0) { - intersection_ratio = intersection_area / target_area; - } - - intersection_rect = try page._factory.create(DOMRect{ - ._x = left, - ._y = top, - ._width = width, - ._height = height, - ._top = top, - ._right = right, - ._bottom = bottom, - ._left = left, - }); - } else { - // No intersection - reuse shared zero rect to avoid allocation - intersection_rect = &zero_rect; - } + // Intersection rect is the same as the target rect (fully visible) + const intersection_rect = target_rect; return .{ .is_intersecting = is_intersecting, .intersection_ratio = intersection_ratio, - .intersection_rect = intersection_rect.?, + .intersection_rect = intersection_rect, .bounding_client_rect = target_rect, .root_bounds = root_rect, }; diff --git a/src/cdp/domains/dom.zig b/src/cdp/domains/dom.zig index 791f94585..7fa429fb6 100644 --- a/src/cdp/domains/dom.zig +++ b/src/cdp/domains/dom.zig @@ -429,12 +429,13 @@ fn getBoxModel(cmd: anytype) !void { const rect = try element.getBoundingClientRect(page); const quad = rectToQuad(rect); + const zero = [_]f64{0.0} ** 8; return cmd.sendResult(.{ .model = BoxModel{ .content = quad, - .padding = quad, - .border = quad, - .margin = quad, + .padding = zero, + .border = zero, + .margin = zero, .width = @intFromFloat(rect._width), .height = @intFromFloat(rect._height), } }, .{}); @@ -649,11 +650,11 @@ test "cdp.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, + .content = Quad{ 0.0, 0.0, 5.0, 0.0, 5.0, 5.0, 0.0, 5.0 }, + .padding = Quad{ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 }, + .border = Quad{ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 }, + .margin = Quad{ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 }, + .width = 5, + .height = 5, } }, .{ .id = 5 }); } From 9c8299f13f0b0da1a2084d40b085745a489765a0 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Wed, 10 Dec 2025 16:39:27 +0800 Subject: [PATCH 194/219] Change to linear scaling for renderer. With the previous exponential approach, a deep site (the deepest element in amazon's product page is 36 levels deep) would be unrealistic. --- src/browser/webapi/Element.zig | 54 +++++++++++++++++----------------- src/cdp/domains/dom.zig | 2 +- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index f38d21a00..4c9287d54 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -765,8 +765,8 @@ fn getElementDimensions(self: *Element, page: *Page) !struct { width: f64, heigh const tag = self.getTag(); // Root containers get large default size to contain descendant positions. - // With calculateDocumentPosition using 10x multipliers per level, deep trees - // can position elements at y=millions, so we need a large container height. + // With calculateDocumentPosition using linear depth scaling (100px per level), + // even very deep trees (100 levels) stay within 10,000px. // 100M pixels is plausible for very long documents. if (tag == .html or tag == .body) { if (width == 5.0) width = 1920.0; @@ -843,51 +843,51 @@ pub fn getClientRects(self: *Element, page: *Page) ![]DOMRect { return ptr[0..1]; } -// Calculates a pseudo-position in the document using an efficient heuristic. +// Calculates a pseudo-position in the document using linear depth scaling. // -// 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 (10x 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. +// This approach uses a fixed pixel offset per depth level (100px) plus sibling +// position within that level. This keeps positions reasonable even for very deep +// DOM trees (e.g., Amazon product pages can be 36+ levels deep). // // 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 0 (depth 0) +//
→ position 100 (depth 1, 0 siblings) +// → position 200 (depth 2, 0 siblings) +// → position 201 (depth 2, 1 sibling) //
-//
→ position 10 (1 sibling at level 1, weighted by 10) -//

→ position 10 (0 siblings at level 2, parent has 10) +//
→ position 101 (depth 1, 1 sibling) +//

→ position 200 (depth 2, 0 siblings) //
// // // Trade-offs: -// - Much faster than full tree-walking for deep/large DOMs -// - Positions reflect document order and parent-child relationships -// - Keeps positions within reasonable bounds (10-level deep tree → ~10M pixels) -// - Not pixel-accurate, but sufficient for layout heuristics +// - O(depth) complexity, very fast +// - Linear scaling: 36 levels ≈ 3,600px, 100 levels ≈ 10,000px +// - Rough document order preserved (depth dominates, siblings differentiate) +// - Fits comfortably in realistic document heights fn calculateDocumentPosition(node: *Node) f64 { - var position: f64 = 0.0; - var multiplier: f64 = 1.0; + var depth: f64 = 0.0; + var sibling_offset: f64 = 0.0; var current = node; - while (current.parentNode()) |parent| { - var count: f64 = 0.0; + // Count siblings at the immediate level + if (current.parentNode()) |parent| { var sibling = parent.firstChild(); while (sibling) |s| { if (s == current) break; - count += 1.0; + sibling_offset += 1.0; sibling = s.nextSibling(); } + } - position += count * multiplier; - multiplier *= 10.0; + // Count depth from root + while (current.parentNode()) |parent| { + depth += 1.0; current = parent; } - return position; + // Each depth level = 100px, siblings add within that level + return (depth * 100.0) + sibling_offset; } const GetElementsByTagNameResult = union(enum) { diff --git a/src/cdp/domains/dom.zig b/src/cdp/domains/dom.zig index 7fa429fb6..4629bfc40 100644 --- a/src/cdp/domains/dom.zig +++ b/src/cdp/domains/dom.zig @@ -650,7 +650,7 @@ test "cdp.dom: getBoxModel" { .params = .{ .nodeId = 6 }, }); try ctx.expectSentResult(.{ .model = BoxModel{ - .content = Quad{ 0.0, 0.0, 5.0, 0.0, 5.0, 5.0, 0.0, 5.0 }, + .content = Quad{ 0.0, 200.0, 5.0, 200.0, 5.0, 205.0, 0.0, 205.0 }, .padding = Quad{ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 }, .border = Quad{ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 }, .margin = Quad{ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 }, From 159165490d4dbc6c4e1015a95809f585e9309810 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Wed, 10 Dec 2025 17:56:49 +0800 Subject: [PATCH 195/219] Allow event listener to remove itself or other pending listeners --- src/browser/EventManager.zig | 41 ++++++++- src/browser/js/Inspector.zig | 2 - src/browser/tests/events.html | 162 ++++++++++++++++++++++++++++++++-- 3 files changed, 195 insertions(+), 10 deletions(-) diff --git a/src/browser/EventManager.zig b/src/browser/EventManager.zig index 408d67446..0b4e3a6de 100644 --- a/src/browser/EventManager.zig +++ b/src/browser/EventManager.zig @@ -39,6 +39,7 @@ page: *Page, arena: Allocator, listener_pool: std.heap.MemoryPool(Listener), lookup: std.AutoHashMapUnmanaged(usize, std.DoublyLinkedList), +dispatch_depth: u32 = 0, pub fn init(page: *Page) EventManager { return .{ @@ -46,6 +47,7 @@ pub fn init(page: *Page) EventManager { .lookup = .{}, .arena = page.arena, .listener_pool = std.heap.MemoryPool(Listener).init(page.arena), + .dispatch_depth = 0, }; } @@ -247,12 +249,27 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe const page = self.page; const typ = event._type_string; + // Track that we're dispatching to prevent immediate removal + self.dispatch_depth += 1; + defer { + self.dispatch_depth -= 1; + // Clean up any marked listeners in this target's list after this phase + // We do this regardless of depth to handle cross-target removals correctly + self.cleanupMarkedListeners(list); + } + 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)); + + // Skip listeners that were marked for removal + if (listener.marked_for_removal) { + continue; + } + if (!listener.typ.eql(typ)) { continue; } @@ -310,8 +327,27 @@ fn dispatchAll(self: *EventManager, list: *std.DoublyLinkedList, current_target: } fn removeListener(self: *EventManager, list: *std.DoublyLinkedList, listener: *Listener) void { - list.remove(&listener.node); - self.listener_pool.destroy(listener); + if (self.dispatch_depth > 0) { + // We're in the middle of dispatching, just mark for removal + // This prevents invalidating the linked list during iteration + listener.marked_for_removal = true; + } else { + // Safe to remove immediately + list.remove(&listener.node); + self.listener_pool.destroy(listener); + } +} + +fn cleanupMarkedListeners(self: *EventManager, list: *std.DoublyLinkedList) void { + var node = list.first; + while (node) |n| { + node = n.next; + const listener: *Listener = @alignCast(@fieldParentPtr("node", n)); + if (listener.marked_for_removal) { + list.remove(&listener.node); + self.listener_pool.destroy(listener); + } + } } fn findListener(list: *const std.DoublyLinkedList, typ: []const u8, function: js.Function, capture: bool) ?*Listener { @@ -341,6 +377,7 @@ const Listener = struct { function: Function, signal: ?*@import("webapi/AbortSignal.zig") = null, node: std.DoublyLinkedList.Node, + marked_for_removal: bool = false, }; const Function = union(enum) { diff --git a/src/browser/js/Inspector.zig b/src/browser/js/Inspector.zig index d53820d84..1da5be808 100644 --- a/src/browser/js/Inspector.zig +++ b/src/browser/js/Inspector.zig @@ -133,12 +133,10 @@ pub fn getNodePtr(self: *const Inspector, allocator: Allocator, object_id: []con // The values context and groupId are not used here const js_val = unwrapped.value; if (js_val.isObject() == false) { - std.debug.print("XX-0\n", .{}); return error.ObjectIdIsNotANode; } const Node = @import("../webapi/Node.zig"); return Context.typeTaggedAnyOpaque(*Node, js_val.castTo(v8.Object)) catch { - std.debug.print("XX-1\n", .{}); return error.ObjectIdIsNotANode; }; } diff --git a/src/browser/tests/events.html b/src/browser/tests/events.html index a0459f403..a3682ae1b 100644 --- a/src/browser/tests/events.html +++ b/src/browser/tests/events.html @@ -319,14 +319,20 @@ + +
+ + +
+ + +
+ + +
+ From 61aca85632c2c7fd35f13cc564901daf9d085e36 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Wed, 10 Dec 2025 18:43:24 +0800 Subject: [PATCH 196/219] Pass Headers legacy tests --- src/browser/tests/legacy/fetch/headers.html | 19 ++- src/browser/tests/net/headers.html | 175 ++++++++++++++++++++ src/browser/webapi/KeyValueList.zig | 23 ++- src/browser/webapi/net/Headers.zig | 24 ++- src/browser/webapi/net/URLSearchParams.zig | 2 +- 5 files changed, 221 insertions(+), 22 deletions(-) diff --git a/src/browser/tests/legacy/fetch/headers.html b/src/browser/tests/legacy/fetch/headers.html index 57d6ce2ee..0626ad3d1 100644 --- a/src/browser/tests/legacy/fetch/headers.html +++ b/src/browser/tests/legacy/fetch/headers.html @@ -1,3 +1,4 @@ + + + + + + + diff --git a/src/browser/webapi/KeyValueList.zig b/src/browser/webapi/KeyValueList.zig index 4ec85f203..f2282fcac 100644 --- a/src/browser/webapi/KeyValueList.zig +++ b/src/browser/webapi/KeyValueList.zig @@ -33,6 +33,7 @@ pub fn registerTypes() []const type { }; } +const Normalizer = *const fn([]const u8, *Page) []const u8; pub const KeyValueList = @This(); _entries: std.ArrayListUnmanaged(Entry) = .empty, @@ -50,7 +51,7 @@ pub fn copy(arena: Allocator, original: KeyValueList) !KeyValueList { return list; } -pub fn fromJsObject(arena: Allocator, js_obj: js.Object) !KeyValueList { +pub fn fromJsObject(arena: Allocator, js_obj: js.Object, comptime normalizer: ?Normalizer, page: *Page) !KeyValueList { var it = js_obj.nameIterator(); var list = KeyValueList.init(); try list.ensureTotalCapacity(arena, it.count); @@ -58,9 +59,10 @@ pub fn fromJsObject(arena: Allocator, js_obj: js.Object) !KeyValueList { while (try it.next()) |name| { const js_value = try js_obj.get(name); const value = try js_value.toString(arena); + const normalized = if (comptime normalizer) |n| n(name, page) else name; - try list._entries.append(arena, .{ - .name = try String.init(arena, name, .{}), + list._entries.appendAssumeCapacity(.{ + .name = try String.init(arena, normalized, .{}), .value = try String.init(arena, value, .{}), }); } @@ -68,6 +70,21 @@ pub fn fromJsObject(arena: Allocator, js_obj: js.Object) !KeyValueList { return list; } +pub fn fromArray(arena: Allocator, kvs: []const [2][]const u8, comptime normalizer: ?Normalizer, page: *Page) !KeyValueList { + var list = KeyValueList.init(); + try list.ensureTotalCapacity(arena, kvs.len); + + for (kvs) |pair| { + const normalized = if (comptime normalizer) |n| n(pair[0], page) else pair[0]; + + list._entries.appendAssumeCapacity(.{ + .name = try String.init(arena, normalized, .{}), + .value = try String.init(arena, pair[1], .{}), + }); + } + return list; +} + pub const Entry = struct { name: String, value: String, diff --git a/src/browser/webapi/net/Headers.zig b/src/browser/webapi/net/Headers.zig index 633771791..8c80c5917 100644 --- a/src/browser/webapi/net/Headers.zig +++ b/src/browser/webapi/net/Headers.zig @@ -1,6 +1,7 @@ const std = @import("std"); const js = @import("../../js/js.zig"); const log = @import("../../../log.zig"); +const String = @import("../../../string.zig").String; const Page = @import("../../Page.zig"); const KeyValueList = @import("../KeyValueList.zig"); @@ -13,13 +14,15 @@ _list: KeyValueList, pub const InitOpts = union(enum) { obj: *Headers, + strings: []const [2][]const u8, js_obj: js.Object, }; pub fn init(opts_: ?InitOpts, page: *Page) !*Headers { const list = if (opts_) |opts| switch (opts) { .obj => |obj| try KeyValueList.copy(page.arena, obj._list), - .js_obj => |js_obj| try KeyValueList.fromJsObject(page.arena, js_obj), + .js_obj => |js_obj| try KeyValueList.fromJsObject(page.arena, js_obj, normalizeHeaderName, page), + .strings => |kvs| try KeyValueList.fromArray(page.arena, kvs, normalizeHeaderName, page), } else KeyValueList.init(); return page._factory.create(Headers{ @@ -27,12 +30,6 @@ pub fn init(opts_: ?InitOpts, page: *Page) !*Headers { }); } -// pub fn fromJsObject(js_obj: js.Object, page: *Page) !*Headers { -// return page._factory.create(Headers{ -// ._list = try KeyValueList.fromJsObject(page.arena, js_obj), -// }); -// } - pub fn append(self: *Headers, name: []const u8, value: []const u8, page: *Page) !void { const normalized_name = normalizeHeaderName(name, page); try self._list.append(page.arena, normalized_name, value); @@ -43,9 +40,17 @@ pub fn delete(self: *Headers, name: []const u8, page: *Page) void { self._list.delete(normalized_name, null); } -pub fn get(self: *const Headers, name: []const u8, page: *Page) ?[]const u8 { +pub fn get(self: *const Headers, name: []const u8, page: *Page) !?[]const u8 { const normalized_name = normalizeHeaderName(name, page); - return self._list.get(normalized_name); + const all_values = try self._list.getAll(normalized_name, page); + + if (all_values.len == 0) { + return null; + } + if (all_values.len == 1) { + return all_values[0]; + } + return try std.mem.join(page.call_arena, ", ", all_values); } pub fn has(self: *const Headers, name: []const u8, page: *Page) bool { @@ -97,6 +102,7 @@ fn normalizeHeaderName(name: []const u8, page: *Page) []const u8 { return std.ascii.lowerString(&page.buf, name); } + pub const JsApi = struct { pub const bridge = js.Bridge(Headers); diff --git a/src/browser/webapi/net/URLSearchParams.zig b/src/browser/webapi/net/URLSearchParams.zig index 73e5e1101..f3069531d 100644 --- a/src/browser/webapi/net/URLSearchParams.zig +++ b/src/browser/webapi/net/URLSearchParams.zig @@ -45,7 +45,7 @@ pub fn init(opts_: ?InitOpts, page: *Page) !*URLSearchParams { .query_string => |qs| break :blk try paramsFromString(arena, qs, &page.buf), .value => |js_val| { if (js_val.isObject()) { - break :blk try KeyValueList.fromJsObject(arena, js_val.toObject()); + break :blk try KeyValueList.fromJsObject(arena, js_val.toObject(), null, page); } if (js_val.isString()) { break :blk try paramsFromString(arena, try js_val.toString(arena), &page.buf); From a355d9e5178b6729f10879f6e321dc65914cf84d Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 11 Dec 2025 07:13:59 +0800 Subject: [PATCH 197/219] Handle infinitely recursive mutation observer FireFox hangs in these cases, but we'd rather handle it gracefully. --- src/browser/Page.zig | 14 ++++ .../tests/legacy/dom/mutation_observer.html | 76 ------------------- 2 files changed, 14 insertions(+), 76 deletions(-) delete mode 100644 src/browser/tests/legacy/dom/mutation_observer.html diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 48daeeb54..aab9b6f71 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -100,6 +100,7 @@ _script_manager: ScriptManager, // List of active MutationObservers _mutation_observers: std.ArrayList(*MutationObserver) = .{}, _mutation_delivery_scheduled: bool = false, +_mutation_delivery_depth: u32 = 0, // List of active IntersectionObservers _intersection_observers: std.ArrayList(*IntersectionObserver) = .{}, @@ -244,6 +245,7 @@ fn reset(self: *Page, comptime initializing: bool) !void { self._mutation_observers = .{}; self._mutation_delivery_scheduled = false; + self._mutation_delivery_depth = 0; self._intersection_observers = .{}; self._intersection_delivery_scheduled = false; self._customized_builtin_definitions = .{}; @@ -849,6 +851,18 @@ pub fn deliverMutations(self: *Page) void { } self._mutation_delivery_scheduled = false; + self._mutation_delivery_depth += 1; + defer if (!self._mutation_delivery_scheduled) { + // reset the depth once nothing is left to be scheduled + self._mutation_delivery_depth = 0; + }; + + if (self._mutation_delivery_depth > 100) { + log.err(.page, "page.MutationLimit", .{}); + self._mutation_delivery_depth = 0; + return; + } + // Iterate backwards to handle observers that disconnect during their callback var i = self._mutation_observers.items.len; while (i > 0) { diff --git a/src/browser/tests/legacy/dom/mutation_observer.html b/src/browser/tests/legacy/dom/mutation_observer.html deleted file mode 100644 index f67cb9247..000000000 --- a/src/browser/tests/legacy/dom/mutation_observer.html +++ /dev/null @@ -1,76 +0,0 @@ - -
-

And

-

And

-

And

- - - - - From 86ae0048256d43246961131d8b4fbfb923d082b5 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 11 Dec 2025 07:41:08 +0800 Subject: [PATCH 198/219] new Comment(?[]const u8) --- src/browser/tests/cdata/comment.html | 7 +++++++ src/browser/webapi/cdata/Comment.zig | 8 ++++++++ 2 files changed, 15 insertions(+) create mode 100644 src/browser/tests/cdata/comment.html diff --git a/src/browser/tests/cdata/comment.html b/src/browser/tests/cdata/comment.html new file mode 100644 index 000000000..453da3848 --- /dev/null +++ b/src/browser/tests/cdata/comment.html @@ -0,0 +1,7 @@ + + + + diff --git a/src/browser/webapi/cdata/Comment.zig b/src/browser/webapi/cdata/Comment.zig index f91faf895..dca43b25d 100644 --- a/src/browser/webapi/cdata/Comment.zig +++ b/src/browser/webapi/cdata/Comment.zig @@ -17,6 +17,7 @@ // along with this program. If not, see . const js = @import("../../js/js.zig"); +const Page = @import("../../Page.zig"); const CData = @import("../CData.zig"); @@ -24,6 +25,11 @@ const Comment = @This(); _proto: *CData, +pub fn init(content: ?[]const u8, page: *Page) !*Comment { + const node = try page.createComment(content orelse ""); + return node.as(Comment); +} + pub const JsApi = struct { pub const bridge = js.Bridge(Comment); @@ -32,4 +38,6 @@ pub const JsApi = struct { pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; }; + + pub const constructor = bridge.constructor(Comment.init, .{}); }; From b25e46de2e4f72895fe21659eca2ab162d7de47e Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 11 Dec 2025 11:48:09 +0800 Subject: [PATCH 199/219] zig fmt --- src/browser/webapi/KeyValueList.zig | 2 +- src/browser/webapi/Node.zig | 2 +- src/browser/webapi/net/Headers.zig | 1 - src/cdp/Node.zig | 6 +++--- src/cdp/domains/dom.zig | 2 +- src/cdp/testing.zig | 2 +- 6 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/browser/webapi/KeyValueList.zig b/src/browser/webapi/KeyValueList.zig index f2282fcac..d94eacb5f 100644 --- a/src/browser/webapi/KeyValueList.zig +++ b/src/browser/webapi/KeyValueList.zig @@ -33,7 +33,7 @@ pub fn registerTypes() []const type { }; } -const Normalizer = *const fn([]const u8, *Page) []const u8; +const Normalizer = *const fn ([]const u8, *Page) []const u8; pub const KeyValueList = @This(); _entries: std.ArrayListUnmanaged(Entry) = .empty, diff --git a/src/browser/webapi/Node.zig b/src/browser/webapi/Node.zig index 619518f74..f203c7ae5 100644 --- a/src/browser/webapi/Node.zig +++ b/src/browser/webapi/Node.zig @@ -777,7 +777,7 @@ pub const JsApi = struct { pub const DOCUMENT_POSITION_CONTAINED_BY = bridge.property(0x10); pub const DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC = bridge.property(0x20); - pub const nodeName = bridge.accessor(struct{ + pub const nodeName = bridge.accessor(struct { fn wrap(self: *const Node, page: *Page) []const u8 { return self.getNodeName(&page.buf); } diff --git a/src/browser/webapi/net/Headers.zig b/src/browser/webapi/net/Headers.zig index 8c80c5917..5c9626553 100644 --- a/src/browser/webapi/net/Headers.zig +++ b/src/browser/webapi/net/Headers.zig @@ -102,7 +102,6 @@ fn normalizeHeaderName(name: []const u8, page: *Page) []const u8 { return std.ascii.lowerString(&page.buf, name); } - pub const JsApi = struct { pub const bridge = js.Bridge(Headers); diff --git a/src/cdp/Node.zig b/src/cdp/Node.zig index ca7c43ca3..b4d6c42a0 100644 --- a/src/cdp/Node.zig +++ b/src/cdp/Node.zig @@ -307,7 +307,7 @@ pub const Writer = struct { try w.write(dom_node.getNodeName(&name_buf)); try w.objectField("nodeValue"); - try w.write(dom_node.getNodeValue() orelse ""); + try w.write(dom_node.getNodeValue() orelse ""); if (include_child_count) { try w.objectField("childNodeCount"); @@ -564,7 +564,7 @@ test "cdp Node: Writer" { .nodeId = 5, .localName = "a", .childNodeCount = 0, - .attributes = &.{"id", "a1"}, + .attributes = &.{ "id", "a1" }, .parentId = 4, }, .{ .nodeId = 6, @@ -576,7 +576,7 @@ test "cdp Node: Writer" { .localName = "a", .childNodeCount = 0, .parentId = 6, - .attributes = &.{"id", "a2"}, + .attributes = &.{ "id", "a2" }, }}, }, .{ .nodeId = 8, diff --git a/src/cdp/domains/dom.zig b/src/cdp/domains/dom.zig index 4629bfc40..ea0c81ef5 100644 --- a/src/cdp/domains/dom.zig +++ b/src/cdp/domains/dom.zig @@ -94,7 +94,7 @@ fn performSearch(cmd: anytype) !void { // dispatch setChildNodesEvents to inform the client of the subpart of node // tree covering the results. - try dispatchSetChildNodes(cmd, list._nodes); + try dispatchSetChildNodes(cmd, list._nodes); return cmd.sendResult(.{ .searchId = search.name, diff --git a/src/cdp/testing.zig b/src/cdp/testing.zig index 8d459a9f4..7d59a7c43 100644 --- a/src/cdp/testing.zig +++ b/src/cdp/testing.zig @@ -127,7 +127,7 @@ const TestContext = struct { const full_url = try std.fmt.allocPrintSentinel( self.arena.allocator(), "http://127.0.0.1:9582/src/browser/tests/{s}", - .{ url }, + .{url}, 0, ); try page.navigate(full_url, .{}); From 34f0857b4fc45fc6b7b59ccfb3a355c4c6873b97 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 11 Dec 2025 12:51:56 +0800 Subject: [PATCH 200/219] Element legacy test passing --- src/browser/tests/document/document.html | 77 ++++++++++++++++ src/browser/tests/element/attributes.html | 81 +++++++++++++++++ src/browser/tests/element/element.html | 90 ++++++++++++++++++- src/browser/tests/element/query_selector.html | 2 + .../tests/element/query_selector_all.html | 3 + src/browser/tests/legacy/dom/element.html | 41 +-------- src/browser/webapi/DOMException.zig | 2 +- src/browser/webapi/Document.zig | 31 +++++++ src/browser/webapi/Element.zig | 43 ++++++++- src/browser/webapi/collections/NodeList.zig | 1 + src/browser/webapi/element/Attribute.zig | 26 ++++++ src/browser/webapi/selector/Selector.zig | 3 +- 12 files changed, 356 insertions(+), 44 deletions(-) diff --git a/src/browser/tests/document/document.html b/src/browser/tests/document/document.html index 1138c132a..4ec114112 100644 --- a/src/browser/tests/document/document.html +++ b/src/browser/tests/document/document.html @@ -181,3 +181,80 @@ document.cookie = 'IgnoreMy=Ghost; HttpOnly'; testing.expectEqual('name=Oeschger; favorite_food=tripe', document.cookie); + + + + + + diff --git a/src/browser/tests/element/attributes.html b/src/browser/tests/element/attributes.html index e33af9ed1..56f6ef834 100644 --- a/src/browser/tests/element/attributes.html +++ b/src/browser/tests/element/attributes.html @@ -165,3 +165,84 @@ testing.expectEqual(false, div.hasAttributes()); } + + diff --git a/src/browser/tests/element/element.html b/src/browser/tests/element/element.html index f779c9cdb..2a7adbaf1 100644 --- a/src/browser/tests/element/element.html +++ b/src/browser/tests/element/element.html @@ -9,7 +9,7 @@ Span 1

Paragraph 2

-
+
+ + + + + + + + diff --git a/src/browser/tests/element/query_selector.html b/src/browser/tests/element/query_selector.html index cb753465a..9750bd1a5 100644 --- a/src/browser/tests/element/query_selector.html +++ b/src/browser/tests/element/query_selector.html @@ -10,6 +10,8 @@ - - @@ -338,4 +301,4 @@ const p = document.createElement('p'); p.textContent = 'XAnge\xa0Privacy'; testing.expectEqual('

XAnge Privacy

', p.outerHTML); - + --> diff --git a/src/browser/webapi/DOMException.zig b/src/browser/webapi/DOMException.zig index 72d795595..7ae241d2d 100644 --- a/src/browser/webapi/DOMException.zig +++ b/src/browser/webapi/DOMException.zig @@ -57,7 +57,7 @@ pub fn getName(self: *const DOMException) []const u8 { pub fn getMessage(self: *const DOMException) []const u8 { return switch (self._code) { .none => "", - .invalid_character_error => "Invalid Character", + .invalid_character_error => "Error: Invalid Character", .index_size_error => "IndexSizeError: Index or size is negative or greater than the allowed amount", .syntax_error => "Syntax Error", .not_supported => "Not Supported", diff --git a/src/browser/webapi/Document.zig b/src/browser/webapi/Document.zig index 1973b8f0e..a665849fe 100644 --- a/src/browser/webapi/Document.zig +++ b/src/browser/webapi/Document.zig @@ -125,6 +125,16 @@ pub fn createElementNS(_: *const Document, namespace: ?[]const u8, name: []const return node.as(Element); } +pub fn createAttribute(_: *const Document, name: []const u8, page: *Page) !?*Element.Attribute { + try Element.Attribute.validateAttributeName(name); + return page._factory.node(Element.Attribute{ + ._proto = undefined, + ._name = try page.dupeString(name), + ._value = "", + ._element = null, + }); +} + pub fn getElementById(self: *const Document, id_: ?[]const u8) ?*Element { const id = id_ orelse return null; return self._elements_by_id.get(id); @@ -317,6 +327,24 @@ pub fn importNode(_: *const Document, node: *Node, deep_: ?bool, page: *Page) !* return node.cloneNode(deep_, page); } +pub fn append(self: *Document, nodes: []const Node.NodeOrText, page: *Page) !void { + const parent = self.asNode(); + for (nodes) |node_or_text| { + const child = try node_or_text.toNode(page); + _ = try parent.appendChild(child, page); + } +} + +pub fn prepend(self: *Document, nodes: []const Node.NodeOrText, page: *Page) !void { + const parent = self.asNode(); + var i = nodes.len; + while (i > 0) { + i -= 1; + const child = try nodes[i].toNode(page); + _ = try parent.insertBefore(child, parent.firstChild(), page); + } +} + const ReadyState = enum { loading, interactive, @@ -360,6 +388,7 @@ pub const JsApi = struct { pub const createDocumentFragment = bridge.function(Document.createDocumentFragment, .{}); pub const createComment = bridge.function(Document.createComment, .{}); pub const createTextNode = bridge.function(Document.createTextNode, .{}); + pub const createAttribute = bridge.function(Document.createAttribute, .{ .dom_exception = true }); pub const createCDATASection = bridge.function(Document.createCDATASection, .{ .dom_exception = true }); pub const createRange = bridge.function(Document.createRange, .{}); pub const createEvent = bridge.function(Document.createEvent, .{ .dom_exception = true }); @@ -373,6 +402,8 @@ pub const JsApi = struct { pub const getElementsByName = bridge.function(Document.getElementsByName, .{}); pub const adoptNode = bridge.function(Document.adoptNode, .{ .dom_exception = true }); pub const importNode = bridge.function(Document.importNode, .{ .dom_exception = true }); + pub const append = bridge.function(Document.append, .{}); + pub const prepend = bridge.function(Document.prepend, .{}); pub const defaultView = bridge.accessor(struct { fn defaultView(_: *const Document, page: *Page) *@import("Window.zig") { diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index 4c9287d54..00b46cdf7 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -343,6 +343,14 @@ pub fn setId(self: *Element, value: []const u8, page: *Page) !void { return self.setAttributeSafe("id", value, page); } +pub fn getDir(self: *const Element) []const u8 { + return self.getAttributeSafe("dir") orelse ""; +} + +pub fn setDir(self: *Element, value: []const u8, page: *Page) !void { + return self.setAttributeSafe("dir", value, page); +} + pub fn getClassName(self: *const Element) []const u8 { return self.getAttributeSafe("class") orelse ""; } @@ -388,6 +396,7 @@ pub fn getAttributeNode(self: *Element, name: []const u8, page: *Page) !?*Attrib } pub fn setAttribute(self: *Element, name: []const u8, value: []const u8, page: *Page) !void { + try Attribute.validateAttributeName(name); const attributes = try self.getOrCreateAttributeList(page); _ = try attributes.put(name, value, self, page); } @@ -503,6 +512,7 @@ pub fn removeAttribute(self: *Element, name: []const u8, page: *Page) !void { } pub fn toggleAttribute(self: *Element, name: []const u8, force: ?bool, page: *Page) !bool { + try Attribute.validateAttributeName(name); const has = try self.hasAttribute(name, page); const should_add = force orelse !has; @@ -647,6 +657,27 @@ pub fn prepend(self: *Element, nodes: []const Node.NodeOrText, page: *Page) !voi } } +pub fn before(self: *Element, nodes: []const Node.NodeOrText, page: *Page) !void { + const node = self.asNode(); + const parent = node.parentNode() orelse return; + + for (nodes) |node_or_text| { + const child = try node_or_text.toNode(page); + _ = try parent.insertBefore(child, node, page); + } +} + +pub fn after(self: *Element, nodes: []const Node.NodeOrText, page: *Page) !void { + const node = self.asNode(); + const parent = node.parentNode() orelse return; + const next = node.nextSibling(); + + for (nodes) |node_or_text| { + const child = try node_or_text.toNode(page); + _ = try parent.insertBefore(child, next, page); + } +} + pub fn firstElementChild(self: *Element) ?*Element { var maybe_child = self.asNode().firstChild(); while (maybe_child) |child| { @@ -946,6 +977,10 @@ pub fn cloneElement(self: *Element, deep: bool, page: *Page) !*Node { return node; } +pub fn scrollIntoViewIfNeeded(_: *const Element, center_if_needed: ?bool) void { + _ = center_if_needed; +} + pub fn format(self: *Element, writer: *std.Io.Writer) !void { try writer.writeByte('<'); try writer.writeAll(self.getTagNameDump()); @@ -1136,6 +1171,7 @@ pub const JsApi = struct { pub const localName = bridge.accessor(Element.getLocalName, null, .{}); pub const id = bridge.accessor(Element.getId, Element.setId, .{}); + pub const dir = bridge.accessor(Element.getDir, Element.setDir, .{}); 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, .{}); @@ -1145,10 +1181,10 @@ pub const JsApi = struct { pub const hasAttributes = bridge.function(Element.hasAttributes, .{}); pub const getAttribute = bridge.function(Element.getAttribute, .{}); pub const getAttributeNode = bridge.function(Element.getAttributeNode, .{}); - pub const setAttribute = bridge.function(Element.setAttribute, .{}); + pub const setAttribute = bridge.function(Element.setAttribute, .{ .dom_exception = true }); pub const setAttributeNode = bridge.function(Element.setAttributeNode, .{}); pub const removeAttribute = bridge.function(Element.removeAttribute, .{}); - pub const toggleAttribute = bridge.function(Element.toggleAttribute, .{}); + pub const toggleAttribute = bridge.function(Element.toggleAttribute, .{ .dom_exception = true }); pub const getAttributeNames = bridge.function(Element.getAttributeNames, .{}); pub const removeAttributeNode = bridge.function(Element.removeAttributeNode, .{ .dom_exception = true }); pub const shadowRoot = bridge.accessor(Element.getShadowRoot, null, .{}); @@ -1167,6 +1203,8 @@ pub const JsApi = struct { pub const remove = bridge.function(Element.remove, .{}); pub const append = bridge.function(Element.append, .{}); pub const prepend = bridge.function(Element.prepend, .{}); + pub const before = bridge.function(Element.before, .{}); + pub const after = bridge.function(Element.after, .{}); pub const firstElementChild = bridge.accessor(Element.firstElementChild, null, .{}); pub const lastElementChild = bridge.accessor(Element.lastElementChild, null, .{}); pub const nextElementSibling = bridge.accessor(Element.nextElementSibling, null, .{}); @@ -1188,6 +1226,7 @@ pub const JsApi = struct { pub const children = bridge.accessor(Element.getChildren, null, .{}); pub const focus = bridge.function(Element.focus, .{}); pub const blur = bridge.function(Element.blur, .{}); + pub const scrollIntoViewIfNeeded = bridge.function(Element.scrollIntoViewIfNeeded, .{}); }; pub const Build = struct { diff --git a/src/browser/webapi/collections/NodeList.zig b/src/browser/webapi/collections/NodeList.zig index dae615098..8ee8b104c 100644 --- a/src/browser/webapi/collections/NodeList.zig +++ b/src/browser/webapi/collections/NodeList.zig @@ -111,6 +111,7 @@ pub const JsApi = struct { pub const length = bridge.accessor(NodeList.length, null, .{}); pub const @"[]" = bridge.indexed(NodeList.getAtIndex, .{ .null_as_undefined = true }); + pub const item = bridge.function(NodeList.getAtIndex, .{}); pub const keys = bridge.function(NodeList.keys, .{}); pub const values = bridge.function(NodeList.values, .{}); pub const entries = bridge.function(NodeList.entries, .{}); diff --git a/src/browser/webapi/element/Attribute.zig b/src/browser/webapi/element/Attribute.zig index b5d45a612..d7fa36e6e 100644 --- a/src/browser/webapi/element/Attribute.zig +++ b/src/browser/webapi/element/Attribute.zig @@ -343,6 +343,32 @@ fn shouldAddToIdMap(normalized_name: []const u8, element: *Element) bool { return node.isConnected(); } +pub fn validateAttributeName(name: []const u8) !void { + if (name.len == 0) { + return error.InvalidCharacterError; + } + + const first = name[0]; + if ((first >= '0' and first <= '9') or first == '-' or first == '.') { + return error.InvalidCharacterError; + } + + for (name) |c| { + if (c == 0 or c == '/' or c == '=' or c == '>' or std.ascii.isWhitespace(c)) { + return error.InvalidCharacterError; + } + + const is_valid = (c >= 'a' and c <= 'z') or + (c >= 'A' and c <= 'Z') or + (c >= '0' and c <= '9') or + c == '_' or c == '-' or c == '.' or c == ':'; + + if (!is_valid) { + return error.InvalidCharacterError; + } + } +} + pub fn normalizeNameForLookup(name: []const u8, page: *Page) ![]const u8 { if (!needsLowerCasing(name)) { return name; diff --git a/src/browser/webapi/selector/Selector.zig b/src/browser/webapi/selector/Selector.zig index 44d7c4387..6f0869eb3 100644 --- a/src/browser/webapi/selector/Selector.zig +++ b/src/browser/webapi/selector/Selector.zig @@ -38,7 +38,8 @@ pub fn querySelector(root: *Node, input: []const u8, page: *Page) !?*Node.Elemen if (first == .id) { const el = page.getElementByIdFromNode(root, first.id) orelse continue; // Check if the element is within the root subtree - if (root.contains(el.asNode())) { + const node = el.asNode(); + if (node != root and root.contains(node)) { return el; } continue; From bead805680eaa60a4a9109b0873b5b4be18a308a Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Tue, 9 Dec 2025 17:58:10 +0300 Subject: [PATCH 201/219] backport: Prefer BoringSSL as TLS backend --- .github/workflows/e2e-test.yml | 2 +- .gitmodules | 3 --- build.zig | 21 ++++++++++++++++++--- build.zig.zon | 4 ++++ 4 files changed, 23 insertions(+), 7 deletions(-) diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index 992c8b2a9..1b8b910b7 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -124,7 +124,7 @@ jobs: needs: zig-build-release env: - MAX_MEMORY: 27000 + MAX_MEMORY: 28000 MAX_AVG_DURATION: 23 LIGHTPANDA_DISABLE_TELEMETRY: true diff --git a/.gitmodules b/.gitmodules index 3358b9a3e..5462f8f0e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,9 +4,6 @@ [submodule "vendor/nghttp2"] path = vendor/nghttp2 url = https://github.com/nghttp2/nghttp2.git -[submodule "vendor/mbedtls"] - path = vendor/mbedtls - url = https://github.com/Mbed-TLS/mbedtls.git [submodule "vendor/zlib"] path = vendor/zlib url = https://github.com/madler/zlib.git diff --git a/build.zig b/build.zig index a273d81f9..2519ec097 100644 --- a/build.zig +++ b/build.zig @@ -433,13 +433,27 @@ fn addDependencies(b: *Build, mod: *Build.Module, opts: *Build.Step.Options) !vo mod.addCMacro("STDC_HEADERS", "1"); mod.addCMacro("TIME_WITH_SYS_TIME", "1"); mod.addCMacro("USE_NGHTTP2", "1"); - mod.addCMacro("USE_MBEDTLS", "1"); + mod.addCMacro("USE_OPENSSL", "1"); + mod.addCMacro("OPENSSL_IS_BORINGSSL", "1"); mod.addCMacro("USE_THREADS_POSIX", "1"); mod.addCMacro("USE_UNIX_SOCKETS", "1"); } try buildZlib(b, mod); try buildBrotli(b, mod); + const boringssl_dep = b.dependency("boringssl-zig", .{ + .target = target, + .optimize = mod.optimize.?, + .force_pic = true, + }); + + const ssl = boringssl_dep.artifact("ssl"); + ssl.bundle_ubsan_rt = false; + const crypto = boringssl_dep.artifact("crypto"); + crypto.bundle_ubsan_rt = false; + + mod.linkLibrary(ssl); + mod.linkLibrary(crypto); try buildMbedtls(b, mod); try buildNghttp2(b, mod); try buildCurl(b, mod); @@ -845,8 +859,9 @@ fn buildCurl(b: *Build, m: *Build.Module) !void { root ++ "lib/vauth/spnego_sspi.c", root ++ "lib/vauth/vauth.c", root ++ "lib/vtls/cipher_suite.c", - root ++ "lib/vtls/mbedtls.c", - root ++ "lib/vtls/mbedtls_threadlock.c", + root ++ "lib/vtls/openssl.c", + root ++ "lib/vtls/hostcheck.c", + root ++ "lib/vtls/keylog.c", root ++ "lib/vtls/vtls.c", root ++ "lib/vtls/vtls_scache.c", root ++ "lib/vtls/x509asn1.c", diff --git a/build.zig.zon b/build.zig.zon index 6d3b20617..cb0136209 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -9,5 +9,9 @@ .hash = "v8-0.0.0-xddH6wTgAwALFCYoZbUIqtsRyP6mr69N7aKT_cySHKN2", }, //.v8 = .{ .path = "../zig-v8-fork" } + .@"boringssl-zig" = .{ + .url = "git+https://github.com/Syndica/boringssl-zig.git#c53df00d06b02b755ad88bbf4d1202ed9687b096", + .hash = "boringssl-0.1.0-VtJeWehMAAA4RNnwRnzEvKcS9rjsR1QVRw1uJrwXxmVK", + }, }, } From 68763d9a30969e8da5965ff2d3579a5cda60b6ef Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 11 Dec 2025 15:23:39 +0800 Subject: [PATCH 202/219] speed up tests --- src/browser/Page.zig | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/browser/Page.zig b/src/browser/Page.zig index aab9b6f71..5a427dc30 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -659,10 +659,13 @@ fn _wait(self: *Page, wait_ms: u32) !Session.WaitResult { const ms = ms_to_next_task orelse blk: { if (wait_ms - ms_remaining < 100) { + if (comptime builtin.is_test) { + return .done; + } // Look, we want to exit ASAP, but we don't want // to exit so fast that we've run none of the // background jobs. - break :blk if (comptime builtin.is_test) 1 else 50; + break :blk 50; } // No http transfers, no cdp extra socket, no // scheduled tasks, we're done. From 7b6776345af46658ab9af0172bc41533f78287d3 Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Tue, 9 Dec 2025 17:25:04 +0300 Subject: [PATCH 203/219] backport: Remove `_TYPED_ARRAY_ID_KLUDGE` hack Bonus: Add `ArrayBuffer`. --- src/browser/js/Context.zig | 100 ++++++++++++++++----------------- src/browser/js/js.zig | 112 ++++++++++++++++++++++--------------- 2 files changed, 117 insertions(+), 95 deletions(-) diff --git a/src/browser/js/Context.zig b/src/browser/js/Context.zig index 24f52decc..6bb3c0620 100644 --- a/src/browser/js/Context.zig +++ b/src/browser/js/Context.zig @@ -821,64 +821,62 @@ pub fn jsValueToZig(self: *Context, comptime T: type, js_value: v8.Value) !T { // Extracted so that it can be used in both jsValueToZig and in // probeJsValueToZig. Avoids having to duplicate this logic when probing. fn jsValueToStruct(self: *Context, comptime T: type, js_value: v8.Value) !?T { - if (T == js.Function) { - if (!js_value.isFunction()) { - return null; - } - return try self.createFunction(js_value); - } - - if (@hasDecl(T, "_TYPED_ARRAY_ID_KLUDGE")) { - const VT = @typeInfo(std.meta.fieldInfo(T, .values).type).pointer.child; - const arr = (try self.jsValueToTypedArray(VT, js_value)) orelse return null; - return .{ .values = arr }; - } - - if (T == js.String) { - return .{ .string = try self.valueToString(js_value, .{ .allocator = self.arena }) }; - } - - if (comptime T == js.Value) { + return switch (T) { + js.Function => { + if (!js_value.isFunction()) { + return null; + } + return try self.createFunction(js_value); + }, + // zig fmt: off + js.TypedArray(u8), js.TypedArray(u16), js.TypedArray(u32), js.TypedArray(u64), + js.TypedArray(i8), js.TypedArray(i16), js.TypedArray(i32), js.TypedArray(i64), + js.TypedArray(f32), js.TypedArray(f64), + // zig fmt: on + => { + const ValueType = @typeInfo(std.meta.fieldInfo(T, .values).type).pointer.child; + const arr = (try self.jsValueToTypedArray(ValueType, js_value)) orelse return null; + return .{ .values = arr }; + }, + js.String => .{ .string = try self.valueToString(js_value, .{ .allocator = self.arena }) }, // Caller wants an opaque js.Object. Probably a parameter - // that it needs to pass back into a callback - return js.Value{ - .context = self, + // that it needs to pass back into a callback. + js.Value => js.Value{ .js_val = js_value, - }; - } - - const js_obj = js_value.castTo(v8.Object); - - if (comptime T == js.Object) { + .context = self, + }, // Caller wants an opaque js.Object. Probably a parameter - // that it needs to pass back into a callback - return js.Object{ - .js_obj = js_obj, + // that it needs to pass back into a callback. + js.Object => js.Object{ + .js_obj = js_value.castTo(v8.Object), .context = self, - }; - } + }, + else => { + if (!js_value.isObject()) { + return null; + } - if (!js_value.isObject()) { - return null; - } + const js_obj = js_value.castTo(v8.Object); + const v8_context = self.v8_context; + const isolate = self.isolate; - const v8_context = self.v8_context; - const isolate = self.isolate; + var value: T = undefined; + inline for (@typeInfo(T).@"struct".fields) |field| { + const name = field.name; + const key = v8.String.initUtf8(isolate, name); + if (js_obj.has(v8_context, key.toValue())) { + @field(value, name) = try self.jsValueToZig(field.type, try js_obj.getValue(v8_context, key)); + } else if (@typeInfo(field.type) == .optional) { + @field(value, name) = null; + } else { + const dflt = field.defaultValue() orelse return null; + @field(value, name) = dflt; + } + } - var value: T = undefined; - inline for (@typeInfo(T).@"struct".fields) |field| { - const name = field.name; - const key = v8.String.initUtf8(isolate, name); - if (js_obj.has(v8_context, key.toValue())) { - @field(value, name) = try self.jsValueToZig(field.type, try js_obj.getValue(v8_context, key)); - } else if (@typeInfo(field.type) == .optional) { - @field(value, name) = null; - } else { - const dflt = field.defaultValue() orelse return null; - @field(value, name) = dflt; - } - } - return value; + return value; + }, + }; } fn jsValueToTypedArray(_: *Context, comptime T: type, js_value: v8.Value) !?[]T { diff --git a/src/browser/js/js.zig b/src/browser/js/js.zig index 203b3b248..9530990be 100644 --- a/src/browser/js/js.zig +++ b/src/browser/js/js.zig @@ -53,8 +53,6 @@ pub fn Bridge(comptime T: type) type { // Env.JsObject. Want a TypedArray? Env.TypedArray. pub fn TypedArray(comptime T: type) type { return struct { - pub const _TYPED_ARRAY_ID_KLUDGE = true; - values: []const T, pub fn dupe(self: TypedArray(T), allocator: Allocator) !TypedArray(T) { @@ -63,6 +61,14 @@ pub fn TypedArray(comptime T: type) type { }; } +pub const ArrayBuffer = struct { + values: []const u8, + + pub fn dupe(self: ArrayBuffer, allocator: Allocator) !ArrayBuffer { + return .{ .values = try allocator.dupe(u8, self.values) }; + } +}; + pub const PromiseResolver = struct { context: *Context, resolver: v8.PromiseResolver, @@ -317,55 +323,73 @@ pub fn simpleZigValueToJs(isolate: v8.Isolate, value: anytype, comptime fail: bo return v8.initNull(isolate).toValue(); }, .@"struct" => { - const T = @TypeOf(value); - if (@hasDecl(T, "_TYPED_ARRAY_ID_KLUDGE")) { - const values = value.values; - const value_type = @typeInfo(@TypeOf(values)).pointer.child; - const len = values.len; - const bits = switch (@typeInfo(value_type)) { - .int => |n| n.bits, - .float => |f| f.bits, - else => @compileError("Invalid TypeArray type: " ++ @typeName(value_type)), - }; - - var array_buffer: v8.ArrayBuffer = undefined; - if (len == 0) { - array_buffer = v8.ArrayBuffer.init(isolate, 0); - } else { - const buffer_len = len * bits / 8; - const backing_store = v8.BackingStore.init(isolate, buffer_len); + switch (@TypeOf(value)) { + ArrayBuffer => { + const values = value.values; + const len = values.len; + var array_buffer: v8.ArrayBuffer = undefined; + const backing_store = v8.BackingStore.init(isolate, len); const data: [*]u8 = @ptrCast(@alignCast(backing_store.getData())); - @memcpy(data[0..buffer_len], @as([]const u8, @ptrCast(values))[0..buffer_len]); + @memcpy(data[0..len], @as([]const u8, @ptrCast(values))[0..len]); array_buffer = v8.ArrayBuffer.initWithBackingStore(isolate, &backing_store.toSharedPtr()); - } - switch (@typeInfo(value_type)) { - .int => |n| switch (n.signedness) { - .unsigned => switch (n.bits) { - 8 => return v8.Uint8Array.init(array_buffer, 0, len).toValue(), - 16 => return v8.Uint16Array.init(array_buffer, 0, len).toValue(), - 32 => return v8.Uint32Array.init(array_buffer, 0, len).toValue(), - 64 => return v8.BigUint64Array.init(array_buffer, 0, len).toValue(), - else => {}, + return .{ .handle = array_buffer.handle }; + }, + // zig fmt: off + TypedArray(u8), TypedArray(u16), TypedArray(u32), TypedArray(u64), + TypedArray(i8), TypedArray(i16), TypedArray(i32), TypedArray(i64), + TypedArray(f32), TypedArray(f64), + // zig fmt: on + => { + const values = value.values; + const value_type = @typeInfo(@TypeOf(values)).pointer.child; + const len = values.len; + const bits = switch (@typeInfo(value_type)) { + .int => |n| n.bits, + .float => |f| f.bits, + else => @compileError("Invalid TypeArray type: " ++ @typeName(value_type)), + }; + + var array_buffer: v8.ArrayBuffer = undefined; + if (len == 0) { + array_buffer = v8.ArrayBuffer.init(isolate, 0); + } else { + const buffer_len = len * bits / 8; + const backing_store = v8.BackingStore.init(isolate, buffer_len); + const data: [*]u8 = @ptrCast(@alignCast(backing_store.getData())); + @memcpy(data[0..buffer_len], @as([]const u8, @ptrCast(values))[0..buffer_len]); + array_buffer = v8.ArrayBuffer.initWithBackingStore(isolate, &backing_store.toSharedPtr()); + } + + switch (@typeInfo(value_type)) { + .int => |n| switch (n.signedness) { + .unsigned => switch (n.bits) { + 8 => return v8.Uint8Array.init(array_buffer, 0, len).toValue(), + 16 => return v8.Uint16Array.init(array_buffer, 0, len).toValue(), + 32 => return v8.Uint32Array.init(array_buffer, 0, len).toValue(), + 64 => return v8.BigUint64Array.init(array_buffer, 0, len).toValue(), + else => {}, + }, + .signed => switch (n.bits) { + 8 => return v8.Int8Array.init(array_buffer, 0, len).toValue(), + 16 => return v8.Int16Array.init(array_buffer, 0, len).toValue(), + 32 => return v8.Int32Array.init(array_buffer, 0, len).toValue(), + 64 => return v8.BigInt64Array.init(array_buffer, 0, len).toValue(), + else => {}, + }, }, - .signed => switch (n.bits) { - 8 => return v8.Int8Array.init(array_buffer, 0, len).toValue(), - 16 => return v8.Int16Array.init(array_buffer, 0, len).toValue(), - 32 => return v8.Int32Array.init(array_buffer, 0, len).toValue(), - 64 => return v8.BigInt64Array.init(array_buffer, 0, len).toValue(), + .float => |f| switch (f.bits) { + 32 => return v8.Float32Array.init(array_buffer, 0, len).toValue(), + 64 => return v8.Float64Array.init(array_buffer, 0, len).toValue(), else => {}, }, - }, - .float => |f| switch (f.bits) { - 32 => return v8.Float32Array.init(array_buffer, 0, len).toValue(), - 64 => return v8.Float64Array.init(array_buffer, 0, len).toValue(), else => {}, - }, - else => {}, - } - // We normally don't fail in this function unless fail == true - // but this can never be valid. - @compileError("Invalid TypeArray type: " ++ @typeName(value_type)); + } + // We normally don't fail in this function unless fail == true + // but this can never be valid. + @compileError("Invalid TypeArray type: " ++ @typeName(value_type)); + }, + else => {}, } }, .@"union" => return simpleZigValueToJs(isolate, std.meta.activeTag(value), fail, null_as_undefined), From 695ed817e470509ed8cf16409225d4c4ad73f60c Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 11 Dec 2025 11:46:59 +0800 Subject: [PATCH 204/219] port remaining blob functionality --- src/browser/Factory.zig | 4 +- src/browser/tests/blob.html | 99 ++++++++++++++++++++++++------------- src/browser/webapi/Blob.zig | 70 ++++++++++++++------------ 3 files changed, 105 insertions(+), 68 deletions(-) diff --git a/src/browser/Factory.zig b/src/browser/Factory.zig index 91e011abb..69cf0f529 100644 --- a/src/browser/Factory.zig +++ b/src/browser/Factory.zig @@ -196,8 +196,8 @@ pub fn blob(self: *Factory, child: anytype) !*@TypeOf(child) { const blob_ptr = chain.get(0); blob_ptr.* = .{ ._type = unionInit(Blob.Type, chain.get(1)), - .slice = "", - .mime = "", + ._slice = "", + ._mime = "", }; chain.setLeaf(1, child); diff --git a/src/browser/tests/blob.html b/src/browser/tests/blob.html index 693095c1f..12cd13f5a 100644 --- a/src/browser/tests/blob.html +++ b/src/browser/tests/blob.html @@ -1,8 +1,6 @@ - - Test Document Title - - + + - - @@ -60,10 +31,11 @@ const parts = ["light ", "panda ", "rocks ", "!"]; const blob = new Blob(parts); - testing.async(blob.bytes(), result => { + testing.async(async() => { const expected = new Uint8Array([108, 105, 103, 104, 116, 32, 112, 97, 110, 100, 97, 32, 114, 111, 99, 107, 115, 32, 33]); + const result = await blob.bytes(); testing.expectEqual(true, result instanceof Uint8Array); testing.expectEqual(expected, result); }); @@ -81,7 +53,7 @@ const blob = new Blob(parts, { type: "text/html", endings: "native" }); testing.expectEqual(161, blob.size); testing.expectEqual("text/html", blob.type); - testing.async(blob.bytes(), result => { + testing.async(async() => { const expected = new Uint8Array([10, 84, 104, 101, 32, 111, 112, 101, 110, 101, 100, 32, 112, 97, 99, 107, 97, 103, 101, 10, 111, 102, 32, 112, 111, 116, 97, @@ -100,8 +72,65 @@ 10, 10, 116, 111, 32, 115, 111, 108, 118, 101, 32, 116, 104, 101, 32, 10, 99, 114, 105, 109, 101, 46, 10]); + const result = await blob.bytes(); testing.expectEqual(true, result instanceof Uint8Array); testing.expectEqual(expected, result); }); } + + + + diff --git a/src/browser/webapi/Blob.zig b/src/browser/webapi/Blob.zig index a60f4b424..58106caf3 100644 --- a/src/browser/webapi/Blob.zig +++ b/src/browser/webapi/Blob.zig @@ -27,14 +27,15 @@ const Page = @import("../Page.zig"); const Blob = @This(); const _prototype_root = true; + _type: Type, /// Immutable slice of blob. /// Note that another blob may hold a pointer/slice to this, /// so its better to leave the deallocation of it to arena allocator. -slice: []const u8, +_slice: []const u8, /// MIME attached to blob. Can be an empty string. -mime: []const u8, +_mime: []const u8, pub const Type = union(enum) { generic, @@ -66,7 +67,7 @@ pub fn init( break :blk try page.arena.dupe(u8, t); }; - const slice = blk: { + const data = blk: { if (maybe_blob_parts) |blob_parts| { var w: Writer.Allocating = .init(page.arena); const use_native_endings = std.mem.eql(u8, options.endings, "native"); @@ -80,8 +81,8 @@ pub fn init( return page._factory.create(Blob{ ._type = .generic, - .slice = slice, - .mime = mime, + ._slice = data, + ._mime = mime, }); } @@ -147,8 +148,8 @@ fn writeBlobParts( while (end + vector_len <= part.len) : (end += vector_len) { const cr: Vec = @splat('\r'); // Load chunk as vectors. - const slice = part[end..][0..vector_len]; - const chunk: Vec = slice.*; + const data = part[end..][0..vector_len]; + const chunk: Vec = data.*; // Look for CR. const match = chunk == cr; @@ -160,16 +161,16 @@ fn writeBlobParts( var iter = bitset.iterator(.{}); var relative_start: usize = 0; while (iter.next()) |index| { - _ = try writer.writeVec(&.{ slice[relative_start..index], "\n" }); + _ = try writer.writeVec(&.{ data[relative_start..index], "\n" }); - if (index + 1 != slice.len and slice[index + 1] == '\n') { + if (index + 1 != data.len and data[index + 1] == '\n') { relative_start = index + 2; } else { relative_start = index + 1; } } - _ = try writer.writeVec(&.{slice[relative_start..]}); + _ = try writer.writeVec(&.{data[relative_start..]}); } } @@ -204,16 +205,21 @@ fn writeBlobParts( /// Returns a Promise that resolves with the contents of the blob /// as binary data contained in an ArrayBuffer. -//pub fn arrayBuffer(self: *const Blob, page: *Page) !js.Promise { -// return page.js.resolvePromise(js.ArrayBuffer{ .values = self.slice }); -//} +pub fn arrayBuffer(self: *const Blob, page: *Page) !js.Promise { + return page.js.resolvePromise(js.ArrayBuffer{ .values = self._slice }); +} -// TODO: Implement `stream`; requires `ReadableStream`. +const ReadableStream = @import("streams/ReadableStream.zig"); +/// Returns a ReadableStream which upon reading returns the data +/// contained within the Blob. +pub fn stream(self: *const Blob, page: *Page) !*ReadableStream { + return ReadableStream.initWithData(self._slice, page); +} /// Returns a Promise that resolves with a string containing /// the contents of the blob, interpreted as UTF-8. pub fn text(self: *const Blob, page: *Page) !js.Promise { - return page.js.resolvePromise(self.slice); + return page.js.resolvePromise(self._slice); } /// Extension to Blob; works on Firefox and Safari. @@ -221,12 +227,12 @@ pub fn text(self: *const Blob, page: *Page) !js.Promise { /// Returns a Promise that resolves with a Uint8Array containing /// the contents of the blob as an array of bytes. pub fn bytes(self: *const Blob, page: *Page) !js.Promise { - return page.js.resolvePromise(js.TypedArray(u8){ .values = self.slice }); + return page.js.resolvePromise(js.TypedArray(u8){ .values = self._slice }); } /// Returns a new Blob object which contains data /// from a subset of the blob on which it's called. -pub fn getSlice( +pub fn slice( self: *const Blob, maybe_start: ?i32, maybe_end: ?i32, @@ -239,56 +245,56 @@ pub fn getSlice( break :blk ""; } - break :blk try page.arena.dupe(u8, content_type); + break :blk try page.dupeString(content_type); } break :blk ""; }; - const slice = self.slice; + const data = self._slice; if (maybe_start) |_start| { const start = blk: { if (_start < 0) { - break :blk slice.len -| @abs(_start); + break :blk data.len -| @abs(_start); } - break :blk @min(slice.len, @as(u31, @intCast(_start))); + break :blk @min(data.len, @as(u31, @intCast(_start))); }; const end: usize = blk: { if (maybe_end) |_end| { if (_end < 0) { - break :blk @max(start, slice.len -| @abs(_end)); + break :blk @max(start, data.len -| @abs(_end)); } - break :blk @min(slice.len, @max(start, @as(u31, @intCast(_end)))); + break :blk @min(data.len, @max(start, @as(u31, @intCast(_end)))); } - break :blk slice.len; + break :blk data.len; }; return page._factory.create(Blob{ ._type = .generic, - .slice = slice[start..end], - .mime = mime, + ._slice = data[start..end], + ._mime = mime, }); } return page._factory.create(Blob{ ._type = .generic, - .slice = slice, - .mime = mime, + ._slice = data, + ._mime = mime, }); } /// Returns the size of the Blob in bytes. pub fn getSize(self: *const Blob) usize { - return self.slice.len; + return self._slice.len; } /// Returns the type of Blob; likely a MIME type, yet anything can be given. pub fn getType(self: *const Blob) []const u8 { - return self.mime; + return self._mime; } pub const JsApi = struct { @@ -303,9 +309,11 @@ pub const JsApi = struct { pub const constructor = bridge.constructor(Blob.init, .{}); pub const text = bridge.function(Blob.text, .{}); pub const bytes = bridge.function(Blob.bytes, .{}); - pub const slice = bridge.function(Blob.getSlice, .{}); + pub const slice = bridge.function(Blob.slice, .{}); pub const size = bridge.accessor(Blob.getSize, null, .{}); pub const @"type" = bridge.accessor(Blob.getType, null, .{}); + pub const stream = bridge.function(Blob.stream, .{}); + pub const arrayBuffer = bridge.function(Blob.arrayBuffer, .{}); }; const testing = @import("../../testing.zig"); From 3d8b1abda4f3870adc811089f8966488f677514b Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 11 Dec 2025 16:45:19 +0800 Subject: [PATCH 205/219] More legacy tests Largely around how URL attributes (a.href, img.href, link.href) handle empty values. --- Makefile | 4 +- src/browser/tests/document/focus.html | 9 + src/browser/tests/element/html/anchor.html | 211 +++++++++++++++++++-- src/browser/tests/element/html/image.html | 12 +- src/browser/tests/element/html/link.html | 12 ++ src/browser/tests/legacy/html/element.html | 4 +- src/browser/webapi/Element.zig | 8 +- src/browser/webapi/element/html/Anchor.zig | 118 +++++++++--- src/browser/webapi/element/html/Image.zig | 12 +- src/browser/webapi/element/html/Link.zig | 15 +- 10 files changed, 351 insertions(+), 54 deletions(-) create mode 100644 src/browser/tests/element/html/link.html diff --git a/Makefile b/Makefile index 7208b9ee9..3f79e1fa2 100644 --- a/Makefile +++ b/Makefile @@ -99,11 +99,11 @@ wpt-summary: ## Test - `grep` is used to filter out the huge compile command on build ifeq ($(OS), macos) test: - @script -q /dev/null sh -c 'TEST_FILTER="${F}" $(ZIG) build test -freference-trace --summary all' 2>&1 \ + @script -q /dev/null sh -c 'TEST_FILTER="${F}" $(ZIG) build test -freference-trace' 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 \ + @script -qec 'TEST_FILTER="${F}" $(ZIG) build test -freference-trace' /dev/null 2>&1 \ | grep --line-buffered -v "^/.*zig test -freference-trace" endif diff --git a/src/browser/tests/document/focus.html b/src/browser/tests/document/focus.html index 5b7b7c078..3e72e1d41 100644 --- a/src/browser/tests/document/focus.html +++ b/src/browser/tests/document/focus.html @@ -79,3 +79,12 @@ testing.expectEqual(1, focusCount); } + + + diff --git a/src/browser/tests/element/html/anchor.html b/src/browser/tests/element/html/anchor.html index a1402688a..2eaa7935a 100644 --- a/src/browser/tests/element/html/anchor.html +++ b/src/browser/tests/element/html/anchor.html @@ -1,29 +1,93 @@ + + +OK + + -OK + + + + + + + + + + + + + + + + + + + diff --git a/src/browser/tests/element/html/image.html b/src/browser/tests/element/html/image.html index 5ad6454df..b9cb81538 100644 --- a/src/browser/tests/element/html/image.html +++ b/src/browser/tests/element/html/image.html @@ -31,9 +31,19 @@ testing.expectEqual('', img.alt); img.src = 'test.png'; - testing.expectEqual('test.png', img.src); + // src property returns resolved absolute URL + testing.expectEqual('http://127.0.0.1:9582/src/browser/tests/element/html/test.png', img.src); + // getAttribute returns the raw attribute value testing.expectEqual('test.png', img.getAttribute('src')); + img.src = '/absolute/path.png'; + testing.expectEqual('http://127.0.0.1:9582/absolute/path.png', img.src); + testing.expectEqual('/absolute/path.png', img.getAttribute('src')); + + img.src = 'https://example.com/image.png'; + testing.expectEqual('https://example.com/image.png', img.src); + testing.expectEqual('https://example.com/image.png', img.getAttribute('src')); + img.alt = 'Test image'; testing.expectEqual('Test image', img.alt); testing.expectEqual('Test image', img.getAttribute('alt')); diff --git a/src/browser/tests/element/html/link.html b/src/browser/tests/element/html/link.html new file mode 100644 index 000000000..25fd5430d --- /dev/null +++ b/src/browser/tests/element/html/link.html @@ -0,0 +1,12 @@ + + + + diff --git a/src/browser/tests/legacy/html/element.html b/src/browser/tests/legacy/html/element.html index 4de1f0581..d1701ae34 100644 --- a/src/browser/tests/legacy/html/element.html +++ b/src/browser/tests/legacy/html/element.html @@ -32,7 +32,7 @@ testing.expectEqual('', a.href); testing.expectEqual('', a.host); a.href = 'about'; - testing.expectEqual('http://localhost:9582/src/tests/html/about', a.href); + testing.expectEqual('http://localhost:9589/html/about', a.href); diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index 00b46cdf7..8d722be9d 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -613,13 +613,17 @@ pub fn focus(self: *Element, page: *Page) !void { const Event = @import("Event.zig"); if (page.document._active_element) |old| { - if (old == self) return; + 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; + if (self.asNode().isConnected()) { + page.document._active_element = self; + } const focus_event = try Event.init("focus", null, page); try page._event_manager.dispatch(self.asEventTarget(), focus_event); diff --git a/src/browser/webapi/element/html/Anchor.zig b/src/browser/webapi/element/html/Anchor.zig index d6b85c46f..006843db2 100644 --- a/src/browser/webapi/element/html/Anchor.zig +++ b/src/browser/webapi/element/html/Anchor.zig @@ -40,17 +40,11 @@ pub fn asNode(self: *Anchor) *Node { pub fn getHref(self: *Anchor, page: *Page) ![]const u8 { const element = self.asElement(); - const href = element.getAttributeSafe("href") orelse ""; + const href = element.getAttributeSafe("href") orelse return ""; if (href.len == 0) { - return page.url; + return ""; } - - const first = href[0]; - if (first == '#' or first == '?' or first == '/' or std.mem.startsWith(u8, href, "../") or std.mem.startsWith(u8, href, "./")) { - return URL.resolve(page.call_arena, page.url, href, .{}); - } - - return href; + return URL.resolve(page.call_arena, page.url, href, .{}); } pub fn setHref(self: *Anchor, value: []const u8, page: *Page) !void { @@ -66,17 +60,30 @@ pub fn setTarget(self: *Anchor, value: []const u8, page: *Page) !void { } pub fn getOrigin(self: *Anchor, page: *Page) ![]const u8 { - const href = try getResolvedHref(self, page); + const href = try getResolvedHref(self, page) orelse return ""; return (try URL.getOrigin(page.call_arena, href)) orelse "null"; } pub fn getHost(self: *Anchor, page: *Page) ![]const u8 { - const href = try getResolvedHref(self, page); - return URL.getHost(href); + const href = try getResolvedHref(self, page) orelse return ""; + const host = URL.getHost(href); + const protocol = URL.getProtocol(href); + const port = URL.getPort(href); + + // Strip default ports + if (port.len > 0) { + if ((std.mem.eql(u8, protocol, "https:") and std.mem.eql(u8, port, "443")) or + (std.mem.eql(u8, protocol, "http:") and std.mem.eql(u8, port, "80"))) + { + return URL.getHostname(href); + } + } + + return host; } pub fn setHost(self: *Anchor, value: []const u8, page: *Page) !void { - const href = try getResolvedHref(self, page); + const href = try getResolvedHref(self, page) orelse return; const protocol = URL.getProtocol(href); const pathname = URL.getPathname(href); const search = URL.getSearch(href); @@ -101,12 +108,12 @@ pub fn setHost(self: *Anchor, value: []const u8, page: *Page) !void { } pub fn getHostname(self: *Anchor, page: *Page) ![]const u8 { - const href = try getResolvedHref(self, page); + const href = try getResolvedHref(self, page) orelse return ""; return URL.getHostname(href); } pub fn setHostname(self: *Anchor, value: []const u8, page: *Page) !void { - const href = try getResolvedHref(self, page); + const href = try getResolvedHref(self, page) orelse return; const current_port = URL.getPort(href); const new_host = if (current_port.len > 0) try std.fmt.allocPrint(page.call_arena, "{s}:{s}", .{ value, current_port }) @@ -117,12 +124,24 @@ pub fn setHostname(self: *Anchor, value: []const u8, page: *Page) !void { } pub fn getPort(self: *Anchor, page: *Page) ![]const u8 { - const href = try getResolvedHref(self, page); - return URL.getPort(href); + const href = try getResolvedHref(self, page) orelse return ""; + const port = URL.getPort(href); + const protocol = URL.getProtocol(href); + + // Return empty string for default ports + if (port.len > 0) { + if ((std.mem.eql(u8, protocol, "https:") and std.mem.eql(u8, port, "443")) or + (std.mem.eql(u8, protocol, "http:") and std.mem.eql(u8, port, "80"))) + { + return ""; + } + } + + return port; } pub fn setPort(self: *Anchor, value: ?[]const u8, page: *Page) !void { - const href = try getResolvedHref(self, page); + const href = try getResolvedHref(self, page) orelse return; const hostname = URL.getHostname(href); const protocol = URL.getProtocol(href); @@ -145,12 +164,12 @@ pub fn setPort(self: *Anchor, value: ?[]const u8, page: *Page) !void { } pub fn getSearch(self: *Anchor, page: *Page) ![]const u8 { - const href = try getResolvedHref(self, page); + const href = try getResolvedHref(self, page) orelse return ""; return URL.getSearch(href); } pub fn setSearch(self: *Anchor, value: []const u8, page: *Page) !void { - const href = try getResolvedHref(self, page); + const href = try getResolvedHref(self, page) orelse return; const protocol = URL.getProtocol(href); const host = URL.getHost(href); const pathname = URL.getPathname(href); @@ -167,12 +186,12 @@ pub fn setSearch(self: *Anchor, value: []const u8, page: *Page) !void { } pub fn getHash(self: *Anchor, page: *Page) ![]const u8 { - const href = try getResolvedHref(self, page); + const href = try getResolvedHref(self, page) orelse return ""; return URL.getHash(href); } pub fn setHash(self: *Anchor, value: []const u8, page: *Page) !void { - const href = try getResolvedHref(self, page); + const href = try getResolvedHref(self, page) orelse return; const protocol = URL.getProtocol(href); const host = URL.getHost(href); const pathname = URL.getPathname(href); @@ -188,6 +207,50 @@ pub fn setHash(self: *Anchor, value: []const u8, page: *Page) !void { try setHref(self, new_href, page); } +pub fn getPathname(self: *Anchor, page: *Page) ![]const u8 { + const href = try getResolvedHref(self, page) orelse return ""; + return URL.getPathname(href); +} + +pub fn setPathname(self: *Anchor, value: []const u8, page: *Page) !void { + const href = try getResolvedHref(self, page) orelse return; + const protocol = URL.getProtocol(href); + const host = URL.getHost(href); + const search = URL.getSearch(href); + const hash = URL.getHash(href); + + // Add / prefix if not present and value is not empty + const pathname = if (value.len > 0 and value[0] != '/') + try std.fmt.allocPrint(page.call_arena, "/{s}", .{value}) + else + value; + + const new_href = try buildUrl(page.call_arena, protocol, host, pathname, search, hash); + try setHref(self, new_href, page); +} + +pub fn getProtocol(self: *Anchor, page: *Page) ![]const u8 { + const href = try getResolvedHref(self, page) orelse return ""; + return URL.getProtocol(href); +} + +pub fn setProtocol(self: *Anchor, value: []const u8, page: *Page) !void { + const href = try getResolvedHref(self, page) orelse return; + const host = URL.getHost(href); + const pathname = URL.getPathname(href); + const search = URL.getSearch(href); + const hash = URL.getHash(href); + + // Add : suffix if not present + const protocol = if (value.len > 0 and value[value.len - 1] != ':') + try std.fmt.allocPrint(page.call_arena, "{s}:", .{value}) + else + value; + + const new_href = try buildUrl(page.call_arena, protocol, host, pathname, search, hash); + try setHref(self, new_href, page); +} + pub fn getType(self: *Anchor) []const u8 { return self.asElement().getAttributeSafe("type") orelse ""; } @@ -212,9 +275,12 @@ pub fn setText(self: *Anchor, value: []const u8, page: *Page) !void { try self.asNode().setTextContent(value, page); } -fn getResolvedHref(self: *Anchor, page: *Page) ![:0]const u8 { - const href = self.asElement().getAttributeSafe("href"); - return URL.resolve(page.call_arena, page.url, href orelse "", .{}); +fn getResolvedHref(self: *Anchor, page: *Page) !?[:0]const u8 { + const href = self.asElement().getAttributeSafe("href") orelse return null; + if (href.len == 0) { + return null; + } + return try URL.resolve(page.call_arena, page.url, href, .{}); } // Helper function to build a new URL from components @@ -248,9 +314,11 @@ pub const JsApi = struct { pub const target = bridge.accessor(Anchor.getTarget, Anchor.setTarget, .{}); pub const name = bridge.accessor(Anchor.getName, Anchor.setName, .{}); pub const origin = bridge.accessor(Anchor.getOrigin, null, .{}); + pub const protocol = bridge.accessor(Anchor.getProtocol, Anchor.setProtocol, .{}); pub const host = bridge.accessor(Anchor.getHost, Anchor.setHost, .{}); pub const hostname = bridge.accessor(Anchor.getHostname, Anchor.setHostname, .{}); pub const port = bridge.accessor(Anchor.getPort, Anchor.setPort, .{}); + pub const pathname = bridge.accessor(Anchor.getPathname, Anchor.setPathname, .{}); pub const search = bridge.accessor(Anchor.getSearch, Anchor.setSearch, .{}); pub const hash = bridge.accessor(Anchor.getHash, Anchor.setHash, .{}); pub const @"type" = bridge.accessor(Anchor.getType, Anchor.setType, .{}); diff --git a/src/browser/webapi/element/html/Image.zig b/src/browser/webapi/element/html/Image.zig index 9576fde75..affaaba0a 100644 --- a/src/browser/webapi/element/html/Image.zig +++ b/src/browser/webapi/element/html/Image.zig @@ -1,6 +1,7 @@ const std = @import("std"); const js = @import("../../../js/js.zig"); const Page = @import("../../../Page.zig"); +const URL = @import("../../../URL.zig"); const Node = @import("../../Node.zig"); const Element = @import("../../Element.zig"); const HtmlElement = @import("../Html.zig"); @@ -33,8 +34,15 @@ pub fn asNode(self: *Image) *Node { return self.asElement().asNode(); } -pub fn getSrc(self: *const Image) []const u8 { - return self.asConstElement().getAttributeSafe("src") orelse ""; +pub fn getSrc(self: *const Image, page: *Page) ![]const u8 { + const element = self.asConstElement(); + const src = element.getAttributeSafe("src") orelse return ""; + if (src.len == 0) { + return ""; + } + + // Always resolve the src against the page URL + return URL.resolve(page.call_arena, page.url, src, .{}); } pub fn setSrc(self: *Image, value: []const u8, page: *Page) !void { diff --git a/src/browser/webapi/element/html/Link.zig b/src/browser/webapi/element/html/Link.zig index b9db1e53c..e381e2274 100644 --- a/src/browser/webapi/element/html/Link.zig +++ b/src/browser/webapi/element/html/Link.zig @@ -35,8 +35,14 @@ pub fn asNode(self: *Link) *Node { } pub fn getHref(self: *Link, page: *Page) ![]const u8 { - const href = self.asElement().getAttributeSafe("href"); - return URL.resolve(page.call_arena, page.url, href orelse "", .{}); + const element = self.asElement(); + const href = element.getAttributeSafe("href") orelse return ""; + if (href.len == 0) { + return ""; + } + + // Always resolve the href against the page URL + return URL.resolve(page.call_arena, page.url, href, .{}); } pub fn setHref(self: *Link, value: []const u8, page: *Page) !void { @@ -63,3 +69,8 @@ pub const JsApi = struct { pub const rel = bridge.accessor(Link.getRel, Link.setRel, .{}); pub const href = bridge.accessor(Link.getHref, Link.setHref, .{}); }; + +const testing = @import("../../../../testing.zig"); +test "WebApi: HTML.Link" { + try testing.htmlRunner("element/html/link.html", .{}); +} From 38fb5b101ec5a13e08fab1bade7b3312f28d14cc Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 11 Dec 2025 19:49:51 +0800 Subject: [PATCH 206/219] add Document.elementFromPoint and elementsFromPoint --- .../tests/document/element_from_point.html | 233 ++++++++++++++++++ src/browser/tests/legacy/html/document.html | 18 +- src/browser/tests/legacy/html/image.html | 4 +- src/browser/tests/legacy/html/link.html | 2 +- src/browser/tests/window/navigator.html | 51 +--- src/browser/webapi/Document.zig | 44 ++++ src/browser/webapi/Navigator.zig | 13 +- .../collections/HTMLOptionsCollection.zig | 6 - src/browser/webapi/element/Html.zig | 32 +-- src/browser/webapi/selector/List.zig | 6 - 10 files changed, 311 insertions(+), 98 deletions(-) create mode 100644 src/browser/tests/document/element_from_point.html diff --git a/src/browser/tests/document/element_from_point.html b/src/browser/tests/document/element_from_point.html new file mode 100644 index 000000000..d3ea7da98 --- /dev/null +++ b/src/browser/tests/document/element_from_point.html @@ -0,0 +1,233 @@ + + + + +
Div 1
+
Div 2
+ +
+
Child
+
+ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/browser/tests/legacy/html/document.html b/src/browser/tests/legacy/html/document.html index 003ee8205..7abb0b034 100644 --- a/src/browser/tests/legacy/html/document.html +++ b/src/browser/tests/legacy/html/document.html @@ -52,13 +52,6 @@ let div1 = document.createElement('div'); document.body.appendChild(div1); div1.getClientRects(); // clal this to position it - testing.expectEqual('[object HTMLDivElement]', document.elementFromPoint(2.5, 2.5).toString()); - - let elems = document.elementsFromPoint(2.5, 2.5); - testing.expectEqual(3, elems.length); - testing.expectEqual('[object HTMLDivElement]', elems[0].toString()); - testing.expectEqual('[object HTMLBodyElement]', elems[1].toString()); - testing.expectEqual('[object HTMLHtmlElement]', elems[2].toString()); let a = document.createElement('a'); a.href = "https://lightpanda.io"; @@ -66,20 +59,11 @@ // Note this will be placed after the div of previous test a.getClientRects(); - let a_again = document.elementFromPoint(7.5, 0.5); - testing.expectEqual('[object HTMLAnchorElement]', a_again.toString()); - testing.expectEqual('https://lightpanda.io', a_again.href); - - let a_agains = document.elementsFromPoint(7.5, 0.5); - testing.expectEqual('https://lightpanda.io', a_agains[0].href); - - testing.expectEqual(true, !document.all); testing.expectEqual(false, !!document.all); - testing.expectEqual('[object HTMLScriptElement]', document.all(5).toString()); + testing.expectEqual('[object HTMLScriptElement]', document.all(6).toString()); testing.expectEqual('[object HTMLDivElement]', document.all('content').toString()); - testing.expectEqual(document, document.defaultView.document ); testing.expectEqual('loading', document.readyState); diff --git a/src/browser/tests/legacy/html/image.html b/src/browser/tests/legacy/html/image.html index 1e3f6aff2..053b2cfa1 100644 --- a/src/browser/tests/legacy/html/image.html +++ b/src/browser/tests/legacy/html/image.html @@ -26,7 +26,7 @@ let lyric = new Image testing.expectEqual('', lyric.src); lyric.src = 'okay'; - testing.expectEqual('okay', lyric.src); + testing.expectEqual('http://localhost:9589/html/okay', lyric.src); lyric.src = 15; - testing.expectEqual('15', lyric.src); + testing.expectEqual('http://localhost:9589/html/15', lyric.src); diff --git a/src/browser/tests/legacy/html/link.html b/src/browser/tests/legacy/html/link.html index 15da64611..958690521 100644 --- a/src/browser/tests/legacy/html/link.html +++ b/src/browser/tests/legacy/html/link.html @@ -9,7 +9,7 @@ testing.expectEqual('_blank', link.target); link.target = ''; - testing.expectEqual('foo', link.href); + testing.expectEqual('http://localhost:9589/html/foo', link.href); link.href = 'https://lightpanda.io/'; testing.expectEqual('https://lightpanda.io/', link.href); diff --git a/src/browser/tests/window/navigator.html b/src/browser/tests/window/navigator.html index e00f429bd..11ad9adec 100644 --- a/src/browser/tests/window/navigator.html +++ b/src/browser/tests/window/navigator.html @@ -4,67 +4,26 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/browser/webapi/Document.zig b/src/browser/webapi/Document.zig index a665849fe..87019a549 100644 --- a/src/browser/webapi/Document.zig +++ b/src/browser/webapi/Document.zig @@ -345,6 +345,48 @@ pub fn prepend(self: *Document, nodes: []const Node.NodeOrText, page: *Page) !vo } } +pub fn elementFromPoint(self: *Document, x: f64, y: f64, page: *Page) !?*Element { + // Traverse document in depth-first order to find the topmost (last in document order) + // element that contains the point (x, y) + var topmost: ?*Element = null; + + const root = self.asNode(); + var stack: std.ArrayList(*Node) = .empty; + try stack.append(page.call_arena, root); + + while (stack.items.len > 0) { + const node = stack.pop() orelse break; + if (node.is(Element)) |element| { + if (try element.checkVisibility(page)) { + const rect = try element.getBoundingClientRect(page); + if (x >= rect._left and x <= rect._right and y >= rect._top and y <= rect._bottom) { + topmost = element; + } + } + } + + // Add children to stack in reverse order so we process them in document order + var child = node.lastChild(); + while (child) |c| { + try stack.append(page.call_arena, c); + child = c.previousSibling(); + } + } + + return topmost; +} + +pub fn elementsFromPoint(self: *Document, x: f64, y: f64, page: *Page) ![]const *Element { + // Get topmost element + var current: ?*Element = (try self.elementFromPoint(x, y, page)) orelse return &.{}; + var result: std.ArrayList(*Element) = .empty; + while (current) |el| { + try result.append(page.call_arena, el); + current = el.parentElement(); + } + return result.items; +} + const ReadyState = enum { loading, interactive, @@ -404,6 +446,8 @@ pub const JsApi = struct { pub const importNode = bridge.function(Document.importNode, .{ .dom_exception = true }); pub const append = bridge.function(Document.append, .{}); pub const prepend = bridge.function(Document.prepend, .{}); + pub const elementFromPoint = bridge.function(Document.elementFromPoint, .{}); + pub const elementsFromPoint = bridge.function(Document.elementsFromPoint, .{}); pub const defaultView = bridge.accessor(struct { fn defaultView(_: *const Document, page: *Page) *@import("Window.zig") { diff --git a/src/browser/webapi/Navigator.zig b/src/browser/webapi/Navigator.zig index 23efd49f6..b38d1093f 100644 --- a/src/browser/webapi/Navigator.zig +++ b/src/browser/webapi/Navigator.zig @@ -25,15 +25,19 @@ _pad: bool = false, pub const init: Navigator = .{}; pub fn getUserAgent(_: *const Navigator) []const u8 { - return "Mozilla/5.0 (compatible; LiteFetch/0.1)"; + return "Lightpanda/1.0"; } pub fn getAppName(_: *const Navigator) []const u8 { - return "LiteFetch"; + return "Netscape"; +} + +pub fn getAppCodeName(_: *const Navigator) []const u8 { + return "Netscape"; } pub fn getAppVersion(_: *const Navigator) []const u8 { - return "0.1"; + return "1.0"; } pub fn getPlatform(_: *const Navigator) []const u8 { @@ -73,7 +77,7 @@ pub fn getMaxTouchPoints(_: *const Navigator) u32 { /// Returns the vendor name pub fn getVendor(_: *const Navigator) []const u8 { - return "LiteFetch"; + return ""; } /// Returns the product name (typically "Gecko" for compatibility) @@ -104,6 +108,7 @@ pub const JsApi = struct { // Read-only properties pub const userAgent = bridge.accessor(Navigator.getUserAgent, null, .{}); pub const appName = bridge.accessor(Navigator.getAppName, null, .{}); + pub const appCodeName = bridge.accessor(Navigator.getAppCodeName, null, .{}); pub const appVersion = bridge.accessor(Navigator.getAppVersion, null, .{}); pub const platform = bridge.accessor(Navigator.getPlatform, null, .{}); pub const language = bridge.accessor(Navigator.getLanguage, null, .{}); diff --git a/src/browser/webapi/collections/HTMLOptionsCollection.zig b/src/browser/webapi/collections/HTMLOptionsCollection.zig index 6a0cadc95..4474cb761 100644 --- a/src/browser/webapi/collections/HTMLOptionsCollection.zig +++ b/src/browser/webapi/collections/HTMLOptionsCollection.zig @@ -30,11 +30,6 @@ 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); @@ -102,7 +97,6 @@ pub const JsApi = 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; }; diff --git a/src/browser/webapi/element/Html.zig b/src/browser/webapi/element/Html.zig index 341fbc949..e0220504f 100644 --- a/src/browser/webapi/element/Html.zig +++ b/src/browser/webapi/element/Html.zig @@ -120,11 +120,11 @@ pub fn is(self: *HtmlElement, comptime T: type) ?*T { pub fn className(self: *const HtmlElement) []const u8 { return switch (self._type) { - .anchor => "[object HtmlAnchorElement]", - .div => "[object HtmlDivElement]", - .embed => "[object HtmlEmbedElement]", + .anchor => "[object HTMLAnchorElement]", + .div => "[object HTMLDivElement]", + .embed => "[object HTMLEmbedElement]", .form => "[object HTMLFormElement]", - .p => "[object HtmlParagraphElement]", + .p => "[object HTMLParagraphElement]", .custom => "[object CUSTOM-TODO]", .data => "[object HTMLDataElement]", .dialog => "[object HTMLDialogElement]", @@ -137,22 +137,22 @@ pub fn className(self: *const HtmlElement) []const u8 { .ul => "[object HTMLULElement]", .ol => "[object HTMLOLElement]", .generic => "[object HTMLElement]", - .script => "[object HtmlScriptElement]", + .script => "[object HTMLScriptElement]", .select => "[object HTMLSelectElement]", .slot => "[object HTMLSlotElement]", .template => "[object HTMLTemplateElement]", .option => "[object HTMLOptionElement]", - .text_area => "[object HtmlTextAreaElement]", - .input => "[object HtmlInputElement]", - .link => "[object HtmlLinkElement]", - .meta => "[object HtmlMetaElement]", - .hr => "[object HtmlHRElement]", - .style => "[object HtmlSyleElement]", - .title => "[object HtmlTitleElement]", - .body => "[object HtmlBodyElement]", - .html => "[object HtmlHtmlElement]", - .head => "[object HtmlHeadElement]", - .unknown => "[object HtmlUnknownElement]", + .text_area => "[object HTMLTextAreaElement]", + .input => "[object HTMLInputElement]", + .link => "[object HTMLLinkElement]", + .meta => "[object HTMLMetaElement]", + .hr => "[object HTMLHRElement]", + .style => "[object HTMLSyleElement]", + .title => "[object HTMLTitleElement]", + .body => "[object HTMLBodyElement]", + .html => "[object HTMLHtmlElement]", + .head => "[object HTMLHeadElement]", + .unknown => "[object HTMLUnknownElement]", }; } diff --git a/src/browser/webapi/selector/List.zig b/src/browser/webapi/selector/List.zig index 06d2045ed..5b1eb6324 100644 --- a/src/browser/webapi/selector/List.zig +++ b/src/browser/webapi/selector/List.zig @@ -77,12 +77,6 @@ pub fn initOne(root: *Node, selector: Selector.Selector, page: *Page) ?*Node { return null; } -pub fn deinit(self: *List) void { - const page = Page.current; - page._mem.releaseArena(self._arena); - page._factory.destroy(self); -} - const OptimizeResult = struct { root: *Node, exclude_root: bool, From fe89aad621edd387b35e0f026ef1ae953af57501 Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Fri, 5 Dec 2025 16:03:10 +0300 Subject: [PATCH 207/219] add `isEqualNode` rework `isEqualNode` Splits equality logic by node types and groups comparisons nicer. prefer ancestor's`isEqualNode` `nodeType` => `getNodeType` fix attribute comparison logic Also introduces attribute counting. remove debug logging add `isEqualNode` test --- src/browser/tests/node/is_equal_node.html | 40 +++++++++++++++++++++ src/browser/webapi/CData.zig | 4 +++ src/browser/webapi/Element.zig | 43 +++++++++++++++++++++++ src/browser/webapi/Node.zig | 22 ++++++++++++ src/browser/webapi/element/Attribute.zig | 43 +++++++++++++++++++++++ 5 files changed, 152 insertions(+) create mode 100644 src/browser/tests/node/is_equal_node.html diff --git a/src/browser/tests/node/is_equal_node.html b/src/browser/tests/node/is_equal_node.html new file mode 100644 index 000000000..ef192fae2 --- /dev/null +++ b/src/browser/tests/node/is_equal_node.html @@ -0,0 +1,40 @@ + + + +
+ we're no strangers to love + you know the rules + + and so do I +
+ +
+ we're no strangers to love + you know the rules + + and so do I +
+ + diff --git a/src/browser/webapi/CData.zig b/src/browser/webapi/CData.zig index cb17aaad5..82afee542 100644 --- a/src/browser/webapi/CData.zig +++ b/src/browser/webapi/CData.zig @@ -146,6 +146,10 @@ pub fn getLength(self: *const CData) usize { return self._data.len; } +pub fn isEqualNode(self: *const CData, other: *const CData) bool { + return std.mem.eql(u8, self.getData(), other.getData()); +} + pub fn appendData(self: *CData, data: []const u8, page: *Page) !void { const new_data = try std.mem.concat(page.arena, u8, &.{ self._data, data }); try self.setData(new_data, page); diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index 00b46cdf7..382bc41f5 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -117,6 +117,49 @@ pub fn className(self: *const Element) []const u8 { }; } +pub fn attributesEql(self: *const Element, other: *Element) bool { + if (self._attributes) |attr_list| { + const other_list = other._attributes orelse return false; + return attr_list.eql(other_list); + } + // Make sure no attrs in both sides. + return other._attributes == null; +} + +/// TODO: localName and prefix comparison. +pub fn isEqualNode(self: *Element, other: *Element) bool { + const self_tag = self.getTagNameDump(); + const other_tag = other.getTagNameDump(); + // Compare namespaces and tags. + const dirty = self._namespace != other._namespace or !std.mem.eql(u8, self_tag, other_tag); + if (dirty) { + return false; + } + + // Compare attributes. + if (!self.attributesEql(other)) { + return false; + } + + // Compare children. + var self_iter = self.asNode().childrenIterator(); + var other_iter = other.asNode().childrenIterator(); + var self_count: usize = 0; + var other_count: usize = 0; + while (self_iter.next()) |self_node| : (self_count += 1) { + const other_node = other_iter.next() orelse return false; + other_count += 1; + if (self_node.isEqualNode(other_node)) { + continue; + } + + return false; + } + + // Make sure both have equal number of children. + return self_count == other_count; +} + pub fn getTagNameLower(self: *const Element) []const u8 { switch (self._type) { .html => |he| switch (he._type) { diff --git a/src/browser/webapi/Node.zig b/src/browser/webapi/Node.zig index f203c7ae5..564bf5990 100644 --- a/src/browser/webapi/Node.zig +++ b/src/browser/webapi/Node.zig @@ -286,6 +286,27 @@ pub fn getNodeType(self: *const Node) u8 { }; } +pub fn isEqualNode(self: *Node, other: *Node) bool { + // Make sure types match. + if (self.getNodeType() != other.getNodeType()) { + return false; + } + + // TODO: Compare `localName` and prefix. + return switch (self._type) { + .element => self.as(Element).isEqualNode(other.as(Element)), + .attribute => self.as(Element.Attribute).isEqualNode(other.as(Element.Attribute)), + .cdata => self.as(CData).isEqualNode(other.as(CData)), + else => { + log.warn(.browser, "not implemented", .{ + .type = self._type, + .feature = "Node.isEqualNode", + }); + return false; + }, + }; +} + pub fn isInShadowTree(self: *Node) bool { var node = self._parent; while (node) |n| { @@ -822,6 +843,7 @@ pub const JsApi = struct { pub const cloneNode = bridge.function(Node.cloneNode, .{ .dom_exception = true }); pub const compareDocumentPosition = bridge.function(Node.compareDocumentPosition, .{}); pub const getRootNode = bridge.function(Node.getRootNode, .{}); + pub const isEqualNode = bridge.function(Node.isEqualNode, .{}); pub const toString = bridge.function(_toString, .{}); fn _toString(self: *const Node) []const u8 { diff --git a/src/browser/webapi/element/Attribute.zig b/src/browser/webapi/element/Attribute.zig index d7fa36e6e..693b3842f 100644 --- a/src/browser/webapi/element/Attribute.zig +++ b/src/browser/webapi/element/Attribute.zig @@ -78,6 +78,10 @@ pub fn getOwnerElement(self: *const Attribute) ?*Element { return self._element; } +pub fn isEqualNode(self: *const Attribute, other: *const Attribute) bool { + return std.mem.eql(u8, self.getName(), other.getName()) and std.mem.eql(u8, self.getValue(), other.getValue()); +} + pub const JsApi = struct { pub const bridge = js.Bridge(Attribute); @@ -119,16 +123,45 @@ pub const JsApi = struct { // attribute in the DOM, and, again, we expect that to almost always be null. pub const List = struct { normalize: bool, + /// Length of items in `_list`. Not usize to increase memory usage. + /// Honestly, this is more than enough. + _len: u32 = 0, _list: std.DoublyLinkedList = .{}, pub fn isEmpty(self: *const List) bool { return self._list.first == null; } + pub fn get(self: *const List, name: []const u8, page: *Page) !?[]const u8 { const entry = (try self.getEntry(name, page)) orelse return null; return entry._value.str(); } + pub inline fn length(self: *const List) usize { + return self._len; + } + + /// Compares 2 attribute lists for equality. + pub fn eql(self: *List, other: *List) bool { + if (self.length() != other.length()) { + return false; + } + + var iter = self.iterator(); + search: while (iter.next()) |attr| { + // Iterate over all `other` attributes. + var other_iter = other.iterator(); + while (other_iter.next()) |other_attr| { + if (attr.eql(other_attr)) { + continue :search; // Found match. + } + } + // Iterated over all `other` and not match. + return false; + } + return true; + } + // meant for internal usage, where the name is known to be properly cased pub fn getSafe(self: *const List, name: []const u8) ?[]const u8 { const entry = self.getEntryWithNormalizedName(name) orelse return null; @@ -180,6 +213,7 @@ pub const List = struct { ._value = try String.init(page.arena, value, .{}), }); self._list.append(&entry._node); + self._len += 1; } if (is_id) { @@ -203,6 +237,7 @@ pub const List = struct { ._value = try String.init(page.arena, value, .{}), }); self._list.append(&entry._node); + self._len += 1; } // not efficient, won't be called often (if ever!) @@ -235,6 +270,7 @@ pub const List = struct { ._value = try String.init(page.arena, value, .{}), }); self._list.append(&entry._node); + self._len += 1; } pub fn delete(self: *List, name: []const u8, element: *Element, page: *Page) !void { @@ -252,6 +288,7 @@ pub const List = struct { page.attributeRemove(element, result.normalized, old_value); _ = page._attribute_lookup.remove(@intFromPtr(entry)); self._list.remove(&entry._node); + self._len -= 1; page._factory.destroy(entry); } @@ -311,6 +348,12 @@ pub const List = struct { return @alignCast(@fieldParentPtr("_node", n)); } + /// Returns true if 2 entries are equal. + /// This doesn't compare `_node` fields. + pub fn eql(self: *const Entry, other: *const Entry) bool { + return self._name.eql(other._name) and self._value.eql(other._value); + } + pub fn format(self: *const Entry, writer: *std.Io.Writer) !void { return formatAttribute(self._name.str(), self._value.str(), writer); } From 4d8d6c10c6281cc3ca02c4fbdff5afeb6ee72737 Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Thu, 11 Dec 2025 12:13:01 -0800 Subject: [PATCH 208/219] add option inheriting for Events --- src/browser/webapi/Event.zig | 49 ++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/src/browser/webapi/Event.zig b/src/browser/webapi/Event.zig index 0a49655b3..e99d71ece 100644 --- a/src/browser/webapi/Event.zig +++ b/src/browser/webapi/Event.zig @@ -18,6 +18,7 @@ const std = @import("std"); const js = @import("../js/js.zig"); +const reflect = @import("../reflect.zig"); const Page = @import("../Page.zig"); const EventTarget = @import("EventTarget.zig"); @@ -195,6 +196,54 @@ pub fn composedPath(self: *Event, page: *Page) ![]const *EventTarget { return path; } +pub fn populateFromOptions(self: *Event, opts: anytype) void { + self._bubbles = opts.bubbles; + self._cancelable = opts.cancelable; + self._composed = opts.composed; +} + +pub fn inheritOptions(comptime T: type, comptime additions: anytype) type { + var all_fields: []const std.builtin.Type.StructField = &.{}; + + if (@hasField(T, "_proto")) { + const t_fields = @typeInfo(T).@"struct".fields; + + inline for (t_fields) |field| { + if (std.mem.eql(u8, field.name, "_proto")) { + const ProtoType = reflect.Struct(field.type); + if (@hasDecl(ProtoType, "Options")) { + const parent_options = @typeInfo(ProtoType.Options); + all_fields = all_fields ++ parent_options.@"struct".fields; + } + } + } + } + + const additions_info = @typeInfo(additions); + all_fields = all_fields ++ additions_info.@"struct".fields; + + return @Type(.{ + .@"struct" = .{ + .layout = .auto, + .fields = all_fields, + .decls = &.{}, + .is_tuple = false, + }, + }); +} + +pub fn populatePrototypes(self: anytype, opts: anytype) void { + const T = @TypeOf(self.*); + + if (@hasField(T, "_proto")) { + populatePrototypes(self._proto, opts); + } + + if (@hasDecl(T, "populateFromOptions")) { + T.populateFromOptions(self, opts); + } +} + pub const JsApi = struct { pub const bridge = js.Bridge(Event); From b568eb4e1e1c35567dbc39533d98a3213e02e4ee Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Thu, 11 Dec 2025 12:13:43 -0800 Subject: [PATCH 209/219] migrate events to use new inheritOptions --- src/browser/webapi/event/CompositionEvent.zig | 14 +++---- src/browser/webapi/event/CustomEvent.zig | 27 ++++++------- src/browser/webapi/event/ErrorEvent.zig | 39 ++++++++++--------- src/browser/webapi/event/MessageEvent.zig | 27 ++++++------- .../NavigationCurrentEntryChangeEvent.zig | 27 +++++++++---- .../webapi/event/PageTransitionEvent.zig | 28 ++++++++----- src/browser/webapi/event/PopStateEvent.zig | 26 ++++++++----- src/browser/webapi/event/ProgressEvent.zig | 28 ++++++++++--- .../webapi/net/XMLHttpRequestEventTarget.zig | 6 ++- 9 files changed, 136 insertions(+), 86 deletions(-) diff --git a/src/browser/webapi/event/CompositionEvent.zig b/src/browser/webapi/event/CompositionEvent.zig index 7fa701bd0..3f2e83156 100644 --- a/src/browser/webapi/event/CompositionEvent.zig +++ b/src/browser/webapi/event/CompositionEvent.zig @@ -26,23 +26,21 @@ const CompositionEvent = @This(); _proto: *Event, _data: []const u8 = "", -pub const InitOptions = struct { +const CompositionEventOptions = struct { data: ?[]const u8 = null, - bubbles: bool = false, - cancelable: bool = false, }; -pub fn init(typ: []const u8, opts_: ?InitOptions, page: *Page) !*CompositionEvent { - const opts = opts_ orelse InitOptions{}; +pub const Options = Event.inheritOptions(CompositionEvent, CompositionEventOptions); + +pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*CompositionEvent { + const opts = opts_ orelse Options{}; const event = try page._factory.event(typ, CompositionEvent{ ._proto = undefined, ._data = if (opts.data) |str| try page.dupeString(str) else "", }); - event._proto._bubbles = opts.bubbles; - event._proto._cancelable = opts.cancelable; - + Event.populatePrototypes(event, opts); return event; } diff --git a/src/browser/webapi/event/CustomEvent.zig b/src/browser/webapi/event/CustomEvent.zig index 1c36fc33e..a5f3db01e 100644 --- a/src/browser/webapi/event/CustomEvent.zig +++ b/src/browser/webapi/event/CustomEvent.zig @@ -29,25 +29,26 @@ _proto: *Event, _detail: ?js.Object = null, _arena: Allocator, -pub const InitOptions = struct { +const CustomEventOptions = struct { detail: ?js.Object = null, - bubbles: bool = false, - cancelable: bool = false, }; -pub fn init(typ: []const u8, opts_: ?InitOptions, page: *Page) !*CustomEvent { - const arena = page.arena; - const opts = opts_ orelse InitOptions{}; +pub const Options = Event.inheritOptions(CustomEvent, CustomEventOptions); - const event = try page._factory.event(typ, CustomEvent{ - ._arena = arena, - ._proto = undefined, - ._detail = if (opts.detail) |detail| try detail.persist() else null, - }); +pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*CustomEvent { + const arena = page.arena; + const opts = opts_ orelse Options{}; - event._proto._bubbles = opts.bubbles; - event._proto._cancelable = opts.cancelable; + const event = try page._factory.event( + typ, + CustomEvent{ + ._arena = arena, + ._proto = undefined, + ._detail = if (opts.detail) |detail| try detail.persist() else null, + }, + ); + Event.populatePrototypes(event, opts); return event; } diff --git a/src/browser/webapi/event/ErrorEvent.zig b/src/browser/webapi/event/ErrorEvent.zig index 9c7f15700..f96073237 100644 --- a/src/browser/webapi/event/ErrorEvent.zig +++ b/src/browser/webapi/event/ErrorEvent.zig @@ -33,33 +33,34 @@ _column_number: u32 = 0, _error: ?js.Object = null, _arena: Allocator, -pub const InitOptions = struct { +pub const ErrorEventOptions = struct { message: ?[]const u8 = null, filename: ?[]const u8 = null, lineno: u32 = 0, colno: u32 = 0, @"error": ?js.Object = null, - bubbles: bool = false, - cancelable: bool = false, }; -pub fn init(typ: []const u8, opts_: ?InitOptions, page: *Page) !*ErrorEvent { - const arena = page.arena; - const opts = opts_ orelse InitOptions{}; - - const event = try page._factory.event(typ, ErrorEvent{ - ._arena = arena, - ._proto = undefined, - ._message = if (opts.message) |str| try arena.dupe(u8, str) else "", - ._filename = if (opts.filename) |str| try arena.dupe(u8, str) else "", - ._line_number = opts.lineno, - ._column_number = opts.colno, - ._error = if (opts.@"error") |err| try err.persist() else null, - }); - - event._proto._bubbles = opts.bubbles; - event._proto._cancelable = opts.cancelable; +pub const Options = Event.inheritOptions(ErrorEvent, ErrorEventOptions); +pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*ErrorEvent { + const arena = page.arena; + const opts = opts_ orelse Options{}; + + const event = try page._factory.event( + typ, + ErrorEvent{ + ._arena = arena, + ._proto = undefined, + ._message = if (opts.message) |str| try arena.dupe(u8, str) else "", + ._filename = if (opts.filename) |str| try arena.dupe(u8, str) else "", + ._line_number = opts.lineno, + ._column_number = opts.colno, + ._error = if (opts.@"error") |err| try err.persist() else null, + }, + ); + + Event.populatePrototypes(event, opts); return event; } diff --git a/src/browser/webapi/event/MessageEvent.zig b/src/browser/webapi/event/MessageEvent.zig index ed59bb2f4..45abe7317 100644 --- a/src/browser/webapi/event/MessageEvent.zig +++ b/src/browser/webapi/event/MessageEvent.zig @@ -29,27 +29,28 @@ _data: ?js.Object = null, _origin: []const u8 = "", _source: ?*Window = null, -pub const InitOptions = struct { +const MessageEventOptions = struct { data: ?js.Object = null, origin: ?[]const u8 = null, source: ?*Window = null, - bubbles: bool = false, - cancelable: bool = false, }; -pub fn init(typ: []const u8, opts_: ?InitOptions, page: *Page) !*MessageEvent { - const opts = opts_ orelse InitOptions{}; +pub const Options = Event.inheritOptions(MessageEvent, MessageEventOptions); - const event = try page._factory.event(typ, MessageEvent{ - ._proto = undefined, - ._data = if (opts.data) |d| try d.persist() else null, - ._origin = if (opts.origin) |str| try page.arena.dupe(u8, str) else "", - ._source = opts.source, - }); +pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*MessageEvent { + const opts = opts_ orelse Options{}; - event._proto._bubbles = opts.bubbles; - event._proto._cancelable = opts.cancelable; + const event = try page._factory.event( + typ, + MessageEvent{ + ._proto = undefined, + ._data = if (opts.data) |d| try d.persist() else null, + ._origin = if (opts.origin) |str| try page.arena.dupe(u8, str) else "", + ._source = opts.source, + }, + ); + Event.populatePrototypes(event, opts); return event; } diff --git a/src/browser/webapi/event/NavigationCurrentEntryChangeEvent.zig b/src/browser/webapi/event/NavigationCurrentEntryChangeEvent.zig index f5d2a9358..6c491a75e 100644 --- a/src/browser/webapi/event/NavigationCurrentEntryChangeEvent.zig +++ b/src/browser/webapi/event/NavigationCurrentEntryChangeEvent.zig @@ -30,26 +30,37 @@ _proto: *Event, _from: *NavigationHistoryEntry, _navigation_type: ?NavigationType, -pub const EventInit = struct { +const NavigationCurrentEntryChangeEventOptions = struct { from: *NavigationHistoryEntry, navigationType: ?[]const u8 = null, }; +pub const Options = Event.inheritOptions( + NavigationCurrentEntryChangeEvent, + NavigationCurrentEntryChangeEventOptions, +); + pub fn init( typ: []const u8, - init_obj: EventInit, + opts: Options, page: *Page, ) !*NavigationCurrentEntryChangeEvent { - const navigation_type = if (init_obj.navigationType) |nav_type_str| + const navigation_type = if (opts.navigationType) |nav_type_str| std.meta.stringToEnum(NavigationType, nav_type_str) else null; - return page._factory.event(typ, NavigationCurrentEntryChangeEvent{ - ._proto = undefined, - ._from = init_obj.from, - ._navigation_type = navigation_type, - }); + const event = try page._factory.event( + typ, + NavigationCurrentEntryChangeEvent{ + ._proto = undefined, + ._from = opts.from, + ._navigation_type = navigation_type, + }, + ); + + Event.populatePrototypes(event, opts); + return event; } pub fn asEvent(self: *NavigationCurrentEntryChangeEvent) *Event { diff --git a/src/browser/webapi/event/PageTransitionEvent.zig b/src/browser/webapi/event/PageTransitionEvent.zig index 44b2c9e71..82470b1ad 100644 --- a/src/browser/webapi/event/PageTransitionEvent.zig +++ b/src/browser/webapi/event/PageTransitionEvent.zig @@ -25,18 +25,28 @@ const Page = @import("../../Page.zig"); // https://developer.mozilla.org/en-US/docs/Web/API/PageTransitionEvent const PageTransitionEvent = @This(); -const EventInit = struct { - persisted: ?bool = null, -}; - _proto: *Event, _persisted: bool, -pub fn init(typ: []const u8, init_obj: EventInit, page: *Page) !*PageTransitionEvent { - return page._factory.event(typ, PageTransitionEvent{ - ._proto = undefined, - ._persisted = init_obj.persisted orelse false, - }); +const PageTransitionEventOptions = struct { + persisted: ?bool = false, +}; + +pub const Options = Event.inheritOptions(PageTransitionEvent, PageTransitionEventOptions); + +pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*PageTransitionEvent { + const opts = _opts orelse Options{}; + + const event = try page._factory.event( + typ, + PageTransitionEvent{ + ._proto = undefined, + ._persisted = opts.persisted orelse false, + }, + ); + + Event.populatePrototypes(event, opts); + return event; } pub fn asEvent(self: *PageTransitionEvent) *Event { diff --git a/src/browser/webapi/event/PopStateEvent.zig b/src/browser/webapi/event/PopStateEvent.zig index 3ecffb997..caceddc73 100644 --- a/src/browser/webapi/event/PopStateEvent.zig +++ b/src/browser/webapi/event/PopStateEvent.zig @@ -25,20 +25,28 @@ const Page = @import("../../Page.zig"); // https://developer.mozilla.org/en-US/docs/Web/API/PopStateEvent const PopStateEvent = @This(); -const EventOptions = struct { +_proto: *Event, +_state: ?[]const u8, + +const PopStateEventOptions = struct { state: ?[]const u8 = null, }; -_proto: *Event, -_state: ?[]const u8, +pub const Options = Event.inheritOptions(PopStateEvent, PopStateEventOptions); + +pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*PopStateEvent { + const opts = _opts orelse Options{}; -pub fn init(typ: []const u8, _options: ?EventOptions, page: *Page) !*PopStateEvent { - const options = _options orelse EventOptions{}; + const event = try page._factory.event( + typ, + PopStateEvent{ + ._proto = undefined, + ._state = opts.state, + }, + ); - return page._factory.event(typ, PopStateEvent{ - ._proto = undefined, - ._state = options.state, - }); + Event.populatePrototypes(event, opts); + return event; } pub fn asEvent(self: *PopStateEvent) *Event { diff --git a/src/browser/webapi/event/ProgressEvent.zig b/src/browser/webapi/event/ProgressEvent.zig index 6e824a787..ea1f6931b 100644 --- a/src/browser/webapi/event/ProgressEvent.zig +++ b/src/browser/webapi/event/ProgressEvent.zig @@ -25,12 +25,28 @@ _total: usize = 0, _loaded: usize = 0, _length_computable: bool = false, -pub fn init(typ: []const u8, total: usize, loaded: usize, page: *Page) !*ProgressEvent { - return page._factory.event(typ, ProgressEvent{ - ._proto = undefined, - ._total = total, - ._loaded = loaded, - }); +const ProgressEventOptions = struct { + total: usize = 0, + loaded: usize = 0, + lengthComputable: bool = false, +}; + +pub const Options = Event.inheritOptions(ProgressEvent, ProgressEventOptions); + +pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*ProgressEvent { + const opts = _opts orelse Options{}; + + const event = try page._factory.event( + typ, + ProgressEvent{ + ._proto = undefined, + ._total = opts.total, + ._loaded = opts.loaded, + }, + ); + + Event.populatePrototypes(event, opts); + return event; } pub fn asEvent(self: *ProgressEvent) *Event { diff --git a/src/browser/webapi/net/XMLHttpRequestEventTarget.zig b/src/browser/webapi/net/XMLHttpRequestEventTarget.zig index 4bc16b236..af861c741 100644 --- a/src/browser/webapi/net/XMLHttpRequestEventTarget.zig +++ b/src/browser/webapi/net/XMLHttpRequestEventTarget.zig @@ -57,7 +57,11 @@ pub fn dispatch(self: *XMLHttpRequestEventTarget, comptime event_type: DispatchT }; const progress = progress_ orelse Progress{}; - const event = try ProgressEvent.init(typ, progress.total, progress.loaded, page); + const event = try ProgressEvent.init( + typ, + .{ .total = progress.total, .loaded = progress.loaded }, + page, + ); return page._event_manager.dispatchWithFunction( self.asEventTarget(), From 669c934ae053c5cfbc8e3efeff3fd1a4be1d211a Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Thu, 11 Dec 2025 12:17:07 -0800 Subject: [PATCH 210/219] Event Options dont need to be pub --- src/browser/webapi/event/CompositionEvent.zig | 2 +- src/browser/webapi/event/CustomEvent.zig | 2 +- src/browser/webapi/event/ErrorEvent.zig | 2 +- src/browser/webapi/event/MessageEvent.zig | 2 +- src/browser/webapi/event/NavigationCurrentEntryChangeEvent.zig | 2 +- src/browser/webapi/event/PageTransitionEvent.zig | 2 +- src/browser/webapi/event/PopStateEvent.zig | 2 +- src/browser/webapi/event/ProgressEvent.zig | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/browser/webapi/event/CompositionEvent.zig b/src/browser/webapi/event/CompositionEvent.zig index 3f2e83156..a758c6212 100644 --- a/src/browser/webapi/event/CompositionEvent.zig +++ b/src/browser/webapi/event/CompositionEvent.zig @@ -30,7 +30,7 @@ const CompositionEventOptions = struct { data: ?[]const u8 = null, }; -pub const Options = Event.inheritOptions(CompositionEvent, CompositionEventOptions); +const Options = Event.inheritOptions(CompositionEvent, CompositionEventOptions); pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*CompositionEvent { const opts = opts_ orelse Options{}; diff --git a/src/browser/webapi/event/CustomEvent.zig b/src/browser/webapi/event/CustomEvent.zig index a5f3db01e..1e420e8cb 100644 --- a/src/browser/webapi/event/CustomEvent.zig +++ b/src/browser/webapi/event/CustomEvent.zig @@ -33,7 +33,7 @@ const CustomEventOptions = struct { detail: ?js.Object = null, }; -pub const Options = Event.inheritOptions(CustomEvent, CustomEventOptions); +const Options = Event.inheritOptions(CustomEvent, CustomEventOptions); pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*CustomEvent { const arena = page.arena; diff --git a/src/browser/webapi/event/ErrorEvent.zig b/src/browser/webapi/event/ErrorEvent.zig index f96073237..257cf1787 100644 --- a/src/browser/webapi/event/ErrorEvent.zig +++ b/src/browser/webapi/event/ErrorEvent.zig @@ -41,7 +41,7 @@ pub const ErrorEventOptions = struct { @"error": ?js.Object = null, }; -pub const Options = Event.inheritOptions(ErrorEvent, ErrorEventOptions); +const Options = Event.inheritOptions(ErrorEvent, ErrorEventOptions); pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*ErrorEvent { const arena = page.arena; diff --git a/src/browser/webapi/event/MessageEvent.zig b/src/browser/webapi/event/MessageEvent.zig index 45abe7317..9f24c517f 100644 --- a/src/browser/webapi/event/MessageEvent.zig +++ b/src/browser/webapi/event/MessageEvent.zig @@ -35,7 +35,7 @@ const MessageEventOptions = struct { source: ?*Window = null, }; -pub const Options = Event.inheritOptions(MessageEvent, MessageEventOptions); +const Options = Event.inheritOptions(MessageEvent, MessageEventOptions); pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*MessageEvent { const opts = opts_ orelse Options{}; diff --git a/src/browser/webapi/event/NavigationCurrentEntryChangeEvent.zig b/src/browser/webapi/event/NavigationCurrentEntryChangeEvent.zig index 6c491a75e..40c8122a3 100644 --- a/src/browser/webapi/event/NavigationCurrentEntryChangeEvent.zig +++ b/src/browser/webapi/event/NavigationCurrentEntryChangeEvent.zig @@ -35,7 +35,7 @@ const NavigationCurrentEntryChangeEventOptions = struct { navigationType: ?[]const u8 = null, }; -pub const Options = Event.inheritOptions( +const Options = Event.inheritOptions( NavigationCurrentEntryChangeEvent, NavigationCurrentEntryChangeEventOptions, ); diff --git a/src/browser/webapi/event/PageTransitionEvent.zig b/src/browser/webapi/event/PageTransitionEvent.zig index 82470b1ad..2b7d063fe 100644 --- a/src/browser/webapi/event/PageTransitionEvent.zig +++ b/src/browser/webapi/event/PageTransitionEvent.zig @@ -32,7 +32,7 @@ const PageTransitionEventOptions = struct { persisted: ?bool = false, }; -pub const Options = Event.inheritOptions(PageTransitionEvent, PageTransitionEventOptions); +const Options = Event.inheritOptions(PageTransitionEvent, PageTransitionEventOptions); pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*PageTransitionEvent { const opts = _opts orelse Options{}; diff --git a/src/browser/webapi/event/PopStateEvent.zig b/src/browser/webapi/event/PopStateEvent.zig index caceddc73..45a088c59 100644 --- a/src/browser/webapi/event/PopStateEvent.zig +++ b/src/browser/webapi/event/PopStateEvent.zig @@ -32,7 +32,7 @@ const PopStateEventOptions = struct { state: ?[]const u8 = null, }; -pub const Options = Event.inheritOptions(PopStateEvent, PopStateEventOptions); +const Options = Event.inheritOptions(PopStateEvent, PopStateEventOptions); pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*PopStateEvent { const opts = _opts orelse Options{}; diff --git a/src/browser/webapi/event/ProgressEvent.zig b/src/browser/webapi/event/ProgressEvent.zig index ea1f6931b..8fbfbb87c 100644 --- a/src/browser/webapi/event/ProgressEvent.zig +++ b/src/browser/webapi/event/ProgressEvent.zig @@ -31,7 +31,7 @@ const ProgressEventOptions = struct { lengthComputable: bool = false, }; -pub const Options = Event.inheritOptions(ProgressEvent, ProgressEventOptions); +const Options = Event.inheritOptions(ProgressEvent, ProgressEventOptions); pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*ProgressEvent { const opts = _opts orelse Options{}; From 5671580c2d4b4759dbedc52c51e7b980268047a3 Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Thu, 11 Dec 2025 12:25:25 -0800 Subject: [PATCH 211/219] properly remove mbedtls submodule --- vendor/mbedtls | 1 - 1 file changed, 1 deletion(-) delete mode 160000 vendor/mbedtls diff --git a/vendor/mbedtls b/vendor/mbedtls deleted file mode 160000 index c765c831e..000000000 --- a/vendor/mbedtls +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c765c831e5c2a0971410692f92f7a81d6ec65ec2 From bd0f1d2884eab2e3c7491a23a487b9d68af56009 Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Thu, 11 Dec 2025 12:25:34 -0800 Subject: [PATCH 212/219] remove mbedtls stuff for build.zig --- build.zig | 121 ------------------------------------------------------ 1 file changed, 121 deletions(-) diff --git a/build.zig b/build.zig index 2519ec097..0070e5769 100644 --- a/build.zig +++ b/build.zig @@ -454,7 +454,6 @@ fn addDependencies(b: *Build, mod: *Build.Module, opts: *Build.Step.Options) !vo mod.linkLibrary(ssl); mod.linkLibrary(crypto); - try buildMbedtls(b, mod); try buildNghttp2(b, mod); try buildCurl(b, mod); @@ -526,126 +525,6 @@ fn buildBrotli(b: *Build, m: *Build.Module) !void { } }); } -fn buildMbedtls(b: *Build, m: *Build.Module) !void { - const mbedtls = b.addLibrary(.{ - .name = "mbedtls", - .root_module = m, - }); - - const root = "vendor/mbedtls/"; - mbedtls.addIncludePath(b.path(root ++ "include")); - mbedtls.addIncludePath(b.path(root ++ "library")); - - mbedtls.addCSourceFiles(.{ .flags = &.{}, .files = &.{ - root ++ "library/aes.c", - root ++ "library/aesni.c", - root ++ "library/aesce.c", - root ++ "library/aria.c", - root ++ "library/asn1parse.c", - root ++ "library/asn1write.c", - root ++ "library/base64.c", - root ++ "library/bignum.c", - root ++ "library/bignum_core.c", - root ++ "library/bignum_mod.c", - root ++ "library/bignum_mod_raw.c", - root ++ "library/camellia.c", - root ++ "library/ccm.c", - root ++ "library/chacha20.c", - root ++ "library/chachapoly.c", - root ++ "library/cipher.c", - root ++ "library/cipher_wrap.c", - root ++ "library/constant_time.c", - root ++ "library/cmac.c", - root ++ "library/ctr_drbg.c", - root ++ "library/des.c", - root ++ "library/dhm.c", - root ++ "library/ecdh.c", - root ++ "library/ecdsa.c", - root ++ "library/ecjpake.c", - root ++ "library/ecp.c", - root ++ "library/ecp_curves.c", - root ++ "library/entropy.c", - root ++ "library/entropy_poll.c", - root ++ "library/error.c", - root ++ "library/gcm.c", - root ++ "library/hkdf.c", - root ++ "library/hmac_drbg.c", - root ++ "library/lmots.c", - root ++ "library/lms.c", - root ++ "library/md.c", - root ++ "library/md5.c", - root ++ "library/memory_buffer_alloc.c", - root ++ "library/nist_kw.c", - root ++ "library/oid.c", - root ++ "library/padlock.c", - root ++ "library/pem.c", - root ++ "library/pk.c", - root ++ "library/pk_ecc.c", - root ++ "library/pk_wrap.c", - root ++ "library/pkcs12.c", - root ++ "library/pkcs5.c", - root ++ "library/pkparse.c", - root ++ "library/pkwrite.c", - root ++ "library/platform.c", - root ++ "library/platform_util.c", - root ++ "library/poly1305.c", - root ++ "library/psa_crypto.c", - root ++ "library/psa_crypto_aead.c", - root ++ "library/psa_crypto_cipher.c", - root ++ "library/psa_crypto_client.c", - root ++ "library/psa_crypto_ffdh.c", - root ++ "library/psa_crypto_driver_wrappers_no_static.c", - root ++ "library/psa_crypto_ecp.c", - root ++ "library/psa_crypto_hash.c", - root ++ "library/psa_crypto_mac.c", - root ++ "library/psa_crypto_pake.c", - root ++ "library/psa_crypto_rsa.c", - root ++ "library/psa_crypto_se.c", - root ++ "library/psa_crypto_slot_management.c", - root ++ "library/psa_crypto_storage.c", - root ++ "library/psa_its_file.c", - root ++ "library/psa_util.c", - root ++ "library/ripemd160.c", - root ++ "library/rsa.c", - root ++ "library/rsa_alt_helpers.c", - root ++ "library/sha1.c", - root ++ "library/sha3.c", - root ++ "library/sha256.c", - root ++ "library/sha512.c", - root ++ "library/threading.c", - root ++ "library/timing.c", - root ++ "library/version.c", - root ++ "library/version_features.c", - root ++ "library/pkcs7.c", - root ++ "library/x509.c", - root ++ "library/x509_create.c", - root ++ "library/x509_crl.c", - root ++ "library/x509_crt.c", - root ++ "library/x509_csr.c", - root ++ "library/x509write.c", - root ++ "library/x509write_crt.c", - root ++ "library/x509write_csr.c", - root ++ "library/debug.c", - root ++ "library/mps_reader.c", - root ++ "library/mps_trace.c", - root ++ "library/net_sockets.c", - root ++ "library/ssl_cache.c", - root ++ "library/ssl_ciphersuites.c", - root ++ "library/ssl_client.c", - root ++ "library/ssl_cookie.c", - root ++ "library/ssl_debug_helpers_generated.c", - root ++ "library/ssl_msg.c", - root ++ "library/ssl_ticket.c", - root ++ "library/ssl_tls.c", - root ++ "library/ssl_tls12_client.c", - root ++ "library/ssl_tls12_server.c", - root ++ "library/ssl_tls13_keys.c", - root ++ "library/ssl_tls13_server.c", - root ++ "library/ssl_tls13_client.c", - root ++ "library/ssl_tls13_generic.c", - } }); -} - fn buildNghttp2(b: *Build, m: *Build.Module) !void { const nghttp2 = b.addLibrary(.{ .name = "nghttp2", From a4fa40743abcee0f068cafd0f95ea6414d635308 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Fri, 12 Dec 2025 07:58:26 +0800 Subject: [PATCH 213/219] ErrorEvent error as undefined --- src/browser/ScriptManager.zig | 2 +- .../tests/element/html/script/order.html | 35 +++++++++++++++++++ .../tests/element/html/script/order.js | 2 ++ .../tests/element/html/script/order_async.js | 2 ++ .../tests/element/html/script/order_defer.js | 2 ++ src/browser/webapi/event/ErrorEvent.zig | 2 +- 6 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 src/browser/tests/element/html/script/order.html create mode 100644 src/browser/tests/element/html/script/order.js create mode 100644 src/browser/tests/element/html/script/order_async.js create mode 100644 src/browser/tests/element/html/script/order_defer.js diff --git a/src/browser/ScriptManager.zig b/src/browser/ScriptManager.zig index 06e940837..5f916fedc 100644 --- a/src/browser/ScriptManager.zig +++ b/src/browser/ScriptManager.zig @@ -218,7 +218,7 @@ pub fn addFromElement(self: *ScriptManager, script_element: *Element.Html.Script .url = remote_url orelse page.url, .mode = blk: { if (source == .@"inline") { - break :blk .normal; + break :blk if (kind == .module) .@"defer" else .normal; } if (element.getAttributeSafe("async") != null) { break :blk .async; diff --git a/src/browser/tests/element/html/script/order.html b/src/browser/tests/element/html/script/order.html new file mode 100644 index 000000000..159e68ccd --- /dev/null +++ b/src/browser/tests/element/html/script/order.html @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + diff --git a/src/browser/tests/element/html/script/order.js b/src/browser/tests/element/html/script/order.js new file mode 100644 index 000000000..31e602fc9 --- /dev/null +++ b/src/browser/tests/element/html/script/order.js @@ -0,0 +1,2 @@ +list += 'a'; +testing.expectEqual('a', list); diff --git a/src/browser/tests/element/html/script/order_async.js b/src/browser/tests/element/html/script/order_async.js new file mode 100644 index 000000000..343b7e69b --- /dev/null +++ b/src/browser/tests/element/html/script/order_async.js @@ -0,0 +1,2 @@ +list += 'f'; +testing.expectEqual('abcdef', list); diff --git a/src/browser/tests/element/html/script/order_defer.js b/src/browser/tests/element/html/script/order_defer.js new file mode 100644 index 000000000..3911b6445 --- /dev/null +++ b/src/browser/tests/element/html/script/order_defer.js @@ -0,0 +1,2 @@ +list += 'e'; +testing.expectEqual('abcde', list); diff --git a/src/browser/webapi/event/ErrorEvent.zig b/src/browser/webapi/event/ErrorEvent.zig index 257cf1787..08124d7f2 100644 --- a/src/browser/webapi/event/ErrorEvent.zig +++ b/src/browser/webapi/event/ErrorEvent.zig @@ -103,7 +103,7 @@ pub const JsApi = struct { pub const filename = bridge.accessor(ErrorEvent.getFilename, null, .{}); pub const lineno = bridge.accessor(ErrorEvent.getLineNumber, null, .{}); pub const colno = bridge.accessor(ErrorEvent.getColumnNumber, null, .{}); - pub const @"error" = bridge.accessor(ErrorEvent.getError, null, .{}); + pub const @"error" = bridge.accessor(ErrorEvent.getError, null, .{ .null_as_undefined = true }); }; const testing = @import("../../../testing.zig"); From 5eb54bbc95a23255bee16c6f832df9be342a335f Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Fri, 12 Dec 2025 17:34:57 +0800 Subject: [PATCH 214/219] Media/Audio/Video elements --- src/browser/Factory.zig | 7 + src/browser/Page.zig | 18 ++ src/browser/js/bridge.zig | 3 + src/browser/tests/element/html/media.html | 249 +++++++++++++++++ src/browser/webapi/Element.zig | 18 ++ src/browser/webapi/element/Html.zig | 45 +-- src/browser/webapi/element/html/Audio.zig | 49 ++++ src/browser/webapi/element/html/Media.zig | 324 ++++++++++++++++++++++ src/browser/webapi/element/html/Video.zig | 81 ++++++ 9 files changed, 775 insertions(+), 19 deletions(-) create mode 100644 src/browser/tests/element/html/media.html create mode 100644 src/browser/webapi/element/html/Audio.zig create mode 100644 src/browser/webapi/element/html/Media.zig create mode 100644 src/browser/webapi/element/html/Video.zig diff --git a/src/browser/Factory.zig b/src/browser/Factory.zig index 69cf0f529..6a0de8037 100644 --- a/src/browser/Factory.zig +++ b/src/browser/Factory.zig @@ -239,6 +239,13 @@ pub fn htmlElement(self: *Factory, child: anytype) !*@TypeOf(child) { ).create(allocator, child); } +pub fn htmlMediaElement(self: *Factory, child: anytype) !*@TypeOf(child) { + const allocator = self._slab.allocator(); + return try AutoPrototypeChain( + &.{ EventTarget, Node, Element, Element.Html, Element.Html.Media, @TypeOf(child) }, + ).create(allocator, child); +} + pub fn svgElement(self: *Factory, tag_name: []const u8, child: anytype) !*@TypeOf(child) { const allocator = self._slab.allocator(); const ChildT = @TypeOf(child); diff --git a/src/browser/Page.zig b/src/browser/Page.zig index aa4cd9e1b..c05fcb14a 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -1186,6 +1186,16 @@ pub fn createElement(self: *Page, ns_: ?[]const u8, name: []const u8, attribute_ attribute_iterator, .{ ._proto = undefined }, ), + asUint("audio") => return self.createHtmlMediaElementT( + Element.Html.Media.Audio, + namespace, + attribute_iterator, + ), + asUint("video") => return self.createHtmlMediaElementT( + Element.Html.Media.Video, + namespace, + attribute_iterator, + ), else => {}, }, 6 => switch (@as(u48, @bitCast(name[0..6].*))) { @@ -1343,6 +1353,14 @@ fn createHtmlElementT(self: *Page, comptime E: type, namespace: Element.Namespac return node; } +fn createHtmlMediaElementT(self: *Page, comptime E: type, namespace: Element.Namespace, attribute_iterator: anytype) !*Node { + const media_element = try self._factory.htmlMediaElement(E{ ._proto = undefined }); + const element = media_element.asElement(); + element._namespace = namespace; + try self.populateElementAttributes(element, attribute_iterator); + return element.asNode(); +} + fn createSvgElementT(self: *Page, comptime E: type, tag_name: []const u8, attribute_iterator: anytype, svg_element: E) !*Node { const svg_element_ptr = try self._factory.svgElement(tag_name, svg_element); var element = svg_element_ptr.asElement(); diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index b79622d06..4059d6a8e 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -526,6 +526,7 @@ pub const JsApis = flattenTypes(&.{ @import("../webapi/element/Html.zig"), @import("../webapi/element/html/IFrame.zig"), @import("../webapi/element/html/Anchor.zig"), + @import("../webapi/element/html/Audio.zig"), @import("../webapi/element/html/Body.zig"), @import("../webapi/element/html/BR.zig"), @import("../webapi/element/html/Button.zig"), @@ -544,6 +545,7 @@ pub const JsApis = flattenTypes(&.{ @import("../webapi/element/html/Input.zig"), @import("../webapi/element/html/LI.zig"), @import("../webapi/element/html/Link.zig"), + @import("../webapi/element/html/Media.zig"), @import("../webapi/element/html/Meta.zig"), @import("../webapi/element/html/OL.zig"), @import("../webapi/element/html/Option.zig"), @@ -555,6 +557,7 @@ pub const JsApis = flattenTypes(&.{ @import("../webapi/element/html/Template.zig"), @import("../webapi/element/html/TextArea.zig"), @import("../webapi/element/html/Title.zig"), + @import("../webapi/element/html/Video.zig"), @import("../webapi/element/html/UL.zig"), @import("../webapi/element/html/Unknown.zig"), @import("../webapi/element/Svg.zig"), diff --git a/src/browser/tests/element/html/media.html b/src/browser/tests/element/html/media.html new file mode 100644 index 000000000..cb6c1523f --- /dev/null +++ b/src/browser/tests/element/html/media.html @@ -0,0 +1,249 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index fd5cd75ca..7de5f14fd 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -188,6 +188,11 @@ pub fn getTagNameLower(self: *const Element) []const u8 { .input => "input", .li => "li", .link => "link", + .media => |m| switch (m._type) { + .audio => "audio", + .video => "video", + .generic => "media", + }, .meta => "meta", .ol => "ol", .option => "option", @@ -236,6 +241,11 @@ pub fn getTagNameSpec(self: *const Element, buf: []u8) []const u8 { .li => "LI", .link => "LINK", .meta => "META", + .media => |m| switch (m._type) { + .audio => "AUDIO", + .video => "VIDEO", + .generic => "MEDIA", + }, .ol => "OL", .option => "OPTION", .p => "P", @@ -1077,6 +1087,11 @@ pub fn getTag(self: *const Element) Tag { .ul => .ul, .ol => .ol, .generic => |g| g._tag, + .media => |m| switch (m._type) { + .audio => .audio, + .video => .video, + .generic => .media, + }, .script => .script, .select => .select, .slot => .slot, @@ -1103,6 +1118,7 @@ pub fn getTag(self: *const Element) Tag { pub const Tag = enum { anchor, + audio, b, body, br, @@ -1137,6 +1153,7 @@ pub const Tag = enum { link, main, meta, + media, nav, ol, option, @@ -1157,6 +1174,7 @@ pub const Tag = enum { textarea, title, ul, + video, unknown, // If the tag is "unknown", we can't use the optimized tag matching, but diff --git a/src/browser/webapi/element/Html.zig b/src/browser/webapi/element/Html.zig index e0220504f..8016c8be6 100644 --- a/src/browser/webapi/element/Html.zig +++ b/src/browser/webapi/element/Html.zig @@ -43,6 +43,7 @@ pub const Image = @import("html/Image.zig"); pub const Input = @import("html/Input.zig"); pub const LI = @import("html/LI.zig"); pub const Link = @import("html/Link.zig"); +pub const Media = @import("html/Media.zig"); pub const Meta = @import("html/Meta.zig"); pub const OL = @import("html/OL.zig"); pub const Option = @import("html/Option.zig"); @@ -89,6 +90,7 @@ pub const Type = union(enum) { input: *Input, li: *LI, link: *Link, + media: *Media, meta: *Meta, ol: *OL, option: *Option, @@ -121,37 +123,42 @@ pub fn is(self: *HtmlElement, comptime T: type) ?*T { pub fn className(self: *const HtmlElement) []const u8 { return switch (self._type) { .anchor => "[object HTMLAnchorElement]", - .div => "[object HTMLDivElement]", - .embed => "[object HTMLEmbedElement]", - .form => "[object HTMLFormElement]", - .p => "[object HTMLParagraphElement]", + .body => "[object HTMLBodyElement]", + .br => "[object HTMLBRElement]", + .button => "[object HTMLButtonElement]", .custom => "[object CUSTOM-TODO]", .data => "[object HTMLDataElement]", .dialog => "[object HTMLDialogElement]", - .img => "[object HTMLImageElement]", - .iframe => "[object HTMLIFrameElement]", - .br => "[object HTMLBRElement]", - .button => "[object HTMLButtonElement]", + .div => "[object HTMLDivElement]", + .embed => "[object HTMLEmbedElement]", + .form => "[object HTMLFormElement]", + .generic => "[object HTMLElement]", + .head => "[object HTMLHeadElement]", .heading => "[object HTMLHeadingElement]", + .hr => "[object HTMLHRElement]", + .html => "[object HTMLHtmlElement]", + .iframe => "[object HTMLIFrameElement]", + .img => "[object HTMLImageElement]", + .input => "[object HTMLInputElement]", .li => "[object HTMLLIElement]", - .ul => "[object HTMLULElement]", + .link => "[object HTMLLinkElement]", + .meta => "[object HTMLMetaElement]", + .media => |m| switch (m._type) { + .audio => "[object HTMLAudioElement]", + .video => "[object HTMLVideoElement]", + .generic => "[object HTMLMediaElement]", + }, .ol => "[object HTMLOLElement]", - .generic => "[object HTMLElement]", + .option => "[object HTMLOptionElement]", + .p => "[object HTMLParagraphElement]", .script => "[object HTMLScriptElement]", .select => "[object HTMLSelectElement]", .slot => "[object HTMLSlotElement]", + .style => "[object HTMLSyleElement]", .template => "[object HTMLTemplateElement]", - .option => "[object HTMLOptionElement]", .text_area => "[object HTMLTextAreaElement]", - .input => "[object HTMLInputElement]", - .link => "[object HTMLLinkElement]", - .meta => "[object HTMLMetaElement]", - .hr => "[object HTMLHRElement]", - .style => "[object HTMLSyleElement]", .title => "[object HTMLTitleElement]", - .body => "[object HTMLBodyElement]", - .html => "[object HTMLHtmlElement]", - .head => "[object HTMLHeadElement]", + .ul => "[object HTMLULElement]", .unknown => "[object HTMLUnknownElement]", }; } diff --git a/src/browser/webapi/element/html/Audio.zig b/src/browser/webapi/element/html/Audio.zig new file mode 100644 index 000000000..929d6acaf --- /dev/null +++ b/src/browser/webapi/element/html/Audio.zig @@ -0,0 +1,49 @@ +// 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"); +const Media = @import("Media.zig"); + +pub const Audio = @This(); + +_proto: *Media, + +pub fn asMedia(self: *Audio) *Media { + return self._proto; +} + +pub fn asElement(self: *Audio) *Element { + return self._proto.asElement(); +} + +pub fn asNode(self: *Audio) *Node { + return self.asElement().asNode(); +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(Audio); + + pub const Meta = struct { + pub const name = "HTMLAudioElement"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; +}; diff --git a/src/browser/webapi/element/html/Media.zig b/src/browser/webapi/element/html/Media.zig new file mode 100644 index 000000000..dc29e1609 --- /dev/null +++ b/src/browser/webapi/element/html/Media.zig @@ -0,0 +1,324 @@ +// 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 Node = @import("../../Node.zig"); +const Element = @import("../../Element.zig"); +const HtmlElement = @import("../Html.zig"); +pub const Audio = @import("Audio.zig"); +pub const Video = @import("Video.zig"); +const MediaError = @import("../../media/MediaError.zig"); + +const Media = @This(); + +pub const ReadyState = enum(u16) { + HAVE_NOTHING = 0, + HAVE_METADATA = 1, + HAVE_CURRENT_DATA = 2, + HAVE_FUTURE_DATA = 3, + HAVE_ENOUGH_DATA = 4, +}; + +pub const NetworkState = enum(u16) { + NETWORK_EMPTY = 0, + NETWORK_IDLE = 1, + NETWORK_LOADING = 2, + NETWORK_NO_SOURCE = 3, +}; + +pub const Type = union(enum) { + generic, + audio: *Audio, + video: *Video, +}; + +_type: Type, +_proto: *HtmlElement, +_paused: bool = true, +_current_time: f64 = 0, +_volume: f64 = 1.0, +_muted: bool = false, +_playback_rate: f64 = 1.0, +_ready_state: ReadyState = .HAVE_NOTHING, +_network_state: NetworkState = .NETWORK_EMPTY, +_error: ?*MediaError = null, + +pub fn asElement(self: *Media) *Element { + return self._proto._proto; +} +pub fn asConstElement(self: *const Media) *const Element { + return self._proto._proto; +} +pub fn asNode(self: *Media) *Node { + return self.asElement().asNode(); +} + +pub fn is(self: *Media, comptime T: type) ?*T { + const type_name = @typeName(T); + switch (self._type) { + .audio => |a| { + if (T == *Audio) return a; + if (comptime std.mem.startsWith(u8, type_name, "browser.webapi.element.html.Audio")) { + return a; + } + }, + .video => |v| { + if (T == *Video) return v; + if (comptime std.mem.startsWith(u8, type_name, "browser.webapi.element.html.Video")) { + return v; + } + }, + .generic => {}, + } + return null; +} + +pub fn as(self: *Media, comptime T: type) *T { + return self.is(T).?; +} + +pub fn canPlayType(_: *const Media, mime_type: []const u8, page: *Page) []const u8 { + const pos = std.mem.indexOfScalar(u8, mime_type, ';') orelse mime_type.len; + const base_type = std.mem.trim(u8, mime_type[0..pos], &std.ascii.whitespace); + + if (base_type.len > page.buf.len) { + return ""; + } + const lower = std.ascii.lowerString(&page.buf, base_type); + + if (isProbablySupported(lower)) { + return "probably"; + } + if (isMaybeSupported(lower)) { + return "maybe"; + } + return ""; +} + +fn isProbablySupported(mime_type: []const u8) bool { + if (std.mem.eql(u8, mime_type, "video/mp4")) return true; + if (std.mem.eql(u8, mime_type, "video/webm")) return true; + if (std.mem.eql(u8, mime_type, "audio/mp4")) return true; + if (std.mem.eql(u8, mime_type, "audio/webm")) return true; + if (std.mem.eql(u8, mime_type, "audio/mpeg")) return true; + if (std.mem.eql(u8, mime_type, "audio/mp3")) return true; + if (std.mem.eql(u8, mime_type, "audio/ogg")) return true; + if (std.mem.eql(u8, mime_type, "video/ogg")) return true; + if (std.mem.eql(u8, mime_type, "audio/wav")) return true; + if (std.mem.eql(u8, mime_type, "audio/wave")) return true; + if (std.mem.eql(u8, mime_type, "audio/x-wav")) return true; + return false; +} + +fn isMaybeSupported(mime_type: []const u8) bool { + if (std.mem.eql(u8, mime_type, "audio/aac")) return true; + if (std.mem.eql(u8, mime_type, "audio/x-m4a")) return true; + if (std.mem.eql(u8, mime_type, "video/x-m4v")) return true; + if (std.mem.eql(u8, mime_type, "audio/flac")) return true; + return false; +} + +pub fn play(self: *Media) void { + self._paused = false; + self._ready_state = .HAVE_ENOUGH_DATA; + self._network_state = .NETWORK_IDLE; + // TODO: Could dispatch 'play' and 'playing' events +} + +pub fn pause(self: *Media) void { + self._paused = true; + // TODO: Could dispatch 'pause' event +} + +pub fn load(self: *Media) void { + self._paused = true; + self._current_time = 0; + self._ready_state = .HAVE_NOTHING; + self._network_state = .NETWORK_LOADING; + self._error = null; + // TODO: Could dispatch events +} + +pub fn getPaused(self: *const Media) bool { + return self._paused; +} + +pub fn getCurrentTime(self: *const Media) f64 { + return self._current_time; +} + +pub fn getDuration(_: *const Media) f64 { + return std.math.nan(f64); +} + +pub fn getReadyState(self: *const Media) u16 { + return @intFromEnum(self._ready_state); +} + +pub fn getNetworkState(self: *const Media) u16 { + return @intFromEnum(self._network_state); +} + +pub fn getEnded(_: *const Media) bool { + return false; +} + +pub fn getSeeking(_: *const Media) bool { + return false; +} + +pub fn getError(self: *const Media) ?*MediaError { + return self._error; +} + +pub fn getVolume(self: *const Media) f64 { + return self._volume; +} + +pub fn setVolume(self: *Media, value: f64) void { + self._volume = @max(0.0, @min(1.0, value)); +} + +pub fn getMuted(self: *const Media) bool { + return self._muted; +} + +pub fn setMuted(self: *Media, value: bool) void { + self._muted = value; +} + +pub fn getPlaybackRate(self: *const Media) f64 { + return self._playback_rate; +} + +pub fn setPlaybackRate(self: *Media, value: f64) void { + self._playback_rate = value; +} + +pub fn setCurrentTime(self: *Media, value: f64) void { + self._current_time = value; +} + +pub fn getSrc(self: *const Media, page: *Page) ![]const u8 { + const element = self.asConstElement(); + const src = element.getAttributeSafe("src") orelse return ""; + if (src.len == 0) { + return ""; + } + const URL = @import("../../URL.zig"); + return URL.resolve(page.call_arena, page.url, src, .{}); +} + +pub fn setSrc(self: *Media, value: []const u8, page: *Page) !void { + try self.asElement().setAttributeSafe("src", value, page); +} + +pub fn getAutoplay(self: *const Media) bool { + return self.asConstElement().getAttributeSafe("autoplay") != null; +} + +pub fn setAutoplay(self: *Media, value: bool, page: *Page) !void { + if (value) { + try self.asElement().setAttributeSafe("autoplay", "", page); + } else { + try self.asElement().removeAttribute("autoplay", page); + } +} + +pub fn getControls(self: *const Media) bool { + return self.asConstElement().getAttributeSafe("controls") != null; +} + +pub fn setControls(self: *Media, value: bool, page: *Page) !void { + if (value) { + try self.asElement().setAttributeSafe("controls", "", page); + } else { + try self.asElement().removeAttribute("controls", page); + } +} + +pub fn getLoop(self: *const Media) bool { + return self.asConstElement().getAttributeSafe("loop") != null; +} + +pub fn setLoop(self: *Media, value: bool, page: *Page) !void { + if (value) { + try self.asElement().setAttributeSafe("loop", "", page); + } else { + try self.asElement().removeAttribute("loop", page); + } +} + +pub fn getPreload(self: *const Media) []const u8 { + return self.asConstElement().getAttributeSafe("preload") orelse "auto"; +} + +pub fn setPreload(self: *Media, value: []const u8, page: *Page) !void { + try self.asElement().setAttributeSafe("preload", value, page); +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(Media); + + pub const Meta = struct { + pub const name = "HTMLMediaElement"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; + + pub const NETWORK_EMPTY = bridge.property(@intFromEnum(NetworkState.NETWORK_EMPTY)); + pub const NETWORK_IDLE = bridge.property(@intFromEnum(NetworkState.NETWORK_IDLE)); + pub const NETWORK_LOADING = bridge.property(@intFromEnum(NetworkState.NETWORK_LOADING)); + pub const NETWORK_NO_SOURCE = bridge.property(@intFromEnum(NetworkState.NETWORK_NO_SOURCE)); + + pub const HAVE_NOTHING = bridge.property(@intFromEnum(ReadyState.HAVE_NOTHING)); + pub const HAVE_METADATA = bridge.property(@intFromEnum(ReadyState.HAVE_METADATA)); + pub const HAVE_CURRENT_DATA = bridge.property(@intFromEnum(ReadyState.HAVE_CURRENT_DATA)); + pub const HAVE_FUTURE_DATA = bridge.property(@intFromEnum(ReadyState.HAVE_FUTURE_DATA)); + pub const HAVE_ENOUGH_DATA = bridge.property(@intFromEnum(ReadyState.HAVE_ENOUGH_DATA)); + + pub const src = bridge.accessor(Media.getSrc, Media.setSrc, .{}); + pub const autoplay = bridge.accessor(Media.getAutoplay, Media.setAutoplay, .{}); + pub const controls = bridge.accessor(Media.getControls, Media.setControls, .{}); + pub const loop = bridge.accessor(Media.getLoop, Media.setLoop, .{}); + pub const muted = bridge.accessor(Media.getMuted, Media.setMuted, .{}); + pub const preload = bridge.accessor(Media.getPreload, Media.setPreload, .{}); + pub const volume = bridge.accessor(Media.getVolume, Media.setVolume, .{}); + pub const playbackRate = bridge.accessor(Media.getPlaybackRate, Media.setPlaybackRate, .{}); + pub const currentTime = bridge.accessor(Media.getCurrentTime, Media.setCurrentTime, .{}); + pub const duration = bridge.accessor(Media.getDuration, null, .{}); + pub const paused = bridge.accessor(Media.getPaused, null, .{}); + pub const ended = bridge.accessor(Media.getEnded, null, .{}); + pub const seeking = bridge.accessor(Media.getSeeking, null, .{}); + pub const readyState = bridge.accessor(Media.getReadyState, null, .{}); + pub const networkState = bridge.accessor(Media.getNetworkState, null, .{}); + pub const @"error" = bridge.accessor(Media.getError, null, .{}); + + pub const canPlayType = bridge.function(Media.canPlayType, .{}); + pub const play = bridge.function(Media.play, .{}); + pub const pause = bridge.function(Media.pause, .{}); + pub const load = bridge.function(Media.load, .{}); +}; + +const testing = @import("../../../../testing.zig"); +test "WebApi: Media" { + try testing.htmlRunner("element/html/media.html", .{}); +} diff --git a/src/browser/webapi/element/html/Video.zig b/src/browser/webapi/element/html/Video.zig new file mode 100644 index 000000000..66eb3f774 --- /dev/null +++ b/src/browser/webapi/element/html/Video.zig @@ -0,0 +1,81 @@ +// 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"); + +const Node = @import("../../Node.zig"); +const Element = @import("../../Element.zig"); +const Media = @import("Media.zig"); + +pub const Video = @This(); + +_proto: *Media, + +pub fn asMedia(self: *Video) *Media { + return self._proto; +} + +pub fn asElement(self: *Video) *Element { + return self._proto.asElement(); +} + +pub fn asConstElement(self: *const Video) *const Element { + return self._proto.asConstElement(); +} + +pub fn asNode(self: *Video) *Node { + return self.asElement().asNode(); +} + +pub fn getVideoWidth(_: *const Video) u32 { + return 0; +} + +pub fn getVideoHeight(_: *const Video) u32 { + return 0; +} + +pub fn getPoster(self: *const Video, page: *Page) ![]const u8 { + const element = self.asConstElement(); + const poster = element.getAttributeSafe("poster") orelse return ""; + if (poster.len == 0) { + return ""; + } + + const URL = @import("../../URL.zig"); + return URL.resolve(page.call_arena, page.url, poster, .{}); +} + +pub fn setPoster(self: *Video, value: []const u8, page: *Page) !void { + try self.asElement().setAttributeSafe("poster", value, page); +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(Video); + + pub const Meta = struct { + pub const name = "HTMLVideoElement"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; + + pub const poster = bridge.accessor(Video.getPoster, Video.setPoster, .{}); + pub const videoWidth = bridge.accessor(Video.getVideoWidth, null, .{}); + pub const videoHeight = bridge.accessor(Video.getVideoHeight, null, .{}); +}; From a6d3a3d0ab71c94b0f77f0e761095cea78c12625 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Fri, 12 Dec 2025 18:01:12 +0800 Subject: [PATCH 215/219] Add properties to HTMLStyleelement --- src/browser/tests/element/html/style.html | 69 +++++++++++++++++++ .../tests/legacy/cssom/css_stylesheet.html | 2 +- src/browser/tests/legacy/html/style.html | 4 +- src/browser/webapi/css/CSSStyleSheet.zig | 18 ++++- src/browser/webapi/element/html/Audio.zig | 2 +- src/browser/webapi/element/html/Style.zig | 60 ++++++++++++++++ src/browser/webapi/element/html/Video.zig | 2 +- 7 files changed, 150 insertions(+), 7 deletions(-) create mode 100644 src/browser/tests/element/html/style.html diff --git a/src/browser/tests/element/html/style.html b/src/browser/tests/element/html/style.html new file mode 100644 index 000000000..6c406d8a1 --- /dev/null +++ b/src/browser/tests/element/html/style.html @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + diff --git a/src/browser/tests/legacy/cssom/css_stylesheet.html b/src/browser/tests/legacy/cssom/css_stylesheet.html index 223ee2cdb..ed2e876bf 100644 --- a/src/browser/tests/legacy/cssom/css_stylesheet.html +++ b/src/browser/tests/legacy/cssom/css_stylesheet.html @@ -8,7 +8,7 @@ let index1 = css.insertRule('body { color: red; }', 0); testing.expectEqual(0, index1); - testing.expectEqual(1, css.cssRules.length); + testing.expectEqual(0, css.cssRules.length); let replaced = false; css.replace('body{}').then(() => replaced = true); diff --git a/src/browser/tests/legacy/html/style.html b/src/browser/tests/legacy/html/style.html index 6463cd815..e92cd3cab 100644 --- a/src/browser/tests/legacy/html/style.html +++ b/src/browser/tests/legacy/html/style.html @@ -2,7 +2,5 @@ diff --git a/src/browser/webapi/css/CSSStyleSheet.zig b/src/browser/webapi/css/CSSStyleSheet.zig index a377618d5..2f8b76fb5 100644 --- a/src/browser/webapi/css/CSSStyleSheet.zig +++ b/src/browser/webapi/css/CSSStyleSheet.zig @@ -62,6 +62,19 @@ pub fn deleteRule(self: *CSSStyleSheet, index: u32, page: *Page) !void { _ = page; } +pub fn replace(self: *CSSStyleSheet, text: []const u8, page: *Page) !js.Promise { + _ = self; + _ = text; + // TODO: clear self.css_rules + return page.js.resolvePromise({}); +} + +pub fn replaceSync(self: *CSSStyleSheet, text: []const u8) !void { + _ = self; + _ = text; + // TODO: clear self.css_rules +} + pub const JsApi = struct { pub const bridge = js.Bridge(CSSStyleSheet); @@ -71,14 +84,17 @@ pub const JsApi = struct { pub var class_id: bridge.ClassId = undefined; }; + pub const constructor = bridge.constructor(CSSStyleSheet.init, .{}); pub const ownerNode = bridge.accessor(CSSStyleSheet.getOwnerNode, null, .{ .null_as_undefined = true }); pub const href = bridge.accessor(CSSStyleSheet.getHref, null, .{ .null_as_undefined = true }); pub const title = bridge.accessor(CSSStyleSheet.getTitle, null, .{}); pub const disabled = bridge.accessor(CSSStyleSheet.getDisabled, CSSStyleSheet.setDisabled, .{}); pub const cssRules = bridge.accessor(CSSStyleSheet.getCssRules, null, .{}); - pub const ownerRule = bridge.accessor(CSSStyleSheet.getOwnerRule, null, .{ .null_as_undefined = true }); + pub const ownerRule = bridge.accessor(CSSStyleSheet.getOwnerRule, null, .{}); pub const insertRule = bridge.function(CSSStyleSheet.insertRule, .{}); pub const deleteRule = bridge.function(CSSStyleSheet.deleteRule, .{}); + pub const replace = bridge.function(CSSStyleSheet.replace, .{}); + pub const replaceSync = bridge.function(CSSStyleSheet.replaceSync, .{}); }; const testing = @import("../../../testing.zig"); diff --git a/src/browser/webapi/element/html/Audio.zig b/src/browser/webapi/element/html/Audio.zig index 929d6acaf..11572f99c 100644 --- a/src/browser/webapi/element/html/Audio.zig +++ b/src/browser/webapi/element/html/Audio.zig @@ -22,7 +22,7 @@ const Node = @import("../../Node.zig"); const Element = @import("../../Element.zig"); const Media = @import("Media.zig"); -pub const Audio = @This(); +const Audio = @This(); _proto: *Media, diff --git a/src/browser/webapi/element/html/Style.zig b/src/browser/webapi/element/html/Style.zig index d774e93e9..7f3852387 100644 --- a/src/browser/webapi/element/html/Style.zig +++ b/src/browser/webapi/element/html/Style.zig @@ -17,6 +17,8 @@ // along with this program. If not, see . const js = @import("../../../js/js.zig"); +const Page = @import("../../../Page.zig"); + const Node = @import("../../Node.zig"); const Element = @import("../../Element.zig"); const HtmlElement = @import("../Html.zig"); @@ -27,10 +29,57 @@ _proto: *HtmlElement, pub fn asElement(self: *Style) *Element { return self._proto._proto; } +pub fn asConstElement(self: *const Style) *const Element { + return self._proto._proto; +} pub fn asNode(self: *Style) *Node { return self.asElement().asNode(); } +// Attribute-backed properties + +pub fn getBlocking(self: *const Style) []const u8 { + return self.asConstElement().getAttributeSafe("blocking") orelse ""; +} + +pub fn setBlocking(self: *Style, value: []const u8, page: *Page) !void { + try self.asElement().setAttributeSafe("blocking", value, page); +} + +pub fn getMedia(self: *const Style) []const u8 { + return self.asConstElement().getAttributeSafe("media") orelse ""; +} + +pub fn setMedia(self: *Style, value: []const u8, page: *Page) !void { + try self.asElement().setAttributeSafe("media", value, page); +} + +pub fn getType(self: *const Style) []const u8 { + return self.asConstElement().getAttributeSafe("type") orelse "text/css"; +} + +pub fn setType(self: *Style, value: []const u8, page: *Page) !void { + try self.asElement().setAttributeSafe("type", value, page); +} + +pub fn getDisabled(self: *const Style) bool { + return self.asConstElement().getAttributeSafe("disabled") != null; +} + +pub fn setDisabled(self: *Style, disabled: bool, page: *Page) !void { + if (disabled) { + try self.asElement().setAttributeSafe("disabled", "", page); + } else { + try self.asElement().removeAttribute("disabled", page); + } +} + +const CSSStyleSheet = @import("../../css/CSSStyleSheet.zig"); +pub fn getSheet(_: *const Style) ?*CSSStyleSheet { + // TODO? + return null; +} + pub const JsApi = struct { pub const bridge = js.Bridge(Style); @@ -39,4 +88,15 @@ pub const JsApi = struct { pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; }; + + pub const blocking = bridge.accessor(Style.getBlocking, Style.setBlocking, .{}); + pub const media = bridge.accessor(Style.getMedia, Style.setMedia, .{}); + pub const @"type" = bridge.accessor(Style.getType, Style.setType, .{}); + pub const disabled = bridge.accessor(Style.getDisabled, Style.setDisabled, .{}); + pub const sheet = bridge.accessor(Style.getSheet, null, .{}); }; + +const testing = @import("../../../../testing.zig"); +test "WebApi: Style" { + try testing.htmlRunner("element/html/style.html", .{}); +} diff --git a/src/browser/webapi/element/html/Video.zig b/src/browser/webapi/element/html/Video.zig index 66eb3f774..cfe19da0f 100644 --- a/src/browser/webapi/element/html/Video.zig +++ b/src/browser/webapi/element/html/Video.zig @@ -23,7 +23,7 @@ const Node = @import("../../Node.zig"); const Element = @import("../../Element.zig"); const Media = @import("Media.zig"); -pub const Video = @This(); +const Video = @This(); _proto: *Media, From 23146f64abc952b306a8b7267763fa74b9590872 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Fri, 12 Dec 2025 18:21:30 +0800 Subject: [PATCH 216/219] Screen and ScreenOrientation (legacy) --- src/browser/EventManager.zig | 2 +- src/browser/Page.zig | 3 + src/browser/tests/legacy/html/screen.html | 2 +- src/browser/tests/legacy/html/template.html | 168 -------------------- src/browser/tests/window/screen.html | 21 +++ src/browser/webapi/EventTarget.zig | 2 + src/browser/webapi/Screen.zig | 77 +++++++-- src/browser/webapi/Window.zig | 4 +- 8 files changed, 97 insertions(+), 182 deletions(-) delete mode 100644 src/browser/tests/legacy/html/template.html create mode 100644 src/browser/tests/window/screen.html diff --git a/src/browser/EventManager.zig b/src/browser/EventManager.zig index 1bda2ec8a..d458a3b2e 100644 --- a/src/browser/EventManager.zig +++ b/src/browser/EventManager.zig @@ -119,7 +119,7 @@ pub fn dispatch(self: *EventManager, target: *EventTarget, event: *Event) !void switch (target._type) { .node => |node| try self.dispatchNode(node, event, &was_handled), - .xhr, .window, .abort_signal, .media_query_list, .message_port, .text_track_cue, .navigation => { + .xhr, .window, .abort_signal, .media_query_list, .message_port, .text_track_cue, .navigation, .screen, .screen_orientation => { const list = self.lookup.getPtr(@intFromPtr(target)) orelse return; try self.dispatchAll(list, target, event, &was_handled); }, diff --git a/src/browser/Page.zig b/src/browser/Page.zig index c05fcb14a..fab0690b3 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -52,6 +52,7 @@ const Document = @import("webapi/Document.zig"); const DocumentFragment = @import("webapi/DocumentFragment.zig"); const ShadowRoot = @import("webapi/ShadowRoot.zig"); const Performance = @import("webapi/Performance.zig"); +const Screen = @import("webapi/Screen.zig"); const HtmlScript = @import("webapi/Element.zig").Html.Script; const MutationObserver = @import("webapi/MutationObserver.zig"); const IntersectionObserver = @import("webapi/IntersectionObserver.zig"); @@ -209,12 +210,14 @@ fn reset(self: *Page, comptime initializing: bool) !void { if (comptime initializing == true) { const storage_bucket = try self._factory.create(storage.Bucket{}); + const screen = try Screen.init(self); self.window = try self._factory.eventTarget(Window{ ._document = self.document, ._storage_bucket = storage_bucket, ._performance = Performance.init(), ._proto = undefined, ._location = &default_location, + ._screen = screen, }); } else { self.window._document = self.document; diff --git a/src/browser/tests/legacy/html/screen.html b/src/browser/tests/legacy/html/screen.html index 82f4b71cc..5239ba434 100644 --- a/src/browser/tests/legacy/html/screen.html +++ b/src/browser/tests/legacy/html/screen.html @@ -17,5 +17,5 @@ diff --git a/src/browser/tests/legacy/html/template.html b/src/browser/tests/legacy/html/template.html deleted file mode 100644 index bc6055846..000000000 --- a/src/browser/tests/legacy/html/template.html +++ /dev/null @@ -1,168 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/browser/tests/window/screen.html b/src/browser/tests/window/screen.html new file mode 100644 index 000000000..5239ba434 --- /dev/null +++ b/src/browser/tests/window/screen.html @@ -0,0 +1,21 @@ + + + + + + diff --git a/src/browser/webapi/EventTarget.zig b/src/browser/webapi/EventTarget.zig index 4f0eabfe5..b399f7cb7 100644 --- a/src/browser/webapi/EventTarget.zig +++ b/src/browser/webapi/EventTarget.zig @@ -38,6 +38,8 @@ pub const Type = union(enum) { message_port: *@import("MessagePort.zig"), text_track_cue: *@import("media/TextTrackCue.zig"), navigation: *@import("navigation/NavigationEventTarget.zig"), + screen: *@import("Screen.zig"), + screen_orientation: *@import("Screen.zig").Orientation, }; pub fn dispatchEvent(self: *EventTarget, event: *Event, page: *Page) !bool { diff --git a/src/browser/webapi/Screen.zig b/src/browser/webapi/Screen.zig index 1ed5b1396..a9e28e8c5 100644 --- a/src/browser/webapi/Screen.zig +++ b/src/browser/webapi/Screen.zig @@ -17,42 +17,65 @@ // along with this program. If not, see . const js = @import("../js/js.zig"); +const Page = @import("../Page.zig"); +const EventTarget = @import("EventTarget.zig"); + +pub fn registerTypes() []const type { + return &.{ + Screen, + Orientation, + }; +} const Screen = @This(); -_pad: bool = false, -pub const init: Screen = .{}; +_proto: *EventTarget, +_orientation: ?*Orientation = null, + +pub fn init(page: *Page) !*Screen { + return page._factory.eventTarget(Screen{ + ._proto = undefined, + ._orientation = null, + }); +} + +pub fn asEventTarget(self: *Screen) *EventTarget { + return self._proto; +} -/// Total width of the screen in pixels pub fn getWidth(_: *const Screen) u32 { return 1920; } -/// Total height of the screen in pixels pub fn getHeight(_: *const Screen) u32 { return 1080; } -/// Available width (excluding OS UI elements like taskbar) pub fn getAvailWidth(_: *const Screen) u32 { return 1920; } -/// Available height (excluding OS UI elements like taskbar) pub fn getAvailHeight(_: *const Screen) u32 { return 1040; // 40px reserved for taskbar/dock } -/// Color depth in bits per pixel pub fn getColorDepth(_: *const Screen) u32 { return 24; } -/// Pixel depth in bits per pixel (typically same as colorDepth) pub fn getPixelDepth(_: *const Screen) u32 { return 24; } +pub fn getOrientation(self: *Screen, page: *Page) !*Orientation { + if (self._orientation) |orientation| { + return orientation; + } + const orientation = try Orientation.init(page); + self._orientation = orientation; + return orientation; +} + pub const JsApi = struct { pub const bridge = js.Bridge(Screen); @@ -60,14 +83,48 @@ pub const JsApi = struct { pub const name = "Screen"; pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; - pub const empty_with_no_proto = true; }; - // Read-only properties pub const width = bridge.accessor(Screen.getWidth, null, .{}); pub const height = bridge.accessor(Screen.getHeight, null, .{}); pub const availWidth = bridge.accessor(Screen.getAvailWidth, null, .{}); pub const availHeight = bridge.accessor(Screen.getAvailHeight, null, .{}); pub const colorDepth = bridge.accessor(Screen.getColorDepth, null, .{}); pub const pixelDepth = bridge.accessor(Screen.getPixelDepth, null, .{}); + pub const orientation = bridge.accessor(Screen.getOrientation, null, .{}); +}; + +pub const Orientation = struct { + _proto: *EventTarget, + + pub fn init(page: *Page) !*Orientation { + return page._factory.eventTarget(Orientation{ + ._proto = undefined, + }); + } + + pub fn asEventTarget(self: *Orientation) *EventTarget { + return self._proto; + } + + pub fn getAngle(_: *const Orientation) u32 { + return 0; + } + + pub fn getType(_: *const Orientation) []const u8 { + return "landscape-primary"; + } + + pub const JsApi = struct { + pub const bridge = js.Bridge(Orientation); + + pub const Meta = struct { + pub const name = "ScreenOrientation"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; + + pub const angle = bridge.accessor(Orientation.getAngle, null, .{}); + pub const @"type" = bridge.accessor(Orientation.getType, null, .{}); + }; }; diff --git a/src/browser/webapi/Window.zig b/src/browser/webapi/Window.zig index 792ca8ddf..51213ecda 100644 --- a/src/browser/webapi/Window.zig +++ b/src/browser/webapi/Window.zig @@ -50,7 +50,7 @@ _css: CSS = .init, _crypto: Crypto = .init, _console: Console = .init, _navigator: Navigator = .init, -_screen: Screen = .init, +_screen: *Screen, _performance: Performance, _storage_bucket: *storage.Bucket, _on_load: ?js.Function = null, @@ -88,7 +88,7 @@ pub fn getNavigator(self: *Window) *Navigator { } pub fn getScreen(self: *Window) *Screen { - return &self._screen; + return self._screen; } pub fn getCrypto(self: *Window) *Crypto { From eab328e2b5f8ce513853f934566111eba42ef731 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Fri, 12 Dec 2025 21:50:13 +0800 Subject: [PATCH 217/219] Tweak URL, refactor Anchor and URL to share more common code --- src/browser/URL.zig | 164 +++++++++++- src/browser/tests/legacy/url/url.html | 10 +- src/browser/tests/url.html | 294 +++++++++++++++++++++ src/browser/webapi/Location.zig | 4 +- src/browser/webapi/URL.zig | 110 +++++++- src/browser/webapi/element/html/Anchor.zig | 121 +-------- src/browser/webapi/net/URLSearchParams.zig | 6 +- 7 files changed, 575 insertions(+), 134 deletions(-) diff --git a/src/browser/URL.zig b/src/browser/URL.zig index 4a399a3b1..7a3547c24 100644 --- a/src/browser/URL.zig +++ b/src/browser/URL.zig @@ -128,7 +128,7 @@ fn isNullTerminated(comptime value: type) bool { } pub fn isCompleteHTTPUrl(url: []const u8) bool { - if (url.len < 6) { + if (url.len < 3) { // Minimum is "x://" return false; } @@ -137,9 +137,32 @@ pub fn isCompleteHTTPUrl(url: []const u8) bool { return false; } - return std.ascii.startsWithIgnoreCase(url, "https://") or - std.ascii.startsWithIgnoreCase(url, "http://") or - std.ascii.startsWithIgnoreCase(url, "ftp://"); + // Check if there's a scheme (protocol) ending with :// + const colon_pos = std.mem.indexOfScalar(u8, url, ':') orelse return false; + + // Check if it's followed by // + if (colon_pos + 2 >= url.len or url[colon_pos + 1] != '/' or url[colon_pos + 2] != '/') { + return false; + } + + // Validate that everything before the colon is a valid scheme + // A scheme must start with a letter and contain only letters, digits, +, -, . + if (colon_pos == 0) { + return false; + } + + const scheme = url[0..colon_pos]; + if (!std.ascii.isAlphabetic(scheme[0])) { + return false; + } + + for (scheme[1..]) |c| { + if (!std.ascii.isAlphanumeric(c) and c != '+' and c != '-' and c != '.') { + return false; + } + } + + return true; } pub fn getUsername(raw: [:0]const u8) []const u8 { @@ -278,6 +301,139 @@ pub fn eqlDocument(first: [:0]const u8, second: [:0]const u8) bool { return std.mem.eql(u8, first[0..first_hash_index], second[0..second_hash_index]); } +// Helper function to build a URL from components +pub fn buildUrl( + allocator: Allocator, + protocol: []const u8, + host: []const u8, + pathname: []const u8, + search: []const u8, + hash: []const u8, +) ![:0]const u8 { + return std.fmt.allocPrintSentinel(allocator, "{s}//{s}{s}{s}{s}", .{ + protocol, + host, + pathname, + search, + hash, + }, 0); +} + +pub fn setProtocol(current: [:0]const u8, value: []const u8, allocator: Allocator) ![:0]const u8 { + const host = getHost(current); + const pathname = getPathname(current); + const search = getSearch(current); + const hash = getHash(current); + + // Add : suffix if not present + const protocol = if (value.len > 0 and value[value.len - 1] != ':') + try std.fmt.allocPrint(allocator, "{s}:", .{value}) + else + value; + + return buildUrl(allocator, protocol, host, pathname, search, hash); +} + +pub fn setHost(current: [:0]const u8, value: []const u8, allocator: Allocator) ![:0]const u8 { + const protocol = getProtocol(current); + const pathname = getPathname(current); + const search = getSearch(current); + const hash = getHash(current); + + // Check if the host includes a port + const colon_pos = std.mem.lastIndexOfScalar(u8, value, ':'); + const clean_host = if (colon_pos) |pos| blk: { + const port_str = value[pos + 1 ..]; + // Remove default ports + if (std.mem.eql(u8, protocol, "https:") and std.mem.eql(u8, port_str, "443")) { + break :blk value[0..pos]; + } + if (std.mem.eql(u8, protocol, "http:") and std.mem.eql(u8, port_str, "80")) { + break :blk value[0..pos]; + } + break :blk value; + } else value; + + return buildUrl(allocator, protocol, clean_host, pathname, search, hash); +} + +pub fn setHostname(current: [:0]const u8, value: []const u8, allocator: Allocator) ![:0]const u8 { + const current_port = getPort(current); + const new_host = if (current_port.len > 0) + try std.fmt.allocPrint(allocator, "{s}:{s}", .{ value, current_port }) + else + value; + + return setHost(current, new_host, allocator); +} + +pub fn setPort(current: [:0]const u8, value: ?[]const u8, allocator: Allocator) ![:0]const u8 { + const hostname = getHostname(current); + const protocol = getProtocol(current); + + // Handle null or default ports + const new_host = if (value) |port_str| blk: { + if (port_str.len == 0) { + break :blk hostname; + } + // Check if this is a default port for the protocol + if (std.mem.eql(u8, protocol, "https:") and std.mem.eql(u8, port_str, "443")) { + break :blk hostname; + } + if (std.mem.eql(u8, protocol, "http:") and std.mem.eql(u8, port_str, "80")) { + break :blk hostname; + } + break :blk try std.fmt.allocPrint(allocator, "{s}:{s}", .{ hostname, port_str }); + } else hostname; + + return setHost(current, new_host, allocator); +} + +pub fn setPathname(current: [:0]const u8, value: []const u8, allocator: Allocator) ![:0]const u8 { + const protocol = getProtocol(current); + const host = getHost(current); + const search = getSearch(current); + const hash = getHash(current); + + // Add / prefix if not present and value is not empty + const pathname = if (value.len > 0 and value[0] != '/') + try std.fmt.allocPrint(allocator, "/{s}", .{value}) + else + value; + + return buildUrl(allocator, protocol, host, pathname, search, hash); +} + +pub fn setSearch(current: [:0]const u8, value: []const u8, allocator: Allocator) ![:0]const u8 { + const protocol = getProtocol(current); + const host = getHost(current); + const pathname = getPathname(current); + const hash = getHash(current); + + // Add ? prefix if not present and value is not empty + const search = if (value.len > 0 and value[0] != '?') + try std.fmt.allocPrint(allocator, "?{s}", .{value}) + else + value; + + return buildUrl(allocator, protocol, host, pathname, search, hash); +} + +pub fn setHash(current: [:0]const u8, value: []const u8, allocator: Allocator) ![:0]const u8 { + const protocol = getProtocol(current); + const host = getHost(current); + const pathname = getPathname(current); + const search = getSearch(current); + + // Add # prefix if not present and value is not empty + const hash = if (value.len > 0 and value[0] != '#') + try std.fmt.allocPrint(allocator, "#{s}", .{value}) + else + value; + + return buildUrl(allocator, protocol, host, pathname, search, hash); +} + const KnownProtocol = enum { @"http:", @"https:", diff --git a/src/browser/tests/legacy/url/url.html b/src/browser/tests/legacy/url/url.html index ef770e461..72ca45f05 100644 --- a/src/browser/tests/legacy/url/url.html +++ b/src/browser/tests/legacy/url/url.html @@ -31,14 +31,14 @@ diff --git a/src/browser/tests/url.html b/src/browser/tests/url.html index 7faefc32b..80b708232 100644 --- a/src/browser/tests/url.html +++ b/src/browser/tests/url.html @@ -313,6 +313,23 @@ url.searchParams.delete('b'); testing.expectEqual('https://example.com/path', url.href); } + + { + let url = new URL("https://foo.bar"); + const searchParams = url.searchParams; + + // SearchParams should be empty. + testing.expectEqual(0, searchParams.size); + + url.href = "https://lightpanda.io?over=9000&light=panda"; + // It won't hurt to check href and host too. + testing.expectEqual("https://lightpanda.io/?over=9000&light=panda", url.href); + testing.expectEqual("lightpanda.io", url.host); + // SearchParams should be updated too when URL is set. + testing.expectEqual(2, searchParams.size); + testing.expectEqual("9000", searchParams.get("over")); + testing.expectEqual("panda", searchParams.get("light")); + } + + + + + + diff --git a/src/browser/webapi/Location.zig b/src/browser/webapi/Location.zig index c2c2b1a8f..87d0c2827 100644 --- a/src/browser/webapi/Location.zig +++ b/src/browser/webapi/Location.zig @@ -57,8 +57,8 @@ pub fn getOrigin(self: *const Location, page: *const Page) ![]const u8 { return self._url.getOrigin(page); } -pub fn getSearch(self: *const Location) []const u8 { - return self._url.getSearch(); +pub fn getSearch(self: *const Location, page: *const Page) ![]const u8 { + return self._url.getSearch(page); } pub fn getHash(self: *const Location) []const u8 { diff --git a/src/browser/webapi/URL.zig b/src/browser/webapi/URL.zig index 49c03b1dc..766bd3c20 100644 --- a/src/browser/webapi/URL.zig +++ b/src/browser/webapi/URL.zig @@ -35,6 +35,23 @@ _search_params: ?*URLSearchParams = null, pub const resolve = @import("../URL.zig").resolve; pub const eqlDocument = @import("../URL.zig").eqlDocument; +pub fn canParse(url: []const u8, base_: ?[]const u8, page: *Page) bool { + _ = page; + const url_is_absolute = U.isCompleteHTTPUrl(url); + + if (base_) |b| { + // Base must be valid even if URL is absolute + if (!U.isCompleteHTTPUrl(b)) { + return false; + } + return true; + } else if (!url_is_absolute) { + return false; + } else { + return true; + } +} + pub fn init(url: [:0]const u8, base_: ?[:0]const u8, page: *Page) !*URL { const url_is_absolute = @import("../URL.zig").isCompleteHTTPUrl(url); @@ -96,7 +113,17 @@ pub fn getOrigin(self: *const URL, page: *const Page) ![]const u8 { }; } -pub fn getSearch(self: *const URL) []const u8 { +pub fn getSearch(self: *const URL, page: *const Page) ![]const u8 { + // If searchParams has been accessed, generate search from it + if (self._search_params) |sp| { + if (sp.getSize() == 0) { + return ""; + } + var buf = std.Io.Writer.Allocating.init(page.call_arena); + try buf.writer.writeByte('?'); + try sp.toString(&buf.writer); + return buf.written(); + } return U.getSearch(self._raw); } @@ -110,7 +137,7 @@ pub fn getSearchParams(self: *URL, page: *Page) !*URLSearchParams { } // Get current search string (without the '?') - const search = self.getSearch(); + const search = try self.getSearch(page); const search_value = if (search.len > 0) search[1..] else ""; const params = try URLSearchParams.init(.{ .query_string = search_value }, page); @@ -118,6 +145,61 @@ pub fn getSearchParams(self: *URL, page: *Page) !*URLSearchParams { return params; } +pub fn setHref(self: *URL, value: []const u8, page: *Page) !void { + const base = if (U.isCompleteHTTPUrl(value)) page.url else self._raw; + const raw = try U.resolve(self._arena orelse page.arena, base, value, .{ .always_dupe = true }); + self._raw = raw; + + // Update existing searchParams if it exists + if (self._search_params) |sp| { + const search = U.getSearch(raw); + const search_value = if (search.len > 0) search[1..] else ""; + try sp.updateFromString(search_value, page); + } +} + +pub fn setProtocol(self: *URL, value: []const u8) !void { + const allocator = self._arena orelse return error.NoAllocator; + self._raw = try U.setProtocol(self._raw, value, allocator); +} + +pub fn setHost(self: *URL, value: []const u8) !void { + const allocator = self._arena orelse return error.NoAllocator; + self._raw = try U.setHost(self._raw, value, allocator); +} + +pub fn setHostname(self: *URL, value: []const u8) !void { + const allocator = self._arena orelse return error.NoAllocator; + self._raw = try U.setHostname(self._raw, value, allocator); +} + +pub fn setPort(self: *URL, value: ?[]const u8) !void { + const allocator = self._arena orelse return error.NoAllocator; + self._raw = try U.setPort(self._raw, value, allocator); +} + +pub fn setPathname(self: *URL, value: []const u8) !void { + const allocator = self._arena orelse return error.NoAllocator; + self._raw = try U.setPathname(self._raw, value, allocator); +} + +pub fn setSearch(self: *URL, value: []const u8, page: *Page) !void { + const allocator = self._arena orelse return error.NoAllocator; + self._raw = try U.setSearch(self._raw, value, allocator); + + // Update existing searchParams if it exists + if (self._search_params) |sp| { + const search = U.getSearch(self._raw); + const search_value = if (search.len > 0) search[1..] else ""; + try sp.updateFromString(search_value, page); + } +} + +pub fn setHash(self: *URL, value: []const u8) !void { + const allocator = self._arena orelse return error.NoAllocator; + self._raw = try U.setHash(self._raw, value, allocator); +} + pub fn toString(self: *const URL, page: *const Page) ![:0]const u8 { const sp = self._search_params orelse { return self._raw; @@ -137,6 +219,13 @@ pub fn toString(self: *const URL, page: *const Page) ![:0]const u8 { var buf = std.Io.Writer.Allocating.init(page.call_arena); try buf.writer.writeAll(base); + // Add / if missing (e.g., "https://example.com" -> "https://example.com/") + // Only add if pathname is just "/" and not already in the base + const pathname = U.getPathname(raw); + if (std.mem.eql(u8, pathname, "/") and !std.mem.endsWith(u8, base, "/")) { + try buf.writer.writeByte('/'); + } + // Only add ? if there are params if (sp.getSize() > 0) { try buf.writer.writeByte('?'); @@ -159,19 +248,20 @@ pub const JsApi = struct { }; pub const constructor = bridge.constructor(URL.init, .{}); + pub const canParse = bridge.function(URL.canParse, .{ .static = true }); pub const toString = bridge.function(URL.toString, .{}); pub const toJSON = bridge.function(URL.toString, .{}); - pub const href = bridge.accessor(URL.toString, null, .{}); - pub const search = bridge.accessor(URL.getSearch, null, .{}); - pub const hash = bridge.accessor(URL.getHash, null, .{}); - pub const pathname = bridge.accessor(URL.getPathname, null, .{}); + pub const href = bridge.accessor(URL.toString, URL.setHref, .{}); + pub const search = bridge.accessor(URL.getSearch, URL.setSearch, .{}); + pub const hash = bridge.accessor(URL.getHash, URL.setHash, .{}); + pub const pathname = bridge.accessor(URL.getPathname, URL.setPathname, .{}); pub const username = bridge.accessor(URL.getUsername, null, .{}); pub const password = bridge.accessor(URL.getPassword, null, .{}); - pub const hostname = bridge.accessor(URL.getHostname, null, .{}); - pub const host = bridge.accessor(URL.getHost, null, .{}); - pub const port = bridge.accessor(URL.getPort, null, .{}); + pub const hostname = bridge.accessor(URL.getHostname, URL.setHostname, .{}); + pub const host = bridge.accessor(URL.getHost, URL.setHost, .{}); + pub const port = bridge.accessor(URL.getPort, URL.setPort, .{}); pub const origin = bridge.accessor(URL.getOrigin, null, .{}); - pub const protocol = bridge.accessor(URL.getProtocol, null, .{}); + pub const protocol = bridge.accessor(URL.getProtocol, URL.setProtocol, .{}); pub const searchParams = bridge.accessor(URL.getSearchParams, null, .{}); }; diff --git a/src/browser/webapi/element/html/Anchor.zig b/src/browser/webapi/element/html/Anchor.zig index 006843db2..75e61c205 100644 --- a/src/browser/webapi/element/html/Anchor.zig +++ b/src/browser/webapi/element/html/Anchor.zig @@ -84,26 +84,7 @@ pub fn getHost(self: *Anchor, page: *Page) ![]const u8 { pub fn setHost(self: *Anchor, value: []const u8, page: *Page) !void { const href = try getResolvedHref(self, page) orelse return; - const protocol = URL.getProtocol(href); - const pathname = URL.getPathname(href); - const search = URL.getSearch(href); - const hash = URL.getHash(href); - - // Check if the host includes a port - const colon_pos = std.mem.lastIndexOfScalar(u8, value, ':'); - const clean_host = if (colon_pos) |pos| blk: { - const port_str = value[pos + 1 ..]; - // Remove default ports - if (std.mem.eql(u8, protocol, "https:") and std.mem.eql(u8, port_str, "443")) { - break :blk value[0..pos]; - } - if (std.mem.eql(u8, protocol, "http:") and std.mem.eql(u8, port_str, "80")) { - break :blk value[0..pos]; - } - break :blk value; - } else value; - - const new_href = try buildUrl(page.call_arena, protocol, clean_host, pathname, search, hash); + const new_href = try URL.setHost(href, value, page.call_arena); try setHref(self, new_href, page); } @@ -114,13 +95,8 @@ pub fn getHostname(self: *Anchor, page: *Page) ![]const u8 { pub fn setHostname(self: *Anchor, value: []const u8, page: *Page) !void { const href = try getResolvedHref(self, page) orelse return; - const current_port = URL.getPort(href); - const new_host = if (current_port.len > 0) - try std.fmt.allocPrint(page.call_arena, "{s}:{s}", .{ value, current_port }) - else - value; - - try setHost(self, new_host, page); + const new_href = try URL.setHostname(href, value, page.call_arena); + try setHref(self, new_href, page); } pub fn getPort(self: *Anchor, page: *Page) ![]const u8 { @@ -142,25 +118,8 @@ pub fn getPort(self: *Anchor, page: *Page) ![]const u8 { pub fn setPort(self: *Anchor, value: ?[]const u8, page: *Page) !void { const href = try getResolvedHref(self, page) orelse return; - const hostname = URL.getHostname(href); - const protocol = URL.getProtocol(href); - - // Handle null or default ports - const new_host = if (value) |port_str| blk: { - if (port_str.len == 0) { - break :blk hostname; - } - // Check if this is a default port for the protocol - if (std.mem.eql(u8, protocol, "https:") and std.mem.eql(u8, port_str, "443")) { - break :blk hostname; - } - if (std.mem.eql(u8, protocol, "http:") and std.mem.eql(u8, port_str, "80")) { - break :blk hostname; - } - break :blk try std.fmt.allocPrint(page.call_arena, "{s}:{s}", .{ hostname, port_str }); - } else hostname; - - try setHost(self, new_host, page); + const new_href = try URL.setPort(href, value, page.call_arena); + try setHref(self, new_href, page); } pub fn getSearch(self: *Anchor, page: *Page) ![]const u8 { @@ -170,18 +129,7 @@ pub fn getSearch(self: *Anchor, page: *Page) ![]const u8 { pub fn setSearch(self: *Anchor, value: []const u8, page: *Page) !void { const href = try getResolvedHref(self, page) orelse return; - const protocol = URL.getProtocol(href); - const host = URL.getHost(href); - const pathname = URL.getPathname(href); - const hash = URL.getHash(href); - - // Add ? prefix if not present and value is not empty - const search = if (value.len > 0 and value[0] != '?') - try std.fmt.allocPrint(page.call_arena, "?{s}", .{value}) - else - value; - - const new_href = try buildUrl(page.call_arena, protocol, host, pathname, search, hash); + const new_href = try URL.setSearch(href, value, page.call_arena); try setHref(self, new_href, page); } @@ -192,18 +140,7 @@ pub fn getHash(self: *Anchor, page: *Page) ![]const u8 { pub fn setHash(self: *Anchor, value: []const u8, page: *Page) !void { const href = try getResolvedHref(self, page) orelse return; - const protocol = URL.getProtocol(href); - const host = URL.getHost(href); - const pathname = URL.getPathname(href); - const search = URL.getSearch(href); - - // Add # prefix if not present and value is not empty - const hash = if (value.len > 0 and value[0] != '#') - try std.fmt.allocPrint(page.call_arena, "#{s}", .{value}) - else - value; - - const new_href = try buildUrl(page.call_arena, protocol, host, pathname, search, hash); + const new_href = try URL.setHash(href, value, page.call_arena); try setHref(self, new_href, page); } @@ -214,18 +151,7 @@ pub fn getPathname(self: *Anchor, page: *Page) ![]const u8 { pub fn setPathname(self: *Anchor, value: []const u8, page: *Page) !void { const href = try getResolvedHref(self, page) orelse return; - const protocol = URL.getProtocol(href); - const host = URL.getHost(href); - const search = URL.getSearch(href); - const hash = URL.getHash(href); - - // Add / prefix if not present and value is not empty - const pathname = if (value.len > 0 and value[0] != '/') - try std.fmt.allocPrint(page.call_arena, "/{s}", .{value}) - else - value; - - const new_href = try buildUrl(page.call_arena, protocol, host, pathname, search, hash); + const new_href = try URL.setPathname(href, value, page.call_arena); try setHref(self, new_href, page); } @@ -236,18 +162,7 @@ pub fn getProtocol(self: *Anchor, page: *Page) ![]const u8 { pub fn setProtocol(self: *Anchor, value: []const u8, page: *Page) !void { const href = try getResolvedHref(self, page) orelse return; - const host = URL.getHost(href); - const pathname = URL.getPathname(href); - const search = URL.getSearch(href); - const hash = URL.getHash(href); - - // Add : suffix if not present - const protocol = if (value.len > 0 and value[value.len - 1] != ':') - try std.fmt.allocPrint(page.call_arena, "{s}:", .{value}) - else - value; - - const new_href = try buildUrl(page.call_arena, protocol, host, pathname, search, hash); + const new_href = try URL.setProtocol(href, value, page.call_arena); try setHref(self, new_href, page); } @@ -283,24 +198,6 @@ fn getResolvedHref(self: *Anchor, page: *Page) !?[:0]const u8 { return try URL.resolve(page.call_arena, page.url, href, .{}); } -// Helper function to build a new URL from components -fn buildUrl( - allocator: std.mem.Allocator, - protocol: []const u8, - host: []const u8, - pathname: []const u8, - search: []const u8, - hash: []const u8, -) ![:0]const u8 { - return std.fmt.allocPrintSentinel(allocator, "{s}//{s}{s}{s}{s}", .{ - protocol, - host, - pathname, - search, - hash, - }, 0); -} - pub const JsApi = struct { pub const bridge = js.Bridge(Anchor); diff --git a/src/browser/webapi/net/URLSearchParams.zig b/src/browser/webapi/net/URLSearchParams.zig index f3069531d..2cfe3b2c3 100644 --- a/src/browser/webapi/net/URLSearchParams.zig +++ b/src/browser/webapi/net/URLSearchParams.zig @@ -61,6 +61,10 @@ pub fn init(opts_: ?InitOpts, page: *Page) !*URLSearchParams { }); } +pub fn updateFromString(self: *URLSearchParams, query_string: []const u8, page: *Page) !void { + self._params = try paramsFromString(self._arena, query_string, &page.buf); +} + pub fn getSize(self: *const URLSearchParams) usize { return self._params.len(); } @@ -277,7 +281,7 @@ fn escape(input: []const u8, writer: *std.Io.Writer) !void { fn isUnreserved(c: u8) bool { return switch (c) { - 'A'...'Z', 'a'...'z', '0'...'9', '-', '.', '_', '~' => true, + 'A'...'Z', 'a'...'z', '0'...'9', '-', '.', '_' => true, else => false, }; } From 52dcc6765af407da356333fdcab86ff56e987fb9 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Sat, 13 Dec 2025 12:47:54 +0800 Subject: [PATCH 218/219] URLSearchParams from FormData --- .../tests/legacy/url/url_search_params.html | 1 - src/browser/tests/net/url_search_params.html | 14 +++++++++++ src/browser/webapi/URL.zig | 24 ++++++------------- src/browser/webapi/net/URLSearchParams.zig | 5 +++- 4 files changed, 25 insertions(+), 19 deletions(-) diff --git a/src/browser/tests/legacy/url/url_search_params.html b/src/browser/tests/legacy/url/url_search_params.html index 344f33cc1..738253d25 100644 --- a/src/browser/tests/legacy/url/url_search_params.html +++ b/src/browser/tests/legacy/url/url_search_params.html @@ -81,7 +81,6 @@ testing.expectEqual(3, ups.size); testing.expectEqual(['1', '2'], ups.getAll('a')); - testing.expectEqual(['3'], ups.getAll('b')); fd.delete('a'); // the two aren't linked, it created a copy diff --git a/src/browser/tests/net/url_search_params.html b/src/browser/tests/net/url_search_params.html index 54b66b3d3..74d7c03bb 100644 --- a/src/browser/tests/net/url_search_params.html +++ b/src/browser/tests/net/url_search_params.html @@ -353,3 +353,17 @@ testing.expectEqual(['a', 'a', 'b', 'b', 'c'], Array.from(usp.keys())); } + + diff --git a/src/browser/webapi/URL.zig b/src/browser/webapi/URL.zig index 766bd3c20..e886c8ebd 100644 --- a/src/browser/webapi/URL.zig +++ b/src/browser/webapi/URL.zig @@ -35,23 +35,6 @@ _search_params: ?*URLSearchParams = null, pub const resolve = @import("../URL.zig").resolve; pub const eqlDocument = @import("../URL.zig").eqlDocument; -pub fn canParse(url: []const u8, base_: ?[]const u8, page: *Page) bool { - _ = page; - const url_is_absolute = U.isCompleteHTTPUrl(url); - - if (base_) |b| { - // Base must be valid even if URL is absolute - if (!U.isCompleteHTTPUrl(b)) { - return false; - } - return true; - } else if (!url_is_absolute) { - return false; - } else { - return true; - } -} - pub fn init(url: [:0]const u8, base_: ?[:0]const u8, page: *Page) !*URL { const url_is_absolute = @import("../URL.zig").isCompleteHTTPUrl(url); @@ -238,6 +221,13 @@ pub fn toString(self: *const URL, page: *const Page) ![:0]const u8 { return buf.written()[0 .. buf.written().len - 1 :0]; } +pub fn canParse(url: []const u8, base_: ?[]const u8) bool { + if (base_) |b| { + return U.isCompleteHTTPUrl(b); + } + return U.isCompleteHTTPUrl(url); +} + pub const JsApi = struct { pub const bridge = js.Bridge(URL); diff --git a/src/browser/webapi/net/URLSearchParams.zig b/src/browser/webapi/net/URLSearchParams.zig index 2cfe3b2c3..482c4c7f4 100644 --- a/src/browser/webapi/net/URLSearchParams.zig +++ b/src/browser/webapi/net/URLSearchParams.zig @@ -24,8 +24,9 @@ const String = @import("../../../string.zig").String; const Allocator = std.mem.Allocator; const Page = @import("../../Page.zig"); -const GenericIterator = @import("../collections/iterator.zig").Entry; +const FormData = @import("FormData.zig"); const KeyValueList = @import("../KeyValueList.zig"); +const GenericIterator = @import("../collections/iterator.zig").Entry; const URLSearchParams = @This(); @@ -33,6 +34,7 @@ _arena: Allocator, _params: KeyValueList, const InitOpts = union(enum) { + form_data: *FormData, value: js.Value, query_string: []const u8, }; @@ -43,6 +45,7 @@ pub fn init(opts_: ?InitOpts, page: *Page) !*URLSearchParams { const opts = opts_ orelse break :blk .empty; switch (opts) { .query_string => |qs| break :blk try paramsFromString(arena, qs, &page.buf), + .form_data => |fd| break :blk try KeyValueList.copy(arena, fd._list), .value => |js_val| { if (js_val.isObject()) { break :blk try KeyValueList.fromJsObject(arena, js_val.toObject(), null, page); From c9b4067686771b1157e26afa52e2a75f9ac6ad83 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Sat, 13 Dec 2025 17:19:53 +0800 Subject: [PATCH 219/219] Event listener can now be an object with a handleEvent function --- src/browser/EventManager.zig | 64 +++++++++++++--- src/browser/js/Object.zig | 4 + src/browser/tests/events.html | 115 +++++++++++++++++++++++++++++ src/browser/webapi/EventTarget.zig | 70 ++++++++++++++---- 4 files changed, 228 insertions(+), 25 deletions(-) diff --git a/src/browser/EventManager.zig b/src/browser/EventManager.zig index d458a3b2e..f4bbdd441 100644 --- a/src/browser/EventManager.zig +++ b/src/browser/EventManager.zig @@ -57,7 +57,13 @@ pub const RegisterOptions = struct { 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 { + +pub const Callback = union(enum) { + function: js.Function, + object: js.Object, +}; + +pub fn register(self: *EventManager, target: *EventTarget, typ: []const u8, callback: Callback, opts: RegisterOptions) !void { if (comptime IS_DEBUG) { log.debug(.event, "eventManager.register", .{ .type = typ, .capture = opts.capture, .once = opts.once }); } @@ -71,11 +77,15 @@ pub fn register(self: *EventManager, target: *EventTarget, typ: []const u8, func const gop = try self.lookup.getOrPut(self.arena, @intFromPtr(target)); if (gop.found_existing) { - // check for duplicate functions already registered + // check for duplicate callbacks 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) { + const is_duplicate = switch (callback) { + .object => |obj| listener.function.eqlObject(obj), + .function => |func| listener.function.eqlFunction(func), + }; + if (is_duplicate and listener.capture == opts.capture) { return; } node = n.next; @@ -84,13 +94,18 @@ pub fn register(self: *EventManager, target: *EventTarget, typ: []const u8, func gop.value_ptr.* = .{}; } + const func = switch (callback) { + .function => |f| Function{ .value = f }, + .object => |o| Function{ .object = o }, + }; + const listener = try self.listener_pool.create(); listener.* = .{ .node = .{}, .once = opts.once, .capture = opts.capture, .passive = opts.passive, - .function = .{ .value = function }, + .function = func, .signal = opts.signal, .typ = try String.init(self.arena, typ, .{}), }; @@ -98,9 +113,9 @@ pub fn register(self: *EventManager, target: *EventTarget, typ: []const u8, func gop.value_ptr.append(&listener.node); } -pub fn remove(self: *EventManager, target: *EventTarget, typ: []const u8, function: js.Function, use_capture: bool) void { +pub fn remove(self: *EventManager, target: *EventTarget, typ: []const u8, callback: Callback, use_capture: bool) void { const list = self.lookup.getPtr(@intFromPtr(target)) orelse return; - if (findListener(list, typ, function, use_capture)) |listener| { + if (findListener(list, typ, callback, use_capture)) |listener| { self.removeListener(list, listener); } } @@ -119,7 +134,17 @@ pub fn dispatch(self: *EventManager, target: *EventTarget, event: *Event) !void switch (target._type) { .node => |node| try self.dispatchNode(node, event, &was_handled), - .xhr, .window, .abort_signal, .media_query_list, .message_port, .text_track_cue, .navigation, .screen, .screen_orientation => { + .xhr, + .window, + .abort_signal, + .media_query_list, + .message_port, + .text_track_cue, + .navigation, + .screen, + .screen_orientation, + .generic, + => { const list = self.lookup.getPtr(@intFromPtr(target)) orelse return; try self.dispatchAll(list, target, event, &was_handled); }, @@ -304,6 +329,11 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe const str = try page.call_arena.dupeZ(u8, string.str()); try self.page.js.eval(str, null); }, + .object => |obj| { + if (try obj.getFunction("handleEvent")) |handleEvent| { + try handleEvent.callWithThis(void, obj, .{event}); + } + }, } // Restore original target (only if we changed it) @@ -350,12 +380,16 @@ fn cleanupMarkedListeners(self: *EventManager, list: *std.DoublyLinkedList) void } } -fn findListener(list: *const std.DoublyLinkedList, typ: []const u8, function: js.Function, capture: bool) ?*Listener { +fn findListener(list: *const std.DoublyLinkedList, typ: []const u8, callback: Callback, 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)) { + const matches = switch (callback) { + .object => |obj| listener.function.eqlObject(obj), + .function => |func| listener.function.eqlFunction(func), + }; + if (!matches) { continue; } if (listener.capture != capture) { @@ -383,11 +417,19 @@ const Listener = struct { const Function = union(enum) { value: js.Function, string: String, + object: js.Object, - fn eql(self: Function, func: js.Function) bool { + fn eqlFunction(self: Function, func: js.Function) bool { return switch (self) { - .string => false, .value => |v| return v.id == func.id, + else => false, + }; + } + + fn eqlObject(self: Function, obj: js.Object) bool { + return switch (self) { + .object => |o| return o.getId() == obj.getId(), + else => false, }; } }; diff --git a/src/browser/js/Object.zig b/src/browser/js/Object.zig index 2e77a54af..0e25b963e 100644 --- a/src/browser/js/Object.zig +++ b/src/browser/js/Object.zig @@ -32,6 +32,10 @@ const Object = @This(); js_obj: v8.Object, context: *js.Context, +pub fn getId(self: Object) u32 { + return self.js_obj.getIdentityHash(); +} + pub const SetOpts = packed struct(u32) { READ_ONLY: bool = false, DONT_ENUM: bool = false, diff --git a/src/browser/tests/events.html b/src/browser/tests/events.html index a3682ae1b..843002175 100644 --- a/src/browser/tests/events.html +++ b/src/browser/tests/events.html @@ -497,3 +497,118 @@ testing.expectEqual('inner1', nested_calls[4]); testing.expectEqual(5, nested_calls.length); + +

+ diff --git a/src/browser/webapi/EventTarget.zig b/src/browser/webapi/EventTarget.zig index b399f7cb7..75996fe31 100644 --- a/src/browser/webapi/EventTarget.zig +++ b/src/browser/webapi/EventTarget.zig @@ -20,7 +20,8 @@ const std = @import("std"); const js = @import("../js/js.zig"); const Page = @import("../Page.zig"); -const RegisterOptions = @import("../EventManager.zig").RegisterOptions; +const EventManager = @import("../EventManager.zig"); +const RegisterOptions = EventManager.RegisterOptions; const Event = @import("Event.zig"); @@ -30,6 +31,7 @@ const _prototype_root = true; _type: Type, pub const Type = union(enum) { + generic: void, node: *@import("Node.zig"), window: *@import("Window.zig"), xhr: *@import("net/XMLHttpRequestEventTarget.zig"), @@ -42,6 +44,12 @@ pub const Type = union(enum) { screen_orientation: *@import("Screen.zig").Orientation, }; +pub fn init(page: *Page) !*EventTarget { + return page._factory.create(EventTarget{ + ._type = .generic, + }); +} + pub fn dispatchEvent(self: *EventTarget, event: *Event, page: *Page) !bool { try page._event_manager.dispatch(self, event); return !event._cancelable or !event._prevent_default; @@ -59,9 +67,15 @@ pub const EventListenerCallback = union(enum) { pub fn addEventListener(self: *EventTarget, typ: []const u8, callback_: ?EventListenerCallback, opts_: ?AddEventListenerOptions, page: *Page) !void { const callback = callback_ orelse return; - const actual_callback = switch (callback) { - .function => |func| func, - .object => |obj| (try obj.getFunction("handleEvent")) orelse return, + if (callback == .object) { + if (try callback.object.getFunction("handleEvent") == null) { + return; + } + } + + const em_callback = switch (callback) { + .function => |func| EventManager.Callback{ .function = func }, + .object => |obj| EventManager.Callback{ .object = try obj.persist() }, }; const options = blk: { @@ -71,7 +85,7 @@ pub fn addEventListener(self: *EventTarget, typ: []const u8, callback_: ?EventLi .capture => |capture| RegisterOptions{ .capture = capture }, }; }; - return page._event_manager.register(self, typ, actual_callback, options); + return page._event_manager.register(self, typ, em_callback, options); } const RemoveEventListenerOptions = union(enum) { @@ -79,36 +93,63 @@ const RemoveEventListenerOptions = union(enum) { options: Options, const Options = struct { - useCapture: bool = false, + capture: bool = false, }; }; pub fn removeEventListener(self: *EventTarget, typ: []const u8, callback_: ?EventListenerCallback, opts_: ?RemoveEventListenerOptions, page: *Page) !void { const callback = callback_ orelse return; - const actual_callback = switch (callback) { - .function => |func| func, - .object => |obj| (try obj.getFunction("handleEvent")) orelse return, + // For object callbacks, check if handleEvent exists + if (callback == .object) { + if (try callback.object.getFunction("handleEvent") == null) { + return; + } + } + + const em_callback = switch (callback) { + .function => |func| EventManager.Callback{ .function = func }, + .object => |obj| EventManager.Callback{ .object = try obj.persist() }, }; const use_capture = blk: { const o = opts_ orelse break :blk false; break :blk switch (o) { .capture => |capture| capture, - .options => |opts| opts.useCapture, + .options => |opts| opts.capture, }; }; - return page._event_manager.remove(self, typ, actual_callback, use_capture); + return page._event_manager.remove(self, typ, em_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(""), + .generic => writer.writeAll(""), + .window => writer.writeAll(""), .xhr => writer.writeAll(""), - .abort_signal => writer.writeAll(""), + .abort_signal => writer.writeAll(""), .media_query_list => writer.writeAll(""), .message_port => writer.writeAll(""), .text_track_cue => writer.writeAll(""), + .navigation => writer.writeAll(""), + .screen => writer.writeAll(""), + .screen_orientation => writer.writeAll(""), + }; +} + +pub fn toString(self: *EventTarget) []const u8 { + return switch (self._type) { + .node => |n| return n.className(), + .generic => return "[object EventTarget]", + .window => return "[object Window]", + .xhr => return "[object XMLHttpRequestEventTarget]", + .abort_signal => return "[object AbortSignal]", + .media_query_list => return "[object MediaQueryList]", + .message_port => return "[object MessagePort]", + .text_track_cue => return "[object TextTrackCue]", + .navigation => return "[object Navigation]", + .screen => return "[object Screen]", + .screen_orientation => return "[object ScreenOrientation]", }; } @@ -122,15 +163,16 @@ pub const JsApi = struct { pub var class_id: bridge.ClassId = undefined; }; + pub const constructor = bridge.constructor(EventTarget.init, .{}); pub const dispatchEvent = bridge.function(EventTarget.dispatchEvent, .{}); pub const addEventListener = bridge.function(EventTarget.addEventListener, .{}); pub const removeEventListener = bridge.function(EventTarget.removeEventListener, .{}); + pub const toString = bridge.function(EventTarget.toString, .{}); }; const testing = @import("../../testing.zig"); test "WebApi: EventTarget" { // we create thousands of these per page. Nothing should bloat it. try testing.expectEqual(16, @sizeOf(EventTarget)); - try testing.htmlRunner("events.html", .{}); }