From b047cb6dc166fca24325947e50a10be0c072f865 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Sat, 18 Oct 2025 07:49:15 +0800 Subject: [PATCH 001/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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/144] 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 60c1f19581989570a3544aa1cff3e9d61f773a9e Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Wed, 3 Dec 2025 20:04:07 +0800 Subject: [PATCH 133/144] 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 134/144] 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 135/144] 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 136/144] 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 137/144] 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 138/144] 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 141/144] 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 142/144] 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 143/144] 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 08d7f544ddfe668ed86470e23bfc0c08a8d75b49 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Sat, 6 Dec 2025 19:15:24 +0100 Subject: [PATCH 144/144] 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}), }; }