From d01c140516b8876525a5a16a46a62881b83e9c85 Mon Sep 17 00:00:00 2001 From: pintariching Date: Wed, 7 Jan 2026 13:19:19 +0100 Subject: [PATCH 1/7] Add HTMX attributes for SSE and WS extensions --- hypertext/src/validation/attributes.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/hypertext/src/validation/attributes.rs b/hypertext/src/validation/attributes.rs index d97621b..c4e381f 100644 --- a/hypertext/src/validation/attributes.rs +++ b/hypertext/src/validation/attributes.rs @@ -398,6 +398,25 @@ pub trait HtmxAttributes: GlobalAttributes { /// (deprecated, please use [`hx-vals`](Self::hx_vals)) #[deprecated = "use `hx-vals` instead"] const hx_vars: Attribute = Attribute; + + /// The URL of the SSE server. + const sse_connect: Attribute = Attribute; + + /// The name of the message to swap into the DOM + const sse_swap: Attribute = Attribute; + + /// To close the EventStream gracefully when that message is received. + /// This might be helpful if you want to send information + /// to a client that will eventually stop. + const sse_close: Attribute = Attribute; + + /// A URL to establish a WebSocket connection against + const ws_connect: Attribute = Attribute; + + /// Sends a message to the nearest websocket based on the trigger value for + /// the element (either the natural event or the event specified by + /// [`hx-trigger`](Self::hx_trigger)) + const ws_send: Attribute = Attribute; } #[cfg(feature = "htmx")] From 3b286df9f4b0e4603f048993d5a1d1acc1c06fb6 Mon Sep 17 00:00:00 2001 From: pintariching Date: Thu, 8 Jan 2026 06:46:27 +0100 Subject: [PATCH 2/7] fix documentation backticks --- hypertext/src/validation/attributes.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hypertext/src/validation/attributes.rs b/hypertext/src/validation/attributes.rs index c4e381f..27d6e38 100644 --- a/hypertext/src/validation/attributes.rs +++ b/hypertext/src/validation/attributes.rs @@ -405,7 +405,7 @@ pub trait HtmxAttributes: GlobalAttributes { /// The name of the message to swap into the DOM const sse_swap: Attribute = Attribute; - /// To close the EventStream gracefully when that message is received. + /// To close the `EventStream` gracefully when that message is received. /// This might be helpful if you want to send information /// to a client that will eventually stop. const sse_close: Attribute = Attribute; From 8051b35601875f6a4d2820001321e3b79b2ca649 Mon Sep 17 00:00:00 2001 From: Mattias Jansson Date: Fri, 20 Feb 2026 21:40:02 +0100 Subject: [PATCH 3/7] Add Default trait impl for Lazy to enable struct field defaults (#167) --- hypertext/src/alloc/mod.rs | 7 ++++++ hypertext/tests/main.rs | 45 +++++++++++++++++++++++++++++++++++++- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/hypertext/src/alloc/mod.rs b/hypertext/src/alloc/mod.rs index 23e850d..ef85b52 100644 --- a/hypertext/src/alloc/mod.rs +++ b/hypertext/src/alloc/mod.rs @@ -312,6 +312,13 @@ impl), C: Context> Debug for Lazy { } } +impl Default for Lazy), C> { + #[inline] + fn default() -> Lazy), C> { + Lazy::dangerously_create(|_| ()) + } +} + /// A value rendered via its [`Display`] implementation. /// /// This will handle escaping special characters for you. diff --git a/hypertext/tests/main.rs b/hypertext/tests/main.rs index 974c0bb..9d93de0 100644 --- a/hypertext/tests/main.rs +++ b/hypertext/tests/main.rs @@ -3,7 +3,7 @@ use std::fmt::{self, Display, Formatter}; -use hypertext::{Buffer, Raw, maud_borrow, maud_static, prelude::*, rsx_borrow, rsx_static}; +use hypertext::{Buffer, Lazy, Raw, maud_borrow, maud_static, prelude::*, rsx_borrow, rsx_static}; #[test] fn readme() { @@ -749,3 +749,46 @@ fn toggles() { ); assert_eq!(rsx_result.as_inner(), r#""#); } + +#[test] +fn derive_default() { + #[derive(Default)] + struct Element<'a> { + pub id: &'a str, + pub tabindex: u32, + pub children: Lazy, + } + + impl<'a> Renderable for Element<'a> { + fn render_to(&self, buf: &mut Buffer) { + rsx! { +
+ (self.children) +
+ } + .render_to(buf) + } + } + + let with_children = rsx! { + +

hello

+
+ } + .render(); + + assert_eq!( + with_children.as_inner(), + r#"

hello

"# + ); + + let without_children = rsx! { + + } + .render(); + + assert_eq!( + without_children.as_inner(), + r#"
"# + ); +} From 360ae72cee5d9be8c3acdceeb2586287497ae557 Mon Sep 17 00:00:00 2001 From: Vidhan Bhatt Date: Fri, 6 Mar 2026 16:49:11 -0500 Subject: [PATCH 4/7] feat: huge refactor --- .envrc | 1 + .github/workflows/{ci.yaml => ci-cd.yaml} | 102 +- .github/workflows/security-audit.yaml | 1 - .gitignore | 52 +- Cargo.lock | 1186 ++++++++++------- Cargo.toml | 18 +- README.md | 5 +- .../hypertext-macros}/Cargo.toml | 2 - .../hypertext-macros}/src/derive.rs | 49 +- .../hypertext-macros}/src/html/basics.rs | 32 +- .../hypertext-macros}/src/html/component.rs | 44 +- .../hypertext-macros}/src/html/control.rs | 157 ++- .../hypertext-macros}/src/html/generate.rs | 161 ++- .../hypertext-macros}/src/html/mod.rs | 162 ++- .../src/html/syntaxes/maud.rs | 7 +- .../src/html/syntaxes/mod.rs | 0 .../src/html/syntaxes/rsx.rs | 61 +- crates/hypertext-macros/src/lib.rs | 78 ++ .../hypertext-macros/src/renderable.rs | 31 +- {hypertext => crates/hypertext}/Cargo.toml | 6 +- .../hypertext}/src/context.rs | 0 {hypertext => crates/hypertext}/src/lib.rs | 199 +-- crates/hypertext/src/macros/attribute.rs | 31 + crates/hypertext/src/macros/maud.rs | 36 + crates/hypertext/src/macros/mod.rs | 89 ++ crates/hypertext/src/macros/renderable.rs | 149 +++ crates/hypertext/src/macros/rsx.rs | 36 + crates/hypertext/src/prelude.rs | 24 + crates/hypertext/src/renderable/buffer.rs | 192 +++ .../hypertext/src/renderable}/impls.rs | 165 ++- crates/hypertext/src/renderable/mod.rs | 404 ++++++ .../hypertext}/src/validation/attributes.rs | 95 +- .../src/validation/hypertext_elements.rs | 178 ++- .../hypertext}/src/validation/mod.rs | 210 ++- .../hypertext}/src/web_frameworks.rs | 31 +- .../hypertext/tests/alloc.rs | 77 +- examples/htmx-rsx/.gitignore | 1 - examples/htmx-rsx/Cargo.lock | 900 ------------- examples/htmx-rsx/Cargo.toml | 15 - examples/htmx-rsx/README.md | 62 - examples/htmx-rsx/build.rs | 22 - examples/htmx-rsx/src/handlers.rs | 34 - examples/htmx-rsx/src/main.rs | 27 - examples/htmx-rsx/src/views/about.rs | 12 - examples/htmx-rsx/src/views/document.rs | 29 - examples/htmx-rsx/src/views/home.rs | 12 - examples/htmx-rsx/src/views/list.rs | 16 - examples/htmx-rsx/src/views/mod.rs | 7 - examples/htmx-rsx/src/views/nav.rs | 32 - examples/htmx-rsx/tailwind.css | 1 - flake.lock | 163 +++ flake.nix | 107 ++ hypertext-macros/src/lib.rs | 101 -- hypertext/src/alloc/mod.rs | 370 ----- hypertext/src/macros/alloc.rs | 228 ---- hypertext/src/macros/mod.rs | 77 -- hypertext/src/prelude.rs | 10 - hypertext/src/validation/mathml.rs | 202 --- 58 files changed, 3099 insertions(+), 3400 deletions(-) create mode 100644 .envrc rename .github/workflows/{ci.yaml => ci-cd.yaml} (57%) rename {hypertext-macros => crates/hypertext-macros}/Cargo.toml (95%) rename {hypertext-macros => crates/hypertext-macros}/src/derive.rs (64%) rename {hypertext-macros => crates/hypertext-macros}/src/html/basics.rs (90%) rename {hypertext-macros => crates/hypertext-macros}/src/html/component.rs (76%) rename {hypertext-macros => crates/hypertext-macros}/src/html/control.rs (68%) rename {hypertext-macros => crates/hypertext-macros}/src/html/generate.rs (73%) rename {hypertext-macros => crates/hypertext-macros}/src/html/mod.rs (86%) rename {hypertext-macros => crates/hypertext-macros}/src/html/syntaxes/maud.rs (95%) rename {hypertext-macros => crates/hypertext-macros}/src/html/syntaxes/mod.rs (100%) rename {hypertext-macros => crates/hypertext-macros}/src/html/syntaxes/rsx.rs (79%) create mode 100644 crates/hypertext-macros/src/lib.rs rename hypertext-macros/src/component.rs => crates/hypertext-macros/src/renderable.rs (78%) rename {hypertext => crates/hypertext}/Cargo.toml (90%) rename {hypertext => crates/hypertext}/src/context.rs (100%) rename {hypertext => crates/hypertext}/src/lib.rs (58%) create mode 100644 crates/hypertext/src/macros/attribute.rs create mode 100644 crates/hypertext/src/macros/maud.rs create mode 100644 crates/hypertext/src/macros/mod.rs create mode 100644 crates/hypertext/src/macros/renderable.rs create mode 100644 crates/hypertext/src/macros/rsx.rs create mode 100644 crates/hypertext/src/prelude.rs create mode 100644 crates/hypertext/src/renderable/buffer.rs rename {hypertext/src/alloc => crates/hypertext/src/renderable}/impls.rs (65%) create mode 100644 crates/hypertext/src/renderable/mod.rs rename {hypertext => crates/hypertext}/src/validation/attributes.rs (87%) rename {hypertext => crates/hypertext}/src/validation/hypertext_elements.rs (91%) rename {hypertext => crates/hypertext}/src/validation/mod.rs (50%) rename {hypertext => crates/hypertext}/src/web_frameworks.rs (88%) rename hypertext/tests/main.rs => crates/hypertext/tests/alloc.rs (92%) delete mode 100644 examples/htmx-rsx/.gitignore delete mode 100644 examples/htmx-rsx/Cargo.lock delete mode 100644 examples/htmx-rsx/Cargo.toml delete mode 100644 examples/htmx-rsx/README.md delete mode 100644 examples/htmx-rsx/build.rs delete mode 100644 examples/htmx-rsx/src/handlers.rs delete mode 100644 examples/htmx-rsx/src/main.rs delete mode 100644 examples/htmx-rsx/src/views/about.rs delete mode 100644 examples/htmx-rsx/src/views/document.rs delete mode 100644 examples/htmx-rsx/src/views/home.rs delete mode 100644 examples/htmx-rsx/src/views/list.rs delete mode 100644 examples/htmx-rsx/src/views/mod.rs delete mode 100644 examples/htmx-rsx/src/views/nav.rs delete mode 100644 examples/htmx-rsx/tailwind.css create mode 100644 flake.lock create mode 100644 flake.nix delete mode 100644 hypertext-macros/src/lib.rs delete mode 100644 hypertext/src/alloc/mod.rs delete mode 100644 hypertext/src/macros/alloc.rs delete mode 100644 hypertext/src/macros/mod.rs delete mode 100644 hypertext/src/prelude.rs delete mode 100644 hypertext/src/validation/mathml.rs diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..8392d15 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake \ No newline at end of file diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci-cd.yaml similarity index 57% rename from .github/workflows/ci.yaml rename to .github/workflows/ci-cd.yaml index 0d1239c..766c76b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci-cd.yaml @@ -1,4 +1,4 @@ -name: Continuous Integration +name: CI/CD on: - push @@ -17,6 +17,17 @@ jobs: runs-on: ubuntu-latest + strategy: + matrix: + features: + - name: No Default Features + flag: "" + - name: Default Features + flag: --features default + - name: All Features + flag: --all-features + fail-fast: false + steps: - name: Checkout repository uses: actions/checkout@v4 @@ -28,13 +39,24 @@ jobs: uses: Swatinem/rust-cache@v2 - name: Run tests - run: cargo test --all-features + run: cargo test --tests --no-default-features ${{ matrix.features.flag }} miri: name: Miri runs-on: ubuntu-latest + strategy: + matrix: + features: + - name: No Default Features + flag: "" + - name: Default Features + flag: --features default + - name: All Features + flag: --all-features + fail-fast: false + steps: - name: Checkout repository uses: actions/checkout@v4 @@ -51,13 +73,24 @@ jobs: run: cargo miri setup - name: Run Miri - run: cargo miri test --all-features + run: cargo miri test --tests --no-default-features ${{ matrix.features.flag }} - check: - name: Check + clippy: + name: Clippy runs-on: ubuntu-latest + strategy: + matrix: + features: + - name: No Default Features + flag: "" + - name: Default Features + flag: --features default + - name: All Features + flag: --all-features + fail-fast: false + steps: - name: Checkout repository uses: actions/checkout@v4 @@ -70,11 +103,50 @@ jobs: - name: Cache dependencies uses: Swatinem/rust-cache@v2 - - name: Check code - run: cargo clippy --all-features + - name: Run Clippy + run: cargo clippy --no-default-features ${{ matrix.features.flag }} + + test-docs: + name: Test Documentation + + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache dependencies + uses: Swatinem/rust-cache@v2 + + - name: Run documentation tests + run: cargo test --doc --all-features + + check-docs: + name: Check Documentation + + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@nightly + + - name: Cache dependencies + uses: Swatinem/rust-cache@v2 + + - name: Install `cargo-docs-rs` + uses: dtolnay/install@cargo-docs-rs + + - name: Check documentation + run: cargo docs-rs - format: - name: Format + check-format: + name: Check Formatting runs-on: ubuntu-latest @@ -104,8 +176,10 @@ jobs: needs: - test - miri - - check - - format + - clippy + - test-docs + - check-docs + - check-format if: github.event_name == 'push' && github.ref == 'refs/heads/main' @@ -138,8 +212,10 @@ jobs: needs: - test - miri - - check - - format + - clippy + - test-docs + - check-docs + - check-format if: github.event_name == 'push' && github.ref == 'refs/heads/main' diff --git a/.github/workflows/security-audit.yaml b/.github/workflows/security-audit.yaml index 1fb2303..92dfa30 100644 --- a/.github/workflows/security-audit.yaml +++ b/.github/workflows/security-audit.yaml @@ -7,7 +7,6 @@ on: push: paths: - "**/Cargo.toml" - - "**/Cargo.lock" - "**/deny.toml" pull_request: diff --git a/.gitignore b/.gitignore index 0f004fb..8adaa53 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,9 @@ -# File created using '.gitignore Generator' for Visual Studio Code: https://bit.ly/vscode-gig -# Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode,macos,rust -# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,macos,rust - -### macOS ### # General .DS_Store .AppleDouble .LSOverride - -# Icon must end with two \r -Icon +Icon[ +] # Thumbnails ._* @@ -30,19 +24,10 @@ Network Trash Folder Temporary Items .apdisk -### macOS Patch ### -# iCloud generated files -*.icloud - -### Rust ### # Generated by Cargo # will have compiled files and executables -debug/ -target/ - -# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries -# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html -# Cargo.lock +debug +target # These are backup files generated by rustfmt **/*.rs.bk @@ -50,26 +35,31 @@ target/ # MSVC Windows builds of rustc generate these, which store debugging information *.pdb -### VisualStudioCode ### +# Generated by cargo mutants +# Contains mutation testing data +**/mutants.out*/ + +# RustRover +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json !.vscode/*.code-snippets - -# Local History for Visual Studio Code -.history/ +!*.code-workspace # Built Visual Studio Code Extensions *.vsix -### VisualStudioCode Patch ### -# Ignore all local history of files -.history -.ionide - -# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,macos,rust +# Ignore build outputs from performing a nix-build or `nix build` command +result +result-* -# Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option) -tmp/ +# Ignore automatically generated direnv output +.direnv diff --git a/Cargo.lock b/Cargo.lock index 3c190d9..a308c6b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,7 +13,7 @@ dependencies = [ "futures-core", "futures-sink", "memchr", - "pin-project-lite 0.2.16", + "pin-project-lite 0.2.17", "tokio", "tokio-util", "tracing", @@ -35,7 +35,7 @@ dependencies = [ "bytestring", "derive_more", "encoding_rs", - "foldhash", + "foldhash 0.1.5", "futures-core", "http 0.2.12", "httparse", @@ -45,7 +45,7 @@ dependencies = [ "local-channel", "mime", "percent-encoding", - "pin-project-lite 0.2.16", + "pin-project-lite 0.2.17", "rand 0.9.2", "sha1 0.10.6", "smallvec", @@ -102,7 +102,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e46f36bf0e5af44bdc4bdb36fbbd421aa98c79a9bce724e1edeb3894e10dc7f" dependencies = [ "futures-core", - "pin-project-lite 0.2.16", + "pin-project-lite 0.2.17", ] [[package]] @@ -112,7 +112,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" dependencies = [ "local-waker", - "pin-project-lite 0.2.16", + "pin-project-lite 0.2.17", ] [[package]] @@ -133,7 +133,7 @@ dependencies = [ "cfg-if", "derive_more", "encoding_rs", - "foldhash", + "foldhash 0.1.5", "futures-core", "futures-util", "impl-more", @@ -142,7 +142,7 @@ dependencies = [ "log", "mime", "once_cell", - "pin-project-lite 0.2.16", + "pin-project-lite 0.2.17", "regex-lite", "serde", "serde_json", @@ -154,21 +154,6 @@ dependencies = [ "url", ] -[[package]] -name = "addr2line" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - [[package]] name = "aead" version = "0.3.2" @@ -225,18 +210,18 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] [[package]] name = "anyhow" -version = "1.0.98" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "async-channel" @@ -258,7 +243,7 @@ dependencies = [ "concurrent-queue", "event-listener-strategy", "futures-core", - "pin-project-lite 0.2.16", + "pin-project-lite 0.2.17", ] [[package]] @@ -271,7 +256,7 @@ dependencies = [ "concurrent-queue", "fastrand 2.3.0", "futures-lite 2.6.1", - "pin-project-lite 0.2.16", + "pin-project-lite 0.2.17", "slab", ] @@ -316,7 +301,7 @@ checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc" dependencies = [ "event-listener 5.4.1", "event-listener-strategy", - "pin-project-lite 0.2.16", + "pin-project-lite 0.2.17", ] [[package]] @@ -390,7 +375,7 @@ dependencies = [ "log", "memchr", "once_cell", - "pin-project-lite 0.2.16", + "pin-project-lite 0.2.17", "pin-utils", "slab", "wasm-bindgen-futures", @@ -404,7 +389,7 @@ checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" dependencies = [ "async-stream-impl", "futures-core", - "pin-project-lite 0.2.16", + "pin-project-lite 0.2.17", ] [[package]] @@ -415,7 +400,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.117", ] [[package]] @@ -426,13 +411,13 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.88" +version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.117", ] [[package]] @@ -462,40 +447,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" -[[package]] -name = "axum" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" -dependencies = [ - "axum-core", - "bytes", - "form_urlencoded", - "futures-util", - "http 1.3.1", - "http-body 1.0.1", - "http-body-util", - "hyper 1.6.0", - "hyper-util", - "itoa", - "matchit", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite 0.2.16", - "rustversion", - "serde", - "serde_json", - "serde_path_to_error", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tower", - "tower-layer", - "tower-service", - "tracing", -] - [[package]] name = "axum-core" version = "0.5.2" @@ -504,41 +455,15 @@ checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" dependencies = [ "bytes", "futures-core", - "http 1.3.1", + "http 1.4.0", "http-body 1.0.1", "http-body-util", "mime", - "pin-project-lite 0.2.16", + "pin-project-lite 0.2.17", "rustversion", "sync_wrapper", "tower-layer", "tower-service", - "tracing", -] - -[[package]] -name = "axum-htmx" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2d4a162b7621482903309c0e8a990a866728b6312350147181230f840252314" -dependencies = [ - "axum-core", - "http 1.3.1", -] - -[[package]] -name = "backtrace" -version = "0.3.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-targets 0.52.6", ] [[package]] @@ -567,9 +492,9 @@ checksum = "383d29d513d8764dcdc42ea295d979eb99c3c9f00607b3692cf68a431f7dca72" [[package]] name = "bitflags" -version = "2.9.1" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "block-buffer" @@ -614,17 +539,11 @@ version = "1.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677" -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - [[package]] name = "bytes" -version = "1.10.1" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "bytestring" @@ -646,9 +565,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.1" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" @@ -656,6 +575,28 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.0", +] + +[[package]] +name = "chardetng" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14b8f0b65b7b08ae3c8187e8d77174de20cb6777864c6b832d8ad365999cf1ea" +dependencies = [ + "cfg-if", + "encoding_rs", + "memchr", +] + [[package]] name = "cipher" version = "0.2.5" @@ -680,6 +621,15 @@ version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f8a2ca5ac02d09563609681103aada9e1777d54fc57a5acd7a41404f9c93b6e" +[[package]] +name = "content_inspector" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7bda66e858c683005a53a9a60c69a4aca7eeaa45d124526e389f7aec8e62f38" +dependencies = [ + "memchr", +] + [[package]] name = "cookie" version = "0.14.4" @@ -728,12 +678,39 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "cpuid-bool" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcb25d077389e53838a8158c8e99174c5a9d902dee4904320db714f3c653ffba" +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -742,9 +719,9 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "typenum", @@ -805,7 +782,7 @@ checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.117", "unicode-xid", ] @@ -839,7 +816,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.104", + "syn 2.0.117", ] [[package]] @@ -875,7 +852,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.117", ] [[package]] @@ -910,23 +887,23 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.117", ] [[package]] name = "env_filter" -version = "0.1.3" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f" dependencies = [ "log", ] [[package]] name = "env_logger" -version = "0.11.8" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d" dependencies = [ "env_filter", "log", @@ -940,22 +917,23 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "erased-serde" -version = "0.4.6" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e004d887f51fcb9fef17317a2f3525c887d8aa3f4f50fed920816a688284a5b7" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" dependencies = [ "serde", + "serde_core", "typeid", ] [[package]] name = "errno" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.52.0", ] [[package]] @@ -972,7 +950,7 @@ checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" dependencies = [ "concurrent-queue", "parking", - "pin-project-lite 0.2.16", + "pin-project-lite 0.2.17", ] [[package]] @@ -982,7 +960,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" dependencies = [ "event-listener 5.4.1", - "pin-project-lite 0.2.16", + "pin-project-lite 0.2.17", ] [[package]] @@ -1026,11 +1004,17 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] @@ -1051,9 +1035,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -1061,15 +1045,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-lite" @@ -1082,7 +1066,7 @@ dependencies = [ "futures-io", "memchr", "parking", - "pin-project-lite 0.2.16", + "pin-project-lite 0.2.17", "waker-fn", ] @@ -1096,31 +1080,31 @@ dependencies = [ "futures-core", "futures-io", "parking", - "pin-project-lite 0.2.16", + "pin-project-lite 0.2.17", ] [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.117", ] [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-timer" @@ -1130,9 +1114,9 @@ checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -1141,20 +1125,10 @@ dependencies = [ "futures-sink", "futures-task", "memchr", - "pin-project-lite 0.2.16", - "pin-utils", + "pin-project-lite 0.2.17", "slab", ] -[[package]] -name = "fxhash" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" -dependencies = [ - "byteorder", -] - [[package]] name = "generator" version = "0.7.5" @@ -1202,14 +1176,28 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.3.3" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", - "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "r-efi 6.0.0", + "rand_core 0.10.0", + "wasip2", + "wasip3", ] [[package]] @@ -1222,12 +1210,6 @@ dependencies = [ "polyval", ] -[[package]] -name = "gimli" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" - [[package]] name = "glob" version = "0.3.2" @@ -1248,16 +1230,16 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" dependencies = [ "atomic-waker", "bytes", "fnv", "futures-core", "futures-sink", - "http 1.3.1", + "http 1.4.0", "indexmap", "slab", "tokio", @@ -1270,6 +1252,15 @@ name = "hashbrown" version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "headers" @@ -1280,7 +1271,7 @@ dependencies = [ "base64 0.22.1", "bytes", "headers-core", - "http 1.3.1", + "http 1.4.0", "httpdate", "mime", "sha1 0.10.6", @@ -1292,9 +1283,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" dependencies = [ - "http 1.3.1", + "http 1.4.0", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.5.2" @@ -1330,18 +1327,6 @@ dependencies = [ "utf8-width", ] -[[package]] -name = "htmx-rsx" -version = "0.0.0" -dependencies = [ - "anyhow", - "axum", - "axum-htmx", - "hypertext", - "tokio", - "tower-http", -] - [[package]] name = "http" version = "0.2.12" @@ -1355,12 +1340,11 @@ dependencies = [ [[package]] name = "http" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" dependencies = [ "bytes", - "fnv", "itoa", ] @@ -1372,7 +1356,7 @@ checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", "http 0.2.12", - "pin-project-lite 0.2.16", + "pin-project-lite 0.2.17", ] [[package]] @@ -1382,7 +1366,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.3.1", + "http 1.4.0", ] [[package]] @@ -1393,9 +1377,9 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http 1.3.1", + "http 1.4.0", "http-body 1.0.1", - "pin-project-lite 0.2.16", + "pin-project-lite 0.2.17", ] [[package]] @@ -1410,12 +1394,6 @@ dependencies = [ "log", ] -[[package]] -name = "http-range-header" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" - [[package]] name = "http-types" version = "2.12.0" @@ -1429,7 +1407,7 @@ dependencies = [ "cookie 0.14.4", "futures-lite 1.13.0", "infer", - "pin-project-lite 0.2.16", + "pin-project-lite 0.2.17", "rand 0.7.3", "serde", "serde_json", @@ -1465,7 +1443,7 @@ dependencies = [ "httparse", "httpdate", "itoa", - "pin-project-lite 0.2.16", + "pin-project-lite 0.2.17", "socket2 0.5.10", "tokio", "tower-service", @@ -1475,20 +1453,22 @@ dependencies = [ [[package]] name = "hyper" -version = "1.6.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" dependencies = [ + "atomic-waker", "bytes", "futures-channel", - "futures-util", + "futures-core", "h2", - "http 1.3.1", + "http 1.4.0", "http-body 1.0.1", "httparse", "httpdate", "itoa", - "pin-project-lite 0.2.16", + "pin-project-lite 0.2.17", + "pin-utils", "smallvec", "tokio", "want", @@ -1496,18 +1476,16 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.16" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ "bytes", - "futures-core", - "http 1.3.1", + "http 1.4.0", "http-body 1.0.1", - "hyper 1.6.0", - "pin-project-lite 0.2.16", + "hyper 1.8.1", + "pin-project-lite 0.2.17", "tokio", - "tower-service", ] [[package]] @@ -1535,7 +1513,7 @@ dependencies = [ "html-escape", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.117", ] [[package]] @@ -1624,6 +1602,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "idna" version = "1.0.3" @@ -1653,13 +1637,14 @@ checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" [[package]] name = "indexmap" -version = "2.10.0" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.16.1", "serde", + "serde_core", ] [[package]] @@ -1683,17 +1668,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "io-uring" -version = "0.7.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" -dependencies = [ - "bitflags", - "cfg-if", - "libc", -] - [[package]] name = "is-terminal" version = "0.4.16" @@ -1702,14 +1676,14 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "js-sys" @@ -1742,17 +1716,23 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" -version = "0.2.174" +version = "0.2.182" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" [[package]] name = "linux-raw-sys" -version = "0.9.4" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" @@ -1789,9 +1769,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.27" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" dependencies = [ "value-bag", ] @@ -1820,17 +1800,11 @@ dependencies = [ "regex-automata 0.1.10", ] -[[package]] -name = "matchit" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" - [[package]] name = "memchr" -version = "2.7.5" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "mime" @@ -1858,25 +1832,16 @@ dependencies = [ "unicase", ] -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", -] - [[package]] name = "mio" -version = "1.0.4" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", "log", "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -1888,7 +1853,7 @@ dependencies = [ "bytes", "encoding_rs", "futures-util", - "http 1.3.1", + "http 1.4.0", "httparse", "memchr", "mime", @@ -1927,9 +1892,9 @@ dependencies = [ [[package]] name = "ntex" -version = "2.15.1" +version = "3.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19dd8e0ee5031fa0de9e1c9da2ba48990ed9c9b230c2987a14ea95be4af6add6" +checksum = "ac57f3f6d756ac54b26245cab7a2e5c11df8362d7ff048af61d65decc5c96e55" dependencies = [ "base64 0.22.1", "bitflags", @@ -1942,6 +1907,7 @@ dependencies = [ "nanorand", "ntex-bytes", "ntex-codec", + "ntex-dispatcher", "ntex-h2", "ntex-http", "ntex-io", @@ -1954,78 +1920,94 @@ dependencies = [ "ntex-tls", "ntex-util", "percent-encoding", - "pin-project-lite 0.2.16", + "pin-project-lite 0.2.17", "regex", "serde", "serde_json", "serde_urlencoded", - "thiserror 2.0.12", + "thiserror 2.0.18", + "uuid", "variadics_please", ] [[package]] name = "ntex-bytes" -version = "0.1.30" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d23b86ef2f4a947e29e959a61bdae71c9d52a80df02936a9992bc6dbda9ddb" +checksum = "27496696418160466ceb3cb3a29cd23668202de08f328610cde991a36b704af4" dependencies = [ - "bitflags", "bytes", - "futures-core", "serde", ] [[package]] name = "ntex-codec" -version = "0.6.2" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69a7e111d946bb915d712df496728ca2a120b1b5643f66c580f13023bce46fda" +checksum = "1f071b0c1daa379de93b4bbaf8ed822f12539901bdb15a5901cd15168f5e8eca" dependencies = [ "ntex-bytes", ] +[[package]] +name = "ntex-dispatcher" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdc658d0b4caced7d7cfeefaeddd50f6c80265dc98e944beae3ac6601337b267" +dependencies = [ + "bitflags", + "log", + "ntex-codec", + "ntex-io", + "ntex-service", + "ntex-util", + "pin-project-lite 0.2.17", +] + [[package]] name = "ntex-h2" -version = "1.12.0" +version = "3.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41420ae3324234a8fbb3007dc924097400aca20de5bc90644ec6771b4c308a60" +checksum = "ff063201f811d97557a5053dc75cbbd417adf4234e57ed8ec11fb05e3f2fdc3d" dependencies = [ "bitflags", - "fxhash", + "foldhash 0.2.0", "log", "nanorand", "ntex-bytes", "ntex-codec", + "ntex-dispatcher", "ntex-http", "ntex-io", "ntex-net", + "ntex-server", "ntex-service", "ntex-util", - "pin-project-lite 0.2.16", - "thiserror 2.0.12", + "pin-project-lite 0.2.17", + "thiserror 2.0.18", ] [[package]] name = "ntex-http" -version = "0.1.14" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3102673534f57dbc7fc9e7f1aac4126f353366c67518eac0a7763bb2515f0a7a" +checksum = "ffb1f0643064d8ab21b70e0c9660b513ad3253744700c874668e6d7693ab3a39" dependencies = [ + "foldhash 0.2.0", "futures-core", - "fxhash", - "http 1.3.1", + "http 1.4.0", "itoa", "log", "ntex-bytes", "serde", - "thiserror 2.0.12", + "thiserror 2.0.18", ] [[package]] name = "ntex-io" -version = "2.14.0" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55eb13ef2e89f799ef0395911b6365052cab4cea65a7d2ef870e39732bf346b2" +checksum = "de281bab9b3f6330a30b620e1b7cbca4bb92de4c4abe634c4f08c472faaa4e01" dependencies = [ "bitflags", "log", @@ -2033,25 +2015,37 @@ dependencies = [ "ntex-codec", "ntex-service", "ntex-util", - "pin-project-lite 0.2.16", + "pin-project-lite 0.2.17", +] + +[[package]] +name = "ntex-io-uring" +version = "0.7.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81e60f4a52aae6b07b8a4c560f05802be74c10c2924838f1007f48684233aaaa" +dependencies = [ + "bitflags", + "cfg-if", + "libc", + "sc", ] [[package]] name = "ntex-macros" -version = "0.1.4" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7389855b7cf0a7cc4cd6748b6d31ad8d45481c9a4d6c977d289a469a362f7766" +checksum = "51138717dfe591b9b4063bf167ddcdc6fa8e3552157316f29f12c321493e3710" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.117", ] [[package]] name = "ntex-net" -version = "2.7.0" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef3d6829da93773089c38939803dd5cc348d0743b60b1b079e5529e3ee88d1fe" +checksum = "28bdb6cd6694011070753d228ba76f7683a0dd78d30ea0d0354702296f5f78bf" dependencies = [ "bitflags", "cfg-if", @@ -2060,19 +2054,38 @@ dependencies = [ "ntex-bytes", "ntex-http", "ntex-io", + "ntex-io-uring", + "ntex-polling", "ntex-rt", "ntex-service", "ntex-util", - "thiserror 2.0.12", + "scoped-tls", + "slab", + "socket2 0.6.3", + "thiserror 2.0.18", +] + +[[package]] +name = "ntex-polling" +version = "3.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad417300c371ebb585b3b67e94d16f5843ea4d59e93e8b59d5c979a363b76bb7" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite 0.2.17", + "rustix", + "windows-sys 0.60.2", ] [[package]] name = "ntex-router" -version = "0.5.3" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb9c68c26a87ffca54339be5f95223339db3e7bcc5d64733fef20812970a746f" +checksum = "203fc4ba8fd22db656cde62cfbfe705f776aa652ce8d7ce5c6093b7c37185f70" dependencies = [ - "http 1.3.1", + "http 1.4.0", "log", "ntex-bytes", "regex", @@ -2081,53 +2094,62 @@ dependencies = [ [[package]] name = "ntex-rt" -version = "0.4.32" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c30a11a3017f0bf2ea00d0bd6ba8f69e52906aa8c1f894a060341056d8b1eef8" +checksum = "6b4ca2ac74462e51e11adf189b250b2697a192f327d74d8c7e4b88ef684d6f0e" dependencies = [ "async-channel 2.5.0", + "async-task", + "crossbeam-channel", + "crossbeam-queue", + "foldhash 0.2.0", "futures-timer", "log", - "oneshot", + "ntex-service", + "oneshot 0.2.1", + "scoped-tls", + "swap-buffer-queue", ] [[package]] name = "ntex-server" -version = "2.8.1" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b886e739e5101ba06f083244bda0557997521c3ddf9b8f85ca74bc2aa165aa29" +checksum = "85ac36e4b11c0cf0ae53fea574a1ab37e0c54331eb78f0b5a5e02cf6604a1f04" dependencies = [ "async-channel 2.5.0", "atomic-waker", "core_affinity", "ctrlc", "log", - "ntex-bytes", + "ntex-io", "ntex-net", + "ntex-polling", "ntex-rt", "ntex-service", "ntex-util", - "oneshot", - "polling", + "oneshot 0.1.11", "signal-hook", - "socket2 0.5.10", + "socket2 0.6.3", + "uuid", ] [[package]] name = "ntex-service" -version = "3.5.0" +version = "4.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35dc63ff1a6d11eac0f27682997e4d8c2055b151e45e10dc1d76347b49fa52b7" +checksum = "7568541fe86432af0e9230341fe798179ca61a30063f992ce8ea2bd2d73e0e45" dependencies = [ + "foldhash 0.2.0", + "log", "slab", - "version_check", ] [[package]] name = "ntex-tls" -version = "2.6.1" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08c6c64b87ddbd44a9140810712ced321d3fec149d74e9b76beef11aa3bc8110" +checksum = "df010ac89eab9dc4ab3ec7507cea08788f964c476b6c0c50de5a88869ac7dc6b" dependencies = [ "log", "ntex-bytes", @@ -2139,21 +2161,21 @@ dependencies = [ [[package]] name = "ntex-util" -version = "2.13.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "546d60d666f713b18357d233aa9da7e38a30c934f0dbfe97526c7e475cd0f12e" +checksum = "092a46f60e64950809011a95d5ec6f634394b80d984bc44d8057b89a6a78c1f1" dependencies = [ "bitflags", + "foldhash 0.2.0", "futures-core", "futures-timer", - "fxhash", "log", "ntex-bytes", "ntex-rt", "ntex-service", - "pin-project-lite 0.2.16", + "pin-project-lite 0.2.17", "slab", - "thiserror 2.0.12", + "thiserror 2.0.18", ] [[package]] @@ -2182,15 +2204,6 @@ dependencies = [ "libc", ] -[[package]] -name = "object" -version = "0.36.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" -dependencies = [ - "memchr", -] - [[package]] name = "once_cell" version = "1.21.3" @@ -2203,6 +2216,12 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ce411919553d3f9fa53a0880544cda985a112117a0444d5ff1e870a893d6ea" +[[package]] +name = "oneshot" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe21416a02c693fb9f980befcb230ecc70b0b3d1cc4abf88b9675c4c1457f0c" + [[package]] name = "opaque-debug" version = "0.3.1" @@ -2264,14 +2283,14 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.104", + "syn 2.0.117", ] [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pin-project" @@ -2290,7 +2309,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.117", ] [[package]] @@ -2301,9 +2320,9 @@ checksum = "257b64915a082f7811703966789728173279bdebb956b143dbcd23f6f970a777" [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pin-utils" @@ -2331,15 +2350,15 @@ dependencies = [ "bytes", "futures-util", "headers", - "http 1.3.1", + "http 1.4.0", "http-body-util", - "hyper 1.6.0", + "hyper 1.8.1", "hyper-util", "mime", "nix", "parking_lot", "percent-encoding", - "pin-project-lite 0.2.16", + "pin-project-lite 0.2.17", "poem-derive", "regex", "rfc7239", @@ -2348,7 +2367,7 @@ dependencies = [ "serde_urlencoded", "smallvec", "sync_wrapper", - "thiserror 2.0.12", + "thiserror 2.0.18", "tokio", "tokio-util", "tracing", @@ -2364,7 +2383,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.117", ] [[package]] @@ -2376,7 +2395,7 @@ dependencies = [ "cfg-if", "concurrent-queue", "hermit-abi", - "pin-project-lite 0.2.16", + "pin-project-lite 0.2.17", "rustix", "windows-sys 0.60.2", ] @@ -2416,6 +2435,16 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + [[package]] name = "proc-macro-crate" version = "3.3.0" @@ -2433,9 +2462,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" [[package]] name = "proc-macro2" -version = "1.0.95" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -2448,16 +2477,16 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.117", "version_check", "yansi", ] [[package]] name = "quote" -version = "1.0.40" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -2468,6 +2497,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rand" version = "0.7.3" @@ -2502,6 +2537,17 @@ dependencies = [ "rand_core 0.9.3", ] +[[package]] +name = "rand" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" +dependencies = [ + "chacha20", + "getrandom 0.4.2", + "rand_core 0.10.0", +] + [[package]] name = "rand_chacha" version = "0.2.2" @@ -2556,9 +2602,15 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", ] +[[package]] +name = "rand_core" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" + [[package]] name = "rand_hc" version = "0.2.0" @@ -2594,19 +2646,19 @@ checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.117", ] [[package]] name = "regex" -version = "1.11.1" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", + "regex-automata 0.4.14", + "regex-syntax 0.8.10", ] [[package]] @@ -2620,13 +2672,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.5", + "regex-syntax 0.8.10", ] [[package]] @@ -2643,9 +2695,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "rfc7239" @@ -2676,7 +2728,7 @@ dependencies = [ "multer", "num_cpus", "parking_lot", - "pin-project-lite 0.2.16", + "pin-project-lite 0.2.17", "rand 0.8.5", "ref-cast", "rocket_codegen", @@ -2705,7 +2757,7 @@ dependencies = [ "proc-macro2", "quote", "rocket_http", - "syn 2.0.104", + "syn 2.0.117", "unicode-xid", "version_check", ] @@ -2726,7 +2778,7 @@ dependencies = [ "memchr", "pear", "percent-encoding", - "pin-project-lite 0.2.16", + "pin-project-lite 0.2.17", "ref-cast", "serde", "smallvec", @@ -2743,32 +2795,26 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56770675ebc04927ded3e60633437841581c285dc6236109ea25fbf3beb7b59e" -[[package]] -name = "rustc-demangle" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" - [[package]] name = "rustc_version" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" dependencies = [ - "semver", + "semver 0.9.0", ] [[package]] name = "rustix" -version = "1.0.8" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", - "windows-sys 0.60.2", + "windows-sys 0.52.0", ] [[package]] @@ -2779,49 +2825,50 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "salvo-serde-util" -version = "0.81.0" +version = "0.89.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b2cd0a2b0073c85f10eefa3722dbae18e3c2c9a2567f9d840ecc712127e30c" +checksum = "d2c771ed55960daafbdb7632ae469b4971815978560a7ff22a3654196b4f93fb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.117", ] [[package]] name = "salvo_core" -version = "0.81.0" +version = "0.89.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0978458bee0102c6c337040ea0b13c497ff1e31015c49c2cc9a387f813e2c1" +checksum = "2be136947f5a3c40e7d83796bdbcded19e957b527808207ce52fa662980a6b4c" dependencies = [ "async-trait", "base64 0.22.1", "bytes", + "chardetng", + "content_inspector", "enumflags2", "form_urlencoded", "futures-channel", "futures-util", "headers", - "http 1.3.1", + "http 1.4.0", "http-body-util", - "hyper 1.6.0", + "hyper 1.8.1", "hyper-util", "indexmap", "mime", "mime-infer", "multer", "multimap", - "nix", "parking_lot", "percent-encoding", "pin-project", - "rand 0.9.2", + "rand 0.10.0", "regex", "salvo_macros", "serde", @@ -2829,7 +2876,7 @@ dependencies = [ "serde_json", "sync_wrapper", "tempfile", - "thiserror 2.0.12", + "thiserror 2.0.18", "tokio", "tokio-util", "tracing", @@ -2837,18 +2884,24 @@ dependencies = [ [[package]] name = "salvo_macros" -version = "0.81.0" +version = "0.89.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b74585140d4b656e51bd95e985c7acb8e57fe3d25f2dbb7c2b21fec30a8a91ef" +checksum = "f86a6f3ed2d22973a1fd8d006e8e28e945a7d5c910251804c2615926d9ad0e2c" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", "regex", "salvo-serde-util", - "syn 2.0.104", + "syn 2.0.117", ] +[[package]] +name = "sc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "010e18bd3bfd1d45a7e666b236c78720df0d9a7698ebaa9c1c559961eb60a38b" + [[package]] name = "scoped-tls" version = "1.0.1" @@ -2870,6 +2923,12 @@ dependencies = [ "semver-parser", ] +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + [[package]] name = "semver-parser" version = "0.7.0" @@ -2878,10 +2937,11 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ + "serde_core", "serde_derive", ] @@ -2897,15 +2957,24 @@ dependencies = [ "xml-rs", ] +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.117", ] [[package]] @@ -2919,24 +2988,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.142" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", - "serde", -] - -[[package]] -name = "serde_path_to_error" -version = "0.1.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a" -dependencies = [ - "itoa", "serde", + "serde_core", + "zmij", ] [[package]] @@ -2987,7 +3047,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest 0.10.7", ] @@ -3005,7 +3065,7 @@ checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" dependencies = [ "block-buffer 0.9.0", "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest 0.9.0", "opaque-debug", ] @@ -3046,9 +3106,9 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" @@ -3068,12 +3128,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.0" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -3248,6 +3308,15 @@ dependencies = [ "sval_nested", ] +[[package]] +name = "swap-buffer-queue" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcec9c96aabcb077bc9c491e2d48d3c50654455a75451c742abe41f5d171bf5" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "syn" version = "1.0.109" @@ -3261,9 +3330,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.104" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -3287,7 +3356,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.117", ] [[package]] @@ -3297,10 +3366,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" dependencies = [ "fastrand 2.3.0", - "getrandom 0.3.3", + "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -3314,11 +3383,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.12", + "thiserror-impl 2.0.18", ] [[package]] @@ -3329,18 +3398,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.117", ] [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.117", ] [[package]] @@ -3366,7 +3435,7 @@ dependencies = [ "http-types", "kv-log-macro", "log", - "pin-project-lite 0.2.16", + "pin-project-lite 0.2.17", "route-recognizer", "serde", "serde_json", @@ -3453,33 +3522,30 @@ dependencies = [ [[package]] name = "tokio" -version = "1.47.1" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ - "backtrace", "bytes", - "io-uring", "libc", "mio", "parking_lot", - "pin-project-lite 0.2.16", + "pin-project-lite 0.2.17", "signal-hook-registry", - "slab", - "socket2 0.6.0", + "socket2 0.6.3", "tokio-macros", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.117", ] [[package]] @@ -3489,20 +3555,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" dependencies = [ "futures-core", - "pin-project-lite 0.2.16", + "pin-project-lite 0.2.17", "tokio", ] [[package]] name = "tokio-util" -version = "0.7.16" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", "futures-sink", - "pin-project-lite 0.2.16", + "pin-project-lite 0.2.17", "tokio", ] @@ -3547,48 +3613,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" -[[package]] -name = "tower" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite 0.2.16", - "sync_wrapper", - "tokio", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tower-http" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" -dependencies = [ - "bitflags", - "bytes", - "futures-core", - "futures-util", - "http 1.3.1", - "http-body 1.0.1", - "http-body-util", - "http-range-header", - "httpdate", - "mime", - "mime_guess", - "percent-encoding", - "pin-project-lite 0.2.16", - "tokio", - "tokio-util", - "tower-layer", - "tower-service", - "tracing", -] - [[package]] name = "tower-layer" version = "0.3.3" @@ -3603,32 +3627,32 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "log", - "pin-project-lite 0.2.16", + "pin-project-lite 0.2.17", "tracing-attributes", "tracing-core", ] [[package]] name = "tracing-attributes" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.117", ] [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", "valuable", @@ -3677,9 +3701,9 @@ checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" [[package]] name = "typenum" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "ubyte" @@ -3708,9 +3732,9 @@ checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-xid" @@ -3752,6 +3776,17 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "uuid" +version = "1.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "valuable" version = "0.1.1" @@ -3760,9 +3795,9 @@ checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "value-bag" -version = "1.11.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "943ce29a8a743eb10d6082545d861b24f9d1b160b7d741e0f2cdf726bec909c5" +checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" dependencies = [ "value-bag-serde1", "value-bag-sval2", @@ -3770,20 +3805,20 @@ dependencies = [ [[package]] name = "value-bag-serde1" -version = "1.11.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35540706617d373b118d550d41f5dfe0b78a0c195dc13c6815e92e2638432306" +checksum = "16530907bfe2999a1773ca5900a65101e092c70f642f25cc23ca0c43573262c5" dependencies = [ "erased-serde", - "serde", + "serde_core", "serde_fmt", ] [[package]] name = "value-bag-sval2" -version = "1.11.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fe7e140a2658cc16f7ee7a86e413e803fc8f9b5127adc8755c19f9fefa63a52" +checksum = "d00ae130edd690eaa877e4f40605d534790d1cf1d651e7685bd6a144521b251f" dependencies = [ "sval", "sval_buffer", @@ -3802,7 +3837,7 @@ checksum = "41b6d82be61465f97d42bd1d15bf20f3b0a3a0905018f38f9d6f6962055b0b5c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.117", ] [[package]] @@ -3836,9 +3871,9 @@ dependencies = [ "futures-channel", "futures-util", "headers", - "http 1.3.1", + "http 1.4.0", "http-body-util", - "hyper 1.6.0", + "hyper 1.8.1", "log", "mime", "mime_guess", @@ -3867,12 +3902,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasi" -version = "0.14.2+wasi-0.2.4" +name = "wasip2" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" dependencies = [ - "wit-bindgen-rt", + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", ] [[package]] @@ -3897,7 +3941,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.117", "wasm-bindgen-shared", ] @@ -3932,7 +3976,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.117", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3946,6 +3990,40 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver 1.0.27", +] + [[package]] name = "web-sys" version = "0.3.77" @@ -3999,6 +4077,12 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-sys" version = "0.52.0" @@ -4026,6 +4110,15 @@ dependencies = [ "windows-targets 0.53.3", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -4063,7 +4156,7 @@ version = "0.53.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" dependencies = [ - "windows-link", + "windows-link 0.1.3", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", @@ -4214,20 +4307,99 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winnow" -version = "0.7.12" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" dependencies = [ "memchr", ] [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver 1.0.27", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", ] [[package]] @@ -4271,7 +4443,7 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.117", "synstructure", ] @@ -4292,7 +4464,7 @@ checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.117", ] [[package]] @@ -4312,7 +4484,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.117", "synstructure", ] @@ -4346,5 +4518,11 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.117", ] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index baca3be..7ff641d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,33 +1,35 @@ [workspace] resolver = "2" -members = ["examples/*", "hypertext", "hypertext-macros"] -default-members = ["hypertext", "hypertext-macros"] +members = ["crates/*"] +default-members = ["crates/*"] [workspace.package] version = "0.12.1" -authors = ["Vidhan Bhatt "] edition = "2024" -description = "A blazing fast type-checked HTML macro crate." +description = "A blazing fast type checked HTML macro crate." readme = "README.md" homepage = "https://github.com/vidhanio/hypertext" repository = "https://github.com/vidhanio/hypertext" license = "MIT" -keywords = ["html", "macro"] +keywords = ["html", "macro", "maud", "rsx"] categories = ["template-engine"] +[workspace.metadata.crane] +name = "hypertext" + [workspace.dependencies] +hypertext = { version = "0.12.1", path = "./crates/hypertext" } +hypertext-macros = { version = "0.12.1", path = "./crates/hypertext-macros" } + html-escape = { version = "0.2", default-features = false } -hypertext-macros = { version = "0.12.1", path = "./hypertext-macros" } [workspace.lints] [workspace.lints.clippy] cargo = { level = "warn", priority = -1 } nursery = { level = "warn", priority = -1 } pedantic = { level = "warn", priority = -1 } -too_long_first_doc_paragraph = "allow" [workspace.lints.rust] missing_copy_implementations = "warn" missing_debug_implementations = "warn" missing_docs = "warn" - diff --git a/README.md b/README.md index 062b2f9..45dfb69 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # `hypertext` -A blazing fast type-checked HTML macro crate. +A blazing fast type checked HTML macro crate. ## Features -- Type checking for element names/attributes, including extensible support for custom frameworks like [htmx](https://htmx.org/) and [Alpine.js](https://alpinejs.dev/) +- Compile-time type checking for element names/attributes, including extensible support for custom frameworks like [htmx](https://htmx.org/) and [Alpine.js](https://alpinejs.dev/) - `#![no_std]` support - [Extremely fast](https://github.com/askama-rs/template-benchmark#benchmark-results), using lazy rendering to minimize allocation @@ -55,6 +55,5 @@ let shopping_list_rsx = rsx! { - [vidhan.io](https://github.com/vidhanio/site) (my website!) - [The Brainmade Mark](https://github.com/0atman/BrainMade-org) - [Lipstick on a pig -- a website for hosting volunteer-built tarballs for KISS Linux](https://github.com/kiedtl/loap) -- [web.youwen.dev](https://web.youwen.dev) ― [@youwen5](https://github.com/youwen5)'s personal website Make a pull request to list your project here! diff --git a/hypertext-macros/Cargo.toml b/crates/hypertext-macros/Cargo.toml similarity index 95% rename from hypertext-macros/Cargo.toml rename to crates/hypertext-macros/Cargo.toml index d7ee51d..e976ab3 100644 --- a/hypertext-macros/Cargo.toml +++ b/crates/hypertext-macros/Cargo.toml @@ -1,7 +1,6 @@ [package] name = "hypertext-macros" version.workspace = true -authors.workspace = true edition.workspace = true description.workspace = true documentation = "https://docs.rs/hypertext-macros" @@ -23,4 +22,3 @@ syn = { version = "2", features = ["extra-traits", "full"] } [lints] workspace = true - diff --git a/hypertext-macros/src/derive.rs b/crates/hypertext-macros/src/derive.rs similarity index 64% rename from hypertext-macros/src/derive.rs rename to crates/hypertext-macros/src/derive.rs index e023353..e451830 100644 --- a/hypertext-macros/src/derive.rs +++ b/crates/hypertext-macros/src/derive.rs @@ -3,16 +3,16 @@ use quote::quote; use syn::{DeriveInput, Error, spanned::Spanned}; use crate::{ - AttributeValueNode, Context, Document, Maud, Nodes, Rsx, - html::{self, generate::Generator}, + AttributeValue, Config, Document, Many, Maud, Rsx, Semantics, + html::{Context, generate::Generator}, }; #[allow(clippy::needless_pass_by_value)] pub fn renderable(input: DeriveInput) -> syn::Result { - match (renderable_element(&input), attribute_renderable(&input)) { + match (renderable_node(&input), renderable_attribute(&input)) { (Ok(None), Ok(None)) => Err(Error::new( Span::call_site(), - "expected at least one of `maud`, `rsx`, or `attribute` attributes", + "expected at least one of `#[maud(...)]`, `#[rsx(...)]`, or `#[attribute(...)]`", )), (Ok(element), Ok(attribute)) => Ok(quote! { #element @@ -26,7 +26,7 @@ pub fn renderable(input: DeriveInput) -> syn::Result { } } -fn renderable_element(input: &DeriveInput) -> syn::Result> { +fn renderable_node(input: &DeriveInput) -> syn::Result> { let mut attrs = input .attrs .iter() @@ -34,14 +34,14 @@ fn renderable_element(input: &DeriveInput) -> syn::Result> { if attr.path().is_ident("maud") { Some(( attr, - html::generate::lazy::> - as fn(TokenStream, bool) -> syn::Result, + (|tokens| Config::Lazy(Semantics::Move).generate::>(tokens)) + as fn(_) -> _, )) } else if attr.path().is_ident("rsx") { Some(( attr, - html::generate::lazy::> - as fn(TokenStream, bool) -> syn::Result, + (|tokens| Config::Lazy(Semantics::Move).generate::>(tokens)) + as fn(_) -> _, )) } else { None @@ -49,17 +49,17 @@ fn renderable_element(input: &DeriveInput) -> syn::Result> { }) .peekable(); - let (lazy_fn, tokens) = match (attrs.next(), attrs.peek()) { + let (generate_fn, tokens) = match (attrs.next(), attrs.peek()) { (Some((attr, f)), None) => (f, attr.meta.require_list()?.tokens.clone()), (Some((attr, _)), Some(_)) => { let mut error = Error::new( attr.span(), - "cannot have multiple `maud` or `rsx` attributes", + "cannot have multiple `#[maud(...)]` or `#[rsx(...)]` attributes", ); for (attr, _) in attrs { - error.combine(syn::Error::new( + error.combine(Error::new( attr.span(), - "cannot have multiple `maud` or `rsx` attributes", + "cannot have multiple `#[maud(...)]` or `#[rsx(...)]` attributes", )); } return Err(error); @@ -69,7 +69,7 @@ fn renderable_element(input: &DeriveInput) -> syn::Result> { } }; - let lazy = lazy_fn(tokens, true)?; + let lazy = generate_fn(tokens)?; let name = &input.ident; let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); @@ -78,14 +78,14 @@ fn renderable_element(input: &DeriveInput) -> syn::Result> { #[automatically_derived] impl #impl_generics ::hypertext::Renderable for #name #ty_generics #where_clause { fn render_to(&self, #buffer_ident: &mut ::hypertext::Buffer) { - ::hypertext::Renderable::render_to(&#lazy, #buffer_ident); + #buffer_ident.push(#lazy); } } }; Ok(Some(output)) } -fn attribute_renderable(input: &DeriveInput) -> syn::Result> { +fn renderable_attribute(input: &DeriveInput) -> syn::Result> { let mut attrs = input .attrs .iter() @@ -97,12 +97,12 @@ fn attribute_renderable(input: &DeriveInput) -> syn::Result> (Some(_), Some(_)) => { let mut error = Error::new( Span::call_site(), - "cannot have multiple `attribute` attributes", + "cannot have multiple `#[attribute(...)]` attributes", ); for attr in attrs { - error.combine(syn::Error::new( + error.combine(Error::new( attr.span(), - "cannot have multiple `attribute` attributes", + "cannot have multiple `#[attribute(...)]` attributes", )); } return Err(error); @@ -112,23 +112,20 @@ fn attribute_renderable(input: &DeriveInput) -> syn::Result> } }; - let lazy = html::generate::lazy::>(tokens, true)?; + let lazy = Config::Lazy(Semantics::Move).generate::>(tokens)?; let name = &input.ident; let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); let buffer_ident = Generator::buffer_ident(); - let context_marker = Context::AttributeValue.marker_type(); + let ctx = AttributeValue::marker_type(); let output = quote! { #[automatically_derived] - impl #impl_generics ::hypertext::Renderable<#context_marker> for #name #ty_generics + impl #impl_generics ::hypertext::Renderable<#ctx> for #name #ty_generics #where_clause { fn render_to( &self, #buffer_ident: &mut ::hypertext::AttributeBuffer, ) { - ::hypertext::Renderable::render_to( - &#lazy, - #buffer_ident, - ); + #buffer_ident.push(#lazy); } } }; diff --git a/hypertext-macros/src/html/basics.rs b/crates/hypertext-macros/src/html/basics.rs similarity index 90% rename from hypertext-macros/src/html/basics.rs rename to crates/hypertext-macros/src/html/basics.rs index 0c68feb..f41cfcf 100644 --- a/hypertext-macros/src/html/basics.rs +++ b/crates/hypertext-macros/src/html/basics.rs @@ -3,14 +3,14 @@ use std::fmt::{self, Display, Formatter, Write}; use proc_macro2::{Span, TokenStream}; use quote::ToTokens; use syn::{ - Ident, LitBool, LitChar, LitFloat, LitInt, LitStr, Token, + Error, Ident, LitBool, LitChar, LitFloat, LitInt, LitStr, Token, ext::IdentExt, parse::{Parse, ParseStream}, spanned::Spanned, }; #[derive(PartialEq, Eq, Clone)] -pub struct UnquotedName(Vec); +pub struct UnquotedName(pub Vec); impl UnquotedName { pub fn ident_string(&self) -> String { @@ -139,7 +139,7 @@ impl Parse for UnquotedName { } #[derive(Clone, PartialEq, Eq)] -enum NameFragment { +pub enum NameFragment { Ident(Ident), Int(LitInt), Hyphen(Token![-]), @@ -213,24 +213,6 @@ impl Literal { Self::Char(lit) => LitStr::new(&lit.value().to_string(), lit.span()), } } - - pub fn parse_any(input: ParseStream) -> syn::Result { - let lookahead = input.lookahead1(); - - if lookahead.peek(LitStr) { - input.parse().map(Self::Str) - } else if lookahead.peek(LitInt) { - input.parse().map(Self::Int) - } else if lookahead.peek(LitBool) { - input.parse().map(Self::Bool) - } else if lookahead.peek(LitFloat) { - input.parse().map(Self::Float) - } else if lookahead.peek(LitChar) { - input.parse().map(Self::Char) - } else { - Err(lookahead.error()) - } - } } impl Parse for Literal { @@ -242,7 +224,7 @@ impl Parse for Literal { if !lit.suffix().is_empty() { let suffix = lit.suffix(); let next_quote = if input.peek(LitStr) { r#"\""# } else { "" }; - return Err(syn::Error::new_spanned( + return Err(Error::new_spanned( &lit, format!( r#"string suffixes are not allowed in literals (you probably meant `"...\"{suffix}{next_quote}..."` or `"..." {suffix}`)"#, @@ -254,7 +236,7 @@ impl Parse for Literal { } else if lookahead.peek(LitInt) { let lit = input.parse::()?; if !lit.suffix().is_empty() { - return Err(syn::Error::new_spanned( + return Err(Error::new_spanned( &lit, "integer literals cannot have suffixes", )); @@ -265,7 +247,7 @@ impl Parse for Literal { } else if lookahead.peek(LitFloat) { let lit = input.parse::()?; if !lit.suffix().is_empty() { - return Err(syn::Error::new_spanned( + return Err(Error::new_spanned( &lit, "float literals cannot have suffixes", )); @@ -274,7 +256,7 @@ impl Parse for Literal { } else if lookahead.peek(LitChar) { let lit = input.parse::()?; if !lit.suffix().is_empty() { - return Err(syn::Error::new_spanned( + return Err(Error::new_spanned( &lit, "character literals cannot have suffixes", )); diff --git a/hypertext-macros/src/html/component.rs b/crates/hypertext-macros/src/html/component.rs similarity index 76% rename from hypertext-macros/src/html/component.rs rename to crates/hypertext-macros/src/html/component.rs index d37d758..06034f0 100644 --- a/hypertext-macros/src/html/component.rs +++ b/crates/hypertext-macros/src/html/component.rs @@ -1,14 +1,14 @@ use proc_macro2::TokenStream; use quote::{ToTokens, quote, quote_spanned}; use syn::{ - Ident, LitBool, LitChar, LitFloat, LitInt, LitStr, Token, + Ident, Lit, Token, parse::{Parse, ParseStream}, spanned::Spanned, token::{Brace, Paren}, }; -use super::{ElementBody, Generate, Generator, Literal, ParenExpr, Syntax}; -use crate::{AttributeValueNode, Context}; +use super::{ElementBody, Generate, Generator, ParenExpr, Syntax}; +use crate::{AttributeValue, html::Node}; pub struct Component { pub name: Ident, @@ -18,14 +18,13 @@ pub struct Component { } impl Generate for Component { - const CONTEXT: Context = Context::Node; + type Context = Node; fn generate(&self, g: &mut Generator) { let fields = self.attrs.iter().map(|attr| { let name = &attr.name; - let value = &attr.value_expr(); - - quote!(#name: #value,) + attr.value_expr() + .map_or_else(|| quote!(#name,), |value| quote!(#name: #value,)) }); let children = match &self.body { @@ -68,18 +67,18 @@ impl Generate for Component { } }; - g.push_expr(Paren::default(), Self::CONTEXT, &init); + g.push_expr::(Paren::default(), &init); } } pub struct ComponentAttribute { name: Ident, - value: ComponentAttributeValue, + value: Option, } impl ComponentAttribute { - fn value_expr(&self) -> TokenStream { - match &self.value { + fn value_expr(&self) -> Option { + self.value.as_ref().map(|value| match value { ComponentAttributeValue::Literal(lit) => lit.to_token_stream(), ComponentAttributeValue::Ident(ident) => ident.to_token_stream(), ComponentAttributeValue::Expr(expr) => { @@ -91,7 +90,7 @@ impl ComponentAttribute { tokens } - } + }) } } @@ -100,31 +99,30 @@ impl Parse for ComponentAttribute { Ok(Self { name: input.parse()?, value: { - input.parse::()?; + if input.peek(Token![=]) { + input.parse::()?; - input.parse()? + Some(input.parse()?) + } else { + None + } }, }) } } pub enum ComponentAttributeValue { - Literal(Literal), + Literal(Lit), Ident(Ident), - Expr(ParenExpr), + Expr(ParenExpr), } impl Parse for ComponentAttributeValue { fn parse(input: ParseStream) -> syn::Result { let lookahead = input.lookahead1(); - if lookahead.peek(LitStr) - || lookahead.peek(LitInt) - || lookahead.peek(LitBool) - || lookahead.peek(LitFloat) - || lookahead.peek(LitChar) - { - input.call(Literal::parse_any).map(Self::Literal) + if lookahead.peek(Lit) { + input.parse().map(Self::Literal) } else if lookahead.peek(Ident) { input.parse().map(Self::Ident) } else if lookahead.peek(Paren) { diff --git a/hypertext-macros/src/html/control.rs b/crates/hypertext-macros/src/html/control.rs similarity index 68% rename from hypertext-macros/src/html/control.rs rename to crates/hypertext-macros/src/html/control.rs index 4e9dea9..c69416f 100644 --- a/hypertext-macros/src/html/control.rs +++ b/crates/hypertext-macros/src/html/control.rs @@ -1,23 +1,24 @@ +use std::convert::Infallible; + use proc_macro2::TokenStream; use quote::{ToTokens, quote}; use syn::{ - Expr, Local, Pat, Stmt, Token, braced, + Expr, Pat, PatType, Token, braced, parse::{Parse, ParseStream}, token::Brace, }; -use super::{AnyBlock, Generate, Generator, Node, Nodes}; -use crate::Context; +use super::{AnyBlock, Context, Generate, Generator, Many}; -pub enum Control { +pub enum Control { Let(Let), - If(If), - For(For), - While(While), - Match(Match), + If(If), + For(For), + While(While), + Match(Match), } -impl Parse for Control { +impl Parse for Control { fn parse(input: ParseStream) -> syn::Result { input.parse::()?; @@ -39,8 +40,8 @@ impl Parse for Control { } } -impl Generate for Control { - const CONTEXT: Context = N::CONTEXT; +impl Generate for Control { + type Context = C; fn generate(&self, g: &mut Generator) { match self { @@ -53,57 +54,89 @@ impl Generate for Control { } } -pub struct Let(Local); +pub struct Let { + let_token: Token![let], + pat: Pat, + init: Option<(Token![=], Expr)>, + semi_token: Token![;], +} impl Parse for Let { fn parse(input: ParseStream) -> syn::Result { - let local = match input.parse()? { - Stmt::Local(local) => local, - stmt => return Err(syn::Error::new_spanned(stmt, "expected `let` statement")), - }; - - Ok(Self(local)) + Ok(Self { + let_token: input.parse()?, + pat: { + let pat = input.call(Pat::parse_single)?; + if input.peek(Token![:]) { + Pat::Type(PatType { + attrs: Vec::new(), + pat: Box::new(pat), + colon_token: input.parse()?, + ty: input.parse()?, + }) + } else { + pat + } + }, + init: if input.peek(Token![=]) { + Some((input.parse()?, input.parse()?)) + } else { + None + }, + semi_token: input.parse()?, + }) } } impl Generate for Let { - const CONTEXT: Context = Context::Node; + type Context = Infallible; fn generate(&self, g: &mut Generator) { - g.push_stmt(&self.0); + let let_token = self.let_token; + let pat = &self.pat; + let (eq_token, expr) = self + .init + .as_ref() + .map(|(eq_token, expr)| (eq_token, expr)) + .unzip(); + let semi_token = self.semi_token; + + g.push_stmt(quote! { + #let_token #pat #eq_token #expr #semi_token + }); } } -pub struct ControlBlock { +pub struct ControlBlock { brace_token: Brace, - nodes: Nodes, + children: Many, } -impl ControlBlock { +impl ControlBlock { fn block(&self, g: &mut Generator) -> AnyBlock { - self.nodes.block(g, self.brace_token) + self.children.block(g, self.brace_token) } } -impl Parse for ControlBlock { +impl Parse for ControlBlock { fn parse(input: ParseStream) -> syn::Result { let content; Ok(Self { brace_token: braced!(content in input), - nodes: content.parse()?, + children: content.parse()?, }) } } -pub struct If { +pub struct If { if_token: Token![if], cond: Expr, - then_block: ControlBlock, - else_branch: Option<(Token![else], Box>)>, + then_block: ControlBlock, + else_branch: Option<(Token![else], Box>)>, } -impl Parse for If { +impl Parse for If { fn parse(input: ParseStream) -> syn::Result { Ok(Self { if_token: input.parse()?, @@ -120,11 +153,11 @@ impl Parse for If { } } -impl Generate for If { - const CONTEXT: Context = N::CONTEXT; +impl Generate for If { + type Context = C; fn generate(&self, g: &mut Generator) { - fn to_expr(if_: &If, g: &mut Generator) -> TokenStream { + fn to_expr(if_: &If, g: &mut Generator) -> TokenStream { let if_token = if_.if_token; let cond = &if_.cond; let then_block = if_.then_block.block(g); @@ -152,12 +185,12 @@ impl Generate for If { } } -pub enum ControlIfOrBlock { - If(If), - Block(ControlBlock), +pub enum ControlIfOrBlock { + If(If), + Block(ControlBlock), } -impl Parse for ControlIfOrBlock { +impl Parse for ControlIfOrBlock { fn parse(input: ParseStream) -> syn::Result { let lookahead = input.lookahead1(); @@ -171,15 +204,15 @@ impl Parse for ControlIfOrBlock { } } -pub struct For { +pub struct For { for_token: Token![for], pat: Pat, in_token: Token![in], expr: Expr, - block: ControlBlock, + block: ControlBlock, } -impl Parse for For { +impl Parse for For { fn parse(input: ParseStream) -> syn::Result { Ok(Self { for_token: input.parse()?, @@ -191,8 +224,8 @@ impl Parse for For { } } -impl Generate for For { - const CONTEXT: Context = N::CONTEXT; +impl Generate for For { + type Context = C; fn generate(&self, g: &mut Generator) { let for_token = self.for_token; @@ -208,13 +241,13 @@ impl Generate for For { } } -pub struct While { +pub struct While { while_token: Token![while], cond: Expr, - block: ControlBlock, + block: ControlBlock, } -impl Parse for While { +impl Parse for While { fn parse(input: ParseStream) -> syn::Result { Ok(Self { while_token: input.parse()?, @@ -224,8 +257,8 @@ impl Parse for While { } } -impl Generate for While { - const CONTEXT: Context = N::CONTEXT; +impl Generate for While { + type Context = C; fn generate(&self, g: &mut Generator) { let while_token = self.while_token; @@ -239,14 +272,14 @@ impl Generate for While { } } -pub struct Match { +pub struct Match { match_token: Token![match], expr: Expr, brace_token: Brace, - arms: Vec>, + arms: Vec>, } -impl Parse for Match { +impl Parse for Match { fn parse(input: ParseStream) -> syn::Result { let content; @@ -267,8 +300,8 @@ impl Parse for Match { } } -impl Generate for Match { - const CONTEXT: Context = N::CONTEXT; +impl Generate for Match { + type Context = C; fn generate(&self, g: &mut Generator) { let arms = self @@ -283,8 +316,8 @@ impl Generate for Match { let fat_arrow_token = arm.fat_arrow_token; let block = match &arm.body { MatchNodeArmBody::Block(block) => block.block(g), - MatchNodeArmBody::Node(node) => { - g.block_with(Brace::default(), |g| g.push(node)) + MatchNodeArmBody::Child(child) => { + g.block_with(Brace::default(), |g| g.push(child)) } }; let comma = arm.comma_token; @@ -305,15 +338,15 @@ impl Generate for Match { } } -pub struct MatchNodeArm { +pub struct MatchNodeArm { pat: Pat, guard: Option<(Token![if], Expr)>, fat_arrow_token: Token![=>], - body: MatchNodeArmBody, + body: MatchNodeArmBody, comma_token: Option, } -impl Parse for MatchNodeArm { +impl Parse for MatchNodeArm { fn parse(input: ParseStream) -> syn::Result { Ok(Self { pat: input.call(Pat::parse_multi_with_leading_vert)?, @@ -329,17 +362,17 @@ impl Parse for MatchNodeArm { } } -pub enum MatchNodeArmBody { - Block(ControlBlock), - Node(N), +pub enum MatchNodeArmBody { + Block(ControlBlock), + Child(C), } -impl Parse for MatchNodeArmBody { +impl Parse for MatchNodeArmBody { fn parse(input: ParseStream) -> syn::Result { if input.peek(Brace) { input.parse().map(Self::Block) } else { - input.parse().map(Self::Node) + input.parse().map(Self::Child) } } } diff --git a/hypertext-macros/src/html/generate.rs b/crates/hypertext-macros/src/html/generate.rs similarity index 73% rename from hypertext-macros/src/html/generate.rs rename to crates/hypertext-macros/src/html/generate.rs index 08e40cd..1569c5b 100644 --- a/hypertext-macros/src/html/generate.rs +++ b/crates/hypertext-macros/src/html/generate.rs @@ -1,58 +1,86 @@ use std::{ + convert::Infallible, iter, ops::{Deref, DerefMut}, }; use proc_macro2::{Ident, Span, TokenStream}; -use quote::{ToTokens, quote, quote_spanned}; +use quote::{quote, quote_spanned, ToTokens}; use syn::{ - LitStr, braced, + braced, parse::Parse, token::{Brace, Paren}, + Error, LitStr, }; use super::UnquotedName; +use crate::html::Context; -pub fn lazy(tokens: TokenStream, move_: bool) -> syn::Result { - let mut g = Generator::new_closure(T::CONTEXT); +#[derive(Debug, Clone, Copy)] +pub enum Config { + Lazy(Semantics), + Simple, +} - g.push(syn::parse2::(tokens)?); +impl Config { + pub fn generate(self, tokens: TokenStream) -> syn::Result { + match self { + Self::Lazy(move_) => { + let mut g = Generator::new_closure(); - let block = g.finish(); + let size_estimate = tokens.to_string().len(); - let buffer_ident = Generator::buffer_ident(); + g.push(syn::parse2::(tokens)?); - let move_token = move_.then(|| quote!(move)); + let block = g.finish(); - let marker_ident = T::CONTEXT.marker_type(); + let buffer_ident = Generator::buffer_ident(); - Ok(quote! { - ::hypertext::Lazy::<_, #marker_ident>::dangerously_create( - #move_token |#buffer_ident: &mut ::hypertext::Buffer<#marker_ident>| { + let ctx = T::Context::marker_type(); - #block + Ok(quote! { + ::hypertext::Lazy::<_, #ctx>::dangerously_create( + #move_ |#buffer_ident: &mut ::hypertext::Buffer<#ctx>| { + #buffer_ident.dangerously_get_string().reserve(#size_estimate); + #block + } + ) + }) } - ) - }) -} + Self::Simple => { + let mut g = Generator::new_static(); -pub fn literal(tokens: TokenStream) -> syn::Result { - let mut g = Generator::new_static(T::CONTEXT); + g.push(syn::parse2::(tokens)?); - g.push(syn::parse2::(tokens)?); + let literal = g.finish().to_token_stream(); - let literal = g.finish().to_token_stream(); + let ctx = T::Context::marker_type(); + + Ok(quote! { + ::hypertext::Raw::<_, #ctx>::dangerously_create(#literal) + }) + } + } + } +} - let marker_ident = T::CONTEXT.marker_type(); +#[derive(Debug, Clone, Copy)] +pub enum Semantics { + Move, + Borrow, +} - Ok(quote! { - ::hypertext::Raw::<_, #marker_ident>::dangerously_create(#literal) - }) +impl ToTokens for Semantics { + fn to_tokens(&self, tokens: &mut TokenStream) { + match self { + Self::Move => quote!(move).to_tokens(tokens), + Self::Borrow => {} + } + } } pub struct Generator { lazy: bool, - context: Context, brace_token: Brace, parts: Vec, checks: Checks, @@ -63,18 +91,17 @@ impl Generator { Ident::new("__hypertext_buffer", Span::mixed_site()) } - fn new_closure(context: Context) -> Self { - Self::new_with_brace(context, true, Brace::default()) + fn new_closure() -> Self { + Self::new_with_brace(true, Brace::default()) } - fn new_static(context: Context) -> Self { - Self::new_with_brace(context, false, Brace::default()) + fn new_static() -> Self { + Self::new_with_brace(false, Brace::default()) } - const fn new_with_brace(context: Context, lazy: bool, brace_token: Brace) -> Self { + const fn new_with_brace(lazy: bool, brace_token: Brace) -> Self { Self { lazy, - context, brace_token, parts: Vec::new(), checks: Checks::new(), @@ -88,23 +115,18 @@ impl Generator { let mut parts = self.parts.into_iter(); - let mut size_estimate = 0; - while let Some(part) = parts.next() { match part { Part::Static(lit) => { let mut dynamic_stmt = None; - let static_parts = iter::once(lit) - .chain(parts.by_ref().map_while(|part| match part { + let static_parts = + iter::once(lit).chain(parts.by_ref().map_while(|part| match part { Part::Static(lit) => Some(lit), Part::Dynamic(stmt) => { dynamic_stmt = Some(stmt); None } - })) - .inspect(|static_part| { - size_estimate += static_part.value().len(); - }); + })); stmts.extend(quote! { #buffer_ident.dangerously_get_string().push_str(::core::concat!(#(#static_parts),*)); @@ -117,10 +139,7 @@ impl Generator { } } - quote! { - #buffer_ident.dangerously_get_string().reserve(#size_estimate); - #stmts - } + stmts } else { let mut static_parts = Vec::new(); let mut errors = TokenStream::new(); @@ -129,11 +148,8 @@ impl Generator { match part { Part::Static(lit) => static_parts.push(lit), Part::Dynamic(stmt) => errors.extend( - syn::Error::new_spanned( - stmt, - "static evaluation cannot contain dynamic parts", - ) - .to_compile_error(), + Error::new_spanned(stmt, "simple evaluation cannot contain dynamic parts") + .to_compile_error(), ), } } @@ -156,7 +172,7 @@ impl Generator { } pub fn block_with(&mut self, brace_token: Brace, f: impl FnOnce(&mut Self)) -> AnyBlock { - let mut g = Self::new_with_brace(self.context, true, brace_token); + let mut g = Self::new_with_brace(true, brace_token); f(&mut g); @@ -178,12 +194,9 @@ impl Generator { self.parts.push(Part::Static(LitStr::new(s, span))); } - pub fn push_escaped_lit(&mut self, context: Context, lit: &LitStr) { + pub fn push_escaped_lit(&mut self, lit: &LitStr) { let value = lit.value(); - let escaped_value = match context { - Context::Node => html_escape::encode_text(&value), - Context::AttributeValue => html_escape::encode_double_quoted_attribute(&value), - }; + let escaped_value = C::escape(&value); self.parts .push(Part::Static(LitStr::new(&escaped_value, lit.span()))); @@ -195,17 +208,10 @@ impl Generator { } } - pub fn push_expr(&mut self, paren_token: Paren, context: Context, expr: impl ToTokens) { + pub fn push_expr(&mut self, paren_token: Paren, expr: impl ToTokens) { let buffer_ident = Self::buffer_ident(); - let buffer_expr = match (self.context, context) { - (Context::Node, Context::Node) | (Context::AttributeValue, Context::AttributeValue) => { - quote!(#buffer_ident) - } - (Context::Node, Context::AttributeValue) => { - quote!(#buffer_ident.as_attribute_buffer()) - } - (Context::AttributeValue, Context::Node) => unreachable!(), - }; + let ctx = C::marker_type(); + let buffer_expr = quote!(#buffer_ident.with_context::<#ctx>()); let mut paren_expr = TokenStream::new(); paren_token.surround(&mut paren_expr, |tokens| expr.to_tokens(tokens)); @@ -249,30 +255,23 @@ enum Part { Dynamic(TokenStream), } -#[derive(Debug, Clone, Copy)] -pub enum Context { - Node, - AttributeValue, +pub trait Generate { + type Context: Context; + + fn generate(&self, g: &mut Generator); } -impl Context { - pub fn marker_type(self) -> TokenStream { - let ident = match self { - Self::Node => Ident::new("Node", Span::mixed_site()), - Self::AttributeValue => Ident::new("AttributeValue", Span::mixed_site()), - }; +impl Generate for Infallible { + type Context = Self; - quote!(::hypertext::context::#ident) + fn generate(&self, _: &mut Generator) { + #[expect(clippy::uninhabited_references)] + match *self {} } } -pub trait Generate { - const CONTEXT: Context; - fn generate(&self, g: &mut Generator); -} - impl Generate for &T { - const CONTEXT: Context = T::CONTEXT; + type Context = T::Context; fn generate(&self, g: &mut Generator) { (*self).generate(g); diff --git a/hypertext-macros/src/html/mod.rs b/crates/hypertext-macros/src/html/mod.rs similarity index 86% rename from hypertext-macros/src/html/mod.rs rename to crates/hypertext-macros/src/html/mod.rs index 56b410c..e67cce7 100644 --- a/hypertext-macros/src/html/mod.rs +++ b/crates/hypertext-macros/src/html/mod.rs @@ -6,7 +6,7 @@ mod control; pub mod generate; mod syntaxes; -use std::marker::PhantomData; +use std::{borrow::Cow, convert::Infallible, marker::PhantomData}; use proc_macro2::{Span, TokenStream}; use quote::{ToTokens, quote, quote_spanned}; @@ -30,7 +30,6 @@ use self::{ Generator, }, }; -use crate::Context; mod kw { use syn::LitStr; @@ -62,13 +61,32 @@ mod kw { pub trait Syntax {} -pub type Document = Nodes>; +pub type Document = Many>; -pub trait Node: Generate { +pub trait Context: Generate { fn is_control(&self) -> bool; + + fn marker_type() -> TokenStream; + + fn escape(s: &str) -> Cow<'_, str>; +} + +impl Context for Infallible { + fn is_control(&self) -> bool { + #[expect(clippy::uninhabited_references)] + match *self {} + } + + fn marker_type() -> TokenStream { + TokenStream::new() + } + + fn escape(s: &str) -> Cow<'_, str> { + Cow::Borrowed(s) + } } -pub enum ElementNode { +pub enum Node { Doctype(Doctype), Element(Element), Component(Component), @@ -80,21 +98,29 @@ pub enum ElementNode { Group(Group), } -impl Node for ElementNode { +impl Context for Node { fn is_control(&self) -> bool { matches!(self, Self::Control(_)) } + + fn marker_type() -> TokenStream { + quote!(::hypertext::context::Node) + } + + fn escape(s: &str) -> Cow<'_, str> { + html_escape::encode_text(s) + } } -impl Generate for ElementNode { - const CONTEXT: Context = Context::Node; +impl Generate for Node { + type Context = Self; fn generate(&self, g: &mut Generator) { match self { Self::Doctype(doctype) => g.push(doctype), Self::Element(element) => g.push(element), Self::Component(component) => g.push(component), - Self::Literal(lit) => g.push_escaped_lit(Self::CONTEXT, &lit.lit_str()), + Self::Literal(lit) => g.push_escaped_lit::(&lit.lit_str()), Self::Control(control) => g.push(control), Self::Expr(expr) => g.push(expr), Self::DisplayExpr(display_expr) => g.push(display_expr), @@ -114,7 +140,7 @@ pub struct Doctype { } impl Generate for Doctype { - const CONTEXT: Context = Context::Node; + type Context = Node; fn generate(&self, g: &mut Generator) { g.push_lits(vec![ @@ -128,13 +154,13 @@ impl Generate for Doctype { } } -pub struct ParenExpr { +pub struct ParenExpr { paren_token: Paren, expr: TokenStream, - phantom: PhantomData, + phantom: PhantomData, } -impl Parse for ParenExpr { +impl Parse for ParenExpr { fn parse(input: ParseStream) -> syn::Result { let content; @@ -146,15 +172,15 @@ impl Parse for ParenExpr { } } -impl Generate for ParenExpr { - const CONTEXT: Context = N::CONTEXT; +impl Generate for ParenExpr { + type Context = C; fn generate(&self, g: &mut Generator) { - g.push_expr(self.paren_token, Self::CONTEXT, &self.expr); + g.push_expr::(self.paren_token, &self.expr); } } -impl ToTokens for ParenExpr { +impl ToTokens for ParenExpr { fn to_tokens(&self, tokens: &mut TokenStream) { self.paren_token.surround(tokens, |tokens| { self.expr.to_tokens(tokens); @@ -162,12 +188,12 @@ impl ToTokens for ParenExpr { } } -pub struct DisplayExpr { +pub struct DisplayExpr { percent_token: Token![%], - paren_expr: ParenExpr, + paren_expr: ParenExpr, } -impl DisplayExpr { +impl DisplayExpr { fn wrapped_expr(&self) -> TokenStream { let wrapper = quote_spanned!(self.percent_token.span=> Displayed); let mut new_paren_expr = TokenStream::new(); @@ -182,7 +208,7 @@ impl DisplayExpr { } } -impl Parse for DisplayExpr { +impl Parse for DisplayExpr { fn parse(input: ParseStream) -> syn::Result { Ok(Self { percent_token: input.parse()?, @@ -191,24 +217,20 @@ impl Parse for DisplayExpr { } } -impl Generate for DisplayExpr { - const CONTEXT: Context = N::CONTEXT; +impl Generate for DisplayExpr { + type Context = C; fn generate(&self, g: &mut Generator) { - g.push_expr( - self.paren_expr.paren_token, - Self::CONTEXT, - self.wrapped_expr(), - ); + g.push_expr::(self.paren_expr.paren_token, self.wrapped_expr()); } } -pub struct DebugExpr { +pub struct DebugExpr { question_token: Token![?], - expr: ParenExpr, + expr: ParenExpr, } -impl DebugExpr { +impl DebugExpr { fn wrapped_expr(&self) -> TokenStream { let wrapper = quote_spanned!(self.question_token.span=> Debugged); let mut new_paren_expr = TokenStream::new(); @@ -223,7 +245,7 @@ impl DebugExpr { } } -impl Parse for DebugExpr { +impl Parse for DebugExpr { fn parse(input: ParseStream) -> syn::Result { Ok(Self { question_token: input.parse()?, @@ -232,17 +254,17 @@ impl Parse for DebugExpr { } } -impl Generate for DebugExpr { - const CONTEXT: Context = N::CONTEXT; +impl Generate for DebugExpr { + type Context = C; fn generate(&self, g: &mut Generator) { - g.push_expr(self.expr.paren_token, Self::CONTEXT, self.wrapped_expr()); + g.push_expr::(self.expr.paren_token, self.wrapped_expr()); } } -pub struct Group(Nodes); +pub struct Group(Many); -impl Parse for Group { +impl Parse for Group { fn parse(input: ParseStream) -> syn::Result { let content; braced!(content in input); @@ -251,17 +273,17 @@ impl Parse for Group { } } -impl Generate for Group { - const CONTEXT: Context = N::CONTEXT; +impl Generate for Group { + type Context = C; fn generate(&self, g: &mut Generator) { g.push(&self.0); } } -pub struct Nodes(Vec); +pub struct Many(Vec); -impl Nodes { +impl Many { fn block(&self, g: &mut Generator, brace_token: Brace) -> AnyBlock { g.block_with(brace_token, |g| { g.push_all(&self.0); @@ -269,25 +291,25 @@ impl Nodes { } } -impl Parse for Nodes { +impl Parse for Many { fn parse(input: ParseStream) -> syn::Result { Ok(Self({ - let mut nodes = Vec::new(); + let mut children = Vec::new(); while !input.is_empty() { - nodes.push(input.parse()?); + children.push(input.parse()?); } - nodes + children })) } } -impl Generate for Nodes { - const CONTEXT: Context = N::CONTEXT; +impl Generate for Many { + type Context = C; fn generate(&self, g: &mut Generator) { - if self.0.iter().any(Node::is_control) { + if self.0.iter().any(Context::is_control) { g.push_in_block(Brace::default(), |g| g.push_all(&self.0)); } else { g.push_all(&self.0); @@ -302,7 +324,7 @@ pub struct Element { } impl Generate for Element { - const CONTEXT: Context = Context::Node; + type Context = Node; fn generate(&self, g: &mut Generator) { let mut el_checks = ElementCheck::new(&self.name, self.body.kind()); @@ -343,7 +365,7 @@ impl Generate for Element { pub enum ElementBody { Normal { - children: Nodes>, + children: Many>, closing_name: Option, }, Void, @@ -369,7 +391,7 @@ impl Attribute { Ok(Self { name: parse_quote_spanned!(pound_token.span()=> id), kind: AttributeKind::Value { - value: input.call(AttributeValueNode::parse_unquoted)?, + value: input.call(AttributeValue::parse_unquoted)?, toggle: None, }, }) @@ -413,7 +435,7 @@ impl Parse for Attribute { } impl Generate for Attribute { - const CONTEXT: Context = Context::AttributeValue; + type Context = AttributeValue; fn generate(&self, g: &mut Generator) { match &self.kind { @@ -445,7 +467,7 @@ impl Generate for Attribute { g.push_str(" "); g.push_lits(self.name.lits()); g.push_str("=\""); - g.push_expr(Paren::default(), Self::CONTEXT, &value); + g.push_expr::(Paren::default(), &value); g.push_str("\""); }, ); @@ -655,7 +677,7 @@ impl Parse for AttributeSymbol { pub enum AttributeKind { Value { - value: AttributeValueNode, + value: AttributeValue, toggle: Option, }, Empty(Option), @@ -663,7 +685,7 @@ pub enum AttributeKind { ClassList(Vec), } -pub enum AttributeValueNode { +pub enum AttributeValue { Literal(Literal), Group(Group), Control(Control), @@ -673,10 +695,10 @@ pub enum AttributeValueNode { Ident(Ident), } -impl AttributeValueNode { +impl AttributeValue { fn parse_unquoted(input: ParseStream) -> syn::Result { if input.peek(Ident::peek_any) || input.peek(LitInt) { - Ok(Self::Group(Group(Nodes( + Ok(Self::Group(Group(Many( input .call(UnquotedName::parse_attr_value)? .lits() @@ -690,13 +712,21 @@ impl AttributeValueNode { } } -impl Node for AttributeValueNode { +impl Context for AttributeValue { fn is_control(&self) -> bool { matches!(self, Self::Control(_)) } + + fn marker_type() -> TokenStream { + quote!(::hypertext::context::AttributeValue) + } + + fn escape(s: &str) -> Cow<'_, str> { + html_escape::encode_double_quoted_attribute(s) + } } -impl Parse for AttributeValueNode { +impl Parse for AttributeValue { fn parse(input: ParseStream) -> syn::Result { let lookahead = input.lookahead1(); @@ -725,25 +755,25 @@ impl Parse for AttributeValueNode { } } -impl Generate for AttributeValueNode { - const CONTEXT: Context = Context::AttributeValue; +impl Generate for AttributeValue { + type Context = Self; fn generate(&self, g: &mut Generator) { match self { - Self::Literal(lit) => g.push_escaped_lit(Self::CONTEXT, &lit.lit_str()), + Self::Literal(lit) => g.push_escaped_lit::(&lit.lit_str()), Self::Group(block) => g.push(block), Self::Control(control) => g.push(control), Self::Expr(expr) => g.push(expr), Self::DisplayExpr(display_expr) => g.push(display_expr), Self::DebugExpr(debug_expr) => g.push(debug_expr), - Self::Ident(ident) => g.push_expr(Paren::default(), Self::CONTEXT, ident), + Self::Ident(ident) => g.push_expr::(Paren::default(), ident), } } } pub enum Class { Value { - value: AttributeValueNode, + value: AttributeValue, toggle: Option, }, Option(Toggle), @@ -777,7 +807,7 @@ impl Class { if index > 0 { g.push_str(" "); } - g.push_expr(Paren::default(), Context::AttributeValue, &value); + g.push_expr::(Paren::default(), &value); }, ); } @@ -793,7 +823,7 @@ impl Parse for Class { Ok(Self::Option(input.parse()?)) } else { Ok(Self::Value { - value: input.call(AttributeValueNode::parse_unquoted)?, + value: input.call(AttributeValue::parse_unquoted)?, toggle: input.call(Toggle::parse_optional)?, }) } diff --git a/hypertext-macros/src/html/syntaxes/maud.rs b/crates/hypertext-macros/src/html/syntaxes/maud.rs similarity index 95% rename from hypertext-macros/src/html/syntaxes/maud.rs rename to crates/hypertext-macros/src/html/syntaxes/maud.rs index 0a47781..4e09f57 100644 --- a/hypertext-macros/src/html/syntaxes/maud.rs +++ b/crates/hypertext-macros/src/html/syntaxes/maud.rs @@ -9,15 +9,14 @@ use syn::{ }; use crate::html::{ - Attribute, Component, Doctype, Element, ElementBody, ElementNode, Group, Syntax, UnquotedName, - kw, + Attribute, Component, Doctype, Element, ElementBody, Group, Node, Syntax, UnquotedName, kw, }; pub struct Maud; impl Syntax for Maud {} -impl Parse for ElementNode { +impl Parse for Node { fn parse(input: ParseStream) -> syn::Result { let lookahead = input.lookahead1(); @@ -65,7 +64,7 @@ impl Parse for Doctype { } } -impl Parse for Group> { +impl Parse for Group> { fn parse(input: ParseStream) -> syn::Result { let content; braced!(content in input); diff --git a/hypertext-macros/src/html/syntaxes/mod.rs b/crates/hypertext-macros/src/html/syntaxes/mod.rs similarity index 100% rename from hypertext-macros/src/html/syntaxes/mod.rs rename to crates/hypertext-macros/src/html/syntaxes/mod.rs diff --git a/hypertext-macros/src/html/syntaxes/rsx.rs b/crates/hypertext-macros/src/html/syntaxes/rsx.rs similarity index 79% rename from hypertext-macros/src/html/syntaxes/rsx.rs rename to crates/hypertext-macros/src/html/syntaxes/rsx.rs index 00cf578..dbc1c44 100644 --- a/hypertext-macros/src/html/syntaxes/rsx.rs +++ b/crates/hypertext-macros/src/html/syntaxes/rsx.rs @@ -1,7 +1,7 @@ use std::marker::PhantomData; use syn::{ - Ident, LitBool, LitChar, LitFloat, LitInt, LitStr, Token, custom_punctuation, + Ident, LitBool, LitChar, LitFloat, LitInt, LitStr, Token, ext::IdentExt, parse::{Parse, ParseStream, discouraged::Speculative}, parse_quote, @@ -9,20 +9,14 @@ use syn::{ }; use crate::html::{ - Component, Doctype, Element, ElementBody, ElementNode, Group, Literal, Nodes, Syntax, - UnquotedName, + Component, Doctype, Element, ElementBody, Group, Literal, Many, Node, Syntax, UnquotedName, }; pub struct Rsx; impl Syntax for Rsx {} -custom_punctuation!(FragmentOpen, <>); -custom_punctuation!(FragmentClose, ); -custom_punctuation!(OpenTagSolidusEnd, />); -custom_punctuation!(CloseTagStart, { +impl Node { fn parse_component(input: ParseStream) -> syn::Result { input.parse::()?; @@ -30,7 +24,11 @@ impl ElementNode { let mut attrs = Vec::new(); - while !(input.peek(Token![..]) || input.peek(Token![>]) || input.peek(OpenTagSolidusEnd)) { + #[allow(clippy::suspicious_operation_groupings)] + while !(input.peek(Token![..]) + || input.peek(Token![>]) + || (input.peek(Token![/]) && input.peek2(Token![>]))) + { attrs.push(input.parse()?); } @@ -49,7 +47,7 @@ impl ElementNode { } else { let mut children = Vec::new(); - while !input.peek(CloseTagStart) { + while !(input.peek(Token![<]) && input.peek2(Token![/])) { if input.is_empty() { children.insert( 0, @@ -61,14 +59,15 @@ impl ElementNode { }), ); - return Ok(Self::Group(Group(Nodes(children)))); + return Ok(Self::Group(Group(Many(children)))); } children.push(input.parse()?); } let fork = input.fork(); - fork.parse::()?; + fork.parse::()?; + fork.parse::()?; let closing_name = fork.parse::()?; if closing_name == name { input.advance_to(&fork); @@ -83,7 +82,7 @@ impl ElementNode { }), ); - return Ok(Self::Group(Group(Nodes(children)))); + return Ok(Self::Group(Group(Many(children)))); } input.parse::]>()?; @@ -92,7 +91,7 @@ impl ElementNode { attrs, dotdot, body: ElementBody::Normal { - children: Nodes(children), + children: Many(children), closing_name: Some(parse_quote!(#closing_name)), }, })) @@ -106,7 +105,7 @@ impl ElementNode { let mut attrs = Vec::new(); - while !(input.peek(Token![>]) || (input.peek(OpenTagSolidusEnd))) { + while !(input.peek(Token![>]) || (input.peek(Token![/]) && input.peek2(Token![>]))) { attrs.push(input.parse()?); } @@ -122,7 +121,7 @@ impl ElementNode { } else { let mut children = Vec::new(); - while !(input.peek(CloseTagStart)) { + while !(input.peek(Token![<]) && input.peek2(Token![/])) { if input.is_empty() { children.insert( 0, @@ -133,13 +132,14 @@ impl ElementNode { }), ); - return Ok(Self::Group(Group(Nodes(children)))); + return Ok(Self::Group(Group(Many(children)))); } children.push(input.parse()?); } let fork = input.fork(); - fork.parse::()?; + fork.parse::()?; + fork.parse::()?; let closing_name = fork.parse()?; if closing_name == name { input.advance_to(&fork); @@ -153,7 +153,7 @@ impl ElementNode { }), ); - return Ok(Self::Group(Group(Nodes(children)))); + return Ok(Self::Group(Group(Many(children)))); } input.parse::]>()?; @@ -161,7 +161,7 @@ impl ElementNode { name, attrs, body: ElementBody::Normal { - children: Nodes(children), + children: Many(children), closing_name: Some(closing_name), }, })) @@ -169,7 +169,7 @@ impl ElementNode { } } -impl Parse for ElementNode { +impl Parse for Node { fn parse(input: ParseStream) -> syn::Result { let lookahead = input.lookahead1(); @@ -241,18 +241,21 @@ impl Parse for Doctype { } } -impl Parse for Group> { +impl Parse for Group> { fn parse(input: ParseStream) -> syn::Result { - input.parse::()?; + input.parse::()?; + input.parse::]>()?; - let mut nodes = Vec::new(); + let mut children = Vec::new(); - while !input.peek(FragmentClose) { - nodes.push(input.parse()?); + while !(input.peek(Token![<]) && input.peek2(Token![/]) && input.peek3(Token![>])) { + children.push(input.parse()?); } - input.parse::()?; + input.parse::()?; + input.parse::()?; + input.parse::]>()?; - Ok(Self(Nodes(nodes))) + Ok(Self(Many(children))) } } diff --git a/crates/hypertext-macros/src/lib.rs b/crates/hypertext-macros/src/lib.rs new file mode 100644 index 0000000..1d9775b --- /dev/null +++ b/crates/hypertext-macros/src/lib.rs @@ -0,0 +1,78 @@ +#![expect(missing_docs)] + +mod derive; +mod html; +mod renderable; + +use html::{AttributeValue, Many}; +use proc_macro::TokenStream; +use syn::{parse::Parse, parse_macro_input}; + +use self::html::{Document, Maud, Rsx}; +use crate::html::generate::{Config, Generate, Semantics}; + +fn generate(config: Config, tokens: TokenStream) -> TokenStream { + config + .generate::(tokens.into()) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} + +macro_rules! create_variants { + { + $($Ty:ty { + $lazy_move:ident + $lazy_borrow:ident + $simple:ident + })* + } => { + $(#[proc_macro] + pub fn $lazy_move(tokens: TokenStream) -> TokenStream { + generate::<$Ty>(Config::Lazy(Semantics::Move), tokens) + } + + #[proc_macro] + pub fn $lazy_borrow(tokens: TokenStream) -> TokenStream { + generate::<$Ty>(Config::Lazy(Semantics::Borrow), tokens) + } + + #[proc_macro] + pub fn $simple(tokens: TokenStream) -> TokenStream { + generate::<$Ty>(Config::Simple, tokens) + })* + }; +} + +create_variants! { + Document { + maud + maud_borrow + maud_simple + } + + Document { + rsx + rsx_borrow + rsx_simple + } + + Many { + attribute + attribute_borrow + attribute_simple + } +} + +#[proc_macro_derive(Renderable, attributes(maud, rsx, attribute))] +pub fn derive_renderable(input: TokenStream) -> TokenStream { + derive::renderable(parse_macro_input!(input)) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} + +#[proc_macro_attribute] +pub fn renderable(attr: TokenStream, item: TokenStream) -> TokenStream { + renderable::generate(parse_macro_input!(attr), parse_macro_input!(item)) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} diff --git a/hypertext-macros/src/component.rs b/crates/hypertext-macros/src/renderable.rs similarity index 78% rename from hypertext-macros/src/component.rs rename to crates/hypertext-macros/src/renderable.rs index 04dcd7b..65b86c5 100644 --- a/hypertext-macros/src/component.rs +++ b/crates/hypertext-macros/src/renderable.rs @@ -1,15 +1,15 @@ use proc_macro2::TokenStream; use quote::quote; -use syn::{FnArg, Ident, ItemFn, Pat, PatType, Type, Visibility, parse::Parse}; +use syn::{Error, FnArg, Ident, ItemFn, Pat, PatType, Type, Visibility, parse::Parse}; use crate::html::generate::Generator; -pub struct ComponentArgs { +pub struct RenderableArgs { visibility: Visibility, ident: Option, } -impl Parse for ComponentArgs { +impl Parse for RenderableArgs { fn parse(input: syn::parse::ParseStream) -> syn::Result { Ok(Self { visibility: input.parse()?, @@ -22,7 +22,8 @@ impl Parse for ComponentArgs { } } -pub fn generate(args: ComponentArgs, fn_item: &ItemFn) -> syn::Result { +#[expect(clippy::needless_pass_by_value)] +pub fn generate(args: RenderableArgs, fn_item: ItemFn) -> syn::Result { let mut fields = Vec::new(); let mut field_names = Vec::new(); let mut field_refs = Vec::new(); @@ -38,18 +39,18 @@ pub fn generate(args: ComponentArgs, fn_item: &ItemFn) -> syn::Result &pat_ident.ident, _ => { - return Err(syn::Error::new_spanned( + return Err(Error::new_spanned( pat, - "component function parameters must be identifiers", + "renderable function parameters must be identifiers", )); } }; let (ty, ref_token) = match &**ty { Type::Reference(ty_ref) => { if ty_ref.mutability.is_some() { - return Err(syn::Error::new_spanned( + return Err(Error::new_spanned( ty_ref, - "component function parameters cannot be mutable references", + "renderable function parameters cannot be mutable references", )); } @@ -67,9 +68,9 @@ pub fn generate(args: ComponentArgs, fn_item: &ItemFn) -> syn::Result syn::Result +//!

"Hello, world!"

+//! @for i in 1..=3 { +//!

+//! "This is paragraph number " (i) +//!

+//! } +//! +//! } +//! # .render(); //! -//! // expands to (roughly): +//! // expands to: //! +//! # assert_eq!(maud_result, rsx_result); +//! # assert_eq!(maud_result, //! Lazy::<_, Node>::dangerously_create(move |buffer: &mut Buffer| { //! const _: fn() = || { //! use hypertext_elements::*; @@ -73,14 +88,20 @@ //! { //! buffer //! .dangerously_get_string() -//! .push_str(r#"

Hello, world!

"#); +//! .push_str(r#"

Hello, world!

"#); //! for i in 1..=3 { -//! buffer.dangerously_get_string().push_str("

()); +//! buffer.dangerously_get_string().push_str(r#"""#); +//! if i % 2 == 0 { +//! buffer +//! .dangerously_get_string() +//! .push_str(r#" style="background: gray""#); +//! } //! buffer //! .dangerously_get_string() -//! .push_str(r#"">This is paragraph number "#); -//! i.render_to(buffer); +//! .push_str(">This is paragraph number "); +//! i.render_to(buffer.with_context::()); //! buffer.dangerously_get_string().push_str("

"); //! } //! } @@ -88,129 +109,33 @@ //! }) //! # .render()); //! ``` -//! -//! This approach is also extremely extensible, as you can define your own -//! traits to add attributes for your favourite libraries! In fact, this is -//! exactly what [`GlobalAttributes`] does, and why it is required in the above -//! example, as it defines the attributes that can be used on any element, for -//! example [`id`], [`class`], and [`title`]. This library comes with built-in -//! support for many popular frontend attribute-based frameworks in -//! [`validation::attributes`], such as [`HtmxAttributes`] and -//! [`AlpineJsAttributes`] -//! -//! Here's an example of how you could define your own attributes for use with -//! the wonderful frontend library [htmx](https://htmx.org): -//! -//! ```rust -//! use hypertext::{ -//! prelude::*, -//! validation::{Attribute, AttributeNamespace}, -//! }; -//! -//! trait HtmxAttributes: GlobalAttributes { -//! const hx_get: Attribute = Attribute; -//! const hx_on: AttributeNamespace = AttributeNamespace; -//! // ... -//! } -//! -//! impl HtmxAttributes for T {} -//! -//! assert_eq!( -//! maud! { -//! div hx-get="/api/endpoint" hx-on:click="alert('Hello, world!')" { -//! // ^^^^^^ note that it converts `-` to `_` for you during checking! -//! "Hello, world!" -//! } -//! } -//! .render() -//! .as_inner(), -//! r#"
Hello, world!
"#, -//! ); -//! ``` -//! -//! Wrapping an attribue name in quotes will bypass the type-checking, so you -//! can use any attribute you want, even if it doesn't exist in the current -//! context. -//! -//! ```rust -//! use hypertext::prelude::*; -//! -//! assert_eq!( -//! maud! { -//! div "custom-attribute"="value" { "Hello, world!" } -//! } -//! .render() -//! .as_inner(), -//! r#"
Hello, world!
"#, -//! ); -//! ``` -//! -//! This library also supports component structs, which are simply structs that -//! implement [`Renderable`]. If an element name is capitalized, it will be -//! treated as a component, with attributes representing the struct fields. The -//! [`#[component]`](component) macro can be used to easily turn functions into -//! components. -//! -//! ```rust -//! use hypertext::{Buffer, prelude::*}; -//! -//! struct Repeater { -//! count: usize, -//! children: R, -//! } -//! -//! impl Renderable for Repeater { -//! fn render_to(&self, buffer: &mut Buffer) { -//! maud! { -//! @for i in 0..self.count { -//! (self.children) -//! } -//! } -//! .render_to(buffer); -//! } -//! } -//! -//! assert_eq!( -//! maud! { -//! div { -//! Repeater count=3 { -//! // children are passed as a `Lazy` to the `children` field -//! p { "Hi!" } -//! } -//! } -//! } -//! .render() -//! .as_inner(), -//! "

Hi!

Hi!

Hi!

" -//! ); -//! ``` -//! -//! [`GlobalAttributes`]: validation::attributes::GlobalAttributes -//! [`id`]: validation::attributes::GlobalAttributes::id -//! [`class`]: validation::attributes::GlobalAttributes::class -//! [`title`]: validation::attributes::GlobalAttributes::title -//! [`HtmxAttributes`]: validation::attributes::HtmxAttributes -//! [`AlpineJsAttributes`]: validation::attributes::AlpineJsAttributes #![no_std] #![warn(clippy::missing_inline_in_public_items)] -#![cfg_attr(docsrs, expect(internal_features))] -#![cfg_attr(docsrs, feature(rustdoc_internals, doc_cfg, doc_auto_cfg))] +#![cfg_attr(all(docsrs, not(doctest)), expect(internal_features))] +#![cfg_attr( + all(docsrs, not(doctest)), + feature(rustdoc_internals, doc_cfg, doc_auto_cfg) +)] #[cfg(feature = "alloc")] -mod alloc; +extern crate alloc; + pub mod context; mod macros; pub mod prelude; +#[cfg(feature = "alloc")] +mod renderable; pub mod validation; +#[cfg(feature = "alloc")] mod web_frameworks; use core::{fmt::Debug, marker::PhantomData}; -#[cfg(feature = "alloc")] -pub use self::alloc::*; use self::context::{AttributeValue, Context, Node}; pub use self::macros::*; +#[cfg(feature = "alloc")] +pub use self::renderable::*; /// A raw pre-escaped string. /// @@ -230,7 +155,7 @@ pub use self::macros::*; /// /// # Example /// -/// ```rust +/// ``` /// use hypertext::{Raw, prelude::*}; /// /// fn get_some_html() -> String { @@ -321,8 +246,8 @@ pub type RawAttribute = Raw; /// A rendered HTML string. /// -/// This type is returned by [`Renderable::render`] ([`Rendered`]), as -/// well as [`Raw::rendered`] ([`Rendered`]). +/// This type is returned by [`RenderableExt::render`] ([`Rendered`]), +/// as well as [`Raw::rendered`] ([`Rendered`]). /// /// This type intentionally does **not** implement [`Renderable`] to discourage /// anti-patterns such as rendering to a string then embedding that HTML string @@ -365,4 +290,4 @@ macro_rules! const_precise_live_drops_hack { (&raw const (*(&raw const this).cast::()).$field).read() }}; } -pub(crate) use const_precise_live_drops_hack; +use const_precise_live_drops_hack; diff --git a/crates/hypertext/src/macros/attribute.rs b/crates/hypertext/src/macros/attribute.rs new file mode 100644 index 0000000..273f05c --- /dev/null +++ b/crates/hypertext/src/macros/attribute.rs @@ -0,0 +1,31 @@ +//! Variants of the [`attribute!`](crate::attribute!) macro. + +/// Generates an attribute value, borrowing the environment. +/// +/// This is identical to [`attribute!`](crate::attribute!), except that it does +/// not take ownership of the environment. This is useful when you want to build +/// a [`LazyAttribute`](crate::LazyAttribute) using some captured variables, but +/// you still want to be able to use the captured variables after the +/// invocation. +#[cfg(feature = "alloc")] +#[cfg_attr(all(docsrs, not(doctest)), doc(cfg(feature = "alloc")))] +pub use hypertext_macros::attribute_borrow as borrow; +/// Generates static HTML attributes. +/// +/// This will return a [`RawAttribute<&'static str>`](crate::RawAttribute), +/// which can be used in `const` contexts. +/// +/// Note that the macro cannot process any dynamic content, so you cannot +/// use any expressions inside the macro. +/// +/// # Example +/// +/// ``` +/// use hypertext::prelude::*; +/// +/// assert_eq!( +/// attribute::simple! { "my attribute " 1 }.into_inner(), +/// "my attribute 1" +/// ); +/// ``` +pub use hypertext_macros::attribute_simple as simple; diff --git a/crates/hypertext/src/macros/maud.rs b/crates/hypertext/src/macros/maud.rs new file mode 100644 index 0000000..10dd286 --- /dev/null +++ b/crates/hypertext/src/macros/maud.rs @@ -0,0 +1,36 @@ +//! Variants of the [`maud!`](crate::maud!) macro. + +/// Generates HTML using [`maud!`](crate::maud!) syntax, borrowing the +/// environment. +/// +/// This is identical to [`maud!`](crate::maud!), except that it does not take +/// ownership of the environment. This is useful when you want to build a +/// [`Lazy`](crate::Lazy) using some captured variables, but you still want to +/// be able to use the captured variables after the invocation. +#[cfg(feature = "alloc")] +#[cfg_attr(all(docsrs, not(doctest)), doc(cfg(feature = "alloc")))] +pub use hypertext_macros::maud_borrow as borrow; +/// Generates static HTML using [`maud!`](crate::maud!) syntax. +/// +/// This will return a [`Raw<&'static str>`](crate::Raw), which can be used +/// in `const` contexts. +///a +/// Note that the macro cannot process any dynamic content, so you cannot +/// use any expressions inside the macro. +/// +/// # Example +/// +/// ``` +/// use hypertext::prelude::*; +/// +/// assert_eq!( +/// maud::simple! { +/// div #profile title="Profile" { +/// h1 { "Alice" } +/// } +/// } +/// .into_inner(), +/// r#"

Alice

"#, +/// ); +/// ``` +pub use hypertext_macros::maud_simple as simple; diff --git a/crates/hypertext/src/macros/mod.rs b/crates/hypertext/src/macros/mod.rs new file mode 100644 index 0000000..d7014b5 --- /dev/null +++ b/crates/hypertext/src/macros/mod.rs @@ -0,0 +1,89 @@ +pub mod attribute; +pub mod maud; +#[cfg(feature = "alloc")] +mod renderable; +pub mod rsx; + +/// Generates an attribute value, returning a +/// [`LazyAttribute`](crate::LazyAttribute). +/// +/// # Example +/// +/// ``` +/// use hypertext::prelude::*; +/// +/// let attr = attribute! { "x" @for i in 0..5 { (i) } }; +/// +/// assert_eq!( +/// maud! { div title=attr { "Hi!" } }.render().as_inner(), +/// r#"
Hi!
"# +/// ); +/// ``` +#[cfg(feature = "alloc")] +#[cfg_attr(all(docsrs, not(doctest)), doc(cfg(feature = "alloc")))] +pub use hypertext_macros::attribute; +/// Generates HTML using Maud syntax, returning a [`Lazy`](crate::Lazy). +/// +/// Note that this is not a complete 1:1 port of [Maud](https://maud.lambda.xyz)'s +/// syntax as it is stricter in some cases to prevent anti-patterns. +/// +/// Some key differences are: +/// - `#` ([`id`](crate::validation::attributes::GlobalAttributes::id) +/// shorthand), if present, must be the first attribute. +/// - `.` ([`class`](crate::validation::attributes::GlobalAttributes::class) +/// shorthand), if present, come after `#` (if present) and before other +/// attributes. +/// +/// Additionally, the `DOCTYPE` constant present in maud is replaced +/// with a new `!DOCTYPE` syntax, which will render `` in its +/// place. +/// +/// For more details on the rest of Maud's syntax, see the [Maud Book](https://maud.lambda.xyz). +/// +/// # Example +/// +/// ``` +/// use hypertext::prelude::*; +/// +/// let name = "Alice"; +/// +/// assert_eq!( +/// maud! { +/// div #profile title="Profile" { +/// h1 { (name) } +/// } +/// } +/// .render() +/// .as_inner(), +/// r#"

Alice

"# +/// ); +/// ``` +#[cfg(feature = "alloc")] +#[cfg_attr(all(docsrs, not(doctest)), doc(cfg(feature = "alloc")))] +pub use hypertext_macros::maud; +/// Generates HTML using RSX syntax, returning a [`Lazy`](crate::Lazy). +/// +/// # Examples +/// +/// ``` +/// use hypertext::prelude::*; +/// +/// let name = "Alice"; +/// +/// assert_eq!( +/// rsx! { +///
+///

(name)

+///
+/// } +/// .render() +/// .as_inner(), +/// r#"

Alice

"# +/// ); +/// ``` +#[cfg(feature = "alloc")] +#[cfg_attr(all(docsrs, not(doctest)), doc(cfg(feature = "alloc")))] +pub use hypertext_macros::rsx; + +#[cfg(feature = "alloc")] +pub use self::renderable::*; diff --git a/crates/hypertext/src/macros/renderable.rs b/crates/hypertext/src/macros/renderable.rs new file mode 100644 index 0000000..a34d6ca --- /dev/null +++ b/crates/hypertext/src/macros/renderable.rs @@ -0,0 +1,149 @@ +#![expect(clippy::doc_markdown)] + +/// Turns a function returning a [`Renderable`](crate::Renderable) into a +/// struct that implements [`Renderable`](crate::Renderable). +/// +/// This macro generates a struct that has fields corresponding to the +/// function's parameters, and implements [`Renderable`](crate::Renderable) +/// by calling the function with the struct's fields as arguments. +/// +/// There are three types of parameters that are supported, described in +/// the table below: +/// +/// | Parameter Type | Stored As | Example Types | +/// |----------------|-----------|---------------| +/// | `T` | `T` | [`bool`], integers, floats, other [`Copy`] types | +/// | `&T` | `T` | [`&String`](crate::alloc::string::String) | +/// | `&'a T` | `&'a T` | [`&'a str`][str], [`&'a [T]`](slice), other cheap borrowed types | +/// +/// The name of the generated struct is derived from the function name by +/// converting it to PascalCase. If you would like to set a different name, +/// you can specify it as `#[renderable(MyStructName)]` on the function. +/// +/// The visibility of the generated struct is determined by the visibility +/// of the function. If you would like to set a different visibility, +/// you can specify it as `#[renderable(pub)]`, +/// `#[renderable(pub(crate))]`, etc. on the function. +/// +/// You can combine both of these by setting an attribute like +/// `#[renderable(pub MyStructName)]`. +/// +/// # Example +/// +/// ``` +/// use hypertext::prelude::*; +/// +/// #[renderable] +/// fn nav_bar<'a>(title: &'a str, subtitle: &String, add_smiley: bool) -> impl Renderable { +/// maud! { +/// nav { +/// h1 { (title) } +/// h2 { (subtitle) } +/// @if add_smiley { +/// span { ":)" } +/// } +/// } +/// } +/// } +/// +/// assert_eq!( +/// maud! { +/// div { +/// NavBar title="My Nav Bar" subtitle=("My Subtitle".to_owned()) add_smiley=true; +/// } +/// } +/// .render() +/// .as_inner(), +/// "
" +/// ); +/// ``` +#[cfg_attr(all(docsrs, not(doctest)), doc(cfg(feature = "alloc")))] +pub use hypertext_macros::renderable; +/// Derives [`Renderable`](crate::Renderable) for a type. +/// +/// This is used in conjunction with `#[maud]`/`#[rsx]`, as well as +/// `#[attribute]`. +/// +/// # Examples +/// +/// ## `#[maud(...)]` +/// +/// Derives [`Renderable`](crate::Renderable) via the contents of +/// `#[maud(...)]`, which will be interpreted as input to +/// [`maud!`](crate::maud!). +/// +/// This is mutually exclusive with `#[rsx(...)]`. +/// +/// ``` +/// use hypertext::prelude::*; +/// +/// #[derive(Renderable)] +/// #[maud(span { "My name is " (self.name) "!" })] +/// pub struct Person { +/// name: String, +/// } +/// +/// assert_eq!( +/// maud! { div { (Person { name: "Alice".into() }) } } +/// .render() +/// .as_inner(), +/// "
My name is Alice!
" +/// ); +/// ``` +/// +/// ## `#[rsx(...)]` +/// +/// Derives [`Renderable`](crate::Renderable) via the contents of `#[rsx(...)]`, +/// which will be interpreted as input to [`rsx!`](crate::rsx!). +/// +/// This is mutually exclusive with `#[maud(...)]`. +/// +/// ``` +/// use hypertext::prelude::*; +/// +/// #[derive(Renderable)] +/// #[rsx( +/// "My name is " (self.name) "!" +/// )] +/// pub struct Person { +/// name: String, +/// } +/// +/// assert_eq!( +/// rsx! {
(Person { name: "Alice".into() })
} +/// .render() +/// .as_inner(), +/// "
My name is Alice!
" +/// ); +/// ``` +/// +/// ## `#[attribute(...)]` +/// +/// Derives [`Renderable`](crate::Renderable) +/// via the contents of `#[attribute(...)]`, which will be interpreted as input +/// to [`attribute!`](crate::attribute!). +/// +/// This can be used in conjunction with `#[rsx]`/`#[maud]`, as this will +/// derive the [`Renderable`](crate::Renderable) implementation, +/// whereas `#[maud(...)]`/`#[rsx(...)]` will derive the +/// [`Renderable`](crate::Renderable) implementation. +/// +/// ``` +/// use hypertext::prelude::*; +/// +/// #[derive(Renderable)] +/// #[attribute((self.x) "," (self.y))] +/// pub struct Coordinates { +/// x: i32, +/// y: i32, +/// } +/// +/// assert_eq!( +/// maud! { div title=(Coordinates { x: 10, y: 20 }) { "Location" } } +/// .render() +/// .as_inner(), +/// r#"
Location
"# +/// ); +/// ``` +#[cfg_attr(all(docsrs, not(doctest)), doc(cfg(feature = "alloc")))] +pub use hypertext_macros::Renderable; diff --git a/crates/hypertext/src/macros/rsx.rs b/crates/hypertext/src/macros/rsx.rs new file mode 100644 index 0000000..9b99593 --- /dev/null +++ b/crates/hypertext/src/macros/rsx.rs @@ -0,0 +1,36 @@ +//! Variants of the [`rsx!`](crate::rsx!) macro. + +/// Generates HTML using [`rsx!`](crate::rsx!) syntax, borrowing the +/// environment. +/// +/// This is identical to [`rsx!`](crate::rsx!), except that it does not take +/// ownership of the environment. This is useful when you want to build a +/// [`Lazy`](crate::Lazy) using some captured variables, but you still want to +/// be able to use the captured variables after the invocation. +#[cfg(feature = "alloc")] +#[cfg_attr(all(docsrs, not(doctest)), doc(cfg(feature = "alloc")))] +pub use hypertext_macros::rsx_borrow as borrow; +/// Generates static HTML using [`rsx!`](crate::rsx!) syntax. +/// +/// This will return a [`Raw<&'static str>`](crate::Raw), which can be used +/// in `const` contexts. +/// +/// Note that the macro cannot process any dynamic content, so you cannot +/// use any expressions inside the macro. +/// +/// # Examples +/// +/// ``` +/// use hypertext::prelude::*; +/// +/// assert_eq!( +/// rsx::simple! { +///
+///

