From b047cb6dc166fca24325947e50a10be0c072f865 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Sat, 18 Oct 2025 07:49:15 +0800 Subject: [PATCH] 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 | 154 +- 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 | 266 +- src/browser/SlotChangeMonitor.zig | 189 - src/browser/State.zig | 77 - src/browser/URL.zig | 264 ++ src/browser/browser.zig | 152 +- 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 | 389 +-- 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 | 361 +- src/browser/js/Context.zig | 382 +- src/browser/js/Env.zig | 472 +-- src/browser/js/ExecutionWorld.zig | 125 +- 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 | 2324 +++++++------ 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 | 278 +- 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 | 277 +- src/notification.zig | 422 +-- src/server.zig | 257 +- src/string.zig | 207 ++ src/telemetry/telemetry.zig | 4 +- src/test_runner.zig | 444 +-- 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, 26409 insertions(+), 33673 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 17c02759..e9864c01 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 2a1fdd52..106e557a 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 ad9ae7b4..9a7968b9 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 717d079b..3358b9a3 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 bcb613f7..919a9a65 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 b0ae6901..957705e2 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-dev: install-submodule install-html5ever-dev -install-netsurf-dev: _install-netsurf -install-netsurf-dev: OPTCFLAGS := -O0 -g -DNDEBUG +install-html5ever: + cd src/html5ever && cargo build --release --target-dir ../../build/html5ever/ -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 - -build-libiconv: clean-libiconv - @cd vendor/libiconv/libiconv-1.17 && \ - ./configure --prefix=$(ICONV) --enable-static && \ - make && make install - -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 a1009e7f..87c393a5 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 3437dfad..d7effb26 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 971f0f44..fd5fbef8 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 00000000..0898d19b --- /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 9867600d..fdc51b90 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 719dd9b7..ef94486b 100644 --- a/src/app.zig +++ b/src/app.zig @@ -6,94 +6,88 @@ 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, +const App = @This(); - pub const RunMode = enum { - help, - fetch, - serve, - version, - }; +http: Http, +config: Config, +platform: Platform, +telemetry: Telemetry, +allocator: Allocator, +app_dir_path: ?[]const u8, +notification: *Notification, - 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 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); - } +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 fn init(allocator: Allocator, config: Config) !*App { + const app = try allocator.create(App); + errdefer allocator.destroy(app); + + 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) { return allocator.dupe(u8, "/tmp") catch unreachable; diff --git a/src/browser/DataURI.zig b/src/browser/DataURI.zig deleted file mode 100644 index 00d3792f..00000000 --- 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 00000000..89cba801 --- /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 00000000..bd04da75 --- /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 00000000..27fe35a8 --- /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 cbe67f5f..00000000 --- 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 62284602..00000000 --- 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 94bf8a27..00000000 --- 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 4b7f5df4..00000000 --- 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 13ab6216..00000000 --- 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 0d3d4628..00000000 --- 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 acfac256..00000000 --- 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 8f7d1e9a..00000000 --- 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 c7f2d3cd..00000000 --- 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 1624ba62..00000000 --- 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 094dc22e..00000000 --- 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 74f1e011..00000000 --- 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 b4ba781f..00000000 --- a/vendor/netsurf/share/netsurf-buildsystem +++ /dev/null @@ -1 +0,0 @@ -Subproject commit b4ba781fe22f356d7c53b1674dff91323af61458