Alice

+///
+/// } +/// .into_inner(), +/// r#"

Alice

"#, +/// ); +/// ``` +pub use hypertext_macros::rsx_simple as simple; diff --git a/crates/hypertext/src/prelude.rs b/crates/hypertext/src/prelude.rs new file mode 100644 index 0000000..aa9293d --- /dev/null +++ b/crates/hypertext/src/prelude.rs @@ -0,0 +1,24 @@ +//! Re-exported items for convenience. +//! +//! This module re-exports all the commonly used items from the crate, +//! so you can use them without having to import them individually. It also +//! re-exports the [`hypertext_elements`] module, and any [framework-specific +//! attribute traits](crate::validation::attributes) that have been enabled, as +//! well as the [`GlobalAttributes`] trait. +#[cfg(feature = "alpine")] +pub use crate::validation::attributes::AlpineJsAttributes; +#[cfg(feature = "htmx")] +pub use crate::validation::attributes::HtmxAttributes; +#[cfg(feature = "hyperscript")] +pub use crate::validation::attributes::HyperscriptAttributes; +#[cfg(feature = "mathml")] +pub use crate::validation::attributes::MathMlGlobalAttributes; +#[cfg(feature = "alloc")] +pub use crate::{Renderable, RenderableExt as _, Rendered}; +pub use crate::{ + macros::*, + validation::{ + attributes::{AriaAttributes, EventHandlerAttributes, GlobalAttributes}, + hypertext_elements, + }, +}; diff --git a/crates/hypertext/src/renderable/buffer.rs b/crates/hypertext/src/renderable/buffer.rs new file mode 100644 index 0000000..e770bf3 --- /dev/null +++ b/crates/hypertext/src/renderable/buffer.rs @@ -0,0 +1,192 @@ +use core::{ + fmt::{self, Debug, Formatter}, + marker::PhantomData, + ptr, +}; + +use super::String; +use crate::{ + Renderable, Rendered, + context::{AttributeValue, Context, Node}, +}; + +/// A buffer used for rendering HTML. +/// +/// This is a wrapper around [`String`] that prevents accidental XSS +/// vulnerabilities by disallowing direct rendering of raw HTML into the buffer +/// without clearly opting into the risk of doing so. +#[derive(Clone, PartialEq, Eq)] +#[repr(transparent)] +pub struct Buffer { + inner: String, + context: PhantomData, +} + +/// A buffer used for rendering attribute values. +/// +/// This is a type alias for [`Buffer`]. +pub type AttributeBuffer = Buffer; + +#[expect( + clippy::missing_const_for_fn, + reason = "`Buffer` does not make sense in `const` contexts" +)] +impl Buffer { + /// Creates a new, empty [`Buffer`]. + #[inline] + #[must_use] + pub fn new() -> Self { + // XSS SAFETY: The buffer is empty and does not contain any HTML. + Self::dangerously_from_string(String::new()) + } + + /// Creates a new [`Buffer`] from the given [`String`]. + /// + /// It is recommended to add a `// XSS SAFETY` comment above the usage of + /// this function to indicate why the original string is safe to be used in + /// this context. + #[inline] + #[must_use] + pub fn dangerously_from_string(string: String) -> Self { + Self { + inner: string, + context: PhantomData, + } + } + + /// Creates a new [`&mut Buffer`](Buffer) from the given [`&mut + /// String`](String). + /// + /// It is recommended to add a `// XSS SAFETY` comment above the usage of + /// this function to indicate why the original string is safe to be used in + /// this context. + #[inline] + #[must_use] + pub fn dangerously_from_string_mut(string: &mut String) -> &mut Self { + // SAFETY: + // - `Buffer` is a `#[repr(transparent)]` wrapper around `String`, differing + // only in the zero-sized `PhantomData` marker type. + // - `PhantomData` does not affect memory layout, so the layout of `Buffer` + // and `String` is guaranteed to be identical by Rust's type system. + // - The lifetime of the reference is preserved, and there are no aliasing or + // validity issues, as both types are functionally identical at runtime. + unsafe { &mut *ptr::from_mut(string).cast::() } + } + + /// Pushes a [`Renderable`] value to the buffer. + /// + /// This is a convenience method that calls + /// [`value.render_to(self)`](Renderable::render_to). + #[inline] + pub fn push(&mut self, value: impl Renderable) { + value.render_to(self); + } + + /// Gets a mutable reference to the inner [`String`]. + /// + /// For [`Buffer`] (a.k.a. [`Buffer`]) writes, the caller must push + /// complete HTML nodes. If rendering string-like types, the pushed contents + /// must escape `&` to `&`, `<` to `<`, and `>` to `>`. + /// + /// For [`Buffer`] (a.k.a. [`AttributeBuffer`]) writes, the + /// caller must push attribute values which will eventually be surrounded by + /// double quotes. The pushed contents must escape `&` to `&`, `<` to + /// `<`, `>` to `>`, and `"` to `"`. + /// + /// This should only be needed in very specific cases, such as manually + /// constructing raw HTML, usually within a [`Renderable::render_to`] + /// implementation. + /// + /// It is recommended to add a `// XSS SAFETY` comment above the usage of + /// this method to indicate why it is safe to directly write to the + /// underlying buffer. + /// + /// # Example + /// + /// ``` + /// use hypertext::{Buffer, prelude::*}; + /// + /// fn get_some_html() -> String { + /// // get html from some source, such as a CMS + /// "

Some HTML from the CMS

".into() + /// } + /// + /// let mut buffer = Buffer::new(); + /// + /// buffer.push(maud! { + /// h1 { "My Document!" } + /// }); + /// + /// // XSS SAFETY: The CMS sanitizes the HTML before returning it. + /// buffer.dangerously_get_string().push_str(&get_some_html()); + /// + /// assert_eq!( + /// buffer.rendered().as_inner(), + /// "

My Document!

Some HTML from the CMS

" + /// ) + /// ``` + #[inline] + pub fn dangerously_get_string(&mut self) -> &mut String { + &mut self.inner + } + + /// Extracts the inner [`String`] from the buffer. + #[inline] + #[must_use] + pub fn into_inner(self) -> String { + self.inner + } + + /// Converts this into an [`&mut Buffer`](Buffer), where `Self: + /// AsMut>`. + /// + /// This is mostly used for converting from [`Buffer`] to + /// [`AttributeBuffer`]. + #[inline] + pub fn with_context(&mut self) -> &mut Buffer + where + Self: AsMut>, + { + self.as_mut() + } +} + +impl Buffer { + /// Renders the buffer to a [`Rendered`]. + #[inline] + #[must_use] + pub fn rendered(self) -> Rendered { + Rendered(self.inner) + } +} + +impl AsMut for Buffer { + #[inline] + fn as_mut(&mut self) -> &mut Self { + self + } +} + +impl AsMut for Buffer { + #[inline] + fn as_mut(&mut self) -> &mut AttributeBuffer { + // SAFETY: Both `Buffer` and `AttributeBuffer` are `#[repr(transparent)]` + // wrappers around `String`, differing only in the zero-sized `PhantomData` + // marker type. + unsafe { &mut *ptr::from_mut(self).cast::() } + } +} + +impl Default for Buffer { + #[inline] + fn default() -> Self { + Self::new() + } +} + +impl Debug for Buffer { + #[inline] + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.debug_tuple("Buffer").field(&self.inner).finish() + } +} diff --git a/hypertext/src/alloc/impls.rs b/crates/hypertext/src/renderable/impls.rs similarity index 65% rename from hypertext/src/alloc/impls.rs rename to crates/hypertext/src/renderable/impls.rs index 1b1b6b8..d8ce42b 100644 --- a/hypertext/src/alloc/impls.rs +++ b/crates/hypertext/src/renderable/impls.rs @@ -1,15 +1,15 @@ use core::fmt::{self, Write}; -use super::alloc::{ - borrow::{Cow, ToOwned}, - boxed::Box, - rc::Rc, - string::String, - sync::Arc, - vec::Vec, -}; use crate::{ - AttributeBuffer, Buffer, Raw, Renderable, Rendered, + AttributeBuffer, Buffer, Raw, Renderable, + alloc::{ + borrow::{Cow, ToOwned}, + boxed::Box, + rc::Rc, + string::String, + sync::Arc, + vec::Vec, + }, context::{AttributeValue, Context, Node}, }; @@ -22,44 +22,33 @@ impl, C: Context> Renderable for Raw { } #[inline] - fn render(&self) -> Rendered { - Rendered(self.as_str().into()) - } -} - -impl Renderable for fmt::Arguments<'_> { - #[inline] - fn render_to(&self, buffer: &mut Buffer) { - struct ElementEscaper<'a>(&'a mut String); - - impl Write for ElementEscaper<'_> { - #[inline] - fn write_str(&mut self, s: &str) -> fmt::Result { - html_escape::encode_text_to_string(s, self.0); - Ok(()) - } - } - - // XSS SAFETY: `ElementEscaper` will escape special characters. - _ = ElementEscaper(buffer.dangerously_get_string()).write_fmt(*self); + fn to_buffer(&self) -> Buffer { + // XSS SAFETY: `Raw` values are expected to be pre-escaped for + // their respective rendering context. + Buffer::dangerously_from_string(self.as_str().into()) } } -impl Renderable for fmt::Arguments<'_> { +impl Renderable for fmt::Arguments<'_> +where + str: Renderable, +{ #[inline] - fn render_to(&self, buffer: &mut AttributeBuffer) { - struct AttributeEscaper<'a>(&'a mut String); + fn render_to(&self, buffer: &mut Buffer) { + struct Escaper<'a, C: Context>(&'a mut Buffer); - impl Write for AttributeEscaper<'_> { + impl Write for Escaper<'_, C> + where + str: Renderable, + { #[inline] fn write_str(&mut self, s: &str) -> fmt::Result { - html_escape::encode_double_quoted_attribute_to_string(s, self.0); + s.render_to(self.0); Ok(()) } } - // XSS SAFETY: `AttributeEscaper` will escape special characters. - _ = AttributeEscaper(buffer.dangerously_get_string()).write_fmt(*self); + _ = Escaper(buffer).write_fmt(*self); } } @@ -76,8 +65,9 @@ impl Renderable for char { } #[inline] - fn render(&self) -> Rendered { - Rendered(match *self { + fn to_buffer(&self) -> Buffer { + // XSS SAFETY: we are manually performing escaping here + Buffer::dangerously_from_string(match *self { '&' => "&".into(), '<' => "<".into(), '>' => ">".into(), @@ -100,6 +90,18 @@ impl Renderable for char { c => s.push(c), } } + + #[inline] + fn to_buffer(&self) -> AttributeBuffer { + // XSS SAFETY: we are manually performing escaping here + AttributeBuffer::dangerously_from_string(match *self { + '&' => "&".into(), + '<' => "<".into(), + '>' => ">".into(), + '"' => """.into(), + c => c.into(), + }) + } } impl Renderable for str { @@ -110,8 +112,9 @@ impl Renderable for str { } #[inline] - fn render(&self) -> Rendered { - Rendered(html_escape::encode_text(self).into_owned()) + fn to_buffer(&self) -> Buffer { + // XSS SAFETY: we use `html_escape` to ensure the text is properly escaped + Buffer::dangerously_from_string(html_escape::encode_text(self).into_owned()) } } @@ -124,24 +127,28 @@ impl Renderable for str { buffer.dangerously_get_string(), ); } -} -impl Renderable for String { #[inline] - fn render_to(&self, buffer: &mut Buffer) { - self.as_str().render_to(buffer); + fn to_buffer(&self) -> AttributeBuffer { + // XSS SAFETY: we use `html_escape` to ensure the text is properly escaped + AttributeBuffer::dangerously_from_string( + html_escape::encode_double_quoted_attribute(self).into_owned(), + ) } +} +impl Renderable for String +where + str: Renderable, +{ #[inline] - fn render(&self) -> Rendered { - Renderable::::render(self.as_str()) + fn render_to(&self, buffer: &mut Buffer) { + self.as_str().render_to(buffer); } -} -impl Renderable for String { #[inline] - fn render_to(&self, buffer: &mut AttributeBuffer) { - self.as_str().render_to(buffer); + fn to_buffer(&self) -> Buffer { + self.as_str().to_buffer() } } @@ -155,8 +162,9 @@ impl Renderable for bool { } #[inline] - fn render(&self) -> Rendered { - Rendered(if *self { "true" } else { "false" }.into()) + fn to_buffer(&self) -> Buffer { + // XSS SAFETY: "true" and "false" are safe strings + Buffer::dangerously_from_string(if *self { "true" } else { "false" }.into()) } } @@ -171,8 +179,9 @@ macro_rules! render_via_itoa { } #[inline] - fn render(&self) -> Rendered { - Rendered(itoa::Buffer::new().format(*self).into()) + fn to_buffer(&self) -> Buffer { + // XSS SAFETY: integers are safe + Buffer::dangerously_from_string(itoa::Buffer::new().format(*self).into()) } } )* @@ -195,8 +204,9 @@ macro_rules! render_via_ryu { } #[inline] - fn render(&self) -> Rendered { - Rendered(ryu::Buffer::new().format(*self).into()) + fn to_buffer(&self) -> Buffer { + // XSS SAFETY: floats are safe + Buffer::dangerously_from_string(ryu::Buffer::new().format(*self).into()) } } )* @@ -210,22 +220,16 @@ render_via_ryu! { macro_rules! render_via_deref { ($($Ty:ty)*) => { $( - impl Renderable for $Ty { - #[inline] - fn render_to(&self, buffer: &mut Buffer) { - T::render_to(&**self, buffer); - } - + impl + ?Sized, C: Context> Renderable for $Ty { #[inline] - fn render(&self) -> Rendered { - T::render(&**self) + fn render_to(&self, buffer: &mut Buffer) { + // T::render_to(&**self, buffer); + (**self).render_to(buffer); } - } - impl + ?Sized> Renderable for $Ty { #[inline] - fn render_to(&self, buffer: &mut AttributeBuffer) { - T::render_to(&**self, buffer); + fn to_buffer(&self) -> Buffer { + (**self).to_buffer() } } )* @@ -240,24 +244,15 @@ render_via_deref! { Arc } -impl<'a, B: 'a + Renderable + ToOwned + ?Sized> Renderable for Cow<'a, B> { - #[inline] - fn render_to(&self, buffer: &mut Buffer) { - B::render_to(&**self, buffer); - } - +impl<'a, B: 'a + Renderable + ToOwned + ?Sized, C: Context> Renderable for Cow<'a, B> { #[inline] - fn render(&self) -> Rendered { - B::render(&**self) + fn render_to(&self, buffer: &mut Buffer) { + (**self).render_to(buffer); } -} -impl<'a, B: 'a + Renderable + ToOwned + ?Sized> Renderable - for Cow<'a, B> -{ #[inline] - fn render_to(&self, buffer: &mut AttributeBuffer) { - B::render_to(&**self, buffer); + fn to_buffer(&self) -> Buffer { + (**self).to_buffer() } } @@ -311,8 +306,8 @@ macro_rules! impl_tuple { } }; (($i:tt $T:ident)) => { - #[cfg_attr(docsrs, doc(fake_variadic))] - #[cfg_attr(docsrs, doc = "This trait is implemented for tuples up to twelve items long.")] + #[cfg_attr(all(docsrs, not(doctest)), doc(fake_variadic))] + #[cfg_attr(all(docsrs, not(doctest)), doc = "This trait is implemented for tuples up to twelve items long.")] impl<$T: Renderable, C: Context> Renderable for ($T,) { #[inline] fn render_to(&self, buffer: &mut Buffer) { @@ -321,7 +316,7 @@ macro_rules! impl_tuple { } }; (($i0:tt $T0:ident) $(($i:tt $T:ident))+) => { - #[cfg_attr(docsrs, doc(hidden))] + #[cfg_attr(all(docsrs, not(doctest)), doc(hidden))] impl<$T0: Renderable, $($T: Renderable),*, C: Context> Renderable for ($T0, $($T,)*) { #[inline] fn render_to(&self, buffer: &mut Buffer) { diff --git a/crates/hypertext/src/renderable/mod.rs b/crates/hypertext/src/renderable/mod.rs new file mode 100644 index 0000000..1a2ca49 --- /dev/null +++ b/crates/hypertext/src/renderable/mod.rs @@ -0,0 +1,404 @@ +#![allow(clippy::doc_markdown)] + +mod buffer; +mod impls; + +use core::{ + fmt::{self, Debug, Display, Formatter}, + marker::PhantomData, +}; + +pub use self::buffer::*; +use crate::{ + Raw, Rendered, + alloc::string::String, + const_precise_live_drops_hack, + context::{AttributeValue, Context, Node}, +}; + +/// A type that can be rendered as an HTML node. +/// +/// For [`Renderable`] (a.k.a. [`Renderable`]) implementations, this +/// must render complete HTML nodes. If rendering string-like types, the +/// implementation must escape `&` to `&`, `<` to `<`, and `>` to `>`. +/// +/// For [`Renderable`] implementations, this must render an +/// attribute value which will eventually be surrounded by double quotes. The +/// implementation must escape `&` to `&`, `<` to `<`, `>` to `>`, and +/// `"` to `"`. +/// +/// +/// # Examples +/// +/// ## Implementing [`Renderable`] +/// +/// There are 3 ways to implement this trait. +/// +/// ### Manual [`impl Renderable`](Renderable) +/// +/// ``` +/// use hypertext::{Buffer, prelude::*}; +/// +/// pub struct Person { +/// name: String, +/// age: u8, +/// } +/// +/// impl Renderable for Person { +/// fn render_to(&self, buffer: &mut Buffer) { +/// buffer.push(maud! { +/// div { +/// h1 { (self.name) } +/// p { "Age: " (self.age) } +/// } +/// }); +/// } +/// } +/// +/// let person = Person { +/// name: "Alice".into(), +/// age: 20, +/// }; +/// +/// assert_eq!( +/// maud! { main { (person) } }.render().as_inner(), +/// "

Alice

Age: 20

", +/// ); +/// ``` +/// +/// ### [`#[derive(Renderable)]`](derive@crate::Renderable) +/// +/// ``` +/// use hypertext::prelude::*; +/// +/// #[derive(Renderable)] +/// #[maud( +/// div { +/// h1 { (self.name) } +/// p { "Age: " (self.age) } +/// } +/// )] +/// struct Person { +/// name: String, +/// age: u8, +/// } +/// +/// let person = Person { +/// name: "Alice".into(), +/// age: 20, +/// }; +/// +/// assert_eq!( +/// maud! { main { (person) } }.render().as_inner(), +/// "

Alice

Age: 20

", +/// ); +/// ``` +/// +/// ### [`#[renderable]`](crate::renderable) +/// +/// ``` +/// use hypertext::prelude::*; +/// #[renderable] +/// fn person(name: &String, age: u8) -> impl Renderable { +/// maud! { +/// div { +/// h1 { (name) } +/// p { "Age: " (age) } +/// } +/// } +/// } +/// +/// assert_eq!( +/// maud! { main { (Person { name: "Alice".into(), age: 20 }) } } +/// .render() +/// .as_inner(), +/// "

Alice

Age: 20

", +/// ); +/// ``` +/// +/// ## Component Syntax +/// +/// In addition to the standard way of rendering a [`Renderable`] struct inside +/// a `(...)` node, you can also use the "component" syntax to make using these +/// types more like popular frontend frameworks such as React.js. +/// +/// ### [`maud!`](crate::maud!) +/// +/// ``` +/// use hypertext::prelude::*; +/// +/// #[renderable] +/// fn person(name: &String, age: u8) -> impl Renderable { +/// maud! { +/// div { +/// h1 { (name) } +/// p { "Age: " (age) } +/// } +/// } +/// } +/// +/// assert_eq!( +/// maud! { main { Person name=("Alice".into()) age=20; } } +/// .render() +/// .as_inner(), +/// "

Alice

Age: 20

", +/// ); +/// ``` +/// +/// ### [`rsx!`](crate::rsx!) +/// +/// ``` +/// use hypertext::prelude::*; +/// +/// #[renderable] +/// fn person(name: &String, age: u8) -> impl Renderable { +/// rsx! { +///
+///

(name)

+///

"Age: " (age)

+///
+/// } +/// } +/// +/// assert_eq!( +/// rsx! { +///
+/// +///
+/// } +/// .render() +/// .as_inner(), +/// "

Alice

Age: 20

", +/// ); +/// ``` +/// +/// ### `children` +/// +/// If you add children to the component node, the macro will pass them to the +/// `children` field of the type as a [`Lazy`]. +/// +/// ``` +/// use hypertext::prelude::*; +/// +/// #[renderable] +/// fn person(name: &String, age: u8, children: &R) -> impl Renderable { +/// maud! { +/// div { +/// h1 { (name) } +/// p { "Age: " (age) } +/// (children) +/// } +/// } +/// } +/// +/// assert_eq!( +/// maud! { +/// main { +/// Person name=("Alice".into()) age=20 { +/// p { "Pronouns: she/her" } +/// } +/// } +/// } +/// .render() +/// .as_inner(), +/// "

Alice

Age: 20

Pronouns: she/her

", +/// ); +/// ``` +pub trait Renderable { + /// Renders this value to the buffer. + fn render_to(&self, buffer: &mut Buffer); + + /// Creates a new [`Buffer`] from this value. + /// + /// This is a convenience method that creates a new [`Buffer`], + /// [`push`](Buffer::push)es `self` to it, then returns it. + /// + /// This may be overridden if `Self` is a string-like pre-escaped type that + /// can more efficiently be turned into a [`Buffer`] via + /// [`Buffer::dangerously_from_string`]. If overriden, the + /// implementation must match what [`render_to`](Renderable::render_to) + /// would produce. + #[inline] + fn to_buffer(&self) -> Buffer { + let mut buffer = Buffer::::new(); + buffer.push(self); + buffer + } +} + +/// An extension trait for [`Renderable`] types. +/// +/// This trait provides an additional method for rendering and memoizing values. +pub trait RenderableExt: Renderable { + /// Renders this value to a [`Rendered`]. + /// + /// This is usually the final step in rendering a value, converting it + /// into a [`Rendered`](Rendered) that can be returned as an HTTP + /// response or written to a file. + #[inline] + fn render(&self) -> Rendered { + self.to_buffer().rendered() + } + + /// Pre-renders the value and stores it in a [`Raw`] so that it can be + /// re-used among multiple renderings without re-computing the value. + /// + /// This should generally be avoided to prevent unnecessary allocations, but + /// may be useful if it is more expensive to compute and render the value. + #[inline] + fn memoize(&self) -> Raw { + // XSS SAFETY: The value has already been rendered and is assumed as safe. + Raw::dangerously_create(self.to_buffer().into_inner()) + } +} + +impl RenderableExt for T {} + +/// A value lazily rendered via a closure. +/// +/// For [`Lazy`] (a.k.a. [`Lazy`]), this must render complete +/// HTML nodes. If rendering string-like types, the closure must escape `&` to +/// `&`, `<` to `<`, and `>` to `>`. +/// +/// For [`Lazy`] (a.k.a. [`LazyAttribute`]), this must +/// render an attribute value which will eventually be surrounded by double +/// quotes. The closure must escape `&` to `&`, `<` to `<`, `>` to +/// `>`, and `"` to `"`. +#[derive(Clone, Copy)] +#[must_use = "`Lazy` does nothing unless `.render()` or `.render_to()` is called"] +pub struct Lazy), C: Context = Node> { + f: F, + context: PhantomData, +} + +/// An attribute value lazily rendered via a closure. +/// +/// This is a type alias for [`Lazy`]. +pub type LazyAttribute = Lazy; + +impl), C: Context> Lazy { + /// Creates a new [`Lazy`] from the given closure. + /// + /// It is recommended to add a `// XSS SAFETY` comment above the usage of + /// this function to indicate why it is safe to assume that the closure will + /// not write possibly unsafe HTML to the buffer. + #[inline] + pub const fn dangerously_create(f: F) -> Self { + Self { + f, + context: PhantomData, + } + } + + /// Extracts the inner closure. + #[inline] + pub const fn into_inner(self) -> F { + // SAFETY: `Lazy` has exactly one non-zero-sized field, which is `f`. + unsafe { const_precise_live_drops_hack!(self.f) } + } + + /// Gets a reference to the inner closure. + #[inline] + pub const fn as_inner(&self) -> &F { + &self.f + } +} + +impl), C: Context> Renderable for Lazy { + #[inline] + fn render_to(&self, buffer: &mut Buffer) { + (self.f)(buffer); + } +} + +impl), C: Context> Debug for Lazy { + #[inline] + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.debug_tuple("Lazy").finish_non_exhaustive() + } +} + +/// A value rendered via its [`Display`] implementation. +/// +/// This will handle escaping special characters for you. +/// +/// This can be created more easily via the `%(...)` syntax in +/// [`maud!`](crate::maud!), [`rsx!`](crate::rsx!), and +/// [`attribute!`](crate::attribute!) which will automatically wrap the +/// expression in this type. +/// +/// # Example +/// +/// ``` +/// use std::fmt::{self, Display, Formatter}; +/// +/// use hypertext::prelude::*; +/// +/// struct Greeting(&'static str); +/// +/// impl Display for Greeting { +/// fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { +/// write!(f, "Hello, {}! - - - -

Hypertext

-