diff --git a/.github/workflows/browser.yml b/.github/workflows/browser.yml index 4356fb4..1271927 100644 --- a/.github/workflows/browser.yml +++ b/.github/workflows/browser.yml @@ -11,7 +11,9 @@ jobs: - name: Install run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh - - run: wasm-pack test --headless --chrome + - run: wasm-pack test --headless --chrome --features dom,app working-directory: crates/hirola-core - - run: wasm-pack test --headless --firefox + - run: wasm-pack test --headless --firefox --features dom,app working-directory: crates/hirola-core + - run: wasm-pack test --headless --chrome --features dom,app + - run: wasm-pack test --headless --firefox --features dom,app diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index b2077a0..646bffe 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -1,4 +1,4 @@ -name: GitHub Actions Vercel Preview Deployment +name: GitHub Actions Vercel Predom Deployment env: VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} @@ -7,7 +7,7 @@ on: branches-ignore: - main jobs: - Deploy-Preview: + Deploy-Predom: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 @@ -17,19 +17,12 @@ jobs: toolchain: stable override: true - - name: Setup "wasm32-unknown-unknown" target - run: rustup target add wasm32-unknown-unknown - - - name: Setup "wasm-bindgen-cli" - run: cargo install wasm-bindgen-cli - - - name: Install Vercel CLI - run: npm install --global vercel@latest - - name: Install Trunk - run: wget -qO- https://github.com/thedodd/trunk/releases/download/v0.16.0/trunk-x86_64-unknown-linux-gnu.tar.gz | tar -xzf- - - name: Build Documentation Artifacts - run: ./trunk --config ./examples/docs/Trunk.toml build + run: cargo run --release + working-directory: examples/docs + + - name: Copy artifacts + run: cp -r ./examples/docs/dist/ ./public/ - name: Copy vercel config run: cp ./vercel.json ./public/vercel.json diff --git a/.github/workflows/production.yml b/.github/workflows/production.yml index 8b35c82..d1ec358 100644 --- a/.github/workflows/production.yml +++ b/.github/workflows/production.yml @@ -17,24 +17,17 @@ jobs: toolchain: stable override: true - - name: Setup "wasm32-unknown-unknown" target - run: rustup target add wasm32-unknown-unknown - - - name: Setup "wasm-bindgen-cli" - run: cargo install wasm-bindgen-cli - - - name: Install Vercel CLI - run: npm install --global vercel@latest - - name: Install Trunk - run: wget -qO- https://github.com/thedodd/trunk/releases/download/v0.16.0/trunk-x86_64-unknown-linux-gnu.tar.gz | tar -xzf- - - name: Build Documentation Artifacts - run: ./trunk --config ./examples/docs/Trunk.toml build + run: cargo run --release + working-directory: examples/docs + + - name: Copy artifacts + run: cp -r ./examples/docs/dist/ ./public/ - name: Copy vercel config run: cp ./vercel.json ./public/vercel.json - name: Pull Vercel Environment Information - run: vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }} + run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }} - name: Deploy Project Artifacts to Vercel run: vercel --prod --token=${{ secrets.VERCEL_TOKEN }} --confirm diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml index 9486096..055bb2b 100644 --- a/.github/workflows/unit.yml +++ b/.github/workflows/unit.yml @@ -12,5 +12,10 @@ jobs: profile: minimal toolchain: stable override: true - - run: cargo test + - run: cargo test --features dom,app working-directory: crates/hirola-core + - run: cargo test + working-directory: crates/hirola-macros + - run: cargo test + working-directory: crates/hirola-kit + - run: cargo test diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..da334ad --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "cSpell.words": [ + "bindgen", + "hirola", + "templating" + ] +} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 947c3d0..77265c9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,26 +2,75 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "aho-corasick" -version = "0.7.15" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5" +checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" dependencies = [ "memchr", ] [[package]] -name = "anyhow" -version = "1.0.63" +name = "anstream" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is-terminal", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd" + +[[package]] +name = "anstyle-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "anstyle-wincon" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a26fa4d7e3f2eebadf743988fc8aec9fa9a9e82611acafd77c1462ed6262440a" +checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" +dependencies = [ + "anstyle", + "windows-sys 0.48.0", +] [[package]] -name = "anymap" -version = "1.0.0-beta.2" +name = "anyhow" +version = "1.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f1f8f5a6f3d50d89e3797d7593a50f96bb2aaa20ca0cc7be1fb673232c91d72" +checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" [[package]] name = "atty" @@ -29,22 +78,64 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ - "hermit-abi", + "hermit-abi 0.1.17", "libc", "winapi", ] [[package]] name = "autocfg" -version = "1.0.1" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2" + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "base64" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" [[package]] name = "bitflags" -version = "1.2.1" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" [[package]] name = "bstr" @@ -54,15 +145,15 @@ checksum = "a40b47ad93e1a5404e6c18dec46b628214fee441c70f4ab5d6942142cc268a3d" dependencies = [ "lazy_static", "memchr", - "regex-automata", + "regex-automata 0.1.10", "serde", ] [[package]] name = "bumpalo" -version = "3.4.0" +version = "3.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820" +checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" [[package]] name = "canvas" @@ -80,6 +171,12 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" + [[package]] name = "cfg-if" version = "0.1.10" @@ -97,29 +194,102 @@ name = "chartjs" version = "0.1.0" [[package]] -name = "chrono" -version = "0.4.19" +name = "clap" +version = "2.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" dependencies = [ - "js-sys", - "libc", - "num-integer", - "num-traits", - "time", - "wasm-bindgen", - "winapi", + "bitflags 1.3.2", + "textwrap", + "unicode-width", ] [[package]] name = "clap" -version = "2.34.0" +version = "4.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +checksum = "1640e5cc7fb47dbb8338fd471b105e7ed6c3cb2aeb00c2e067127ffd3764a05d" dependencies = [ - "bitflags", - "textwrap", - "unicode-width", + "clap_builder", + "clap_derive", + "once_cell", +] + +[[package]] +name = "clap_builder" +version = "4.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98c59138d527eeaf9b53f35a77fcc1fad9d883116070c63d5de1c7dc7b00c72b" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", + "terminal_size", +] + +[[package]] +name = "clap_derive" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8cd2b2a819ad6eec39e8f1d6b53001af1e5469f8c177579cdaeb313115b825f" +dependencies = [ + "heck", + "proc-macro2 1.0.63", + "quote 1.0.28", + "syn 2.0.23", +] + +[[package]] +name = "clap_lex" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" + +[[package]] +name = "cloudabi" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "comrak" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "482aa5695bca086022be453c700a40c02893f1ba7098a2c88351de55341ae894" +dependencies = [ + "clap 4.3.11", + "entities", + "memchr", + "once_cell", + "regex", + "shell-words", + "slug", + "syntect", + "typed-arena", + "unicode_categories", + "xdg", +] + +[[package]] +name = "console" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "windows-sys 0.45.0", ] [[package]] @@ -137,9 +307,16 @@ name = "counter" version = "0.1.0" dependencies = [ "hirola", - "wasm-bindgen", "wasm-bindgen-test", - "web-sys", +] + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if 1.0.0", ] [[package]] @@ -150,7 +327,7 @@ checksum = "b01d6de93b2b6c65e17c634a26653a29d107b3c98c607c765bf38d041531cd8f" dependencies = [ "atty", "cast", - "clap", + "clap 2.34.0", "criterion-plot", "csv", "itertools", @@ -178,51 +355,56 @@ dependencies = [ "itertools", ] +[[package]] +name = "crop" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "959e53a6ca6070e1819d23930c0147cfc0de843451a3b3d78827130c00fb33d2" +dependencies = [ + "str_indices", +] + [[package]] name = "crossbeam-channel" -version = "0.4.4" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b153fe7cbef478c567df0f972e02e6d736db11affe43dfc9c56a9374d1adfb87" +checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" dependencies = [ + "cfg-if 1.0.0", "crossbeam-utils", - "maybe-uninit", ] [[package]] name = "crossbeam-deque" -version = "0.7.4" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c20ff29ded3204c5106278a81a38f4b482636ed4fa1e6cfbeef193291beb29ed" +checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" dependencies = [ + "cfg-if 1.0.0", "crossbeam-epoch", "crossbeam-utils", - "maybe-uninit", ] [[package]] name = "crossbeam-epoch" -version = "0.8.2" +version = "0.9.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "058ed274caafc1f60c4997b5fc07bf7dc7cca454af7c6e81edffe5f33f70dace" +checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" dependencies = [ - "autocfg", - "cfg-if 0.1.10", + "autocfg 1.1.0", + "cfg-if 1.0.0", "crossbeam-utils", - "lazy_static", - "maybe-uninit", "memoffset", "scopeguard", ] [[package]] name = "crossbeam-utils" -version = "0.7.2" +version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" dependencies = [ - "autocfg", - "cfg-if 0.1.10", - "lazy_static", + "cfg-if 1.0.0", ] [[package]] @@ -247,14 +429,33 @@ dependencies = [ "memchr", ] +[[package]] +name = "deranged" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7684a49fb1af197853ef7b2ee694bc1f5b4179556f1e5710e1760c5db6f5e929" + +[[package]] +name = "deunicode" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d95203a6a50906215a502507c0f879a0ce7ff205a6111e2db2a5ef8e4bb92e43" + +[[package]] +name = "discard" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" + [[package]] name = "docs" version = "0.1.0" dependencies = [ + "comrak", + "fronma", + "glob", "hirola", - "wasm-bindgen", - "wasm-bindgen-test", - "web-sys", + "serde", ] [[package]] @@ -269,19 +470,82 @@ version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + +[[package]] +name = "entities" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca" + +[[package]] +name = "equivalent" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1" + +[[package]] +name = "errno" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "fake-api" version = "0.1.0" dependencies = [ + "anyhow", "hirola", - "js-sys", + "reqwasm", "serde", - "serde_json", - "wasm-bindgen", - "wasm-bindgen-futures", "web-sys", ] +[[package]] +name = "fancy-regex" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" +dependencies = [ + "bit-set", + "regex", +] + +[[package]] +name = "flate2" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "form" version = "0.1.0" @@ -306,95 +570,267 @@ dependencies = [ ] [[package]] -name = "glob" -version = "0.3.0" +name = "fronma" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" +checksum = "da11047dc6b6b3f21012056a0f9177e435a0ce4f34e1c5e7990b01342c0d4e49" +dependencies = [ + "serde", + "serde_yaml", +] [[package]] -name = "half" -version = "1.8.2" +name = "fuchsia-cprng" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" [[package]] -name = "hermit-abi" -version = "0.1.17" +name = "futures-channel" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aca5565f760fb5b220e499d72710ed156fdb74e631659e99377d9ebfbd13ae8" +checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" dependencies = [ - "libc", + "futures-core", ] [[package]] -name = "hirola" -version = "0.2.0" -dependencies = [ - "document-features", - "hirola", - "hirola-core", - "hirola-form", - "hirola-macros", - "wasm-bindgen", - "wasm-bindgen-test", -] +name = "futures-core" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" [[package]] -name = "hirola-core" -version = "0.2.0" +name = "futures-macro" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ - "anyhow", - "anymap", - "chrono", - "criterion", - "hirola", - "hirola-macros", - "html-escape", - "matchit", - "ref-cast", - "regex", - "serde", - "thiserror", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-bindgen-test", - "web-sys", + "proc-macro2 1.0.63", + "quote 1.0.28", + "syn 2.0.23", ] [[package]] -name = "hirola-form" -version = "0.2.0" +name = "futures-signals" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36a12cb78961d5c0bc0e358599bba98ec09201090a22339cd8ea27e815c11b25" dependencies = [ - "hirola-core", - "json_dotpath", + "discard", + "futures-channel", + "futures-core", + "futures-util", + "gensym", + "log", + "pin-project", "serde", - "serde_json", - "validator", - "validator_derive", - "wasm-bindgen", - "web-sys", ] [[package]] -name = "hirola-macros" -version = "0.2.0" +name = "futures-sink" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" + +[[package]] +name = "futures-task" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" + +[[package]] +name = "futures-util" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +dependencies = [ + "futures-core", + "futures-macro", + "futures-task", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "gensym" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb328fe25cbf075818a3e57bb5ee39b49b4f26c94d685356426154c5962cccd" +dependencies = [ + "proc-macro2 0.4.30", + "quote 0.6.13", + "syn 0.15.44", + "uuid", +] + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "gloo-net" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2899cb1a13be9020b010967adc6b2a8a343b6f1428b90238c9d53ca24decc6db" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-utils" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037fcb07216cb3a30f7292bd0176b050b7b9a052ba830ef7d5d65f6dc64ba58e" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "half" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aca5565f760fb5b220e499d72710ed156fdb74e631659e99377d9ebfbd13ae8" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" + +[[package]] +name = "hirola" +version = "0.3.0" dependencies = [ + "document-features", + "futures-util", + "hirola", "hirola-core", - "once_cell", + "hirola-form", + "hirola-macros", + "wasm-bindgen", + "wasm-bindgen-test", + "web-sys", +] + +[[package]] +name = "hirola-core" +version = "0.3.0" +dependencies = [ + "criterion", + "discard", + "futures-signals", + "futures-util", + "hirola", + "hirola-macros", + "html-escape", + "log", + "matchit", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test", + "web-sys", +] + +[[package]] +name = "hirola-form" +version = "0.3.0" +dependencies = [ + "hirola-core", + "json_dotpath", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "hirola-kit" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap 4.3.11", + "crop", + "glob", + "indoc", + "insta", + "leptosfmt-pretty-printer", + "leptosfmt-prettyplease", + "proc-macro2 1.0.63", + "quote 1.0.28", + "rayon", + "rstml", + "serde", + "syn 2.0.23", + "thiserror", + "toml 0.7.6", +] + +[[package]] +name = "hirola-macros" +version = "0.3.0" +dependencies = [ + "hirola", + "paste", "proc-macro-error", "proc-macro-hack", - "proc-macro2", - "quote", - "syn", - "syn-rsx", + "proc-macro2 1.0.63", + "quote 1.0.28", + "rstml", + "syn 2.0.23", "trybuild", ] [[package]] name = "html-escape" -version = "0.2.11" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e7479fa1ef38eb49fb6a42c426be515df2d063f06cb8efd3e50af073dbc26c" +checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476" dependencies = [ "utf8-width", ] @@ -416,6 +852,67 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed" +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg 1.1.0", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +dependencies = [ + "equivalent", + "hashbrown 0.14.0", +] + +[[package]] +name = "indoc" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "761cde40c27e2a9877f8c928fd248b7eec9dd48623dd514b256858ca593fbba7" + +[[package]] +name = "insta" +version = "1.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28491f7753051e5704d4d0ae7860d45fae3238d7d235bc4289dcd45c48d3cec3" +dependencies = [ + "console", + "lazy_static", + "linked-hash-map", + "similar", + "yaml-rust", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi 0.3.2", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "is-terminal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +dependencies = [ + "hermit-abi 0.3.2", + "rustix 0.38.3", + "windows-sys 0.48.0", +] + [[package]] name = "itertools" version = "0.10.3" @@ -433,15 +930,15 @@ checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" [[package]] name = "itoa" -version = "1.0.2" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[package]] name = "js-sys" -version = "0.3.59" +version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "258451ab10b34f8af53416d1fdab72c22e805f0c92a1136d59470ec0b11138b2" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" dependencies = [ "wasm-bindgen", ] @@ -464,21 +961,62 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "leptosfmt-pretty-printer" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc570d5bcaa4786e36be800c3b1a243e98e228ba0ca9493f022f4dea03d5b041" + +[[package]] +name = "leptosfmt-prettyplease" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "378f8543271c41684f968e6a7478eae7b573d62e6cf3a5042efc45f9a7b9c815" +dependencies = [ + "leptosfmt-pretty-printer", + "proc-macro2 1.0.63", + "syn 2.0.23", +] + [[package]] name = "libc" -version = "0.2.126" +version = "0.2.147" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" [[package]] -name = "log" -version = "0.4.17" +name = "line-wrap" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +checksum = "f30344350a2a51da54c1d53be93fade8a237e545dbcc4bdbe635413f2117cab9" dependencies = [ - "cfg-if 1.0.0", + "safemem", ] +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + +[[package]] +name = "linux-raw-sys" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09fc20d2ca12cb9f044c93e3bd6d32d523e6e2ec3db4f7b2939cd99026ecd3f0" + +[[package]] +name = "log" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" + [[package]] name = "matches" version = "0.1.9" @@ -487,29 +1025,32 @@ checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" [[package]] name = "matchit" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dfc802da7b1cf80aefffa0c7b2f77247c8b32206cc83c270b61264f5b360a80" +checksum = "b87248edafb776e59e6ee64a79086f65890d3510f2c656c000bf2a7e8a0aea40" [[package]] -name = "maybe-uninit" -version = "2.0.0" +name = "memchr" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] -name = "memchr" -version = "2.3.4" +name = "memoffset" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg 1.1.0", +] [[package]] -name = "memoffset" -version = "0.5.6" +name = "miniz_oxide" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "043175f069eda7b85febe4a74abbaeff828d9f8b448515d3151a14a3542811aa" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" dependencies = [ - "autocfg", + "adler", ] [[package]] @@ -521,23 +1062,13 @@ dependencies = [ "web-sys", ] -[[package]] -name = "num-integer" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" -dependencies = [ - "autocfg", - "num-traits", -] - [[package]] name = "num-traits" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" dependencies = [ - "autocfg", + "autocfg 1.1.0", ] [[package]] @@ -546,15 +1077,37 @@ version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" dependencies = [ - "hermit-abi", + "hermit-abi 0.1.17", "libc", ] [[package]] name = "once_cell" -version = "1.13.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "onig" +version = "6.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c4b31c8722ad9171c6d77d3557db078cab2bd50afcc9d09c8b315c59df8ca4f" +dependencies = [ + "bitflags 1.3.2", + "libc", + "once_cell", + "onig_sys", +] + +[[package]] +name = "onig_sys" +version = "69.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b829e3d7e9cc74c7e315ee8edb185bf4190da5acde74afd7fc59c35b1f086e7" +dependencies = [ + "cc", + "pkg-config", +] [[package]] name = "oorandom" @@ -562,12 +1115,70 @@ version = "11.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" +[[package]] +name = "paste" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4b27ab7be369122c218afc2079489cdcb4b517c0a3fc386ff11e1fedfcc2b35" + [[package]] name = "percent-encoding" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +[[package]] +name = "pin-project" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" +dependencies = [ + "proc-macro2 1.0.63", + "quote 1.0.28", + "syn 1.0.109", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" + +[[package]] +name = "plist" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdc0001cfea3db57a2e24bc0d818e9e20e554b5f97fabb9bc231dc240269ae06" +dependencies = [ + "base64", + "indexmap 1.9.3", + "line-wrap", + "quick-xml", + "serde", + "time", +] + [[package]] name = "plotters" version = "0.3.2" @@ -603,9 +1214,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" dependencies = [ "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.63", + "quote 1.0.28", + "syn 1.0.109", "version_check", ] @@ -613,92 +1224,224 @@ dependencies = [ name = "proc-macro-error-attr" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2 1.0.63", + "quote 1.0.28", + "version_check", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + +[[package]] +name = "proc-macro2" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "proc-macro2" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "606c4ba35817e2922a308af55ad51bab3645b59eae5c570d4a6cf07e36bd493b" +dependencies = [ + "proc-macro2 1.0.63", + "quote 1.0.28", + "syn 2.0.23", + "version_check", + "yansi", +] + +[[package]] +name = "quick-xml" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81b9228215d82c7b61490fec1de287136b5de6f5700f6e58ea9ad61a7964ca51" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "0.6.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1" +dependencies = [ + "proc-macro2 0.4.30", +] + +[[package]] +name = "quote" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" +dependencies = [ + "proc-macro2 1.0.63", +] + +[[package]] +name = "rand" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" +dependencies = [ + "autocfg 0.1.7", + "libc", + "rand_chacha", + "rand_core 0.4.2", + "rand_hc", + "rand_isaac", + "rand_jitter", + "rand_os", + "rand_pcg", + "rand_xorshift", + "winapi", +] + +[[package]] +name = "rand_chacha" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" +dependencies = [ + "autocfg 0.1.7", + "rand_core 0.3.1", +] + +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + +[[package]] +name = "rand_hc" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rand_isaac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rand_jitter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b" dependencies = [ - "proc-macro2", - "quote", - "version_check", + "libc", + "rand_core 0.4.2", + "winapi", ] [[package]] -name = "proc-macro-hack" -version = "0.5.19" +name = "rand_os" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" +checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" +dependencies = [ + "cloudabi", + "fuchsia-cprng", + "libc", + "rand_core 0.4.2", + "rdrand", + "winapi", +] [[package]] -name = "proc-macro2" -version = "1.0.42" +name = "rand_pcg" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c278e965f1d8cf32d6e0e96de3d3e79712178ae67986d9cf9151f51e95aac89b" +checksum = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" dependencies = [ - "unicode-ident", + "autocfg 0.1.7", + "rand_core 0.4.2", ] [[package]] -name = "quote" -version = "1.0.20" +name = "rand_xorshift" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804" +checksum = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" dependencies = [ - "proc-macro2", + "rand_core 0.3.1", ] [[package]] name = "rayon" -version = "1.4.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf6960dc9a5b4ee8d3e4c5787b4a112a8818e0290a42ff664ad60692fdf2032" +checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" dependencies = [ - "autocfg", - "crossbeam-deque", "either", "rayon-core", ] [[package]] name = "rayon-core" -version = "1.8.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8c4fec834fb6e6d2dd5eece3c7b432a52f0ba887cf40e595190c4107edc08bf" +checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" dependencies = [ "crossbeam-channel", "crossbeam-deque", "crossbeam-utils", - "lazy_static", "num_cpus", ] [[package]] -name = "ref-cast" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "776c8940430cf563f66a93f9111d1cd39306dc6c68149ecc6b934742a44a828a" -dependencies = [ - "ref-cast-impl", -] - -[[package]] -name = "ref-cast-impl" -version = "1.0.8" +name = "rdrand" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f26c4704460286103bff62ea1fb78d137febc86aaf76952e6c5a2249af01f54" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" dependencies = [ - "proc-macro2", - "quote", - "syn", + "rand_core 0.3.1", ] [[package]] name = "regex" -version = "1.4.2" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38cf2c13ed4745de91a5eb834e11c00bcc3709e773173b2ce4c56c9fbde04b9c" +checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575" dependencies = [ "aho-corasick", "memchr", + "regex-automata 0.3.4", "regex-syntax", - "thread_local", ] [[package]] @@ -707,11 +1450,72 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +[[package]] +name = "regex-automata" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7b6d6190b7594385f61bd3911cd1be99dfddcfc365a4160cc2ab5bff4aed294" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + [[package]] name = "regex-syntax" -version = "0.6.21" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" + +[[package]] +name = "reqwasm" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b89870d729c501fa7a68c43bf4d938bbb3a8c156d333d90faa0e8b3e3212fb" +dependencies = [ + "gloo-net", +] + +[[package]] +name = "rstml" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7afcc74cab5d3118523b1f75900e1fcbeae7cac6c6cb800430621bf58add0bd" +dependencies = [ + "proc-macro2 1.0.63", + "proc-macro2-diagnostics", + "quote 1.0.28", + "syn 2.0.23", + "syn_derive", + "thiserror", +] + +[[package]] +name = "rustix" +version = "0.37.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06" +dependencies = [ + "bitflags 1.3.2", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys 0.3.8", + "windows-sys 0.48.0", +] + +[[package]] +name = "rustix" +version = "0.38.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b181ba2dcf07aaccad5448e8ead58db5b742cf85dfe035e2227f137a539a189" +checksum = "ac5ffa1efe7548069688cd7028f32591853cd7b5b756d41bcffd2353e4fc75b4" +dependencies = [ + "bitflags 2.3.3", + "errno", + "libc", + "linux-raw-sys 0.4.3", + "windows-sys 0.48.0", +] [[package]] name = "ryu" @@ -719,6 +1523,12 @@ version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" +[[package]] +name = "safemem" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" + [[package]] name = "same-file" version = "1.0.6" @@ -742,9 +1552,9 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "serde" -version = "1.0.143" +version = "1.0.166" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e8e5d5b70924f74ff5c6d64d9a5acd91422117c60f48c4e07855238a254553" +checksum = "d01b7404f9d441d3ad40e6a636a7782c377d2abdbe4fa2440e2edcc2f4f10db8" dependencies = [ "serde_derive", ] @@ -761,46 +1571,154 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.143" +version = "1.0.166" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3d8e8de557aee63c26b85b947f5e59b690d0454c753f3adeb5cd7835ab88391" +checksum = "5dd83d6dde2b6b2d466e14d9d1acce8816dedee94f735eac6395808b3483c6d6" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.63", + "quote 1.0.28", + "syn 2.0.23", ] [[package]] name = "serde_json" -version = "1.0.82" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f1e14e89be7aa4c4b78bdbdc9eb5bf8517829a600ae8eaa39a6e1d960b5185c" +dependencies = [ + "itoa 1.0.9", + "ryu", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82c2c1fdcd807d1098552c5b9a36e425e42e9fbd7c6a37a8425f390f781f7fa7" +checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186" dependencies = [ - "itoa 1.0.2", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b" +dependencies = [ + "indexmap 1.9.3", "ryu", "serde", + "yaml-rust", +] + +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + +[[package]] +name = "similar" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "420acb44afdae038210c99e69aae24109f32f15500aa708e81d46c9f29d55fcf" + +[[package]] +name = "slab" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +dependencies = [ + "autocfg 1.1.0", +] + +[[package]] +name = "slug" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3bc762e6a4b6c6fcaade73e77f9ebc6991b676f88bb2358bddb56560f073373" +dependencies = [ + "deunicode", +] + +[[package]] +name = "str_indices" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f026164926842ec52deb1938fae44f83dfdb82d0a5b0270c5bd5935ab74d6dd" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "0.15.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5" +dependencies = [ + "proc-macro2 0.4.30", + "quote 0.6.13", + "unicode-xid", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2 1.0.63", + "quote 1.0.28", + "unicode-ident", ] [[package]] name = "syn" -version = "1.0.98" +version = "2.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd" +checksum = "59fb7d6d8281a51045d62b8eb3a7d1ce347b76f312af50cd3dc0af39c87c1737" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.63", + "quote 1.0.28", "unicode-ident", ] [[package]] -name = "syn-rsx" -version = "0.8.1" +name = "syn_derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8128874d02f9a114ade6d9ad252078cb32d3cb240e26477ac73d7e9c495c605e" +dependencies = [ + "proc-macro-error", + "proc-macro2 1.0.63", + "quote 1.0.28", + "syn 2.0.23", +] + +[[package]] +name = "syntect" +version = "5.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d793e849cad8b35561e8d9652ff5269d19231b7037977863b50500651952ccc" +checksum = "e02b4b303bf8d08bfeb0445cba5068a3d306b6baece1d5582171a9bf49188f91" dependencies = [ - "proc-macro2", - "quote", - "syn", + "bincode", + "bitflags 1.3.2", + "fancy-regex", + "flate2", + "fnv", + "once_cell", + "onig", + "plist", + "regex-syntax", + "serde", + "serde_json", + "thiserror", + "walkdir", + "yaml-rust", ] [[package]] @@ -812,6 +1730,16 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "terminal_size" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e6bf6f19e9f8ed8d4048dc22981458ebcf406d67e94cd422e5ecd73d63b3237" +dependencies = [ + "rustix 0.37.23", + "windows-sys 0.48.0", +] + [[package]] name = "textwrap" version = "0.11.0" @@ -823,42 +1751,50 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.32" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5f6586b7f764adc0231f4c79be7b920e766bb2f3e51b3661cdb263828f19994" +checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.32" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12bafc5b54507e0149cdf1b145a5d80ab80a90bcd9275df43d4fff68460f6c21" +checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.63", + "quote 1.0.28", + "syn 2.0.23", ] [[package]] -name = "thread_local" -version = "1.1.4" +name = "time" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" +checksum = "b0fdd63d58b18d663fbdf70e049f00a22c8e42be082203be7f26589213cd75ea" dependencies = [ - "once_cell", + "deranged", + "itoa 1.0.9", + "serde", + "time-core", + "time-macros", ] [[package]] -name = "time" -version = "0.1.44" +name = "time-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" + +[[package]] +name = "time-macros" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +checksum = "eb71511c991639bb078fd5bf97757e03914361c48100d52878b8e52b46fb92cd" dependencies = [ - "libc", - "wasi", - "winapi", + "time-core", ] [[package]] @@ -904,6 +1840,40 @@ dependencies = [ "serde", ] +[[package]] +name = "toml" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17e963a819c331dcacd7ab957d80bc2b9a9c1e71c804826d2f283dd65306542" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c500344a19072298cd05a7224b3c0c629348b78692bf48466c5238656e315a78" +dependencies = [ + "indexmap 2.0.0", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "trybuild" version = "1.0.63" @@ -916,9 +1886,15 @@ dependencies = [ "serde_derive", "serde_json", "termcolor", - "toml", + "toml 0.5.9", ] +[[package]] +name = "typed-arena" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" + [[package]] name = "unicode-bidi" version = "0.3.8" @@ -946,6 +1922,18 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" +[[package]] +name = "unicode-xid" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" + +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + [[package]] name = "url" version = "2.2.2" @@ -964,6 +1952,21 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5190c9442dcdaf0ddd50f37420417d219ae5261bbf5db120d0f9bab996c9cba1" +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "uuid" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90dbc611eb48397705a6b0f6e917da23ae517e4d127123d2cf7674206627d32a" +dependencies = [ + "rand", +] + [[package]] name = "validator" version = "0.10.1" @@ -987,10 +1990,10 @@ checksum = "0d577dfb8ca9440a5c0b053d5a19b68f5c92ef57064bac87c8205c3f6072c20f" dependencies = [ "if_chain", "lazy_static", - "proc-macro2", - "quote", + "proc-macro2 1.0.63", + "quote 1.0.28", "regex", - "syn", + "syn 1.0.109", "validator", ] @@ -1011,17 +2014,11 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" - [[package]] name = "wasm-bindgen" -version = "0.2.82" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7652e3f6c4706c8d9cd54832c4a4ccb9b5336e2c3bd154d5cccfbf1c1f5f7d" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" dependencies = [ "cfg-if 1.0.0", "serde", @@ -1031,16 +2028,16 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.82" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "662cd44805586bd52971b9586b1df85cdbbd9112e4ef4d8f41559c334dc6ac3f" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" dependencies = [ "bumpalo", "log", "once_cell", - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.63", + "quote 1.0.28", + "syn 2.0.23", "wasm-bindgen-shared", ] @@ -1058,32 +2055,32 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.82" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b260f13d3012071dfb1512849c033b1925038373aea48ced3012c09df952c602" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" dependencies = [ - "quote", + "quote 1.0.28", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.82" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be8e654bdd9b79216c2929ab90721aa82faf65c48cdf08bdc4e7f51357b80da" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.63", + "quote 1.0.28", + "syn 2.0.23", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.82" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6598dd0bd3c7d51095ff6531a5b23e02acdc81804e30d8f07afb77b7215a140a" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" [[package]] name = "wasm-bindgen-test" @@ -1105,15 +2102,15 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6150d36a03e90a3cf6c12650be10626a9902d70c5270fd47d7a47e5389a10d56" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.63", + "quote 1.0.28", ] [[package]] name = "web-sys" -version = "0.3.59" +version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed055ab27f941423197eb86b2035720b1a3ce40504df082cac2ecc6ed73335a1" +checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" dependencies = [ "js-sys", "wasm-bindgen", @@ -1150,6 +2147,147 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" +dependencies = [ + "windows_aarch64_gnullvm 0.48.0", + "windows_aarch64_msvc 0.48.0", + "windows_i686_gnu 0.48.0", + "windows_i686_msvc 0.48.0", + "windows_x86_64_gnu 0.48.0", + "windows_x86_64_gnullvm 0.48.0", + "windows_x86_64_msvc 0.48.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" + +[[package]] +name = "winnow" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81a2094c43cc94775293eaa0e499fbc30048a6d824ac82c0351a8c0bf9112529" +dependencies = [ + "memchr", +] + [[package]] name = "x-for" version = "0.1.0" @@ -1158,3 +2296,24 @@ dependencies = [ "wasm-bindgen", "web-sys", ] + +[[package]] +name = "xdg" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546" + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" diff --git a/Cargo.toml b/Cargo.toml index 0af6487..dbf4f43 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hirola" -version = "0.2.0" +version = "0.3.0" authors = ["Geoffrey Mureithi "] description = "Hirola is an un-opinionated web framework that is focused on simplicity and predictability" repository = "https://github.com/geofmureithi/hirola" @@ -11,28 +11,26 @@ keywords = ["wasm", "html", "dom", "web"] edition = "2021" [dependencies] -hirola-core = { path = "crates/hirola-core", version = "0.2.0" } -hirola-macros = { path = "crates/hirola-macros", version = "0.2.0" } -hirola-form = { path = "crates/hirola-form", version = "0.2.0", optional = true } +hirola-core = { path = "crates/hirola-core", version = "0.3.0" } +hirola-macros = { path = "crates/hirola-macros", version = "0.3.0" } +hirola-form = { path = "crates/hirola-form", version = "0.3.0", optional = true } [features] default = ["hirola-core/default"] -## Enables serialization of state -serde = ["hirola-core/serde"] -## Enables server side reendering -ssr = ["hirola-core/ssr"] +## Enables dom based rendering +dom = ["hirola-core/dom"] -## Enables Isomorphic Routing -router = ["hirola-core/router"] +# ## Enables serialization of state +# serde = ["hirola-core/serde"] -## Enables global state management -global-state = ["hirola-core/global-state"] +## Enables server side reendering +ssr = ["hirola-core/ssr"] -## Enables async utilities -async = ["hirola-core/async"] +## Enables app features like isomorphic routing +app = ["hirola-core/app"] docsrs = ["document-features"] @@ -40,17 +38,19 @@ docsrs = ["document-features"] form = ["hirola-form"] - [dev-dependencies] wasm-bindgen-test = "0.3.0" wasm-bindgen = { version = "0.2.79" } -hirola = { path = ".", default-features=false, features =["router", "global-state", "ssr"]} +hirola = { path = ".", features = ["app", "dom"] } +futures-util = "0.3" +web-sys = { version = "0.3", features = ["Document", "Node", "Element"] } [workspace] members = [ "crates/hirola-core", "crates/hirola-macros", "crates/hirola-form", + "crates/hirola-kit", "examples/counter", "examples/todo", "examples/canvas", @@ -59,7 +59,7 @@ members = [ "examples/x-for", "examples/mixin", "examples/form", - "examples/docs" + "examples/docs", ] [dependencies.document-features] @@ -75,5 +75,10 @@ default-target = "wasm32-unknown-unknown" [profile.release] +opt-level = "z" +overflow-checks = false +debug = 0 +strip = "symbols" +debug-assertions = false +codegen-units = 1 lto = true -opt-level = 's' \ No newline at end of file diff --git a/INSTALATION.md b/INSTALATION.md index b0af9ac..7c8e422 100644 --- a/INSTALATION.md +++ b/INSTALATION.md @@ -3,54 +3,28 @@ lib.rs ```rust +use std::fmt::Display; use hirola::prelude::*; -use wasm_bindgen::JsCast; -use web_sys::Event; -use web_sys::HtmlInputElement; - -fn home() -> Dom { - let state = Signal::new(99); - - let decerement = state.mut_callback(|count, _e| *count - 1); - - let incerement = state.mut_callback(|count, _e| *count + 1); +use hirola::signal::Mutable; +fn counter() -> Dom { + let count = Mutable::new(0i32); + let decrement = count.callback(|s| *s.lock_mut() -= 1); + let increment = count.callback(|s| *s.lock_mut() += 1); html! { -
- -
-
- -
- -
- -
-
-
+ <> + + {count} + + } } fn main() { - let window = web_sys::window().unwrap(); - let document = window.document().unwrap(); - let body = document.body().unwrap(); - - let mut app = HirolaApp::new(); - app.mount(&body, home); + let root = render(counter()).unwrap(); + // We prevent the root from being dropped + std::mem::forget(root); } - ``` index.html @@ -77,7 +51,7 @@ version = "0.1.0" [dependencies] -hirola = "0.1" +hirola = "0.3" console_error_panic_hook = "0.1" log = "0.4" console_log = "0.2" diff --git a/README.md b/README.md index c9ed395..25d6ecf 100644 --- a/README.md +++ b/README.md @@ -5,26 +5,26 @@ [![Unit Tests](https://github.com/geofmureithi/hirola/actions/workflows/unit.yml/badge.svg)](https://github.com/geofmureithi/hirola/actions/workflows/unit.yml) [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE) -**Hirola** is an un-opinionated webf ramework that is focused on simplicity and predictability. +**Hirola** is a declarative frontend framework that is focused on simplicity and reactivity. ## Goals -1. Keep it simple. A simple and declarative way to build web UIs in rust with a small learning curve. -2. Make it easy to read, extend and share code. Mixins and components are kept simple and macro-free. -3. No context, you can choose passing props down, and/or use the `global-state`. -4. Familiality. Uses rsx which is very similar to jsx. +1. KISS: A simple and declarative way to build frontend UIs in rust. +2. Make it easy to read, extend and share code. +3. Frp signals allowing fine-grained reactivity. +4. Familiarity: Uses rsx which is very similar to jsx. ## Example We are going to create a simple counter program. -``` +```bash cargo new counter ``` With a new project, we need to create an index file which is the entry point and required by trunk -``` +```bash cd counter ``` @@ -43,37 +43,34 @@ Create an `index.html` in the root of counter. Add the contents below Lets add some code to `src/main.rs` -```rust +```rust,no_run use hirola::prelude::*; +use hirola::signal::Mutable; -fn counter(app: &HirolaApp) -> Dom { - let count = Signal::new(0); - let increment = count.mut_callback(|c, _| c + 1) +fn counter() -> Dom { + let count = Mutable::new(0i32); + let decrement = count.callback(|s| *s.lock_mut() -= 1); + let increment = count.callback(|s| *s.lock_mut() += 1); html! { -
- - {count.get()} -
+ <> + + {count} + + } } fn main() { - let window = web_sys::window().unwrap(); - let document = window.document().unwrap(); - let body = document.body().unwrap(); - - let app = HirolaApp::new(); - app.mount(&body, counter); + let root = render(counter()).unwrap(); + std::mem::forget(root); } ``` Now lets run our project -``` +```bash trunk serve ``` -You should be able to get counter running: [Live Example](https://hirola-docs.vercel.app/basics/getting-started) - ## Ecosystem Check out [Hirola Docs](https://hirola-docs.vercel.app/basics/getting-started) written with Hirola itself! @@ -83,37 +80,18 @@ Here are some extensions for hirola: 1. [Form](https://hirola-docs.vercel.app/plugins/form) 2. [Router](https://hirola-docs.vercel.app/plugins/router) 3. [State](https://hirola-docs.vercel.app/plugins/state) +4. [Markdown](https://hirola-docs.vercel.app/plugins/mdx) ### Milestones -| Status | Goal | Labels | -| :----: | :------------------------------------------------------------------------ | ------- | -| ✔ | Write code that is declarative and easy to follow | `ready` | -| ✔ | Allow extensibility via mixins | `ready` | -| 🚀 | [Standardize Components](https://github.com/geofmureithi/hirola/issues/1) | `ready` | -| 🚀 | SSR | `ready` | -| 🚀 | Hydration | `todo` | -| 🚀 | Serverside integrations | `todo` | - -### Inspiration - -- Sycamore -- Alpine.js -- React.js -- Yew - -#### Demo examples - -> This API will certainly change. - -Go to `examples` and use trunk - -``` -$ trunk serve -``` - -#### Prerequisite: - -You need need to have `rust`, `cargo` and `trunk` installed. - -License: MIT +| Status | Goal | Labels | +| :----: | :--------------------------------- | --------- | +| ✔ | Basic templating with rust and rsx | `ready` | +| ✔ | Extend functionality with mixins | `ready` | +| ✔ | Components | `ready` | +| ✔ | SSR | `ready` | +| ✔ | Signals | `ready` | +| 🚧 | Form management | `started` | +| ⏳ | Markdown templating | `pending` | +| 🚧 | Styling | `started` | +| ⏳ | SSG | `pending` | diff --git a/crates/hirola-core/Cargo.toml b/crates/hirola-core/Cargo.toml index 0b12d78..310024d 100644 --- a/crates/hirola-core/Cargo.toml +++ b/crates/hirola-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hirola-core" -version = "0.2.0" +version = "0.3.0" authors = ["Geoffrey Mureithi "] edition = "2021" description = "An html library for building client side webapps" @@ -14,56 +14,49 @@ keywords = ["wasm", "html", "dom", "web"] [dependencies] -chrono = { version = "0.4", features = ["wasmbind"] } -anyhow = "1.0" -thiserror = "1.0" html-escape = { version = "0.2.7", optional = true } -hirola-macros = { path = "../hirola-macros", version = "0.2.0" } -ref-cast = "1.0" -serde = { version = "1.0", optional = true } -wasm-bindgen = { version = "0.2", optional = true } -regex = "1" -matchit = { version = "0.6", optional = true } -anymap = { version = "1.0.0-beta.2", optional = true } -wasm-bindgen-futures = { version = "0.4.29", optional = true } - - +hirola-macros = { path = "../hirola-macros", version = "0.3.0" } +wasm-bindgen = { version = "0.2", optional = true } +matchit = { version = "0.7", optional = true } +wasm-bindgen-futures = { optional = true , version = "0.4.29" } +futures-signals = "0.3.32" +futures-util = "0.3" +discard = "1" +log = "0.4.6" [dependencies.web-sys] features = [ - "console", "Comment", "Document", "DocumentFragment", "Element", "Event", "HtmlElement", - "HtmlInputElement", "Node", "Text", - "HtmlCollection", - "HtmlStyleElement", - "CssRuleList", - "CssStyleSheet", - "CssStyleDeclaration", - "Window" + "Window", ] optional = true -version = "0.3" +version = "0.3.64" [dev-dependencies] -criterion = {version = "0.3", features = ["html_reports"]} +criterion = { version = "0.3", features = ["html_reports"] } wasm-bindgen-test = "0.3" -hirola = { path ="../../" } -web-sys = { version = "0.3", features =["DomTokenList", "Element", "Window"]} +hirola = { path = "../../" } +web-sys = { version = "0.3", features = ["DomTokenList", "Element", "Window"] } + [features] -default = ["dom", "wasm-bindgen", "web-sys"] -dom = [] -ssr = ["html-escape", "web-sys/Event"] -router = ["matchit", "web-sys/History", "web-sys/Location", "web-sys/HtmlLinkElement"] -global-state = ["anymap"] -async = ["wasm-bindgen-futures"] +default = [] +dom = ["web-sys", "wasm-bindgen", "wasm-bindgen-futures"] +ssr = ["html-escape"] +app = [ + "matchit", + "web-sys/History", + "web-sys/Location", + "web-sys/HtmlLinkElement", +] + [[bench]] harness = false diff --git a/crates/hirola-core/benches/reactivity.rs b/crates/hirola-core/benches/reactivity.rs index 23db82d..a35bb27 100644 --- a/crates/hirola-core/benches/reactivity.rs +++ b/crates/hirola-core/benches/reactivity.rs @@ -1,27 +1,15 @@ use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use futures_signals::signal::Mutable; use hirola_core::prelude::*; pub fn bench(c: &mut Criterion) { c.bench_function("reactivity_signals", |b| { b.iter(|| { - let state = Signal::new(black_box(0)); + let state = Mutable::new(black_box(0)); for _i in 0..1000 { let value = state.get(); - state.set(*value + 1); - } - }) - }); - - c.bench_function("reactivity_effects", |b| { - b.iter(|| { - let state = Signal::new(black_box(0)); - create_effect(cloned!((state) => move || { - let _double = *state.get() * 2; - })); - - for _i in 0..1000 { - state.set(*state.get() + 1); + state.set(value + 1); } }) }); diff --git a/crates/hirola-core/benches/ssr.rs b/crates/hirola-core/benches/ssr.rs index d3a00df..ce41c39 100644 --- a/crates/hirola-core/benches/ssr.rs +++ b/crates/hirola-core/benches/ssr.rs @@ -6,50 +6,50 @@ use hirola_core::prelude::*; pub fn bench(c: &mut Criterion) { c.bench_function("ssr_small", |b| { b.iter(|| { - fn App() -> TemplateResult { - template! { - div(class="my-container") { - p { "Hello World!" } - } + fn App() -> Dom { + html! { +
+

"Hello World!"

+
} } - let _ssr = render_to_string(|| template! { App() }); + let _ssr = render_to_string(App()); }) }); - c.bench_function("ssr_medium", |b| { - b.iter(|| { - fn ListItem(value: i32) -> TemplateResult { - template! { - p { - span(class="placeholder") - i { (value) } - button(class="delete") { - i(class="delete-icon") - } - } - } - } - - fn App() -> TemplateResult { - let values = Signal::new((0i32..=10).collect::>()); - - template! { - div(class="my-container") { - Indexed(IndexedProps { - iterable: values.handle(), - template: |x| template! { - ListItem(x) - } - }) - } - } - } - - let _ssr = render_to_string(|| template! { App() }); - }) - }); + // c.bench_function("ssr_medium", |b| { + // b.iter(|| { + // fn ListItem(value: i32) -> Dom { + // template! { + // p { + // span(class="placeholder") + // i { (value) } + // button(class="delete") { + // i(class="delete-icon") + // } + // } + // } + // } + + // fn App() -> Dom { + // let values = Signal::new((0i32..=10).collect::>()); + + // template! { + // div(class="my-container") { + // Indexed(IndexedProps { + // iterable: values.handle(), + // template: |x| template! { + // ListItem(x) + // } + // }) + // } + // } + // } + + // let _ssr = render_to_string(|| template! { App() }); + // }) + // }); } criterion_group! { diff --git a/crates/hirola-core/src/app/mod.rs b/crates/hirola-core/src/app/mod.rs index c953505..c3bef6d 100644 --- a/crates/hirola-core/src/app/mod.rs +++ b/crates/hirola-core/src/app/mod.rs @@ -1,84 +1,330 @@ -use crate::prelude::*; +pub mod router; +use router::Router; +use std::fmt::Debug; -#[cfg(feature = "global-state")] -#[cfg_attr(docsrs, doc(cfg(feature = "global-state")))] -use anymap::{CloneAny, Map}; +use crate::{dom::Dom}; -#[cfg(feature = "global-state")] -#[cfg_attr(docsrs, doc(cfg(feature = "global-state")))] -type ExtensionMap = Map; - -/// Represents an instance of a mountable app -#[derive(Clone)] -pub struct HirolaApp { - #[cfg(feature = "global-state")] - #[cfg_attr(docsrs, doc(cfg(feature = "global-state")))] - extensions: ExtensionMap, -} - -/// Represents a view that can be mounted -pub trait Mountable { - fn mount(&self, app: &HirolaApp) -> Dom; +#[derive(Debug, Clone)] +pub struct App { + router: Router, + state: S, } -pub type Dom = TemplateResult; +/// The main application struct for the frontend app. +/// +/// This struct represents the core of the frontend application and holds the application state +/// as well as the routing information. It is parameterized over the state type `S`, which should +/// be clone-able and 'static to ensure proper lifetime management. The `App` struct is created +/// using the `new` method, which takes an initial state `S` and returns a new instance of the `App`. +/// +/// # Example +/// ```no_run +/// use hirola::prelude::*; +/// #[derive(Clone)] +/// struct AppState { +/// // ... fields and methods for your application state ... +/// } +/// +/// fn main() { +/// let initial_state = AppState { /* ... */ }; +/// let app = App::new(initial_state); +/// } +/// ``` +impl App { + /// Creates a new instance of the App with the given initial state. + /// + /// # Arguments + /// + /// * `state` - The initial state for the application. + /// + /// # Returns + /// + /// A new instance of `App`. + pub fn new(state: S) -> Self { + Self { + state, + router: Router::new(), + } + } -#[cfg(feature = "ssr")] -pub type DomType = SsrNode; + /// Get a reference to the current application state. + /// + /// # Returns + /// + /// A reference to the application state of type `S`. + pub fn state(&self) -> &S { + &self.state + } -#[cfg(not(feature = "ssr"))] -pub type DomType = DomNode; + /// Get a reference to the router associated with the application. + /// + /// # Returns + /// + /// A reference to the `Router` instance responsible for handling routing in the app. + pub fn router(&self) -> &Router { + &self.router + } -impl Mountable for F -where - F: Fn(&HirolaApp) -> Dom, -{ - fn mount(&self, app: &HirolaApp) -> Dom { - self(app) + /// Add a new route to the application. + /// + /// # Arguments + /// + /// * `path` - The path for the new route, a string representing the route pattern. + /// * `page` - A function that takes a reference to the `App` and returns a `Dom` element. + /// + /// # Example + /// ```no_run + /// use hirola::prelude::*; + /// #[derive(Clone)] + /// struct AppState { + /// // ... fields and methods for your application state ... + /// } + /// + /// fn home_page(app: &App) -> Dom { + /// html! {

"Home"

} + /// } + /// + /// fn about_page(app: &App) -> Dom { + /// html! {

"About"

} + /// } + /// + /// let mut app = App::new(AppState { /* ... */ }); + /// app.route("/", home_page); + /// app.route("/about", about_page); + /// ``` + pub fn route(&mut self, path: &str, page: fn(&Self) -> Dom) { + self.router.handler.insert(path.to_string(), page).unwrap(); } -} -impl HirolaApp { - /// Create a new app - pub fn new() -> Self { - #[cfg(feature = "global-state")] - #[cfg_attr(docsrs, doc(cfg(feature = "global-state")))] - let extensions = ExtensionMap::new(); - HirolaApp { - #[cfg(feature = "global-state")] - #[cfg_attr(docsrs, doc(cfg(feature = "global-state")))] - extensions, - } + /// Set the not-found page for the application. + /// + /// This page will be displayed when the requested route does not match any registered routes. + /// + /// # Arguments + /// + /// * `page` - A function that takes a reference to the `App` and returns a `Dom` element. + /// + /// # Example + /// ```no_run + /// use hirola::prelude::*; + /// + /// #[derive(Clone)] + /// struct AppState { + /// // ... fields and methods for your application state ... + /// } + /// + /// fn not_found_page(app: &App) -> Dom { + /// html! {

"Not Found"

} + /// } + /// + /// let mut app = App::new(AppState { /* ... */ }); + /// app.set_not_found(not_found_page); + /// ``` + pub fn set_not_found(&mut self, page: fn(&Self) -> Dom) { + self.router.set_not_found(page); } +} - /// Fetch global data - #[cfg(feature = "global-state")] - #[cfg_attr(docsrs, doc(cfg(feature = "global-state")))] - pub fn data(&self) -> Option<&T> - where - T: Clone + 'static, - { - self.extensions.get::() +#[cfg(feature = "dom")] +impl App { + /// Mounts the application on the web page body and starts the rendering process. + /// + /// This method should be called after setting up all the routes and configuring the application. + /// It mounts the application on the web page body, rendering the appropriate page based on the + /// current route. The rendering process will be managed by the `Router` associated with the app. + /// + /// # Panics + /// + /// This method will panic if it fails to access the `window` or `document` objects from the + /// `web_sys` module. Make sure to run the application in a browser environment with WebAssembly + /// support to avoid panics. + /// + /// # Example + /// + /// ```no_run + /// fn main() { + /// use hirola::prelude::*; + /// + /// #[derive(Clone)] + /// struct AppState { + /// // ... fields and methods for your application state ... + /// } + /// let initial_state = AppState { /* ... */ }; + /// let app = App::new(initial_state); + /// + /// // ... add routes and set up the app ... + /// + /// // Mount the app on the web page body and start rendering + /// app.mount(); + /// } + /// ``` + pub fn mount(&self) { + let window = web_sys::window().unwrap(); + let document = window.document().unwrap(); + let router = self.router.clone(); + let dom = router.render( + &self, + &crate::generic_node::DomNode { + node: document.body().unwrap().into(), + }, + ); + // We leak the root node to avoid callbacks and futures being dropped + std::mem::forget(dom); } - /// Render a view - #[cfg(not(feature = "ssr"))] - pub fn mount(self, element: &web_sys::Node, view: M) { - render_to(|| view.mount(&self), element); + /// Mounts the application on a specified parent node and starts the rendering process. + /// + /// This method should be called after setting up all the routes and configuring the application. + /// It mounts the application on the provided parent node, rendering the appropriate page based on + /// the current route. The rendering process will be managed by the `Router` associated with the app. + /// + /// # Arguments + /// + /// * `parent` - The web_sys::Node to which the application should be mounted. + /// + /// # Example + /// + /// ```no_run + /// fn main() { + /// use hirola::prelude::*; + /// #[derive(Clone)] + /// struct AppState { + /// // ... fields and methods for your application state ... + /// } + /// let app = App::new(AppState { }); + /// // ... add routes and set up the app ... + /// + /// // Find the parent node where the app should be mounted + /// let parent_node = web_sys::window() + /// .unwrap() + /// .document() + /// .unwrap() + /// .get_element_by_id("app-container") + /// .unwrap(); + /// + /// // Mount the app on the specified parent node and start rendering + /// app.mount_to(&parent_node); + /// } + /// ``` + pub fn mount_to(&self, parent: &web_sys::Node) { + let router = self.router.clone(); + let dom = router.render( + &self, + &crate::generic_node::DomNode { + node: parent.clone(), + }, + ); + // We leak the root node to avoid callbacks and futures being dropped + std::mem::forget(dom); } - /// Extend global data - #[cfg(feature = "global-state")] - #[cfg_attr(docsrs, doc(cfg(feature = "global-state")))] - pub fn extend(&mut self, extension: T) { - self.extensions.insert(extension); + /// Mounts the application on a specified parent node and starts the rendering process. + /// + /// This method should be called after setting up all the routes and configuring the application. + /// It mounts the application on the provided parent node, rendering the appropriate page based on + /// the current route. The rendering process will be managed by the `Router` associated with the app. + /// + /// # Arguments + /// + /// * `parent` - The `web_sys::Node` to which the application should be mounted. + /// * `cb` - A callback function that takes the generated `Dom` element representing the rendered + /// content as input and returns a modified `Dom` element. This callback can be used to + /// wrap the rendered content with layout components or apply any additional transformations. + /// + /// # Example + /// + /// ```no_run + /// fn main() { + /// use hirola::prelude::*; + /// #[derive(Clone)] + /// struct AppState { + /// // ... fields and methods for your application state ... + /// } + /// let app = App::new(AppState { }); + /// // ... add routes and set up the app ... + /// + /// // Find the parent node where the app should be mounted + /// let parent_node = web_sys::window() + /// .unwrap() + /// .document() + /// .unwrap() + /// .get_element_by_id("app-container") + /// .unwrap(); + /// + /// // Mount the app on the specified parent node and start rendering + /// // In this example, we wrap the rendered content with a layout component + /// app.mount_with(&parent_node, |app| { + /// let router = app.router().clone(); + /// let inner = router.render(app, &DomType { + /// node: parent_node.clone().into() + /// }); + /// html! { + ///
+ /// + ///
+ /// {inner} + ///
+ ///
+ /// } + /// }); + /// } + /// ``` + pub fn mount_with(&self, parent: &web_sys::Node, cb: impl Fn(&Self) -> Dom) { + let res = cb(self); + parent.append_child(&res.node().inner_element()).unwrap(); + // We leak the root node to avoid callbacks and futures being dropped + std::mem::forget(res); } +} - #[cfg(feature = "ssr")] - pub fn render_to_string( - &self, - dom: impl FnOnce(&HirolaApp) -> TemplateResult, - ) -> String { - render_to_string(|| dom(self)) +#[cfg(feature = "ssr")] +impl App { + /// Renders the application to a string representation based on the specified route path. + /// + /// This method is useful for server-side rendering (SSR) scenarios where you want to generate + /// the initial HTML content on the server and send it to the client. It renders the application + /// for the provided route path and returns the result as a string. + /// + /// # Arguments + /// + /// * `path` - The path for the route to render, a string representing the route pattern. + /// + /// # Returns + /// + /// A string containing the HTML representation of the rendered content. + /// + /// # Example + /// + /// ```no_run + /// fn main() { + /// use hirola::prelude::*; + /// + /// #[derive(Clone)] + /// struct AppState { + /// // ... fields and methods for your application state ... + /// } + /// + /// // ... add routes and set up the app ... + /// + /// // Render the application for the "/about" route and get the result as a string + /// let rendered_html = app.render_to_string("/about"); + /// + /// // ... send `rendered_html` to the client for server-side rendering ... + /// } + /// ``` + pub fn render_to_string(&self, path: &str) -> String { + use crate::generic_node::GenericNode; + let fragment = crate::generic_node::SsrNode::fragment(); + let router = self.router().clone(); + // Set the path + router.push(path); + // Render path to fragment + router.render(self, &fragment); + format!("{fragment}") } } diff --git a/crates/hirola-core/src/app/router.rs b/crates/hirola-core/src/app/router.rs new file mode 100644 index 0000000..4a70245 --- /dev/null +++ b/crates/hirola-core/src/app/router.rs @@ -0,0 +1,472 @@ +use crate::{dom::Dom, prelude::*}; +use futures_signals::signal::{Mutable, MutableSignalCloned, SignalExt}; +use std::collections::HashMap; +use std::fmt; +#[cfg(feature = "dom")] +use wasm_bindgen::{prelude::Closure, JsCast, JsValue}; +#[cfg(feature = "dom")] +use web_sys::{Element, Event}; + +/// Router struct for handling routing in the frontend application. +/// +/// This struct manages the routing functionality for the frontend application. It keeps track of +/// the current route, the registered route handlers, and the not-found page handler. The `Router` +/// is parameterized over the state type `S`, which allows it to interact with the `App` state when +/// handling routes. +/// +/// # Example +/// +/// ```no_run +/// use hirola::prelude::*; +/// #[derive(Clone)] +/// struct AppState { +/// // ... fields and methods for your application state ... +/// } +/// +/// fn home_page(app: &App) -> Dom { +/// html! {

"Home"

} +/// } +/// +/// fn about_page(app: &App) -> Dom { +/// html! {

"Home"

} +/// } +/// +/// let mut app = App::new(AppState { /* ... */ }); +/// app.route("/", home_page); +/// app.route("/about", about_page); +/// app.mount(); +/// ``` +#[derive(Clone)] +pub struct Router { + current: Mutable, + /// The internal router used to map route paths to corresponding route handler functions. + pub(crate) handler: matchit::Router) -> Dom>, + /// The function that will be executed when the requested route does not match any registered routes. + pub(crate) not_found: Box) -> Dom>, +} + +impl fmt::Debug for Router { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Router") + .field("current", &self.current) + .field( + "handler", + &format_args!("matchit::Router) -> Dom>"), + ) + .finish() + } +} +impl Router { + /// Creates a new instance of the Router with default settings. + /// + /// The `Router` manages the routing functionality for the frontend application. This method + /// creates a new instance of the `Router` with an empty route handler and a default not-found + /// page handler. + /// + /// # Returns + /// + /// A new instance of `Router`. + /// + /// # Example + /// + /// ```no_run + /// use hirola::prelude::*; + /// use hirola::prelude::router::Router; + /// let router = Router::<()>::new(); + /// ``` + pub fn new() -> Self { + #[allow(unused_mut)] + let mut path = String::from("/"); + #[cfg(feature = "dom")] + if let Some(window) = web_sys::window() { + path = window.location().pathname().unwrap_or("/".to_string()); + } + Router { + current: Mutable::new(path), + handler: Default::default(), + not_found: Box::new(|_| Dom::text("Not Found")), + } + } + + /// Retrieves the current parameters from the current route. + /// + /// This method returns a HashMap containing the parameters parsed from the current route + /// URL. The parameters are extracted from the route's path segments based on the route pattern + /// defined during registration. + /// + /// # Returns + /// + /// A HashMap with parameter names as keys and their corresponding values as values. + /// + /// # Example + /// + /// ```no_run + /// use hirola::prelude::router::Router; + /// let router = Router::<()>::new(); + /// let params = router.current_params(); + /// ``` + pub fn current_params(&self) -> HashMap { + let path = self.current.get_cloned(); + let binding = &self.handler; + match binding.at(&path) { + Ok(inner) => { + let params = &inner.params.clone(); + let params = params.iter().fold(HashMap::new(), |mut map, c| { + map.insert(c.0.to_string(), c.1.to_string()); + map + }); + params + } + Err(_) => HashMap::new(), + } + } + + /// Navigates to the specified route path. + /// + /// This method updates the current route to the provided `path`. It will trigger the + /// rendering process for the new route and update the application's UI accordingly. + /// + /// # Arguments + /// + /// * `path` - The path for the route to navigate to, a string representing the route pattern. + /// + /// # Example + /// + /// ```no_run + /// use hirola::prelude::router::Router; + /// let router = Router::<()>::new(); + /// router.push("/about"); + pub fn push(&self, path: &str) { + #[cfg(feature = "dom")] + let window = web_sys::window().unwrap(); + #[cfg(feature = "dom")] + window + .history() + .unwrap() + .push_state_with_url(&JsValue::default(), "", Some(&path)) + .unwrap(); + self.current.set(path.to_owned()); + } + + /// Generates a link handler function that can be used to navigate to a specific route. + /// + /// This method returns a boxed closure that takes a reference to a DOM element (`Dom`) and + /// updates the current route to the specified path when triggered. It can be used to create + /// link handlers for HTML elements, such as anchors (``), buttons, or custom elements, + /// allowing users to navigate to different routes within the frontend application. + /// + /// # Returns + /// + /// A boxed closure that can be attached as a mixin for a DOM element. + /// + /// # Example + /// + /// ```no_run + /// use hirola::prelude::router::Router; + /// let router = Router::<()>::new(); + /// let link_handler = router.link(); + /// + /// // ... attach `link_handler` as an event handler to an anchor or button element ... + /// ``` + pub fn link(&self) -> Box () + '_> { + #[cfg(feature = "dom")] + let router = self.clone(); + #[allow(unused_variables)] + let cb = move |node: &Dom| { + #[cfg(feature = "dom")] + let router = router.clone(); + #[cfg(feature = "dom")] + let handle_click = Box::new(move |e: Event| { + e.prevent_default(); + let element = e.current_target().unwrap().dyn_into::().unwrap(); + let href = element.get_attribute("href").unwrap(); + router.push(&href); + }) as Box; + #[cfg(feature = "dom")] + node.event("click", handle_click); + }; + Box::new(cb) + } + + /// Retrieves a signal for listening to route changes. + /// + /// This method returns a `MutableSignalCloned` that can be used to listen for changes + /// to the current route. It allows you to observe route changes and perform additional actions + /// or updates in response to route navigation. + /// + /// # Returns + /// + /// A `MutableSignalCloned` that represents the signal for route changes. + /// + /// # Example + /// + /// ```no_run + /// use hirola::prelude::router::Router; + /// let router = Router::<()>::new(); + /// let signal = router.signal(); + /// + /// // ... use the `signal` to listen for route changes ... + /// ``` + pub fn signal(&self) -> MutableSignalCloned { + self.current.signal_cloned() + } + + /// Renders the appropriate content for the current route and appends it to the specified parent. + /// + /// This method is used internally to render the content associated with the current route and + /// append it as a child to the provided `parent` DOM element (`DomType`). It is automatically + /// called by the `mount` and `mount_to` methods of the `App` struct when mounting the frontend + /// application. + /// + /// # Arguments + /// + /// * `app` - A reference to the `App` instance to access the application state. + /// * `parent` - A reference to the parent DOM element (`DomType`) where the content should be appended. + /// + /// # Returns + /// + /// A `Dom` element representing the rendered content for the current route. + /// + /// # Example + /// + /// ```no_run + /// use hirola::prelude::router::Router; + /// use hirola::prelude::*; + /// #[derive(Clone)] + /// struct AppState { + /// // ... fields and methods for your application state ... + /// } + /// + /// fn home_page(app: &App) -> Dom { + /// html! {

"Home"

} + /// } + /// + /// fn about_page(app: &App) -> Dom { + /// html! {

"About"

} + /// } + /// + /// let mut app = App::new(AppState { /* ... */ }); + /// app.route("/", home_page); + /// app.route("/about", about_page); + /// let router = app.router().clone(); + /// let doc = web_sys::window().unwrap().document().unwrap(); + /// router.render(&app, &DomType::fragment()); + /// ``` + pub fn render(self, app: &App, parent: &DomType) -> Dom { + let router = &self.handler; + #[cfg(feature = "dom")] + let current = self.current.clone(); + #[cfg(feature = "dom")] + //Hash routing forward in history and URL rewrite + let handle_hash = Closure::wrap(Box::new(move |_evt: web_sys::Event| { + let l: String = web_sys::window() + .unwrap() + .location() + .hash() + .unwrap() + .chars() + .skip(1) + .collect(); + + log::debug!("hash handle : {l}"); + + let h = web_sys::window().unwrap().history().unwrap(); + h.replace_state_with_url(&JsValue::NULL, "", Some(l.as_str())) + .unwrap(); + + current.set(l.to_string()); + }) as Box); + #[cfg(feature = "dom")] + web_sys::window() + .unwrap() + .set_onhashchange(Some(handle_hash.as_ref().unchecked_ref())); + #[cfg(feature = "dom")] + handle_hash.forget(); + + #[cfg(feature = "dom")] + let current = self.current.clone(); + //Routing for navigating in history and escaping hash routes + #[cfg(feature = "dom")] + let handle_pop = Closure::wrap(Box::new(move |_evt: web_sys::Event| { + let path_name = web_sys::window().unwrap().location().pathname().unwrap(); + + if web_sys::window() + .unwrap() + .location() + .hash() + .unwrap() + .chars() + .count() + > 0 + { + log::debug!("hash detected"); + return (); + } + current.set(path_name.to_string()); + log::debug!("pop handle : {path_name}"); + }) as Box); + + #[cfg(feature = "dom")] + web_sys::window() + .unwrap() + .set_onpopstate(Some(handle_pop.as_ref().unchecked_ref())); + + #[cfg(feature = "dom")] + handle_pop.forget(); + let route = &self.current.clone(); + + let path = route.get_cloned(); + let match_result = router.at(&path); + let page_fn = match match_result { + Ok(v) => v.value, + Err(_) => &self.not_found, + }; + + let builder = page_fn(&app); + let dom = builder.mount(&parent).unwrap(); + + let router = router.clone(); + let app = app.clone(); + let node = parent.clone(); + let not_found = self.not_found.clone(); + let wait_for_next_route = route + .signal_cloned() + .map(move |route_match| { + let match_result = router.at(&route_match); + let page_fn = match match_result { + Ok(v) => v.value, + Err(_) => ¬_found, + }; + + let builder = page_fn(&app); + let dom = builder.mount(&DomType::fragment()).unwrap(); + node.replace_children_with(&dom.node()); + #[cfg(feature = "dom")] + let window = web_sys::window().unwrap(); + #[cfg(feature = "dom")] + window + .history() + .unwrap() + .push_state_with_url(&JsValue::default(), "", Some(&route_match)) + .unwrap(); + log::debug!("Router received new path: {route_match}"); + }) + .to_future(); + dom.effect(wait_for_next_route); + dom + } + + /// Inserts a new route and its corresponding page rendering function into the router. + /// + /// This method registers a new route pattern and its associated page rendering function in the router. + /// When a user navigates to the specified `path`, the corresponding `page` function will be called + /// to render the content for that route. + /// + /// # Arguments + /// + /// * `path` - A string representing the route pattern to match. This can include path parameters + /// enclosed in curly braces, e.g., "/users/{id}". + /// * `page` - A function that takes a reference to the `App` instance and returns the rendered + /// DOM content (`Dom`). This function is responsible for generating the DOM structure + /// for the specified route. + /// + /// # Panics + /// + /// If the insertion into the router fails, this method will panic. However, in most cases, such a + /// scenario is unlikely if there are no conflicts with existing routes or issues with the provided + /// page rendering function. + /// + /// # Example + /// + /// ```no_run + /// use hirola::prelude::router::Router; + /// use hirola::prelude::*; + /// + /// // Define a custom function to render the home page + /// fn home_page(_: &App<()>) -> Dom { + /// html! { + ///

"Home"

+ /// } + /// } + /// + /// // Create a new router and add a route for the home page + /// let mut router = Router::<()>::new(); + /// router.insert("/", home_page); + /// ``` + pub fn insert(&mut self, path: &str, page: fn(&App) -> Dom) { + self.handler.insert(path.to_string(), page).unwrap(); + } + + /// Sets the page rendering function for the not-found route. + /// + /// This method sets the page rendering function for the not-found route. When a user navigates to + /// a route that does not match any registered paths in the router, the specified `page` function + /// will be called to render the content for the not-found page. + /// + /// # Arguments + /// + /// * `page` - A function that takes a reference to the `App` instance and returns the rendered + /// DOM content (`Dom`). This function is responsible for generating the DOM structure + /// for the not-found page. + /// + /// # Example + /// + /// ```no_run + /// use hirola::prelude::router::Router; + /// use hirola::prelude::*; + /// // Define a custom function to render the not-found page + /// fn not_found_page(_: &App<()>) -> Dom { + /// html! { + ///

"Not Found"

+ /// } + /// } + /// + /// // Create a new router and set the not-found page + /// let mut router = Router::<()>::new(); + /// router.set_not_found(not_found_page); + /// ``` + pub fn set_not_found(&mut self, page: fn(&App) -> Dom) { + self.not_found = Box::new(page); + } + + /// Retrieves a clone of the route handler from the router. + /// + /// This method returns a clone of the route handler, which contains all the registered routes + /// and their corresponding page rendering functions. The route handler is a part of the internal + /// state of the router. + /// + /// # Returns + /// + /// A clone of the route handler, which is an instance of `matchit::Router) -> Dom>`. + /// + /// # Example + /// + /// ```no_run + /// use hirola::prelude::router::Router; + /// use hirola::prelude::*; + /// + /// // Define custom functions to render the home and about pages + /// fn home_page(_: &App<()>) -> Dom { + /// html! { + ///

"Home"

+ /// } + /// } + /// + /// fn about_page(_: &App<()>) -> Dom { + /// html! { + ///

"About"

+ /// } + /// } + /// + /// // Create a new router and add routes for the home and about pages + /// let mut router = Router::new(); + /// router.insert("/", home_page); + /// router.insert("/about", about_page); + /// + /// // Get a clone of the route handler + /// let cloned_handler = router.handler(); + /// ``` + pub fn handler(&self) -> matchit::Router) -> Dom> { + self.handler.clone() + } +} diff --git a/crates/hirola-core/src/callback.rs b/crates/hirola-core/src/callback.rs index d76e699..c6fc3cb 100644 --- a/crates/hirola-core/src/callback.rs +++ b/crates/hirola-core/src/callback.rs @@ -1,44 +1,61 @@ -use crate::prelude::DomNode; +use futures_signals::{signal::Mutable, signal_vec::MutableVec}; +use web_sys::Event; -pub trait StateReduce { - fn mut_callback(&self, f: F) -> Box +pub trait Callback { + /// Pass a callback that allows interacting with the inner value and the dom event + /// This method returns the new value and this updates the signal. + fn callback_with(&self, f: F) -> Box where - F: Fn(&T, E) -> T + 'static; + F: Fn(&Self, Event) + 'static; + /// Pass a callback that allows interacting with self and the dom event + fn callback(&self, f: F) -> Box + where + F: Fn(&Self) + 'static, + Self: Sized; } -use thiserror::Error; - -#[derive(Error, Debug)] -pub enum MixinError { - #[error("Invalid namespace (expected {expected:?}, got {found:?})")] - InvalidNamespace { expected: String, found: String }, - #[error("Could not bind mixin to Node: {0:?}")] - NodeError(DomNode), -} +impl Callback for Mutable { + fn callback(&self, f: F) -> Box + where + F: Fn(&Self) + 'static, + { + let state = self.clone(); + let cb = move |_| { + f(&state); + }; + Box::new(cb) + } -pub trait Mixin { - fn mixin(&self, namespace: &str, node: DomNode) -> Result<(), MixinError>; + fn callback_with(&self, f: F) -> Box + where + F: Fn(&Self, Event) + 'static, + { + let state = self.clone(); + let cb = move |e| { + f(&state, e); + }; + Box::new(cb) + } } -impl Mixin for T -where - T: Fn(DomNode), -{ - fn mixin(&self, _ns: &str, node: DomNode) -> Result<(), MixinError> { - (&self)(node); - Ok(()) +impl Callback for MutableVec { + fn callback(&self, f: F) -> Box + where + F: Fn(&Self) + 'static, + { + let state = self.clone(); + let cb = move |_| { + (f(&state)); + }; + Box::new(cb) } -} -pub trait State: Clone { - // Get a callback that allows interacting with state - fn callback(&self, f: F) -> Box + fn callback_with(&self, f: F) -> Box where - F: Fn(&Self, E) + 'static, - Self: 'static, + F: Fn(&Self, Event) + 'static, { let state = self.clone(); - let cb = move |e: E| { + let cb = move |e: Event| { f(&state, e); }; Box::new(cb) diff --git a/crates/hirola-core/src/dom.rs b/crates/hirola-core/src/dom.rs new file mode 100644 index 0000000..653f13f --- /dev/null +++ b/crates/hirola-core/src/dom.rs @@ -0,0 +1,160 @@ +use crate::{ + generic_node::{DomType, GenericNode}, + render::{Error, Render}, + spawn, BoxedLocal, +}; +use discard::{Discard, DiscardOnDrop}; +use futures_signals::CancelableFutureHandle; +use std::{cell::RefCell, future::Future, rc::Rc}; +#[cfg(feature = "dom")] +use wasm_bindgen::JsCast; +#[cfg(feature = "dom")] +use web_sys::HtmlElement; + +#[cfg(feature = "dom")] +use wasm_bindgen::prelude::Closure; + +#[cfg(feature = "dom")] +use crate::generic_node::EventListener; + +pub enum DomSideEffect { + UnMounted(BoxedLocal<()>), + Mounted(CancelableFutureHandle), +} + +#[derive(Clone)] +pub struct Dom { + node: DomType, + pub side_effects: Rc>>, + #[cfg(feature = "dom")] + event_handlers: Rc>>>, + children: RefCell>, +} + +impl Dom { + pub fn new() -> Dom { + Dom::new_from_node(&DomType::fragment()) + } + + pub fn element(tag: &str) -> Dom { + Dom::new_from_node(&DomType::element(tag)) + } + + pub fn text(tag: &str) -> Dom { + Dom::new_from_node(&DomType::text_node(tag)) + } + + pub fn append_child(&self, child: Dom) -> Result<(), Error> { + self.node.append_child(&child.node); + self.children.borrow_mut().push(child); + Ok(()) + } + + pub fn children(&self) -> &RefCell> { + &self.children + } + + pub fn node(&self) -> &DomType { + &self.node + } + + pub fn new_from_node(node: &DomType) -> Dom { + Self { + node: node.clone(), + children: Default::default(), + #[cfg(feature = "dom")] + event_handlers: Default::default(), + side_effects: Default::default(), + } + } + + #[cfg(feature = "dom")] + #[inline] + pub fn event(&self, name: &str, handler: Box) { + let closure = self.node.event(name, handler); + if let Some(closure) = closure { + self.event_handlers.borrow_mut().push(closure); + } + } + + #[inline] + pub fn attribute(&self, name: &str, value: &str) { + self.node.set_attribute(name, value); + } + #[inline] + pub fn effect(&self, future: impl Future + 'static) { + self.side_effects + .borrow_mut() + .push(DomSideEffect::Mounted(DiscardOnDrop::leak(spawn(future)))); + } + + pub fn append_render(&self, render: impl Render + 'static) { + Box::new(render).render_into(&self).unwrap(); + } + + #[inline] + pub fn discard(&mut self) { + #[cfg(feature = "dom")] + { + let _cleanup: Vec<()> = self + .event_handlers + .take() + .into_iter() + .map(|c| c.forget()) + .collect(); + } + let _cleanup: Vec<()> = self + .side_effects + .take() + .into_iter() + .map(|e| match e { + DomSideEffect::Mounted(e) => e.discard(), + DomSideEffect::UnMounted(_) => { + log::warn!("Dropping a side effect that was not mounted") + } + }) + .collect(); + } + + pub fn mount(self, node: &DomType) -> Result { + let dom = Dom::new_from_node(node); + Box::new(self).render_into(&dom)?; + Ok(dom) + } + + pub fn inner_html(&self) -> String { + #[cfg(feature = "dom")] + { + let window = web_sys::window().unwrap(); + let document = window.document().unwrap(); + let element = document.create_element("div").unwrap(); + + let dom = crate::render_to(self.clone(), &element.try_into().unwrap()).unwrap(); + return dom + .node() + .inner_element() + .dyn_ref::() + .unwrap() + .inner_html(); + } + + #[cfg(feature = "ssr")] + #[allow(unreachable_code)] + { + return crate::render_to_string(self.clone()); + } + } +} + +impl Drop for Dom { + fn drop(&mut self) { + self.discard() + } +} + +impl Render for Dom { + fn render_into(self: Box, parent: &Dom) -> Result<(), Error> { + parent.append_child(*self)?; + Ok(()) + } +} diff --git a/crates/hirola-core/src/easing.rs b/crates/hirola-core/src/easing.rs deleted file mode 100644 index 8955050..0000000 --- a/crates/hirola-core/src/easing.rs +++ /dev/null @@ -1,188 +0,0 @@ -//! Easing functions. - -use core::f32; -use std::f32::consts::PI; - -const EXP_BASE: f32 = 2.; -const BOUNCE_GRAVITY: f32 = 2.75; -const BOUNCE_AMPLITUDE: f32 = 7.5625; - -// Linear - -pub fn linear(t: f32) -> f32 { - t -} - -// Quadratic - -pub fn quad_in(t: f32) -> f32 { - t * t -} - -pub fn quad_out(t: f32) -> f32 { - -t * (t - 2.0) -} - -pub fn quad_inout(t: f32) -> f32 { - if t < 0.5 { - 2.0 * t * t - } else { - -2.0 * t * t + 4.0 * t - 1.0 - } -} - -// Cubic - -pub fn cubic_in(t: f32) -> f32 { - t * t * t -} - -pub fn cubic_out(t: f32) -> f32 { - let f = t - 1.0; - f * f * f + 1.0 -} - -pub fn cubic_inout(t: f32) -> f32 { - if t < 0.5 { - 4.0 * t * t * t - } else { - let f = 2.0 * t - 2.0; - 0.5 * f * f * f + 1.0 - } -} - -// Quartic - -pub fn quart_in(t: f32) -> f32 { - t * t * t * t -} - -pub fn quart_out(t: f32) -> f32 { - let f = t - 1.0; - f * f * f * (1.0 - t) + 1.0 -} - -pub fn quart_inout(t: f32) -> f32 { - if t < 0.5 { - 8.0 * t * t * t * t - } else { - let f = t - 1.0; - -8.0 * f * f * f * f + 1.0 - } -} - -// Quintic - -pub fn quint_in(t: f32) -> f32 { - t * t * t * t * t -} - -pub fn quint_out(t: f32) -> f32 { - let f = t - 1.0; - f * f * f * f * f + 1.0 -} - -pub fn quint_inout(t: f32) -> f32 { - if t < 0.5 { - 16.0 * t * t * t * t * t - } else { - let f = (2.0 * t) - 2.0; - 0.5 * f * f * f * f * f + 1.0 - } -} - -// Circular - -pub fn circ_in(t: f32) -> f32 { - 1.0 - f32::sqrt(1.0 - f32::powi(t, 2)) -} - -pub fn circ_out(t: f32) -> f32 { - f32::sqrt(1.0 - f32::powi(t - 1.0, 2).powi(2)) -} - -pub fn circ_inout(t: f32) -> f32 { - if t < 0.5 { - (1.0 - f32::sqrt(1.0 - f32::powi(2.0 * t, 2))) / 2.0 - } else { - (f32::sqrt(1.0 - f32::powi(-2.0 * t + 2.0, 2)) + 1.0) / 2.0 - } -} - -// Exponential - -pub fn expo_in(t: f32) -> f32 { - if t.abs() <= f32::EPSILON { - 0. - } else { - EXP_BASE.powf(10. * t - 10.) - } -} - -pub fn expo_out(t: f32) -> f32 { - if (t - 1.0).abs() <= f32::EPSILON { - 0. - } else { - 1.0 - EXP_BASE.powf(-10. * t) - } -} - -pub fn expo_inout(t: f32) -> f32 { - if t.abs() <= f32::EPSILON { - 0. - } else if (t - 1.0) <= f32::EPSILON { - 1. - } else if t <= 0.5 { - f32::powf(EXP_BASE, 20. * t - 10.) / 2.0 - } else { - 1.0 + f32::powf(EXP_BASE, -20. * t + 10.) / -2.0 - } -} - -// Sine - -pub fn sine_in(t: f32) -> f32 { - f32::cos(1.0 - (t * PI / 2.0)) -} - -pub fn sine_out(t: f32) -> f32 { - f32::sin(t * PI / 2.0) -} - -pub fn sine_inout(t: f32) -> f32 { - -(f32::cos(PI * t) - 1.0) / 2.0 -} - -// Bounce - -pub fn bounce_in(t: f32) -> f32 { - 1.0 - bounce_out(1.0 - t) -} - -pub fn bounce_out(t: f32) -> f32 { - // TODO: Refactor? Code seems like a repetition. - // Further, it is unclear why the numbers here are - // picked. - if t < 1.0 / BOUNCE_GRAVITY { - BOUNCE_AMPLITUDE * t * t - } else if t < 2.0 / BOUNCE_GRAVITY { - let t = t - 1.5 / BOUNCE_GRAVITY; - BOUNCE_AMPLITUDE * t * t + 0.75 - } else if t < 2.5 / BOUNCE_GRAVITY { - let t = t - 2.25 / BOUNCE_GRAVITY; - BOUNCE_AMPLITUDE * t * t + 0.9375 - } else { - let t = t - 2.625 / BOUNCE_GRAVITY; - BOUNCE_AMPLITUDE * t * t + 0.984375 - } -} - -pub fn bounce_inout(t: f32) -> f32 { - if t < 0.5 { - (1.0 - bounce_out(1.0 - 2.0 * t)) / 2.0 - } else { - (1.0 + bounce_out(-1.0 + 2.0 * t)) / 2.0 - } -} - -// TODO: add more easing functions diff --git a/crates/hirola-core/src/effect.rs b/crates/hirola-core/src/effect.rs new file mode 100644 index 0000000..1225c9b --- /dev/null +++ b/crates/hirola-core/src/effect.rs @@ -0,0 +1,88 @@ +use std::future::Future; + +use crate::BoxedLocal; + +/// Trait for defining side effects that execute asynchronously as futures. +/// +/// The `SideEffect` trait allows defining asynchronous side effects that are executed as futures. +/// Implementations of this trait should represent tasks that need to be performed concurrently +/// with the rendering process, such as making HTTP requests, updating global state, or scheduling +/// timers. +/// +/// When used in conjunction with the `Dom`, side effects can be attached to specific DOM nodes and +/// executed during the rendering process, ensuring proper handling of asynchronous operations +/// within the frontend application. +/// +/// # Example +/// +/// ``` +/// use std::future::ready; +/// use hirola::prelude::*; +/// // Define a custom side effect that executes asynchronously +/// struct CustomSideEffect; +/// +/// impl SideEffect for CustomSideEffect { +/// fn effect(self) -> BoxedLocal<()> { +/// // Perform some asynchronous task and return a future that represents its completion +/// Box::pin(ready(())) +/// } +/// } +/// ``` +pub trait SideEffect { + /// Executes the side effect and returns a boxed future representing its completion. + /// + /// This method executes the side effect asynchronously and returns a boxed future that + /// represents the completion of the task. Implementations should ensure that the future's + /// output is `()`, indicating the task's successful completion. + /// + /// # Returns + /// + /// A boxed future that represents the completion of the side effect task. + /// + /// # Example + /// + /// ``` + /// use std::future::ready; + /// use hirola::prelude::*; + /// // Define a custom side effect that executes asynchronously + /// struct CustomSideEffect; + /// + /// impl SideEffect for CustomSideEffect { + /// fn effect(self) -> BoxedLocal<()> { + /// // Perform some asynchronous task and return a future that represents its completion + /// Box::pin(ready(())) + /// } + /// } + /// ``` + fn effect(self) -> BoxedLocal<()>; +} + +impl SideEffect for F +where + F: Future, +{ + /// Converts the provided future into a boxed future of `()` as a side effect. + /// + /// This implementation allows any future that produces `()` as its output to be converted + /// into a `BoxedLocal<()>` to fulfill the requirements of the `SideEffect` trait. + /// + /// # Returns + /// + /// A boxed future that represents the completion of the provided future task. + /// + /// # Example + /// + /// ```no_run + /// use std::future::ready; + /// use hirola::prelude::*; + /// // Create a future that produces `()` as its output + /// let my_future = ready(()); + /// + /// let render = html! { + ///
+ /// }; + /// ``` + fn effect(self) -> BoxedLocal<()> { + Box::pin(self) + } +} diff --git a/crates/hirola-core/src/flow.rs b/crates/hirola-core/src/flow.rs deleted file mode 100644 index 83fc6e6..0000000 --- a/crates/hirola-core/src/flow.rs +++ /dev/null @@ -1,380 +0,0 @@ -//! Iteration utility components for [`dom`](crate::html). -//! -//! Iteration can be either _"keyed"_ or _"non keyed"_. -//! Use the [`Keyed`] and [`Indexed`] utility components respectively. - -use std::cell::RefCell; -use std::collections::{HashMap, HashSet}; -use std::hash::Hash; -use std::mem; -use std::rc::Rc; - -use crate::generic_node::GenericNode; -use crate::prelude::*; -use crate::reactive::Owner; - -/// Props for [`Keyed`]. -#[derive(Clone)] -pub struct KeyedProps -where - F: Fn(T) -> TemplateResult, - K: Fn(&T) -> Key, - Key: Hash + Eq, - F: Clone, - K: Clone, -{ - pub iterable: StateHandle>, - pub template: F, - pub key: K, -} - -/// Keyed iteration. Use this instead of directly rendering an array of [`TemplateResult`]s. -/// Using this will minimize re-renders instead of re-rendering every single node on every state change. -/// -/// For non keyed iteration, see [`Indexed`]. -/// -/// # Example -/// ```ignore -/// use hirola::prelude::*; -/// -/// let count = Signal::new(vec![1, 2]); -/// -/// let _node: TemplateResult = html! { -/// { item } -/// }, -/// key: |item| *item, -/// }} -/// /> -/// }; -/// ``` -//#[component] - -pub struct Keyed -where - F: Fn(T) -> TemplateResult, - K: Fn(&T) -> Key, - Key: Hash + Eq, - G: GenericNode, -{ - pub props: KeyedProps, -} - -impl< - T: 'static + Clone + PartialEq, - F: 'static + Clone, - G, - K: 'static + Clone, - Key: 'static + Clone, - > Render for Keyed -where - F: Fn(T) -> TemplateResult, - K: Fn(&T) -> Key, - Key: Hash + Eq, - G: GenericNode, -{ - fn render(&self) -> TemplateResult { - let KeyedProps { - iterable, - template, - key: key_fn, - } = self.props.clone(); - let iterable = Rc::new(iterable); - let key_fn = Rc::new(key_fn); - - type TemplateValue = (Owner, T, TemplateResult, usize /* index */); - - // A tuple with a value of type `T` and the `TemplateResult` produces by calling `props.template` with the first value. - let templates: Rc>>> = Default::default(); - - let fragment = G::fragment(); - - let marker = G::marker(); - - fragment.append_child(&marker); - - create_effect({ - let iterable = Rc::clone(&iterable); - let key_fn = Rc::clone(&key_fn); - let templates = Rc::clone(&templates); - move || { - // Fast path for empty array. Remove all nodes from DOM in templates. - if iterable.get().is_empty() { - for (_, (owner, _value, template, _i)) in templates.borrow_mut().drain() { - drop(owner); // destroy owner - template.node.remove_self(); - } - return; - } - - // Remove old nodes not in iterable. - { - let mut templates = templates.borrow_mut(); - let new_keys: HashSet = - iterable.get().iter().map(|item| key_fn(item)).collect(); - - let excess_nodes = templates - .iter() - .filter(|item| new_keys.get(item.0).is_none()) - .map(|x| (x.0.clone(), (x.1 .2.clone(), x.1 .3))) - .collect::>(); - - for node in &excess_nodes { - let removed_index = node.1 .1; - templates.remove(&node.0); - - // Offset indexes of other templates by 1. - for (_, _, _, i) in templates.values_mut() { - if *i > removed_index { - *i -= 1; - } - } - } - - for node in excess_nodes { - node.1 .0.node.remove_self(); - } - } - - struct PreviousData { - value: T, - index: usize, - } - - let previous_values: HashMap<_, PreviousData> = { - let templates = templates.borrow(); - templates - .iter() - .map(|x| { - ( - (*x.0).clone(), - PreviousData { - value: x.1 .1.clone(), - index: x.1 .3, - }, - ) - }) - .collect() - }; - - // Find values that changed by comparing to previous_values. - for (i, item) in iterable.get().iter().enumerate() { - let key = key_fn(item); - - let previous_value = previous_values.get(&key); - - if previous_value.is_none() { - // Create new DOM node. - - let mut new_template = None; - let owner = create_root(|| new_template = Some(template(item.clone()))); - - templates.borrow_mut().insert( - key.clone(), - (owner, item.clone(), new_template.clone().unwrap(), i), - ); - - if let Some(next_item) = iterable.get().get(i + 1) { - let templates = templates.borrow(); - if let Some(next_node) = templates.get(&key_fn(next_item)) { - next_node - .2 - .node - .insert_sibling_before(&new_template.unwrap().node); - } else { - marker.insert_sibling_before(&new_template.unwrap().node); - } - } else { - marker.insert_sibling_before(&new_template.unwrap().node); - } - } else if match previous_value { - Some(prev) => prev.index, - _ => unreachable!(), - } != i - { - // Location changed, move from old location to new location - // Node was moved in the DOM. Move node to new index. - - let node = templates.borrow().get(&key).unwrap().2.node.clone(); - - if let Some(next_item) = iterable.get().get(i + 1) { - let templates = templates.borrow(); - let next_node = templates.get(&key_fn(next_item)).unwrap(); - next_node.2.node.insert_sibling_before(&node); // Move to before next node - } else { - marker.insert_sibling_before(&node); // Move to end. - } - - templates.borrow_mut().get_mut(&key).unwrap().3 = i; - } else if match previous_value { - Some(prev) => &prev.value, - _ => unreachable!(), - } != item - { - // Value changed. Re-render node (with same previous key and index). - - // Destroy old template owner. - let mut templates = templates.borrow_mut(); - let (old_owner, _, _, _) = templates - .get_mut(&key) - .expect("previous value is different but must be valid"); - let old_owner = - mem::replace(old_owner, Owner::new() /* placeholder */); - drop(old_owner); - - let mut new_template = None; - let owner = create_root(|| new_template = Some(template(item.clone()))); - - let (_, _, old_node, _) = mem::replace( - templates.get_mut(&key).unwrap(), - (owner, item.clone(), new_template.clone().unwrap(), i), - ); - - let parent = old_node.node.parent_node().unwrap(); - parent.replace_child(&new_template.unwrap().node, &old_node.node); - } - } - } - }); - - TemplateResult::new(fragment) - } -} -/// Props for [`Indexed`]. -/// -#[derive(Debug)] -pub struct IndexedProps -where - F: Fn(T) -> TemplateResult, -{ - pub iterable: StateHandle>, - pub template: F, -} - -/// Non keyed iteration (or keyed by index). Use this instead of directly rendering an array of [`TemplateResult`]s. -/// Using this will minimize re-renders instead of re-rendering every single node on every state change. -/// -/// For keyed iteration, see [`Keyed`]. -/// -/// # Example -/// ```ignore -/// use hirola::prelude::*; -/// -/// let count = Signal::new(vec![1, 2]); -/// -/// let node = html! { -/// { item } -/// } -/// }} -/// /> -/// }; -/// # let _ : Dom = node; -/// ``` -// #[component] - -pub struct Indexed -where - F: Fn(T) -> TemplateResult, -{ - pub props: IndexedProps, -} - -impl Render for Indexed -where - F: Fn(T) -> TemplateResult + 'static + Clone, - G: GenericNode, - T: Clone + PartialEq + 'static, -{ - fn render(&self) -> TemplateResult { - let props = &self.props; - let template = props.template.clone(); - let iterable = props.iterable.clone(); - type TemplateData = (Owner, TemplateResult); - let templates: Rc>>> = Default::default(); - - // Previous values for diffing purposes. - let previous_values = RefCell::new(Vec::new()); - - let fragment = G::fragment(); - - let marker = G::marker(); - - fragment.append_child(&marker); - - create_effect({ - let templates = Rc::clone(&templates); - move || { - // Fast path for empty array. Remove all nodes from DOM in templates. - if iterable.get().is_empty() { - for (owner, template) in templates.borrow_mut().drain(..) { - drop(owner); // destroy owner - template.node.remove_self(); - } - return; - } - - // Find values that changed by comparing to previous_values. - for (i, item) in iterable.get().iter().enumerate() { - let previous_values = previous_values.borrow(); - let previous_value = previous_values.get(i); - - if previous_value.is_none() || previous_value.unwrap() != item { - // Value changed, re-render item. - - templates.borrow_mut().get_mut(i).and_then(|(owner, _)| { - // destroy old owner - let old_owner = - mem::replace(owner, Owner::new() /* placeholder */); - drop(old_owner); - None::<()> - }); - - let mut new_template = None; - let owner = create_root(|| new_template = Some((template)(item.clone()))); - - if templates.borrow().get(i).is_some() { - let old_node = mem::replace( - &mut templates.borrow_mut()[i], - (owner, new_template.as_ref().unwrap().clone()), - ); - - let parent = old_node.1.node.parent_node().unwrap(); - parent.replace_child(&new_template.unwrap().node, &old_node.1.node); - } else { - debug_assert!( - templates.borrow().len() == i, - "pushing new value scenario" - ); - - templates - .borrow_mut() - .push((owner, new_template.as_ref().unwrap().clone())); - - marker.insert_sibling_before(&new_template.unwrap().node); - } - } - } - - if templates.borrow().len() > iterable.get().len() { - let mut templates = templates.borrow_mut(); - let excess_nodes = templates.drain(iterable.get().len()..); - - for node in excess_nodes { - node.1.node.remove_self(); - } - } - - *previous_values.borrow_mut() = (*iterable.get()).clone(); - } - }); - - TemplateResult::new(fragment) - } -} diff --git a/crates/hirola-core/src/generic_node.rs b/crates/hirola-core/src/generic_node.rs index 0fb1813..b5a6ad7 100644 --- a/crates/hirola-core/src/generic_node.rs +++ b/crates/hirola-core/src/generic_node.rs @@ -1,5 +1,3 @@ -//! Abstraction over a rendering backend. - #[cfg(feature = "dom")] pub mod dom_node; #[cfg(feature = "ssr")] @@ -10,31 +8,26 @@ pub use dom_node::*; #[cfg(feature = "ssr")] pub use ssr_node::*; -use std::cell::RefCell; +#[cfg(feature = "dom")] +use wasm_bindgen::prelude::Closure; + use std::fmt; -use std::rc::Rc; +#[cfg(feature = "dom")] use web_sys::Event; -use crate::prelude::*; +#[cfg(feature = "ssr")] +pub type Event = (); pub type EventListener = dyn Fn(Event); -/// Abstraction over a rendering backend. -/// -/// You would probably use this trait as a trait bound when you want to accept any rendering backend. -/// For example, components are often generic over [`GenericNode`] to be able to render to different backends. -/// -/// Note that components are **NOT** represented by [`GenericNode`]. Instead, components are _disappearing_, meaning -/// that they are simply functions that generate [`GenericNode`]s inside a new reactive context. This means that there -/// is no overhead whatsoever when using components. -/// -/// Hirola ships with 2 rendering backends out of the box: -/// * [`DomNode`] - Rendering in the browser (to real DOM nodes). -/// * [`SsrNode`] - Render to a static string (often on the server side for Server Side Rendering, aka. SSR). -/// -/// To implement your own rendering backend, you will need to create a new struct which implements [`GenericNode`]. -pub trait GenericNode: fmt::Debug + Clone + PartialEq + Eq + 'static { +#[cfg(feature = "dom")] +pub type DomType = dom_node::DomNode; + +#[cfg(feature = "ssr")] +pub type DomType = ssr_node::SsrNode; + +pub trait GenericNode: fmt::Debug + Clone + PartialEq + std::cmp::Eq + 'static { /// Create a new element node. fn element(tag: &str) -> Self; @@ -75,31 +68,17 @@ pub trait GenericNode: fmt::Debug + Clone + PartialEq + Eq + 'static { fn next_sibling(&self) -> Option; /// Remove this node from the tree. - /// - /// TODO: Remove this node on Drop. fn remove_self(&self); + #[cfg(feature = "dom")] /// Add a [`EventListener`] to the event `name`. - fn event(&self, name: &str, handler: Box); + fn event(&self, _name: &str, _handler: Box) -> Option> { + None + } /// Update inner text of the node. If the node has elements, all the elements are replaced with a new text node. fn update_inner_text(&self, text: &str); - /// Append an item that implements [`Render`] and automatically updates the DOM inside an effect. - fn append_render(&self, child: Box Box>>) { - let parent = self.clone(); - - let node = create_effect_initial(cloned!((parent) => move || { - let node = RefCell::new(child().render().node); - - let effect = cloned!((node) => move || { - let new_node = child().update_node(&parent, &node.borrow()); - *node.borrow_mut() = new_node; - }); - - (Rc::new(effect), node) - })); - - parent.append_child(&node.borrow()); - } + /// Replace all the children in a node with a new node + fn replace_children_with(&self, node: &Self); } diff --git a/crates/hirola-core/src/generic_node/dom_node.rs b/crates/hirola-core/src/generic_node/dom_node.rs index 299faef..42aa10b 100644 --- a/crates/hirola-core/src/generic_node/dom_node.rs +++ b/crates/hirola-core/src/generic_node/dom_node.rs @@ -1,30 +1,66 @@ -use std::cell::RefCell; - -use ref_cast::RefCast; +use super::{EventListener, GenericNode}; use wasm_bindgen::{prelude::*, JsCast}; use web_sys::{Element, Event, Node, Text}; -use crate::generic_node::{EventListener, GenericNode}; - /// Rendering backend for the DOM. /// +/// The `DomNode` struct represents a node in the Document Object Model (DOM) and serves as the +/// rendering backend for the frontend application. It allows interacting with DOM nodes directly +/// and provides utility methods for type conversion and cloning. +/// /// _This API requires the following crate features to be activated: `dom`_ -#[derive(Debug, Clone, PartialEq, Eq, RefCast)] +/// +#[derive(Debug, Clone, PartialEq, Eq)] #[repr(transparent)] pub struct DomNode { - node: Node, + pub node: Node, } impl DomNode { + /// Retrieves the inner DOM node contained within the `DomNode`. + /// + /// # Returns + /// + /// The underlying DOM node represented by this `DomNode`. pub fn inner_element(&self) -> Node { self.node.clone() } + /// Converts the `DomNode` into a specified type using unchecked casting. + /// + /// This method allows converting the `DomNode` into a specific type, without performing a + /// runtime type check. It can be used when you are confident about the type of the DOM node, + /// and it avoids the overhead of dynamic type checking. + /// + /// # Type Parameters + /// + /// * `T` - The target type to convert the `DomNode` into. It should implement the `JsCast` + /// trait, which provides the unchecked casting functionality. + /// + /// # Returns + /// + /// The converted `DomNode` as the target type `T`. pub fn unchecked_into(self) -> T { self.node.unchecked_into() } - // pub fn dyn_into(self) -> Result { - // self.node.dyn_into() - // } + /// Attempts to dynamically cast the `DomNode` into a specified type. + /// + /// This method performs a runtime type check to determine if the `DomNode` can be converted + /// into the desired type. If the conversion succeeds, it returns the converted value; + /// otherwise, it returns an error containing the original `DomNode`. + /// + /// # Type Parameters + /// + /// * `T` - The target type to cast the `DomNode` into. It should implement the `JsCast` + /// trait, which provides the dynamic type casting functionality. + /// + /// # Returns + /// + /// - `Ok(T)` if the `DomNode` was successfully cast into the target type `T`. + /// - `Err(Node)` if the `DomNode` could not be cast into the target type `T`. + + pub fn dyn_into(self) -> Result { + self.node.dyn_into() + } } impl AsRef for DomNode { @@ -39,22 +75,6 @@ impl From for JsValue { } } -impl JsCast for DomNode { - fn instanceof(val: &JsValue) -> bool { - Node::instanceof(val) - } - - fn unchecked_from_js(val: JsValue) -> Self { - DomNode { - node: Node::unchecked_from_js(val), - } - } - - fn unchecked_from_js_ref(val: &JsValue) -> &Self { - DomNode::ref_cast(Node::unchecked_from_js_ref(val)) - } -} - fn document() -> web_sys::Document { web_sys::window().unwrap().document().unwrap() } @@ -74,7 +94,7 @@ impl GenericNode for DomNode { fn fragment() -> Self { DomNode { - node: document().create_document_fragment().into(), + node: document().create_document_fragment().dyn_into().unwrap(), } } @@ -92,21 +112,34 @@ impl GenericNode for DomNode { } fn append_child(&self, child: &Self) { - self.node.append_child(&child.node).unwrap(); + match self.node.append_child(&child.node) { + Err(e) => log::warn!("Could not append child: {e:?}"), + _ => {} + } } fn insert_child_before(&self, new_node: &Self, reference_node: Option<&Self>) { - self.node + match self + .node .insert_before(&new_node.node, reference_node.map(|n| &n.node)) - .unwrap(); + { + Ok(_) => {} + Err(e) => log::warn!("Failed to insert child: {e:?}"), + } } fn remove_child(&self, child: &Self) { - self.node.remove_child(&child.node).unwrap(); + match self.node.remove_child(&child.node) { + Ok(_) => {} + Err(e) => log::warn!("Failed to remove child: {e:?}"), + }; } fn replace_child(&self, old: &Self, new: &Self) { - self.node.replace_child(&old.node, &new.node).unwrap(); + match self.node.replace_child(&old.node, &new.node) { + Ok(_) => {} + Err(e) => log::warn!("Failed to replace child: {e:?}"), + }; } fn insert_sibling_before(&self, child: &Self) { @@ -128,21 +161,12 @@ impl GenericNode for DomNode { self.node.unchecked_ref::().remove(); } - fn event(&self, name: &str, handler: Box) { - type EventListener = dyn Fn(Event); - - thread_local! { - /// A global event listener pool to prevent [`Closure`]s from being deallocated. - /// TODO: remove events when elements are detached. - static EVENT_LISTENERS: RefCell>> = RefCell::new(Vec::new()); - } - + fn event(&self, name: &str, handler: Box) -> Option> { let closure = Closure::wrap(handler); self.node .add_event_listener_with_callback(name, closure.as_ref().unchecked_ref()) .unwrap(); - - EVENT_LISTENERS.with(|event_listeners| event_listeners.borrow_mut().push(closure)); + Some(closure) } fn update_inner_text(&self, text: &str) { @@ -151,4 +175,8 @@ impl GenericNode for DomNode { .unwrap() .set_text_content(Some(text)); } + fn replace_children_with(&self, node: &Self) { + let element = self.node.unchecked_ref::(); + element.replace_children_with_node_1(&node.inner_element()) + } } diff --git a/crates/hirola-core/src/generic_node/ssr_node.rs b/crates/hirola-core/src/generic_node/ssr_node.rs index 65b4221..8c3addb 100644 --- a/crates/hirola-core/src/generic_node/ssr_node.rs +++ b/crates/hirola-core/src/generic_node/ssr_node.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use std::rc::{Rc, Weak}; use std::{fmt, mem}; -use crate::generic_node::{EventListener, GenericNode}; +use crate::generic_node::GenericNode; /// Rendering backend for Server Side Rendering, aka. SSR. /// @@ -231,13 +231,17 @@ impl GenericNode for SsrNode { unimplemented!() } - fn event(&self, _name: &str, _handler: Box) { - // Don't do anything. Events are attached on client side. - } + // fn event(&self, _name: &str, _handler: Box) { + // // Don't do anything. Events are attached on client side. + // } fn update_inner_text(&self, text: &str) { self.unwrap_text().borrow_mut().0 = text.to_string(); } + + fn replace_children_with(&self, _node: &Self) { + unimplemented!() + } } impl fmt::Display for SsrNode { diff --git a/crates/hirola-core/src/lib.rs b/crates/hirola-core/src/lib.rs index 25010d1..670b380 100644 --- a/crates/hirola-core/src/lib.rs +++ b/crates/hirola-core/src/lib.rs @@ -1,10 +1,31 @@ -//! # Hirola API Documentation +//! # hirola-core //! -//! Hirola is based on [Marple](https://github.com/lukechu10/maple). +//! ## Example +//! ```rust,no_run +//! use hirola::prelude::*; +//! use hirola::signal::Mutable; //! +//! fn counter() -> Dom { +//! let count = Mutable::new(0i32); +//! let decrement = count.callback(|s| *s.lock_mut() -= 1); +//! let increment = count.callback(|s| *s.lock_mut() += 1); +//! html! { +//! <> +//! +//! {count} +//! +//! +//! } +//! } +//! +//! fn main() { +//! let root = render(counter()).unwrap(); +//! std::mem::forget(root); +//! } +//! ``` //! ## Features //! - `dom` (_default_) - Enables rendering templates to DOM nodes. Only useful on `wasm32-unknown-unknown` target. -//! - `ssr` - Enables rendering templates to static strings (useful for Server Side Rendering / Pre-rendering). +//! - `ssr` - Enables rendering templates to static strings (useful for Server Side Rendering / Server side Generation). //! - `serde` - Enables serializing and deserializing `Signal`s and other wrapper types using `serde`. #![allow(non_snake_case)] @@ -12,159 +33,137 @@ #![warn(clippy::rc_buffer)] #![deny(clippy::trait_duplication_in_bounds)] #![deny(clippy::type_repetition_in_bounds)] - -use generic_node::GenericNode; +use crate::dom::*; +use discard::DiscardOnDrop; +use futures_signals::{cancelable_future, CancelableFutureHandle}; pub use hirola_macros::html; +use std::{future::Future, pin::Pin}; + +pub type BoxedLocal = Pin + 'static>>; +#[cfg(feature = "app")] pub mod app; +#[cfg(feature = "dom")] pub mod callback; -pub mod easing; -pub mod flow; +pub mod dom; +pub mod effect; pub mod generic_node; -pub mod macros; -pub mod noderef; -pub mod reactive; -pub mod render; - -#[macro_use] -pub mod styled; - -#[cfg(feature = "router")] -#[cfg_attr(docsrs, doc(cfg(feature = "router")))] -pub mod router; - pub mod mixins; +pub mod render; +pub mod templating; -pub mod utils; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct TemplateResult { - node: G, -} - -impl TemplateResult { - /// Create a new [`TemplateResult`] from a [`GenericNode`]. - pub fn new(node: G) -> Self { - Self { node } - } - - /// Create a new [`TemplateResult`] with a blank comment node - pub fn empty() -> Self { - Self::new(G::marker()) - } - - pub fn inner_element(&self) -> G { - self.node.clone() - } -} +#[cfg(feature = "dom")] +use crate::generic_node::DomNode; -/// Render a [`TemplateResult`] into the DOM. +/// Render a [`Dom`] into the DOM. /// Alias for [`render_to`] with `parent` being the `` tag. /// /// _This API requires the following crate features to be activated: `dom`_ #[cfg(feature = "dom")] -pub fn render(template_result: impl FnOnce() -> TemplateResult) { +pub fn render(dom: Dom) -> Result { let window = web_sys::window().unwrap(); let document = window.document().unwrap(); - render_to(template_result, &document.body().unwrap()); + render_to(dom, &document.body().unwrap()) } -/// Render a [`TemplateResult`] under a `parent` node. +/// Render a [`Dom`] under a `parent` node. /// For rendering under the `` tag, use [`render()`] instead. /// /// _This API requires the following crate features to be activated: `dom`_ #[cfg(feature = "dom")] -pub fn render_to( - template_result: impl FnOnce() -> TemplateResult, - parent: &web_sys::Node, -) { - let owner = reactive::create_root(|| { - parent - .append_child(&template_result().node.inner_element()) - .unwrap(); - }); - - thread_local! { - static GLOBAL_OWNERS: std::cell::RefCell> = std::cell::RefCell::new(Vec::new()); - } - - GLOBAL_OWNERS.with(|global_owners| global_owners.borrow_mut().push(owner)); +pub fn render_to(dom: dom::Dom, parent: &web_sys::Node) -> Result { + dom.mount(&DomNode { + node: parent.clone(), + }) } -/// Render a [`TemplateResult`] into a static [`String`]. Useful for rendering to a string on the server side. +/// Render a [`Dom`] into a static [`String`]. Useful for rendering to a string on the server side. /// /// _This API requires the following crate features to be activated: `ssr`_ #[cfg(feature = "ssr")] -pub fn render_to_string( - template_result: impl FnOnce() -> TemplateResult, -) -> String { - let mut ret = None; - let _owner = - reactive::create_root(|| ret = Some(format!("{}", template_result().inner_element()))); - - ret.unwrap() +pub fn render_to_string(dom: Dom) -> String { + use crate::generic_node::GenericNode; + use crate::generic_node::SsrNode; + use crate::render::Render; + let node = SsrNode::fragment(); + let root = Dom::new_from_node(&node); + Render::render_into(Box::new(dom), &root).unwrap(); + format!("{}", root.node()) } -#[cfg(feature = "async")] -#[cfg_attr(docsrs, doc(cfg(feature = "async")))] - -pub type AsyncResult = prelude::Signal>>; - -/// Helper for making async calls -#[cfg(feature = "async")] -#[cfg_attr(docsrs, doc(cfg(feature = "async")))] -pub fn use_async(future: F) -> prelude::Signal> +#[inline] +pub fn spawn(future: F) -> DiscardOnDrop where - F: std::future::Future + 'static, + F: Future + 'static, { - let handler = prelude::Signal::new(None); - let inner = handler.clone(); - wasm_bindgen_futures::spawn_local(async move { - let res = future.await; - inner.set(Some(res)); - }); - handler + let (handle, future) = cancelable_future(future, || ()); + + #[cfg(feature = "dom")] + wasm_bindgen_futures::spawn_local(future); + + #[cfg(not(feature = "dom"))] + drop(future); + // tokio::task::spawn_local(future); + + handle } -/// The maple prelude. pub mod prelude { - pub use hirola_macros::{component, html}; - pub use crate::cloned; - pub use crate::flow::{Indexed, IndexedProps, Keyed, KeyedProps}; + // pub use crate::spawn; + pub use crate::effect::SideEffect; #[cfg(feature = "dom")] - pub use crate::generic_node::DomNode; + pub use crate::generic_node::DomNode as DomType; pub use crate::generic_node::GenericNode; #[cfg(feature = "ssr")] - pub use crate::generic_node::SsrNode; - pub use crate::noderef::NodeRef; - pub use crate::reactive::{ - create_effect, create_effect_initial, create_memo, create_root, create_selector, - create_selector_with, on_cleanup, untrack, Signal, StateHandle, - }; - pub use crate::render::Render; + pub use crate::generic_node::SsrNode as DomType; + pub use crate::templating::flow::{Indexed, IndexedProps}; + pub use crate::templating::noderef::NodeRef; + pub use crate::templating::suspense::{Suspend, Suspense, SuspenseResult::*}; + pub use crate::templating::switch::Switch; + pub use futures_signals::*; + pub use hirola_macros::{component, html}; + + #[cfg(feature = "dom")] + pub use crate::callback::Callback; + pub use crate::dom::Dom; #[cfg(feature = "ssr")] pub use crate::render_to_string; - pub use crate::TemplateResult; #[cfg(feature = "dom")] pub use crate::{render, render_to}; - pub use crate::callback::Mixin; - pub use crate::callback::State; - pub use crate::callback::StateReduce; - + #[cfg(feature = "app")] pub use crate::app::*; - #[cfg(feature = "router")] - pub use crate::router::*; - #[cfg(feature = "async")] - pub use crate::use_async; - #[cfg(feature = "async")] - pub use crate::AsyncResult; - pub use crate::styled::*; + pub use crate::mixins::*; + pub use crate::render::*; + pub use crate::BoxedLocal; + + pub use futures_signals::signal::Mutable; + pub use futures_signals::signal_map::MutableBTreeMap; + pub use futures_signals::signal_vec::MutableVec; +} - pub use crate::style; +#[cfg(feature = "dom")] +pub mod dom_test_utils { + use wasm_bindgen::{prelude::Closure, JsCast}; + + pub fn next_tick_with(with: &N, f: impl Fn(&N) -> () + 'static) { + let with = with.clone(); + let f: Box ()> = Box::new(move || f(&with)); + let a = Closure::::new(f); + web_sys::window() + .unwrap() + .set_timeout_with_callback(a.as_ref().unchecked_ref()) + .unwrap(); + } - pub use crate::mixins; + pub fn next_tick(f: F) { + let a = Closure::::new(move || f()); + web_sys::window() + .unwrap() + .set_timeout_with_callback(a.as_ref().unchecked_ref()) + .unwrap(); + } } diff --git a/crates/hirola-core/src/macros.rs b/crates/hirola-core/src/macros.rs deleted file mode 100644 index 730af6c..0000000 --- a/crates/hirola-core/src/macros.rs +++ /dev/null @@ -1,55 +0,0 @@ -//! Definition of `cloned!` macro. Proc-macros are defined in the separate `hirola-macros` crate. - -/// Utility macro for cloning all the arguments and expanding the expression. -/// -/// Temporary workaround for [Rust RFC #2407](https://github.com/rust-lang/rfcs/issues/2407). -/// -/// # Example -/// ``` -/// use hirola_core::prelude::*; -/// -/// let state = Signal::new(0); -/// -/// create_effect(cloned!((state) => move || { -/// state.get(); -/// })); -/// -/// // state still accessible outside of the effect -/// let _ = state.get(); -/// ``` -#[macro_export] -macro_rules! cloned { - (($($arg:ident),*) => $e:expr) => {{ - // clone all the args - $( let $arg = ::std::clone::Clone::clone(&$arg); )* - - $e - }}; -} - -#[cfg(test)] -mod tests { - use crate::prelude::*; - - #[test] - fn cloned() { - let state = Signal::new(0); - - let _x = cloned!((state) => state); - - // state still accessible because it was cloned instead of moved - let _ = state.get(); - } - - #[test] - fn cloned_closure() { - let state = Signal::new(0); - - create_effect(cloned!((state) => move || { - state.get(); - })); - - // state still accessible outside of the effect - let _ = state.get(); - } -} diff --git a/crates/hirola-core/src/mixins.rs b/crates/hirola-core/src/mixins.rs index 447c3eb..d8cc494 100644 --- a/crates/hirola-core/src/mixins.rs +++ b/crates/hirola-core/src/mixins.rs @@ -5,10 +5,11 @@ //! use hirola::prelude::*; //! use web_sys::Element; //! // Mixin that controls tailwind opacity based on a bool signal -//! fn opacity<'a>(signal: &'a Signal) -> Box () + 'a> { -//! let cb = move |node: DomNode| { +//! fn opacity<'a>(signal: &'a Mutable) -> Box () + 'a> { +//! let cb = move |dom: &Dom| { +//! let node = dom.node().clone(); //! let element = node.unchecked_into::(); -//! if *signal.get() { +//! if signal.get() { //! element.class_list().add_1("opacity-100").unwrap(); //! element.class_list().remove_1("opacity-0").unwrap(); //! } else { @@ -19,15 +20,18 @@ //! Box::new(cb) //! } //! -//! fn mixin_demo(_app: &HirolaApp) -> Dom { -//! let is_shown = Signal::new(true); -//! let toggle = is_shown.mut_callback(|show, _e| !show); +//! fn mixin_demo() -> Dom { +//! let is_shown = Mutable::new(true); +//! let toggle = is_shown.callback(|show| { +//! let current = show.get(); +//! *show.lock_mut() = !current; +//! }); //! html! { //!
//!
+//! mixin:identity=&opacity(&is_shown)/> //!
}; - render_to(|| node, &test_div()); + let _ = render_to(node, &test_div()); let input_ref = document().query_selector("input").unwrap().unwrap(); assert_eq!( Node::from(input_ref), - noderef.get::().unchecked_into() + noderef.get().unchecked_into() ); } diff --git a/crates/hirola-core/tests/integration/non_keyed.rs b/crates/hirola-core/tests/integration/non_keyed.rs index 8e34ad3..9b571c9 100644 --- a/crates/hirola-core/tests/integration/non_keyed.rs +++ b/crates/hirola-core/tests/integration/non_keyed.rs @@ -1,191 +1,203 @@ +use futures_signals::{signal::Mutable, signal_vec::MutableVec}; +use hirola_core::dom_test_utils::{next_tick, next_tick_with}; + use super::*; #[wasm_bindgen_test] fn append() { - let count = Signal::new(vec![1, 2]); + let count = MutableVec::new_with_values(vec![1, 2]); - let node = cloned!((count) => html! { + let node = html! {
    - { item } - }, - }} - /> + { + count.signal_vec().render_map(|item| { + html! { +
  • { item.to_string() }
  • + + } + } ) + }
- }); + }; - render_to(|| node, &test_div()); + let _ = render_to(node, &test_div()); let p = document().query_selector("ul").unwrap().unwrap(); - assert_eq!(p.text_content().unwrap(), "12"); - - count.set({ - let mut tmp = (*count.get()).clone(); - tmp.push(3); - tmp + next_tick_with(&p, |p| { + assert_eq!(p.text_content().unwrap(), "12"); }); - assert_eq!(p.text_content().unwrap(), "123"); - count.set(count.get()[1..].into()); - assert_eq!(p.text_content().unwrap(), "23"); + count.lock_mut().push(3); + next_tick_with(&p, |p| { + assert_eq!(p.text_content().unwrap(), "123"); + }); + let new_value = count.lock_ref()[1..].to_vec(); + count.lock_mut().replace(new_value); + next_tick_with(&p, |p| { + assert_eq!(p.text_content().unwrap(), "23"); + }); } #[wasm_bindgen_test] fn swap_rows() { - let count = Signal::new(vec![1, 2, 3]); + let count = MutableVec::new_with_values(vec![1, 2, 3]); - let node = cloned!((count) => html! { + let node = html! {
    - { item } - }, - }} - /> + { + count.signal_vec().render_map(|item| { + html! { +
  • { item.to_string() }
  • + + } + } ) + }
- }); + }; - render_to(|| node, &test_div()); + let _ = render_to(node, &test_div()); let p = document().query_selector("ul").unwrap().unwrap(); - assert_eq!(p.text_content().unwrap(), "123"); + next_tick_with(&p, |p| { + assert_eq!(p.text_content().unwrap(), "123"); + }); - count.set({ - let mut tmp = (*count.get()).clone(); - tmp.swap(0, 2); - tmp + count.lock_mut().swap(0, 2); + next_tick_with(&p, |p| { + assert_eq!(p.text_content().unwrap(), "321"); }); - assert_eq!(p.text_content().unwrap(), "321"); - count.set({ - let mut tmp = (*count.get()).clone(); - tmp.swap(0, 2); - tmp + count.lock_mut().swap(0, 2); + next_tick_with(&p, |p| { + assert_eq!(p.text_content().unwrap(), "123"); }); - assert_eq!(p.text_content().unwrap(), "123"); } #[wasm_bindgen_test] fn delete_row() { - let count = Signal::new(vec![1, 2, 3]); + let count = MutableVec::new_with_values(vec![1, 2, 3]); - let node = cloned!((count) => html! { + let node = html! {
    - { item } - }, - }} - /> + { + count.signal_vec().render_map(|item| { + html! { +
  • { item.to_string() }
  • + + } + } ) + }
- }); + }; - render_to(|| node, &test_div()); + let _ = render_to(node, &test_div()); let p = document().query_selector("ul").unwrap().unwrap(); - assert_eq!(p.text_content().unwrap(), "123"); + next_tick_with(&p, |p| { + assert_eq!(p.text_content().unwrap(), "123"); + }); - count.set({ - let mut tmp = (*count.get()).clone(); - tmp.remove(1); - tmp + count.lock_mut().remove(1); + next_tick_with(&p, |p| { + assert_eq!(p.text_content().unwrap(), "13"); }); - assert_eq!(p.text_content().unwrap(), "13"); } #[wasm_bindgen_test] fn clear() { - let count = Signal::new(vec![1, 2, 3]); + let count = MutableVec::new(); - let node = cloned!((count) => html! { + let node = html! {
    - { item } - }, - }} - /> + { + count.signal_vec().render_map(|item: i32| { + html! { +
  • { item.to_string() }
  • + } + } ) + }
- }); + }; - render_to(|| node, &test_div()); + let _ = render_to(node, &test_div()).unwrap(); let p = document().query_selector("ul").unwrap().unwrap(); - assert_eq!(p.text_content().unwrap(), "123"); - - count.set(Vec::new()); - assert_eq!(p.text_content().unwrap(), ""); + next_tick_with(&p, |p| { + assert_eq!(p.text_content().unwrap(), ""); + }); + count.lock_mut().replace(vec![1, 2, 3]); + next_tick_with(&p, |p| { + assert_eq!(p.inner_html(), "123"); + }); + count.lock_mut().replace(Vec::new()); + next_tick_with(&p, |p| { + assert_eq!(p.text_content().unwrap(), ""); + }); } #[wasm_bindgen_test] fn insert_front() { - let count = Signal::new(vec![1, 2, 3]); + let count = MutableVec::new_with_values(vec![1, 2, 3]); - let node = cloned!((count) => html! { + let node = html! {
    - { item } - }, - }} - /> + { + count.signal_vec().render_map(|item| { + html! { +
  • { item.to_string() }
  • + } + } ) + }
- }); - - render_to(|| node, &test_div()); - - let p = document().query_selector("ul").unwrap().unwrap(); - assert_eq!(p.text_content().unwrap(), "123"); + }; - count.set({ - let mut tmp = (*count.get()).clone(); - tmp.insert(0, 4); - tmp + let _ = render_to(node, &test_div()); + next_tick(|| { + let p = document().query_selector("ul").unwrap().unwrap(); + assert_eq!(p.text_content().unwrap(), "123"); + }); + count.lock_mut().insert(0, 4); + next_tick(|| { + let p = document().query_selector("ul").unwrap().unwrap(); + assert_eq!(p.text_content().unwrap(), "4123"); }); - assert_eq!(p.text_content().unwrap(), "4123"); } #[wasm_bindgen_test] fn nested_reactivity() { - let count = Signal::new(vec![1, 2, 3].into_iter().map(Signal::new).collect()); + let count = + MutableVec::new_with_values(vec![1u32, 2, 3].into_iter().map(Mutable::new).collect()); - let node = cloned!((count) => html! { + let node = html! {
    - { item.get() } - }, - }} - /> + { + count.signal_vec_cloned().render_map(|item| { + html! { +
  • { item }
  • + + } + } ) + }
- }); + }; - render_to(|| node, &test_div()); + let _ = render_to(node, &test_div()); - let p = document().query_selector("ul").unwrap().unwrap(); - assert_eq!(p.text_content().unwrap(), "123"); + next_tick(|| { + let p = document().query_selector("ul").unwrap().unwrap(); + assert_eq!(p.text_content().unwrap(), "123"); + }); - count.get()[0].set(4); - assert_eq!(p.text_content().unwrap(), "423"); + count.lock_ref()[0].set(4); + next_tick(|| { + let p = document().query_selector("ul").unwrap().unwrap(); + assert_eq!(p.text_content().unwrap(), "423"); + }); - count.set({ - let mut tmp = (*count.get()).clone(); - tmp.push(Signal::new(5)); - tmp + count.lock_mut().push_cloned(Mutable::new(5)); + next_tick(|| { + let p = document().query_selector("ul").unwrap().unwrap(); + assert_eq!(p.text_content().unwrap(), "4235"); }); - assert_eq!(p.text_content().unwrap(), "4235"); } diff --git a/crates/hirola-core/tests/integration/router.rs b/crates/hirola-core/tests/integration/router.rs new file mode 100644 index 0000000..ee916a6 --- /dev/null +++ b/crates/hirola-core/tests/integration/router.rs @@ -0,0 +1,108 @@ +use hirola::prelude::*; +use hirola_core::dom_test_utils::next_tick; +use hirola_core::prelude::router::Router; +use wasm_bindgen_test::*; + +wasm_bindgen_test_configure!(run_in_browser); + +#[derive(Clone)] +struct AppState { + // ... fields and methods for your application state ... +} + +fn home_page(_: &App) -> Dom { + Dom::text("Home") +} + +fn about_page(_: &App) -> Dom { + Dom::text("About") +} + +fn not_found_page(_: &App) -> Dom { + Dom::text("NotFound") +} + +fn user(_: &App) -> Dom { + Dom::text("User") +} + +fn body() -> DomType { + DomType::fragment() +} + +// Helper function to set up a test instance of Router +fn create_test_router() -> Router { + let mut router = Router::new(); + router.insert("/", home_page); + router.insert("/about", about_page); + router.insert("/users/:id", user); + router.set_not_found(not_found_page); + router.push("/"); + router +} + +#[wasm_bindgen_test] +fn test_new_router_is_empty() { + let router = Router::::new(); + assert!(router.current_params().is_empty()); +} +#[wasm_bindgen_test] +fn test_router_insert_and_render() { + let router = create_test_router(); + + let app = App::new(AppState {}); + let body = &body(); + let home_dom = (router.handler().at("/").unwrap().value)(&app); + let rendered = router.clone().render(&app, &body); + assert_eq!(rendered.inner_html(), home_dom.inner_html()); + router.push("/about"); + + let about_dom = (router.handler().at("/about").unwrap().value)(&app); + next_tick(move || { + assert_eq!(rendered.inner_html(), about_dom.inner_html()); + }) +} + +#[wasm_bindgen_test] +fn test_router_current_params() { + let router = create_test_router(); + router.push("/users/42"); + let params = router.current_params(); + assert_eq!(params.get("id"), Some(&"42".to_string())); + + router.push("/about"); + let params = router.current_params(); + assert!(params.is_empty()); +} + +#[wasm_bindgen_test] +fn test_router_push_and_render() { + let router = create_test_router(); + let app = App::new(AppState {}); + let body = body(); + router.push("/about"); + let about_dom = (router.handler().at("/about").unwrap().value)(&app); + assert_eq!( + router.clone().render(&app, &body).inner_html(), + about_dom.inner_html() + ); + + router.push("/"); + let home_dom = (router.handler().at("/").unwrap().value)(&app); + assert_eq!( + router.render(&app, &body).inner_html(), + home_dom.inner_html() + ); +} +#[wasm_bindgen_test] +fn test_router_not_found() { + let router = create_test_router(); + let app = App::new(AppState {}); + + router.push("/non_existent_route"); + let not_found_dom = not_found_page(&app); + assert_eq!( + router.render(&app, &body()).inner_html(), + not_found_dom.inner_html() + ); +} diff --git a/crates/hirola-core/tests/ssr/main.rs b/crates/hirola-core/tests/ssr/main.rs index e0048c6..785c3da 100644 --- a/crates/hirola-core/tests/ssr/main.rs +++ b/crates/hirola-core/tests/ssr/main.rs @@ -1,3 +1,4 @@ +use futures_signals::{signal::Mutable, signal_vec::MutableVec}; use hirola::prelude::*; #[test] @@ -6,19 +7,45 @@ fn hello_world() {

"Hello World!"

}; - assert_eq!(render_to_string(|| node), "

Hello World!

"); + assert_eq!(render_to_string(node), "

Hello World!

"); } #[test] fn reactive_text() { - let count = Signal::new(0); + let count = Mutable::new(0); - let node = cloned!((count) => html! { -

{ (count.get()) }

- }); - - assert_eq!(render_to_string(cloned!((node) => || node)), "

0

"); + assert_eq!( + render_to_string(html! { +

{count.clone()}

+ }), + "

0

" + ); count.set(1); - assert_eq!(render_to_string(|| node), "

1

"); + assert_eq!( + render_to_string(html! { +

{count}

+ }), + "

1

" + ); +} + +#[test] +fn check_effects() { + let count = MutableVec::new_with_values(vec![1, 2, 3]); + + let node = html! { +
    + { + count.signal_vec().render_map(move |item| { + html! { +
  • {item.to_string()}
  • + } + } ) + } +
+ }; + + let dom = render_to_string(node); + assert_eq!("
  • 1
  • 2
  • 3
", dom); } diff --git a/crates/hirola-form/Cargo.toml b/crates/hirola-form/Cargo.toml index a0395a7..e902e12 100644 --- a/crates/hirola-form/Cargo.toml +++ b/crates/hirola-form/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hirola-form" -version = "0.2.0" +version = "0.3.0" edition = "2021" description = "Form mixins and utilities for hirola" repository = "https://github.com/geofmureithi/hirola" @@ -12,10 +12,10 @@ keywords = ["wasm", "html", "form", "web"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -hirola-core = { path = "../hirola-core", version = "0.2.0", features=["serde"] } -wasm-bindgen = {version = "0.2", features= ["serde-serialize"]} -validator = "0.10" -validator_derive = "0.10" +hirola-core = { path = "../hirola-core", features = [ + "dom", +], version = "0.3.0" } +wasm-bindgen = { version = "0.2", features = ["serde-serialize"] } serde = { version = "1.0.80", features = ["derive"] } serde_json = "1" json_dotpath = "1.1.0" @@ -37,7 +37,7 @@ features = [ "HtmlTextAreaElement", "HtmlSelectElement", "HtmlFormElement", - "FormData" + "FormData", ] version = "0.3" diff --git a/crates/hirola-form/src/bind.rs b/crates/hirola-form/src/bind.rs new file mode 100644 index 0000000..4f5c265 --- /dev/null +++ b/crates/hirola-form/src/bind.rs @@ -0,0 +1,52 @@ +use std::{ + fmt::{Debug, Display}, + marker::PhantomData, + str::FromStr, +}; + +use hirola_core::prelude::{*, signal::SignalExt}; +use wasm_bindgen::JsCast; +use web_sys::{Event, HtmlInputElement}; + +/// Model allows 2-way binding eg between a signal and an input +pub struct Model(Mutable, PhantomData); + +impl Mixin for Model +where + ::Err: Debug, +{ + fn mixin(&self, node: &Dom) { + let input = { + let node = node.node().as_ref().clone(); + node.dyn_into::().unwrap() + }; + let signal = self.0.clone(); + node.effect( + signal + .signal_ref(move |value| { + input.set_value(&format!("{}", value)); + }) + .to_future(), + ); + let handler = Box::new(move |e: Event| { + let input = e + .current_target() + .unwrap() + .dyn_into::() + .unwrap(); + let new_value = input.value().parse().unwrap(); + signal.set(new_value); + }); + + node.event("input", handler); + } +} + +/// Two way binding for input and signals +pub mod model { + use super::*; + /// Bind a [HtmlInputElement] to a [Mutable] + pub fn input(s: &Mutable) -> Model { + Model(s.clone(), PhantomData) + } +} diff --git a/crates/hirola-form/src/lib.rs b/crates/hirola-form/src/lib.rs index 5a66adc..0949144 100644 --- a/crates/hirola-form/src/lib.rs +++ b/crates/hirola-form/src/lib.rs @@ -1,61 +1,72 @@ -use std::{marker::PhantomData, rc::Rc}; +pub mod bind; -use hirola_core::{ - callback::MixinError, - cloned, - prelude::{DomNode, DomType, GenericNode, Mixin, NodeRef, State}, - reactive::{create_effect, Signal, StateHandle}, +use hirola_core::prelude::{ + signal::{Mutable, MutableSignalRef, ReadOnlyMutable}, + Dom, GenericNode, Mixin, NodeRef, }; use json_dotpath::DotPaths; use serde::{de::DeserializeOwned, Serialize}; -use validator::{Validate, ValidationErrors}; +use std::{collections::HashMap, marker::PhantomData}; use wasm_bindgen::JsCast; use web_sys::{Event, HtmlInputElement, HtmlSelectElement}; +#[derive(Debug)] +pub enum Error { + Json(serde_json::Error), + InvalidSetter(json_dotpath::Error), + InvalidGetter(json_dotpath::Error), +} + +pub trait Validate: Sized { + type Error; + fn validate(&self) -> Result<(), Self::Error>; + fn errors(&self) -> HashMap<&'static str, String>; +} + +pub struct Form; + /// Form plugin for hirola #[derive(Clone, Debug)] pub struct FormHandler { - node_ref: NodeRef, - value: Signal, + node_ref: NodeRef, + value: Mutable, } -impl FormHandler { +impl FormHandler { /// Build a new reactive form pub fn new(value: T) -> Self { Self { node_ref: NodeRef::new(), - value: Signal::new(value), + value: Mutable::new(value), } } /// Get the immutable handle for form value - pub fn handle(&self) -> StateHandle { - (&self.value).clone().into_handle() + pub fn handle(&self) -> ReadOnlyMutable { + (&self.value).read_only() } /// Update a specific field using the dot notation. /// Eg you can update person.email - pub fn update_field(&self, name: &str, value: S) { + pub fn update_field(&self, name: &str, value: S) -> Result<(), Error> { let current_value = self.value.clone(); - let mut json = serde_json::to_value(¤t_value).unwrap(); - json.dot_set(&name, value).unwrap(); - let ser: T = serde_json::from_value(json).unwrap(); + let mut json = serde_json::to_value(¤t_value).map_err(Error::Json)?; + json.dot_set(&name, value).map_err(Error::InvalidSetter)?; + let ser: T = serde_json::from_value(json).map_err(Error::Json)?; current_value.set(ser); + Ok(()) } /// Get and cast a field value - pub fn get_value_by_field( - &self, - name: &str, - ) -> Result, json_dotpath::Error> { + pub fn get_value_by_field(&self, name: &str) -> Result, Error> { let current_value = self.value.clone(); - let json = serde_json::to_value(¤t_value)?; - json.dot_get(name) + let json = serde_json::to_value(¤t_value).map_err(Error::Json)?; + json.dot_get(name).map_err(Error::InvalidGetter) } // Get form value - pub fn get_value(&self) -> Rc { - self.value.get() + pub fn get_value(&self) -> T { + self.value.get_cloned() } } @@ -66,15 +77,8 @@ pub struct Register { element_type: PhantomData, } -impl Mixin for Register { - fn mixin(&self, ns: &str, node: DomNode) -> Result<(), MixinError> { - if ns != "form" { - return Err(MixinError::InvalidNamespace { - expected: "form".to_string(), - found: ns.to_string(), - }); - } - +impl Mixin
for Register { + fn mixin(&self, dom: &Dom) { let form = self.form.clone(); let handler = Box::new(move |e: Event| { let input = e @@ -85,30 +89,21 @@ impl Mixin for Register() - .map_err(MixinError::NodeError)? + let node = dom.node().clone(); + node.dyn_into::().unwrap() }; let name = input.name(); let value: String = self.form.get_value_by_field(&name).unwrap().unwrap(); - node.set_attribute("value", &value); - Ok(()) + dom.node().set_attribute("value", &value); } } -impl Mixin for Register { - fn mixin(&self, ns: &str, node: DomNode) -> Result<(), MixinError> { - if ns != "form" { - return Err(MixinError::InvalidNamespace { - expected: "form".to_string(), - found: ns.to_string(), - }); - } - +impl Mixin for Register { + fn mixin(&self, node: &Dom) { let form = self.form.clone(); let handler = Box::new(move |e: Event| { let input = e @@ -119,10 +114,9 @@ impl Mixin for Register Mixin for Register(&'static str, FormHandler, PhantomData); -impl Bind { +impl Bind { /// Manually set the value of the bound field - pub fn set_value(&self, value: B) { - self.1.update_field(self.0, value); + pub fn set_value(&self, value: B) -> Result<(), Error> { + self.1.update_field(self.0, value) } /// Get value of bound field - pub fn get_value(&self) -> B { + pub fn get_value(&self) -> MutableSignalRef B> { let current_value = self.1.value.clone(); - let json = serde_json::to_value(&*current_value.get()).unwrap(); - json.dot_get(&self.0).unwrap().unwrap() + let name = self.0; + fn read_inner_value(value: &F, name: &str) -> B + where + B: Serialize + DeserializeOwned, + F: Serialize + DeserializeOwned, + { + let json = serde_json::to_value(value).unwrap(); + json.dot_get(name).unwrap().unwrap() + } + current_value.signal_ref(|value| read_inner_value::(&value, name)) } } -impl State for Bind {} - -impl FormHandler { +impl FormHandler { /// Perform validation - pub fn validate(&self) -> Result<(), ValidationErrors> { - self.value.get().validate() + pub fn validate(&self) -> Result<(), ::Error> { + self.value.get_cloned().validate() } /// Get error specific field - pub fn error_for(&self, name: &'static str) -> Signal { - let signal = Signal::new(String::new()); - let value = self.value.clone(); - create_effect(cloned!((signal) => move || { - let res = value.get().validate(); - if ValidationErrors::has_error(&res, name) { - let err = res.err().unwrap(); - let err = err.field_errors(); - let value = err.get(name).unwrap().first(); - if let Some(v) = value { - signal.set(format!("{}", v)) - } else { - signal.set(String::new()) - } - - } else { - signal.set(String::new()) + pub fn error_for(&self, name: &'static str) -> MutableSignalRef String> { + self.value.signal_ref(|value: &T| { + let errors = value.errors(); + if let Some(err) = errors.get(name) { + return err.clone(); } - })); - signal + "String::new()".to_owned() + }) } } @@ -191,7 +179,7 @@ impl FormHandler { } /// Get the reference for the form - pub fn node_ref(&self) -> NodeRef { + pub fn node_ref(&self) -> NodeRef { self.node_ref.clone() } } diff --git a/crates/hirola-kit/Cargo.toml b/crates/hirola-kit/Cargo.toml new file mode 100644 index 0000000..90b0bc8 --- /dev/null +++ b/crates/hirola-kit/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "hirola-kit" +version = "0.1.0" +edition = "2021" +documentation = "https://docs.rs/hirola" +readme = "../../README.md" +license = "MIT OR Apache-2.0" +description = "A CLI for hirola" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +leptosfmt-pretty-printer = "0.1.6" +rstml = "0.10.6" +syn = { version = "2.0.18", features = [ "visit", "full", "extra-traits" ] } +leptosfmt-prettyplease = { features = [ "verbatim" ], version = "0.2.11" } +proc-macro2 = { version = "1.0.52", features = [ "span-locations" ] } +thiserror = "1.0.40" +crop = "0.3.0" +serde = { version = "1.0.163", features = [ "derive" ] } + + +# Deps for cli +clap = { version = "4.1.11", features = ["derive"] } +rayon = "1.7.0" +glob = "0.3.1" +anyhow = "1.0.70" +toml = "0.7.4" + +[dev-dependencies] +indoc = "2.0.1" +insta = "1.28.0" +quote = "1.0.26" \ No newline at end of file diff --git a/crates/hirola-kit/README.md b/crates/hirola-kit/README.md new file mode 100644 index 0000000..2ec031b --- /dev/null +++ b/crates/hirola-kit/README.md @@ -0,0 +1,11 @@ +SUBCOMMANDS + build Build the Rust WASM app and all of its assets + clean Clean output artifacts + config Trunk config controls + export Export the app as a standalone html app + format Format hirola html! macro code + help Print this message or the help of the given subcommand(s) + lint Lint html! macro code + new Generate a new hirola project + serve Build, watch & serve the Rust WASM app and all of its assets + watch Build & watch the Rust WASM app and all of its assets \ No newline at end of file diff --git a/crates/hirola-kit/src/formatter/collect.rs b/crates/hirola-kit/src/formatter/collect.rs new file mode 100644 index 0000000..a01166a --- /dev/null +++ b/crates/hirola-kit/src/formatter/collect.rs @@ -0,0 +1,67 @@ +use proc_macro2::LineColumn; +use syn::{ + spanned::Spanned, + visit::{self, Visit}, + Expr, File, Macro, +}; + +use super::format::HtmlMacro; + +#[derive(Default)] +struct DomMacroVisitor<'ast> { + indent_stack: Vec, + macros: Vec>, +} + +impl<'ast> Visit<'ast> for DomMacroVisitor<'ast> { + fn visit_stmt(&mut self, i: &'ast syn::Stmt) { + self.indent_stack.push(i.span().start()); + visit::visit_stmt(self, i); + self.indent_stack.pop(); + } + + fn visit_expr(&mut self, i: &'ast Expr) { + self.indent_stack.push(i.span().start()); + visit::visit_expr(self, i); + self.indent_stack.pop(); + } + + fn visit_arm(&mut self, i: &'ast syn::Arm) { + self.indent_stack.push(i.span().start()); + visit::visit_arm(self, i); + self.indent_stack.pop(); + } + + fn visit_macro(&mut self, node: &'ast Macro) { + if node.path.is_ident("html") { + let span_start = node.span().start().column; + let span_line = node.span().start().line; + let indent = self + .indent_stack + .iter() + .filter(|v| v.line == span_line && v.column < span_start) + .map(|v| v.column) + .min() + .unwrap_or( + self.indent_stack + .iter() + .rev() + .find(|v| v.column < span_start) + .map(|i| i.column) + .unwrap_or(0), + ); + + if let Some(dom_mac) = HtmlMacro::try_parse(Some(indent), node) { + self.macros.push(dom_mac); + } + } + + visit::visit_macro(self, node); + } +} + +pub fn collect_macros_in_file(file: &File) -> Vec { + let mut visitor = DomMacroVisitor::default(); + visitor.visit_file(file); + visitor.macros +} diff --git a/crates/hirola-kit/src/formatter/format/attribute.rs b/crates/hirola-kit/src/formatter/format/attribute.rs new file mode 100644 index 0000000..2c89c1a --- /dev/null +++ b/crates/hirola-kit/src/formatter/format/attribute.rs @@ -0,0 +1,178 @@ +use rstml::node::{KeyedAttribute, NodeAttribute}; +use syn::{spanned::Spanned, Expr}; + +use crate::{formatter::AttributeValueBraceStyle as Braces, formatter::Formatter}; + +impl Formatter<'_> { + pub fn attribute(&mut self, attribute: &NodeAttribute) { + self.write_comments(attribute.span().start().line - 1); + + match attribute { + NodeAttribute::Attribute(k) => self.keyed_attribute(k), + NodeAttribute::Block(b) => self.node_block(b), + } + } + + pub fn keyed_attribute(&mut self, attribute: &KeyedAttribute) { + self.node_name(&attribute.key); + + if let Some(value) = attribute.value() { + self.printer.word("="); + self.attribute_value(value); + } + } + + fn attribute_value(&mut self, value: &Expr) { + match (self.settings.attr_value_brace_style, value) { + (Braces::Always, syn::Expr::Block(_)) => self.node_value_expr(value, false, false), + (Braces::AlwaysUnlessLit, syn::Expr::Block(_) | syn::Expr::Lit(_)) => { + self.node_value_expr(value, false, true) + } + (Braces::Always | Braces::AlwaysUnlessLit, _) => { + self.printer.word("{"); + self.node_value_expr(value, false, false); + self.printer.word("}"); + } + (Braces::WhenRequired, _) => self.node_value_expr(value, true, true), + (Braces::Preserve, _) => self.node_value_expr(value, false, false), + } + } +} + +#[cfg(test)] +mod tests { + use crate::formatter::test_helpers::*; + use insta::assert_snapshot; + + use crate::formatter::{AttributeValueBraceStyle, FormatterSettings}; + + macro_rules! format_attribute { + ($($tt:tt)*) => {{ + let attr = attribute! { $($tt)* }; + format_with(FormatterSettings::default(), |formatter| { + formatter.attribute(&attr); + }) + }}; + } + + macro_rules! format_attr_with_brace_style { + ($style:ident => $($tt:tt)*) => {{ + let attr = attribute! { $($tt)* }; + let settings = FormatterSettings { + attr_value_brace_style: AttributeValueBraceStyle:: $style, + ..FormatterSettings::default() + }; + + format_with(settings, |formatter| { + formatter.attribute(&attr); + }) + }}; + } + + #[test] + fn key_only_attr() { + let formatted = format_attribute! { test }; + assert_snapshot!(formatted, @"test"); + } + + #[test] + fn key_only_dash_attr() { + let formatted = format_attribute! { test-dash }; + assert_snapshot!(formatted, @"test-dash"); + } + + #[test] + fn key_value_integer_attr() { + let formatted = format_attribute! { key=123 }; + assert_snapshot!(formatted, @"key=123"); + } + + #[test] + fn key_value_str_attr() { + let formatted = format_attribute! { key="K-123" }; + assert_snapshot!(formatted, @r###"key="K-123""###); + } + + #[test] + fn key_value_expr_attr() { + let formatted = format_attribute! { on:click= move |_| set_value(0) }; + assert_snapshot!(formatted, @"on:click=move |_| set_value(0)"); + } + + #[test] + fn key_value_expr_attr_always_braces() { + // sinle expr without braces + let f = format_attr_with_brace_style! { Always => on:click=move |_| set_value(0) }; + assert_snapshot!(f, @"on:click={move |_| set_value(0)}"); + + // single expr with braces + let f = format_attr_with_brace_style! { Always => on:click={move |_| set_value(0)} }; + assert_snapshot!(f, @"on:click={move |_| set_value(0)}"); + + // literal numeric value + let f = format_attr_with_brace_style! { Always => width=100 }; + assert_snapshot!(f, @"width={100}"); + + // literal string value + let f = format_attr_with_brace_style! { Always => alt="test img" }; + assert_snapshot!(f, @r###"alt={"test img"}"###); + } + + #[test] + fn key_value_expr_attr_always_unless_lit_braces() { + // sinle expr without braces + let f = format_attr_with_brace_style! { AlwaysUnlessLit => on:click=move |_| set_value(0) }; + assert_snapshot!(f, @"on:click={move |_| set_value(0)}"); + + // single expr with braces + let f = + format_attr_with_brace_style! { AlwaysUnlessLit => on:click={move |_| set_value(0)} }; + assert_snapshot!(f, @"on:click={move |_| set_value(0)}"); + + // literal numeric value + let f = format_attr_with_brace_style! { AlwaysUnlessLit => width={100} }; + assert_snapshot!(f, @"width=100"); + + // literal string value + let f = format_attr_with_brace_style! { AlwaysUnlessLit => alt="test img" }; + assert_snapshot!(f, @r###"alt="test img""###); + } + + #[test] + fn key_value_expr_attr_preserve_braces() { + // single expr without braces + let f = format_attr_with_brace_style! { Preserve => on:click=move |_| set_value(0) }; + assert_snapshot!(f, @"on:click=move |_| set_value(0)"); + + // single expr with braces + let f = format_attr_with_brace_style! { Preserve => on:click={move |_| set_value(0)} }; + assert_snapshot!(f, @"on:click={move |_| set_value(0)}"); + + // literal numeric value with braces + let f = format_attr_with_brace_style! { Preserve => width={100} }; + assert_snapshot!(f, @"width={100}"); + + // literal string value without braces + let f = format_attr_with_brace_style! { Preserve => alt="test img" }; + assert_snapshot!(f, @r###"alt="test img""###); + } + + #[test] + fn key_value_expr_attr_braces_when_required() { + // single expr without braces + let f = format_attr_with_brace_style! { WhenRequired => on:click=move |_| set_value(0) }; + assert_snapshot!(f, @"on:click=move |_| set_value(0)"); + + // single expr with braces + let f = format_attr_with_brace_style! { WhenRequired => on:click={move |_| set_value(0)} }; + assert_snapshot!(f, @"on:click=move |_| set_value(0)"); + + // literal numeric value + let f = format_attr_with_brace_style! { WhenRequired => width={100} }; + assert_snapshot!(f, @"width=100"); + + // literal string value + let f = format_attr_with_brace_style! { WhenRequired => alt={"test img"} }; + assert_snapshot!(f, @r###"alt="test img""###); + } +} diff --git a/crates/hirola-kit/src/formatter/format/element.rs b/crates/hirola-kit/src/formatter/format/element.rs new file mode 100644 index 0000000..2094d9d --- /dev/null +++ b/crates/hirola-kit/src/formatter/format/element.rs @@ -0,0 +1,363 @@ +use rstml::node::{Node, NodeAttribute, NodeElement}; +use syn::spanned::Spanned; + +use crate::formatter::Formatter; + +impl Formatter<'_> { + pub fn element(&mut self, element: &NodeElement) { + let name = element.name().to_string(); + let is_void = is_void_element(&name, !element.children.is_empty()); + self.opening_tag(element, is_void); + + if !is_void { + self.children(&element.children, element.attributes().len()); + self.write_comments(element.close_tag.span().end().line - 1); + self.closing_tag(element) + } + } + + fn opening_tag(&mut self, element: &NodeElement, is_void: bool) { + self.printer.word("<"); + self.node_name(element.name()); + + self.attributes(element.attributes()); + + if is_void { + self.printer.word("/>"); + } else { + self.printer.word(">") + } + } + + fn closing_tag(&mut self, element: &NodeElement) { + self.printer.word(""); + } + + fn attributes(&mut self, attributes: &[NodeAttribute]) { + if attributes.is_empty() { + return; + } + + if let [attribute] = attributes { + self.printer.nbsp(); + self.attribute(attribute); + } else { + self.printer.cbox_indent(); + self.printer.space(); + + let mut iter = attributes.iter().peekable(); + while let Some(attr) = iter.next() { + self.attribute(attr); + + if iter.peek().is_some() { + self.printer.space() + } + } + + self.printer.zerobreak(); + self.printer.end_dedent(); + } + } + + pub fn children(&mut self, children: &Vec, attribute_count: usize) { + if children.is_empty() { + return; + } + + let is_textual = children + .first() + .map(|n| matches!(n, Node::Text(_) | Node::Block(_))) + .unwrap_or_default(); + + let soft_break = is_textual && attribute_count <= 1; + + if soft_break { + self.printer.cbox_indent(); + self.printer.zerobreak(); + self.printer.ibox(0); + } else { + self.printer.neverbreak(); + self.printer.cbox_indent(); + self.printer.hardbreak(); + } + + let mut iter = children.iter().peekable(); + while let Some(child) = iter.next() { + self.node(child); + + if iter.peek().is_some() { + self.printer.space() + } + } + + if soft_break { + self.printer.end(); + self.printer.zerobreak(); + } else { + self.printer.hardbreak(); + } + + self.printer.end_dedent(); + } +} + +fn is_void_element(name: &str, has_children: bool) -> bool { + if name.chars().next().unwrap().is_uppercase() { + !has_children + } else { + matches!( + name, + "area" + | "base" + | "br" + | "col" + | "embed" + | "hr" + | "img" + | "input" + | "link" + | "meta" + | "param" + | "source" + | "track" + | "wbr" + ) + } +} + +#[cfg(test)] +mod tests { + use indoc::indoc; + + use crate::{ + formatter::test_helpers::{element, element_from_string, format_with, format_with_source}, + formatter::FormatterSettings, + }; + + macro_rules! format_element { + ($($tt:tt)*) => {{ + let element = element! { $($tt)* }; + format_with(FormatterSettings { max_width: 40, ..Default::default() }, |formatter| { + formatter.element(&element) + }) + }}; + } + macro_rules! format_element_from_string { + ($val:expr) => {{ + let element = element_from_string! { $val }; + + format_with_source( + FormatterSettings { + max_width: 40, + ..Default::default() + }, + $val, + |formatter| formatter.element(&element), + ) + }}; + } + + #[test] + fn no_children() { + let formatted = format_element! { < div > < / div > }; + insta::assert_snapshot!(formatted, @"
"); + } + + #[test] + fn no_children_single_attr() { + let formatted = format_element! { < div width=12 > < / div > }; + insta::assert_snapshot!(formatted, @"
"); + } + + #[test] + fn no_children_multi_attr() { + let formatted = format_element! {
}; + insta::assert_snapshot!(formatted, @"
"); + } + + #[test] + fn no_children_single_long_attr() { + let formatted = + format_element! {
}; + + insta::assert_snapshot!(formatted, @"
"); + } + + #[test] + fn no_children_multi_long_attr() { + let formatted = format_element! {
}; + insta::assert_snapshot!(formatted, @r###" +
+ "###); + } + + #[test] + fn child_element() { + let formatted = format_element! {
"hello"
}; + insta::assert_snapshot!(formatted, @r###" +
+ "hello" +
+ "###); + } + + #[test] + fn child_element_single_textual() { + let formatted = format_element! {
"hello"
}; + insta::assert_snapshot!(formatted, @r###"
"hello"
"###); + } + + #[test] + fn child_element_single_textual_single_attr() { + let formatted = format_element! {
"hello"
}; + insta::assert_snapshot!(formatted, @r###"
"hello"
"###); + } + + #[test] + fn child_element_single_textual_multi_attr() { + let formatted = format_element! {
"hello"
}; + insta::assert_snapshot!(formatted, @r###" +
+ "hello" +
+ "###); + } + + #[test] + fn child_element_two_textual() { + let formatted = format_element! {
"The count is" {count}
}; + insta::assert_snapshot!(formatted, @r###"
"The count is" {count}
"###); + } + + #[test] + fn child_element_many_textual() { + let formatted = format_element! {
"The current count is: " {count} ". Increment by one is this: " {count + 1}
}; + insta::assert_snapshot!(formatted, @r###" +
+ "The current count is: " {count} + ". Increment by one is this: " {count + 1} +
+ "###); + } + + #[test] + fn html_unquoted_text() { + let formatted = format_element_from_string!(r##"
Unquoted text
"##); + insta::assert_snapshot!(formatted, @r#" +
+ Unquoted text +
"#); + } + + #[test] + fn html_unquoted_text_with_surrounding_spaces() { + let formatted = format_element_from_string!(r##"
Unquoted text with spaces
"##); + insta::assert_snapshot!(formatted, @r#" +
+ Unquoted text with spaces +
"#); + } + + #[test] + fn html_unquoted_text_multiline() { + let formatted = format_element_from_string!(indoc! {" +
+ Unquoted text + with spaces +
+ "}); + + insta::assert_snapshot!(formatted, @r###" +
+ Unquoted text + with spaces +
"###); + } + + #[test] + fn single_empty_line() { + let formatted = format_element_from_string!(indoc! {r#" +
+
+ "#}); + + insta::assert_snapshot!(formatted, @r###" +
+
+ "###); + } + + #[test] + fn multiple_empty_lines() { + let formatted = format_element_from_string!(indoc! {r#" +
+
+ "#}); + + insta::assert_snapshot!(formatted, @r###" +
+
+ "###); + } + + #[test] + fn surrounded_by_empty_lines() { + let formatted = format_element_from_string!(indoc! {r#" + +
+
+ + "#}); + + insta::assert_snapshot!(formatted, @r###" +
+
+ "###); + } + + #[test] + fn other_test() { + let formatted = format_element_from_string!(indoc! {r#" +
+
+ + "Sign in with google" +
+
+ "#}); + + insta::assert_snapshot!(formatted, @r###" +
+
+ + "Sign in with google" +
+
+ "###); + } +} diff --git a/crates/hirola-kit/src/formatter/format/expr.rs b/crates/hirola-kit/src/formatter/format/expr.rs new file mode 100644 index 0000000..0dcf79a --- /dev/null +++ b/crates/hirola-kit/src/formatter/format/expr.rs @@ -0,0 +1,200 @@ +use std::collections::HashMap; + +use syn::{spanned::Spanned, Block, Expr, ExprBlock, ExprLit, LitStr}; + +use crate::{formatter::format::Formatter, formatter::html_macro::HtmlMacroFormatter}; + +fn trim_start_with_max(str: &str, max_chars: usize) -> &str { + let mut chars = 0; + str.trim_start_matches(|c: char| { + if c.is_whitespace() { + chars += 1; + chars <= max_chars + } else { + false + } + }) +} + +impl Formatter<'_> { + pub fn string(&mut self, string: &str, start_column: usize) { + let mut iter = string.lines().enumerate().peekable(); + while let Some((line_num, line)) = iter.next() { + if line_num == 0 { + self.printer.word(line.to_string()) + } else { + self.printer + .word(trim_start_with_max(line, start_column).to_string()); + } + + if iter.peek().is_some() { + self.printer.hardbreak(); + } + } + } + + pub fn literal_str(&mut self, lit_str: &LitStr) { + self.printer.word("\""); + let string = lit_str.value(); + + let start_span = lit_str.span().start(); + self.string(&string, start_span.column); + self.printer.word("\""); + } + + pub fn node_value_block_expr( + &mut self, + block: &Block, + unwrap_single_expr_blocks: bool, + unwrap_single_lit_blocks: bool, + ) { + if let [syn::Stmt::Expr(single_expr, None)] = &block.stmts[..] { + // wrap with braces and do NOT insert spaces + if unwrap_single_expr_blocks + || (unwrap_single_lit_blocks && matches!(single_expr, syn::Expr::Lit(_))) + { + self.expr(single_expr); + } else { + self.printer.word("{"); + self.expr(single_expr); + self.printer.word("}"); + } + return; + } + + self.expr(&Expr::Block(ExprBlock { + attrs: vec![], + label: None, + block: block.clone(), + })) + } + + pub fn node_value_expr( + &mut self, + value: &syn::Expr, + unwrap_single_expr_blocks: bool, + unwrap_single_lit_blocks: bool, + ) { + // if single line expression, format as '{expr}' instead of '{ expr }' (prettyplease inserts a space) + if let syn::Expr::Block(expr_block) = value { + if expr_block.attrs.is_empty() { + return self.node_value_block_expr( + &expr_block.block, + unwrap_single_expr_blocks, + unwrap_single_lit_blocks, + ); + } + } + + self.expr(value) + } + + fn expr(&mut self, expr: &syn::Expr) { + if let syn::Expr::Lit(ExprLit { + lit: syn::Lit::Str(lit_str), + .. + }) = expr + { + self.literal_str(lit_str); + return; + } + + let start_line = expr.span().start().line; + let end_line = expr.span().end().line; + + let comments: HashMap = self + .comments + .iter() + .filter_map(|(line, _comment)| { + let line = *line; + if line >= start_line && line < end_line { + Some((line, *_comment)) + } else { + None + } + }) + .collect(); + + for line in comments.keys() { + self.comments.remove(line); + } + + leptosfmt_prettyplease::unparse_expr( + expr, + self.printer, + Some(&HtmlMacroFormatter::new(self.settings, comments)), + ); + } +} + +#[cfg(test)] +mod tests { + use rstml::node::Node; + + use crate::formatter::test_helpers::{element_from_string, format_with}; + use crate::formatter::*; + + macro_rules! format_element { + ($($tt:tt)*) => {{ + let comment = element_from_string! { $($tt)* }; + let settings = FormatterSettings { max_width: 40, ..Default::default() }; + + format_with(settings, |formatter| { + formatter.node(&Node::Element(comment)); + }) + }}; + } + + #[test] + fn multiline_string_as_child() { + let formatted = format_element! {r#"
+ "Lorem ipsum dolor sit amet, consectetur adipiscing elit, + sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. + Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. + Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." +
"#}; + + insta::assert_snapshot!(formatted, @r###" +
+ "Lorem ipsum dolor sit amet, consectetur adipiscing elit, + sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. + Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. + Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." +
+ "###); + } + + #[test] + fn string_whitespace_prefix() { + let formatted = format_element! {r#"
+ " foo" +
"#}; + + insta::assert_snapshot!(formatted, @r###" +
" foo"
+ "###); + } + + #[test] + fn multiline_string_whitespace_prefix() { + let formatted = format_element! {r#"
+ " Lorem ipsum dolor sit amet, consectetur adipiscing elit, + sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. + Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. + Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." +
"#}; + + insta::assert_snapshot!(formatted, @r###" +
+ " Lorem ipsum dolor sit amet, consectetur adipiscing elit, + sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. + Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. + Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." +
+ "###); + } +} diff --git a/crates/hirola-kit/src/formatter/format/fragment.rs b/crates/hirola-kit/src/formatter/format/fragment.rs new file mode 100644 index 0000000..8377df1 --- /dev/null +++ b/crates/hirola-kit/src/formatter/format/fragment.rs @@ -0,0 +1,68 @@ +use rstml::node::NodeFragment; + +use crate::formatter::Formatter; + +impl Formatter<'_> { + pub fn fragment(&mut self, fragment: &NodeFragment) { + self.printer.word("<>"); + self.children(&fragment.children, 0); + self.printer.word(""); + } +} + +#[cfg(test)] +mod tests { + use crate::{ + formatter::test_helpers::{format_with, fragment}, + formatter::FormatterSettings, + }; + + macro_rules! format_fragment { + ($($tt:tt)*) => {{ + let fragment = fragment! { $($tt)* }; + let settings = FormatterSettings { max_width: 40, ..Default::default() }; + format_with(settings, |formatter| { + formatter.fragment(&fragment); + }) + }}; + } + + #[test] + fn fragment_no_children() { + let formatted = format_fragment! { <> }; + insta::assert_snapshot!(formatted, @"<>"); + } + + #[test] + fn fragment_child_element() { + let formatted = format_fragment! { <>"hello" }; + insta::assert_snapshot!(formatted, @r###" + <> + "hello" + + "###); + } + + #[test] + fn fragment_child_element_single_textual() { + let formatted = format_fragment! { <>"hello" }; + insta::assert_snapshot!(formatted, @r###"<>"hello""###); + } + + #[test] + fn fragment_child_element_two_textual() { + let formatted = format_fragment! { <>"The count is" {count} }; + insta::assert_snapshot!(formatted, @r###"<>"The count is" {count}"###); + } + + #[test] + fn fragment_child_element_many_textual() { + let formatted = format_fragment! { <>"The current count is: " {count} ". Increment by one is this: " {count + 1} }; + insta::assert_snapshot!(formatted, @r###" + <> + "The current count is: " {count} + ". Increment by one is this: " {count + 1} + + "###); + } +} diff --git a/crates/hirola-kit/src/formatter/format/mac.rs b/crates/hirola-kit/src/formatter/format/mac.rs new file mode 100644 index 0000000..0755cb8 --- /dev/null +++ b/crates/hirola-kit/src/formatter/format/mac.rs @@ -0,0 +1,118 @@ +use std::collections::HashMap; + +use leptosfmt_pretty_printer::Printer; +use proc_macro2::Span; +use rstml::node::Node; +use syn::{spanned::Spanned, Macro}; + +use super::{Formatter, FormatterSettings}; + +pub struct HtmlMacro<'a> { + pub parent_ident: Option, + pub nodes: Vec, + pub span: Span, + pub mac: &'a Macro, +} + +impl<'a> HtmlMacro<'a> { + pub fn try_parse(parent_ident: Option, mac: &'a Macro) -> Option { + let tokens = mac.tokens.clone().into_iter(); + let nodes = rstml::parse2(tokens.collect()).ok()?; + + Some(Self { + parent_ident, + nodes, + span: mac.span(), + mac, + }) + } + + pub fn inner(&self) -> &Macro { + self.mac + } +} + +impl Formatter<'_> { + pub fn html_macro(&mut self, dom_mac: &HtmlMacro) { + let HtmlMacro { + parent_ident, + nodes, + span, + .. + } = dom_mac; + + self.start_line_offset = Some(span.start().line - 1); + + let indent = parent_ident + .map(|i| i + self.settings.tab_spaces) + .unwrap_or(0); + + self.printer.cbox(indent as isize); + + self.printer.word("html! {"); + self.dom_macro_nodes(nodes); + self.printer.word("}"); + self.printer.end(); + } + + fn dom_macro_nodes(&mut self, nodes: &[Node]) { + self.printer.cbox_indent(); + self.printer.space(); + + let mut iter = nodes.iter().peekable(); + while let Some(node) = iter.next() { + self.node(node); + + if iter.peek().is_some() { + self.printer.hardbreak(); + } + } + + self.printer.space(); + self.printer.end_dedent(); + } +} + +pub fn format_macro(mac: &HtmlMacro, settings: &FormatterSettings, source: Option<&str>) -> String { + let mut printer = Printer::new(settings.into()); + let mut formatter = match source { + Some(source) => Formatter::with_source(*settings, &mut printer, source), + None => Formatter::new(*settings, &mut printer, HashMap::new()), + }; + + formatter.html_macro(mac); + printer.eof() +} + +#[cfg(test)] +mod tests { + use super::format_macro; + use super::HtmlMacro; + use quote::quote; + use syn::Macro; + + macro_rules! dom_macro { + ($($tt:tt)*) => {{ + let mac: Macro = syn::parse2(quote! { $($tt)* }).unwrap(); + format_macro(&HtmlMacro::try_parse(None, &mac).unwrap(), &Default::default(), None) + }} + } + + #[test] + fn one_liner() { + let formatted = dom_macro!(html! {
"hi"
}); + insta::assert_snapshot!(formatted, @r###"html! {
"hi"
}"###); + } + + #[test] + fn with_nested_nodes() { + let formatted = dom_macro!(html! {
"hi"
}); + insta::assert_snapshot!(formatted, @r###" + html! { +
+ "hi" +
+ } + "###); + } +} diff --git a/crates/hirola-kit/src/formatter/format/mod.rs b/crates/hirola-kit/src/formatter/format/mod.rs new file mode 100644 index 0000000..c1d9855 --- /dev/null +++ b/crates/hirola-kit/src/formatter/format/mod.rs @@ -0,0 +1,134 @@ +use std::collections::HashMap; + +use leptosfmt_pretty_printer::{Printer, PrinterSettings}; + +mod attribute; +mod element; +mod expr; +mod fragment; +mod mac; +mod node; + +pub use mac::format_macro; +pub use mac::HtmlMacro; +use serde::Deserialize; +use serde::Serialize; + +#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)] +pub enum AttributeValueBraceStyle { + Always, + AlwaysUnlessLit, + WhenRequired, + Preserve, +} + +#[derive(Clone, Copy, Debug, Deserialize, Serialize)] +#[serde(default)] +pub struct FormatterSettings { + // Maximum width of each line + pub max_width: usize, + + // Number of spaces per tab + pub tab_spaces: usize, + + // Determines placement of braces around single expression attribute values + pub attr_value_brace_style: AttributeValueBraceStyle, +} + +impl Default for FormatterSettings { + fn default() -> Self { + Self { + max_width: 100, + tab_spaces: 4, + attr_value_brace_style: AttributeValueBraceStyle::WhenRequired, + } + } +} + +impl From<&FormatterSettings> for PrinterSettings { + fn from(value: &FormatterSettings) -> Self { + Self { + margin: value.max_width as isize, + indent: value.tab_spaces as isize, + min_space: 60, + } + } +} + +pub struct Formatter<'a> { + pub printer: &'a mut leptosfmt_pretty_printer::Printer, + pub settings: FormatterSettings, + line_offset: Option, + start_line_offset: Option, + comments: HashMap>, +} + +impl<'a> Formatter<'a> { + pub fn new( + settings: FormatterSettings, + printer: &'a mut Printer, + comments: HashMap>, + ) -> Self { + Self { + printer, + settings, + comments, + line_offset: None, + start_line_offset: None, + } + } + + pub fn with_source( + settings: FormatterSettings, + printer: &'a mut Printer, + source: &'a str, + ) -> Self { + Self { + printer, + settings, + comments: source + .lines() + .enumerate() + .filter_map(|(i, l)| { + if l.trim().is_empty() { + Some((i, None)) + } else { + l.split("//").nth(1).map(|l| (i, Some(l))) + } + }) + .collect(), + line_offset: None, + start_line_offset: None, + } + } + + pub fn write_comments(&mut self, line_index: usize) { + let last = self + .line_offset + .unwrap_or(self.start_line_offset.unwrap_or(0)); + + let comments_or_empty_lines: Vec<_> = (last..=line_index) + .filter_map(|l| self.comments.remove(&l)) + .collect(); + + let mut prev_is_empty_line = false; + + for comment_or_empty in comments_or_empty_lines { + if let Some(comment) = comment_or_empty { + self.printer.word("//"); + self.printer.word(comment.to_string()); + self.printer.hardbreak(); + prev_is_empty_line = false; + } else if self.line_offset.is_some() { + // Do not print multiple consecutive empty lines + if !prev_is_empty_line { + self.printer.hardbreak(); + } + + prev_is_empty_line = true; + } + } + + self.line_offset = Some(line_index); + } +} diff --git a/crates/hirola-kit/src/formatter/format/node.rs b/crates/hirola-kit/src/formatter/format/node.rs new file mode 100644 index 0000000..e2c6934 --- /dev/null +++ b/crates/hirola-kit/src/formatter/format/node.rs @@ -0,0 +1,104 @@ +use rstml::node::{Node, NodeBlock, NodeComment, NodeDoctype, NodeName, NodeText, RawText}; +use syn::spanned::Spanned; + +use crate::formatter::Formatter; + +impl Formatter<'_> { + pub fn node(&mut self, node: &Node) { + self.write_comments(node.span().start().line - 1); + + match node { + Node::Element(ele) => self.element(ele), + Node::Text(text) => self.node_text(text), + Node::RawText(text) => self.raw_text(text, true), + Node::Comment(comment) => self.comment(comment), + Node::Doctype(doctype) => self.doctype(doctype), + Node::Block(block) => self.node_block(block), + Node::Fragment(frag) => self.fragment(frag), + }; + } + + pub fn comment(&mut self, comment: &NodeComment) { + self.printer.word(""); + } + + pub fn doctype(&mut self, doctype: &NodeDoctype) { + self.printer.word(" "); + } + + pub fn node_text(&mut self, text: &NodeText) { + self.literal_str(&text.value); + } + + pub fn raw_text(&mut self, raw_text: &RawText, use_source_text: bool) { + let text = if use_source_text { + raw_text.to_source_text(false) + .expect("Cannot format unquoted text, no source text available, or unquoted text is used outside of element.") + } else { + raw_text.to_token_stream_string() + }; + + self.string(&text, raw_text.span().start().column); + // TODO: can convert it to quoted if need + // self.printer.word(text) + } + + pub fn node_name(&mut self, name: &NodeName) { + self.printer.word(name.to_string()); + } + + pub fn node_block(&mut self, block: &NodeBlock) { + match block { + NodeBlock::Invalid { .. } => panic!("Invalid block will not pass cargo check"), // but we can keep them instead of panic + NodeBlock::ValidBlock(b) => self.node_value_block_expr(b, false, false), + } + } +} + +#[cfg(test)] +mod tests { + use crate::formatter::test_helpers::{comment, doctype, format_with}; + use crate::formatter::*; + + macro_rules! format_comment { + ($($tt:tt)*) => {{ + let comment = comment! { $($tt)* }; + let settings = FormatterSettings { max_width: 40, ..Default::default() }; + format_with(settings, |formatter| { + formatter.comment(&comment); + }) + }}; + } + + macro_rules! format_doctype { + ($($tt:tt)*) => {{ + let doctype = doctype! { $($tt)* }; + let settings = FormatterSettings { max_width: 40, ..Default::default() }; + format_with(settings, |formatter| { + formatter.doctype(&doctype); + }) + }}; + } + + #[test] + fn html_comment() { + let formatted = format_comment!(); + insta::assert_snapshot!(formatted, @r###""###); + } + + #[test] + fn html_comment_long() { + let formatted = format_comment!(); + insta::assert_snapshot!(formatted, @r###""###); + } + + #[test] + fn html_doctype() { + let formatted = format_doctype!(< !DOCTYPE html >); + insta::assert_snapshot!(formatted, @" "); + } +} diff --git a/crates/hirola-kit/src/formatter/html_macro.rs b/crates/hirola-kit/src/formatter/html_macro.rs new file mode 100644 index 0000000..ea60b5e --- /dev/null +++ b/crates/hirola-kit/src/formatter/html_macro.rs @@ -0,0 +1,30 @@ +use std::collections::HashMap; + +use leptosfmt_prettyplease::MacroFormatter; + +use crate::formatter::format::{Formatter, FormatterSettings, HtmlMacro}; + +pub struct HtmlMacroFormatter<'a> { + settings: FormatterSettings, + comments: HashMap>, +} + +impl<'a> HtmlMacroFormatter<'a> { + pub fn new(settings: FormatterSettings, comments: HashMap>) -> Self { + Self { settings, comments } + } +} + +impl MacroFormatter for HtmlMacroFormatter<'_> { + fn format(&self, printer: &mut leptosfmt_pretty_printer::Printer, mac: &syn::Macro) -> bool { + if !mac.path.is_ident("html") { + return false; + } + + let Some(m) = HtmlMacro::try_parse(None, mac) else { return false; }; + + let mut formatter = Formatter::new(self.settings, printer, self.comments.clone()); + formatter.html_macro(&m); + true + } +} diff --git a/crates/hirola-kit/src/formatter/mod.rs b/crates/hirola-kit/src/formatter/mod.rs new file mode 100644 index 0000000..2f2df5a --- /dev/null +++ b/crates/hirola-kit/src/formatter/mod.rs @@ -0,0 +1,19 @@ +mod html_macro; +use std::path::Path; + +use source_file::{format_file_source, FormatError}; + +mod collect; +mod format; +mod source_file; + +#[cfg(test)] +mod test_helpers; + +pub use self::format::*; +pub use collect::collect_macros_in_file; + +pub fn format_file(path: &Path, settings: FormatterSettings) -> Result { + let file = std::fs::read_to_string(path)?; + format_file_source(&file, settings) +} diff --git a/crates/hirola-kit/src/formatter/source_file.rs b/crates/hirola-kit/src/formatter/source_file.rs new file mode 100644 index 0000000..f4993ef --- /dev/null +++ b/crates/hirola-kit/src/formatter/source_file.rs @@ -0,0 +1,382 @@ +use std::{io, ops::Range}; + +use crop::Rope; + +use syn::spanned::Spanned; +use thiserror::Error; + +use crate::{ + formatter::collect::collect_macros_in_file, + formatter::HtmlMacro, + formatter::{format_macro, FormatterSettings}, +}; + +#[derive(Error, Debug)] +pub enum FormatError { + #[error("could not read file")] + IoError(#[from] io::Error), + #[error("could not parse file")] + ParseError(#[from] syn::Error), +} + +#[derive(Debug)] +struct TextEdit { + range: Range, + new_text: String, +} + +pub(crate) fn format_file_source( + source: &str, + settings: FormatterSettings, +) -> Result { + let ast = syn::parse_file(source)?; + let macros = collect_macros_in_file(&ast); + format_source(source, macros, settings) +} + +fn format_source<'a>( + source: &'a str, + macros: Vec>, + settings: FormatterSettings, +) -> Result { + let mut rope: Rope = source.parse().unwrap(); + let mut edits = Vec::new(); + + for dom_mac in macros { + let mac = dom_mac.inner(); + let start = mac.path.span().start(); + let end = mac.delimiter.span().close().end(); + let start_byte = line_column_to_byte(&rope, start); + let end_byte = line_column_to_byte(&rope, end); + let new_text = format_macro(&dom_mac, &settings, Some(source)); + + edits.push(TextEdit { + range: start_byte..end_byte, + new_text, + }); + } + + let mut last_offset: isize = 0; + for edit in edits { + let start = edit.range.start; + let end = edit.range.end; + let new_text = edit.new_text; + + rope.replace( + (start as isize + last_offset) as usize..(end as isize + last_offset) as usize, + &new_text, + ); + last_offset += new_text.len() as isize - (end as isize - start as isize); + } + + Ok(rope.to_string()) +} + +fn line_column_to_byte(source: &Rope, point: proc_macro2::LineColumn) -> usize { + let line_byte = source.byte_of_line(point.line - 1); + let line = source.line(point.line - 1); + let char_byte: usize = line.chars().take(point.column).map(|c| c.len_utf8()).sum(); + line_byte + char_byte +} + +#[cfg(test)] +mod tests { + use indoc::indoc; + + use super::*; + + #[test] + fn it_works() { + let source = indoc! {r#" + fn main() { + html! {
"hello"
}; + } + "#}; + + let result = format_file_source(source, Default::default()).unwrap(); + insta::assert_snapshot!(result, @r###" + fn main() { + html! { +
+ "hello" +
+ }; + } + + "###); + } + + #[test] + fn for_each() { + let source = indoc! {r#" + fn main() { + html! {
+ {for item in (0..3).enumerate() { + html! {
  • "Her name is Kitty White."
  • } + }}
    }; + } + "#}; + + let result = format_file_source(source, Default::default()).unwrap(); + insta::assert_snapshot!(result, @r###" + fn main() { + html! { +
    + {for item in (0..3).enumerate() { + html! {
  • "Her name is Kitty White."
  • } + }} +
    + }; + } + + "###); + } + + #[test] + fn with_comments() { + let source = indoc! {r#" + // comment outside dom macro + fn main() { + html! { + // Top level comment +
    + // This is one beautiful message + "hello" // at the end of the line +
    // at the end of the line + // double + // comments + "hello"
    + +
    // same line comment + // with comment on the next line +
    + // comments with empty lines inbetween + + // and some more + // on the next line +
    }; + } + + // comment after dom macro + "#}; + + let result = format_file_source(source, Default::default()).unwrap(); + insta::assert_snapshot!(result, @r###" + // comment outside dom macro + fn main() { + html! { + // Top level comment +
    + // This is one beautiful message + // at the end of the line + "hello" + // at the end of the line +
    + // double + // comments + "hello" +
    + + // same line comment +
    // with comment on the next line +
    + // comments with empty lines inbetween + + // and some more + // on the next line +
    + }; + } + + // comment after dom macro + "###); + } + + #[test] + fn nested() { + let source = indoc! {r#" + fn main() { + html! {
    { + let a = 12; + + html! { + + {a} + } + }
    }; + } + "#}; + + let result = format_file_source(source, Default::default()).unwrap(); + insta::assert_snapshot!(result, @r###" + fn main() { + html! { +
    + + { + let a = 12; + html! { {a} } + } + +
    + }; + } + "###); + } + + #[test] + fn nested_with_comments() { + let source = indoc! {r#" + fn main() { + html! { + // parent div +
    + + // parent span + { + let a = 12; + + html! { + // wow, a span + {a} + } + }
    }; + } + "#}; + + let result = format_file_source(source, Default::default()).unwrap(); + insta::assert_snapshot!(result, @r###" + fn main() { + html! { + // parent div +
    + + // parent span + + { + let a = 12; + html! { + // wow, a span + {a} + } + } + +
    + }; + } + "###); + } + + #[test] + fn multiple() { + let source = indoc! {r#" + fn main() { + html! {
    "hello"
    }; + html! {
    "hello"
    }; + } + "#}; + + let result = format_file_source(source, Default::default()).unwrap(); + insta::assert_snapshot!(result, @r###" + fn main() { + html! { +
    + "hello" +
    + }; + html! { +
    + "hello" +
    + }; + } + "###); + } + + #[test] + fn with_special_characters() { + let source = indoc! {r#" + fn main() { + html! {
    "hello²💣"
    }; + } + "#}; + + let result = format_file_source(source, Default::default()).unwrap(); + insta::assert_snapshot!(result, @r###" + fn main() { + html! { +
    + "hello²💣" +
    + }; + } + "###); + } + + #[test] + fn inside_match_case() { + let source = indoc! {r#" + use hirola::prelude::*; + + enum ExampleEnum { + ValueOneWithAReallyLongName, + ValueTwoWithAReallyLongName, + } + + #[component] + fn Component(val: ExampleEnum) -> Dom { + match val { + ExampleEnum::ValueOneWithAReallyLongName => + html! { +
    +
    "Value One"
    +
    + }, + ExampleEnum::ValueTwoWithAReallyLongName => html! { +
    +
    "Value Two"
    +
    + }, + }; + } + "#}; + + let result = format_file_source(source, Default::default()).unwrap(); + insta::assert_snapshot!(result, @r###" + use hirola::prelude::*; + + enum ExampleEnum { + ValueOneWithAReallyLongName, + ValueTwoWithAReallyLongName, + } + + #[component] + fn Component(val: ExampleEnum) -> Dom { + match val { + ExampleEnum::ValueOneWithAReallyLongName => + html! { +
    +
    "Value One"
    +
    + }, + ExampleEnum::ValueTwoWithAReallyLongName => html! { +
    +
    "Value Two"
    +
    + }, + }; + } + "###); + } +} diff --git a/crates/hirola-kit/src/formatter/test_helpers.rs b/crates/hirola-kit/src/formatter/test_helpers.rs new file mode 100644 index 0000000..7332946 --- /dev/null +++ b/crates/hirola-kit/src/formatter/test_helpers.rs @@ -0,0 +1,122 @@ +use std::collections::HashMap; + +use leptosfmt_pretty_printer::Printer; +use rstml::node::{Node, NodeAttribute, NodeComment, NodeDoctype, NodeElement, NodeFragment}; + +macro_rules! attribute { + ($($tt:tt)*) => { + { + let tokens = quote::quote! { }; + let nodes = rstml::parse2(tokens).unwrap(); + crate::formatter::test_helpers::get_element_attribute(nodes, 0, 0) + }}; +} + +macro_rules! element { + ($($tt:tt)*) => { + { + let tokens = quote::quote! { $($tt)* }; + let nodes = rstml::parse2(tokens).unwrap(); + crate::formatter::test_helpers::get_element(nodes, 0) + }}; +} + +// Same as element, but use string representation of token stream. +// This is useful when testing unquoted text, +// because current `quote!` implementation cannot provide `Span::source_text` +// that is used in `raw_text` handler +macro_rules! element_from_string { + ($val: expr) => {{ + let tokens = ::from_str($val).unwrap(); + let nodes = rstml::parse2(tokens).unwrap(); + crate::formatter::test_helpers::get_element(nodes, 0) + }}; +} + +macro_rules! fragment { + ($($tt:tt)*) => { + { + let tokens = quote::quote! { $($tt)* }; + let nodes = rstml::parse2(tokens).unwrap(); + crate::formatter::test_helpers::get_fragment(nodes, 0) + }}; +} + +macro_rules! comment { + ($($tt:tt)*) => { + { + let tokens = quote::quote! { $($tt)* }; + let nodes = rstml::parse2(tokens).unwrap(); + crate::formatter::test_helpers::get_comment(nodes, 0) + }}; +} + +macro_rules! doctype { + ($($tt:tt)*) => { + { + let tokens = quote::quote! { $($tt)* }; + let nodes = rstml::parse2(tokens).unwrap(); + crate::formatter::test_helpers::get_doctype(nodes, 0) + }}; +} + +pub(crate) use attribute; +pub(crate) use comment; +pub(crate) use doctype; +pub(crate) use element; +pub(crate) use element_from_string; +pub(crate) use fragment; + +use crate::formatter::{Formatter, FormatterSettings}; + +pub fn get_element_attribute( + mut nodes: Vec, + element_index: usize, + attribute_index: usize, +) -> NodeAttribute { + let Node::Element(element) = + nodes.swap_remove(element_index) else { panic!("expected element") }; + element + .attributes() + .get(attribute_index) + .expect("attribute exist") + .clone() +} + +pub fn get_element(mut nodes: Vec, element_index: usize) -> NodeElement { + let Node::Element(element) = nodes.swap_remove(element_index) else { panic!("expected element") }; + element +} + +pub fn get_fragment(mut nodes: Vec, fragment_index: usize) -> NodeFragment { + let Node::Fragment(fragment) = nodes.swap_remove(fragment_index) else { panic!("expected fragment") }; + fragment +} + +pub fn get_comment(mut nodes: Vec, comment_index: usize) -> NodeComment { + let Node::Comment(comment) = nodes.swap_remove(comment_index) else { panic!("expected comment") }; + comment +} + +pub fn get_doctype(mut nodes: Vec, doctype_index: usize) -> NodeDoctype { + let Node::Doctype(doctype) = nodes.swap_remove(doctype_index) else { panic!("expected doctype") }; + doctype +} + +pub fn format_with_source( + settings: FormatterSettings, + source: &str, + run: impl FnOnce(&mut Formatter), +) -> String { + let mut printer = Printer::new((&settings).into()); + let mut formatter = Formatter::with_source(settings, &mut printer, source); + run(&mut formatter); + printer.eof() +} + +pub fn format_with(settings: FormatterSettings, run: impl FnOnce(&mut Formatter)) -> String { + let mut printer = Printer::new((&settings).into()); + let mut formatter = Formatter::new(settings, &mut printer, HashMap::new()); + run(&mut formatter); + printer.eof() +} diff --git a/crates/hirola-kit/src/main.rs b/crates/hirola-kit/src/main.rs new file mode 100644 index 0000000..ed0e119 --- /dev/null +++ b/crates/hirola-kit/src/main.rs @@ -0,0 +1,139 @@ +use std::{ + fs, panic, + path::{Path, PathBuf}, + time::Instant, +}; + +use crate::formatter::{format_file, FormatterSettings}; +use anyhow::Context; +use clap::{Args, Parser, Subcommand}; +use glob::glob; +use rayon::{iter::ParallelIterator, prelude::IntoParallelIterator}; +mod formatter; + +#[derive(Debug, Parser)] +#[command(name = "hirola-kit")] +#[command(about = "hirola development kit and tools", long_about = None)] +struct Cli { + #[command(subcommand)] + command: Command, +} + +#[derive(Debug, Subcommand)] +enum Command { + /// Format hirola html! macro code + #[command(arg_required_else_help = true)] + Format(Format), +} + +#[derive(Debug, Args)] +struct Format { + /// A file, directory or glob + input_pattern: String, + + // Maximum width of each line + #[arg(short, long)] + max_width: Option, + + // Number of spaces per tab + #[arg(short, long)] + tab_spaces: Option, + + // Config file + #[arg(short, long)] + config_file: Option, +} + +fn main() { + let args = Cli::parse(); + + match args.command { + Command::Format(args) => { + let settings = create_settings(&args).unwrap(); + + // Print settings + println!("{}", toml::to_string_pretty(&settings).unwrap()); + + let is_dir = fs::metadata(&args.input_pattern) + .map(|meta| meta.is_dir()) + .unwrap_or(false); + + let glob_pattern = if is_dir { + format!("{}/**/*.rs", &args.input_pattern) + } else { + args.input_pattern + }; + + let file_paths: Vec<_> = glob(&glob_pattern) + .expect("failed to read glob pattern") + .collect(); + + let total_files = file_paths.len(); + let start_formatting = Instant::now(); + file_paths.into_par_iter().for_each(|result| { + let print_err = |path: &Path, err| { + println!("❌ {}", path.display()); + eprintln!("\t\t{}", err); + }; + + match result { + Ok(path) => match format_glob_result(&path, settings) { + Ok(_) => println!("✅ {}", path.display()), + Err(err) => print_err(&path, &err.to_string()), + }, + Err(err) => print_err(err.path(), &err.error().to_string()), + }; + }); + let end_formatting = Instant::now(); + println!( + "Formatted {} files in {} ms", + total_files, + (end_formatting - start_formatting).as_millis() + ) + } + } +} + +fn format_glob_result(file: &PathBuf, settings: FormatterSettings) -> anyhow::Result<()> { + let formatted = panic::catch_unwind(|| format_file(file, settings)) + .map_err(|e| anyhow::anyhow!(e.downcast::().unwrap()))??; + fs::write(file, formatted)?; + Ok(()) +} + +fn create_settings(args: &Format) -> anyhow::Result { + let mut settings = args + .config_file + .as_ref() + .map(|path| { + load_config(path) + .with_context(|| format!("failed to load config file: {}", path.display())) + }) + .unwrap_or_else(|| { + let default_config: PathBuf = "hirola.toml".into(); + if default_config.exists() { + load_config(&default_config).with_context(|| { + format!("failed to load config file: {}", default_config.display()) + }) + } else { + Ok(FormatterSettings::default()) + } + })?; + + if let Some(max_width) = args.max_width { + settings.max_width = max_width; + } + + if let Some(tab_spaces) = args.tab_spaces { + settings.tab_spaces = tab_spaces; + } + Ok(settings) +} + +fn load_config(path: &PathBuf) -> anyhow::Result { + let config = fs::read_to_string(path).context("could not read config file")?; + let settings: FormatterSettings = + toml::from_str(&config).context("could not parse config file")?; + + Ok(settings) +} diff --git a/crates/hirola-macros/Cargo.toml b/crates/hirola-macros/Cargo.toml index 8fe327b..efc33a4 100644 --- a/crates/hirola-macros/Cargo.toml +++ b/crates/hirola-macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hirola-macros" -version = "0.2.0" +version = "0.3.0" authors = ["Geoffrey Mureithi "] edition = "2021" description = "Hirola is an un-opinionated web framework that is focused on simplicity and predictability" @@ -17,14 +17,14 @@ proc-macro = true [dependencies] -syn = { version = "1", features = ["full"] } -quote = { package = "quote", version = "1" } -proc-macro2 = "1" +proc-macro2 = "1.0.47" +quote = "1.0.21" +syn = "2.0.15" +rstml = "0.10" proc-macro-error = "1.0" proc-macro-hack = "0.5" -syn-rsx = { version = "0.8" } -once_cell = "1.8" +paste = { version = "1" } [dev-dependencies] -hirola-core = {path = "../hirola-core"} -trybuild = "1.0" \ No newline at end of file +hirola = { path = "../../", features=["ssr"] } +trybuild = "1.0" diff --git a/crates/hirola-macros/src/component.rs b/crates/hirola-macros/src/component.rs index 33c726b..c160634 100644 --- a/crates/hirola-macros/src/component.rs +++ b/crates/hirola-macros/src/component.rs @@ -1,5 +1,3 @@ -// Borrowed from render.rs -// https://github.com/render-rs/render.rs use proc_macro::TokenStream; use proc_macro_error::emit_error; use quote::quote; @@ -38,21 +36,22 @@ pub fn create_function_component(f: syn::ItemFn) -> TokenStream { }) .collect(); quote!( - let #struct_name { #(#input_names),* } = self; + let #struct_name { #(#input_names),* } = *self; ) }; TokenStream::from(quote! { - //#[derive(Debug)] + // #[derive(Debug)] #vis struct #struct_name #impl_generics #inputs_block - impl #impl_generics ::hirola::prelude::Render for #struct_name #ty_generics #where_clause { - fn render(&self) -> ::hirola::prelude::Dom { + impl #impl_generics ::hirola::prelude::Render for #struct_name #ty_generics #where_clause { + fn render_into(self: Box, dom: &Dom) -> Result<(), Error> { let result = { #inputs_reading #block }; - result + Box::new(result).render_into(&dom)?; + Ok(()) } } }) diff --git a/crates/hirola-macros/src/lib.rs b/crates/hirola-macros/src/lib.rs index ff14efe..1142f2b 100644 --- a/crates/hirola-macros/src/lib.rs +++ b/crates/hirola-macros/src/lib.rs @@ -1,23 +1,35 @@ -mod component; - -use proc_macro2::TokenStream; +use proc_macro2::{Ident, Span, TokenStream}; use proc_macro_error::proc_macro_error; -use quote::quote; -use syn::{parse_macro_input, Expr, ExprBlock, ExprForLoop, Ident, Stmt}; -use syn_rsx::{Node, NodeType, ParserConfig}; +use quote::{format_ident, quote}; +use rstml::{ + node::{Node, NodeAttribute, NodeBlock}, + Parser, ParserConfig, +}; +use syn::{ + parse_macro_input, spanned::Spanned, Block, Expr, ExprCast, ExprForLoop, ExprIf, ExprMatch, + Stmt, Type, +}; + +mod component; fn to_token_stream(input: proc_macro::TokenStream) -> TokenStream { - match syn_rsx::parse_with_config(input, ParserConfig::new()) { - Ok(mut nodes) => { - if nodes.len() == 1 { - let node = nodes.pop().expect("unable to convert node to tokens"); - node_to_tokens(node) - } else { - fragment_to_tokens(nodes) - } + let config = ParserConfig::default().recover_block(true); + let parser = Parser::new(config); + let (mut nodes, errors) = parser.parse_recoverable(input).split_vec(); + let errors = errors.into_iter().map(|e| e.emit_as_expr_tokens()); + let nodes_output = if nodes.len() == 1 { + let node = nodes.pop().expect("unable to convert node to tokens"); + node_to_tokens(node) + } else { + fragment_to_tokens(nodes) + }; + quote! { + { + #(#errors;)* + #nodes_output } - Err(error) => error.to_compile_error(), } + .into() } fn fragment_to_tokens(nodes: Vec) -> TokenStream { @@ -25,11 +37,10 @@ fn fragment_to_tokens(nodes: Vec) -> TokenStream { let children_tokens = children_to_tokens(nodes); tokens.extend(quote! { { - let element: ::hirola::prelude::DomType = ::hirola::prelude::GenericNode::fragment(); + let mut template = ::hirola::prelude::Dom::new(); #children_tokens - element + template } - }); tokens } @@ -37,163 +48,159 @@ fn fragment_to_tokens(nodes: Vec) -> TokenStream { fn node_to_tokens(node: Node) -> TokenStream { let mut tokens = TokenStream::new(); - // NodeType::Element nodes can't have no name - let name = node.name_as_string(); + match node { + Node::Element(node) => { + let name = node.name().to_string(); + if name[0..1].to_lowercase() == name[0..1] { + let attributes = node.attributes().iter().map(attribute_to_tokens); - if let Some(name) = name { - if name[0..1].to_lowercase() == name[0..1] { - let attributes = node - .attributes - .iter() - .map(attribute_to_tokens); + let children_tokens = children_to_tokens(node.children.clone()); - let children_tokens = children_to_tokens(node.children); - - tokens.extend(quote! { - - { - - let element: ::hirola::prelude::DomType = ::hirola::prelude::GenericNode::element(#name); - #children_tokens - #(#attributes)* - element - } - }); - } else { - let fnname: Ident = syn::parse_str(&name).unwrap(); + tokens.extend(quote! { + { + let mut template: ::hirola::prelude::Dom = ::hirola::prelude::Dom::element(#name); + #children_tokens + #(#attributes)* + template + } + }); + } else { + let fnname: Ident = syn::parse_str(&name).unwrap(); - let mut attributes = node - .attributes - .iter() - .map(|attribute| match &attribute.value { - Some(expr) => { - let ident: proc_macro2::TokenStream = - attribute.name_as_string().unwrap().parse().unwrap(); - quote! { - #ident: #expr + let mut attributes = node + .attributes() + .iter() + .map(|attribute| match &attribute { + NodeAttribute::Block(expr) => { + quote! { + #expr + } } - } - None => quote! {}, - }) - .collect::>(); - if !node.children.is_empty() { - let children_tokens = children_to_tokens(node.children); - attributes.extend(vec![quote! { - children: { - let element: ::hirola::prelude::DomType = ::hirola::prelude::GenericNode::fragment(); - #children_tokens - ::hirola::prelude::TemplateResult::new(element) - } - }]); - } + NodeAttribute::Attribute(attr) => { + let key = &attr.key; + let value = &attr.value(); + quote! { + #key : #value + } + } + }) + .collect::>(); + if !node.children.is_empty() { + let children_tokens = children_to_tokens(node.children); + attributes.extend(vec![quote! { + children: { + Box::new(#children_tokens) + } + }]); + } - let quoted = if attributes.is_empty() { - quote!({&#fnname }) - } else { - quote!({ &#fnname {#(#attributes),*} }) - }; - tokens.extend(quote! { - { - ::hirola::prelude::untrack(|| ::hirola::prelude::TemplateResult::inner_element( &#quoted.render())) - } - }); + let quoted = if attributes.is_empty() { + quote!(#fnname) + } else { + quote!(#fnname {#(#attributes),*}) + }; + tokens.extend(quote! { + #quoted + }); + } } - } else { - tokens.extend(fragment_to_tokens(node.children)); + Node::Fragment(fragment) => tokens.extend(fragment_to_tokens(fragment.children)), + _ => {} } tokens } -fn attribute_to_tokens(attribute: &Node) -> TokenStream { - match &attribute.value { - Some(value) => { - match attribute.node_type { - NodeType::Block => { +fn some_kind_of_uppercase_first_letter(s: &str) -> String { + let mut c = s.chars(); + match c.next() { + None => String::new(), + Some(f) => f.to_uppercase().collect::() + c.as_str(), + } +} + +fn attribute_to_tokens(attribute: &NodeAttribute) -> TokenStream { + match attribute { + NodeAttribute::Block(block) => quote! { + #block + } + .into(), + NodeAttribute::Attribute(attr) => { + let name = attr.key.to_string(); + let value = attr.value(); + if name.starts_with("on:") { + let name = name.replace("on:", ""); + quote! { + ::hirola::prelude::Dom::event( + &mut template, + #name, + ::std::boxed::Box::new(#value), + ); + } + } else if name.starts_with("use:") { + let effect = if value.is_some() { quote! { #value } - } - NodeType::Attribute => { - // NodeType::Attribute nodes can't have no name - let name = attribute - .name_as_string() - .expect("attribute should have name"); + } else { + let cleaned_name = Ident::new(&name.replace("use:", ""), Span::call_site()); + quote! { + #cleaned_name + } + }; + quote! { + ::hirola::prelude::Dom::effect( + &template, + #effect + ); - if name.starts_with("on:") { - let name = name.replace("on:", ""); - quote! { - ::hirola::prelude::GenericNode::event( - &element, - #name, - ::std::boxed::Box::new(#value), - ); - } - } else if name.starts_with("mixin:") { - let name_space = name.replace("mixin:", ""); - quote! { - let element = ::std::clone::Clone::clone(&element); - { - let element = ::std::clone::Clone::clone(&element); - #[allow(unused_braces)] - let res = hirola::prelude::Mixin::mixin(#value, #name_space, element); - if let Err(err) = res { - let current_line = std::line!(); - let this_file = std::file!(); - web_sys::console::error_1(&format!("{}, LINE: {}, FILE: {}", err, current_line, this_file).into()); - } - } + } + } else if name.starts_with("mixin:") { + let name_space = name.replace("mixin:", ""); + let ns_struct = + format_ident!("{}", &some_kind_of_uppercase_first_letter(&name_space)); + quote! { + hirola::prelude::Mixin::<#ns_struct>::mixin(#value, &template); + } + } else if &name == "ref" { + quote! { + let _ = ::hirola::prelude::NodeRef::set( + &#value, + ::std::clone::Clone::clone(&template.node()), + ); - } - } else if &name == "ref" { - quote! { - ::hirola::prelude::NodeRef::set( - &#value, - ::std::clone::Clone::clone(&element), + } + } else if name.starts_with("bind:") { + let attribute_name = convert_name(&name).replace("bind:", ""); + quote! { + { + use hirola::signal::SignalExt; + let t = template.clone(); + ::hirola::prelude::Dom::attribute( + &template, + #attribute_name, + &::std::format!("{}", #value.get_cloned()), + ); + template.effect(#value.signal_ref(move |value| { + ::hirola::prelude::Dom::attribute( + &t, + #attribute_name, + &::std::format!("{}", value), ); + }).to_future()); + } - } - } else { - let attribute_name = convert_name(&name); - quote! { - ::hirola::prelude::create_effect({ - let element = ::std::clone::Clone::clone(&element); - move || { - ::hirola::prelude::GenericNode::set_attribute( - &element, - #attribute_name, - &::std::format!("{}", #value), - ); - } - }); - } - } } - _ => { - quote! { - compile_error!("Unexpected NodeType") - } + } else { + let attribute_name = convert_name(&name); + quote! { + ::hirola::prelude::Dom::attribute( + &mut template, + #attribute_name, + &::std::format!("{}", #value), + ); } } } - None => { - let name = convert_name( - &attribute - .name_as_string() - .expect("attribute should have a name"), - ); - quote! { - ::hirola::prelude::create_effect({ - let element = ::std::clone::Clone::clone(&element); - move || { - ::hirola::prelude::GenericNode::set_attribute( - &element, - #name, - &::std::format!(""), - ); - } - }); - } - } } } @@ -202,71 +209,219 @@ fn children_to_tokens(children: Vec) -> TokenStream { let mut tokens = TokenStream::new(); if !children.is_empty() { for child in children { - match child.node_type { - NodeType::Element => { + match child { + Node::Element(_) => { let node = node_to_tokens(child); append_children.extend(quote! { - ::hirola::prelude::GenericNode::append_child(&element, &#node); + ::hirola::prelude::Dom::append_render(&mut template, #node ); }); } - NodeType::Text => { - let s = child - .value_as_string() - .expect("expecting a string on a text node"); + Node::Text(text) => { append_children.extend(quote! { - ::hirola::prelude::GenericNode::append_child( - &element, + ::hirola::prelude::Dom::append_render( + &mut template, #[allow(unused_braces)] - &::hirola::prelude::GenericNode::text_node(#s), + ::hirola::prelude::Dom::text(#text), ); }); } - NodeType::Comment => { - // let s = child - // .value_as_string() - // .expect("expecting a string on a comment node"); - // tokens.extend(quote! { - // #receiver.push(sauron_core::prelude::html::comment(#s)); - // }); - } - NodeType::Doctype => { - // let value = child - // .value_as_string() - // .expect("expecting a string value on a doctype"); - // tokens.extend(quote! { - // #receiver.push(sauron_core::prelude::html::doctype(#value)); - // }); + Node::Comment(comment) => { + let s = comment.value; + append_children.extend(quote! { + ::hirola::prelude::Dom::append_render( + &mut template, + #[allow(unused_braces)] + ::hirola::prelude::Dom::new_from_node(::hirola::prelude::GenericNode::comment(#s)), + ); + }); } - NodeType::Block => match child.value { - Some(syn::Expr::Block(expr)) => match braced_for_loop(&expr) { - Some(ExprForLoop { + Node::Doctype(_) => {} + Node::Block(block) => match block { + NodeBlock::ValidBlock(block) => match braced_for_control(&block) { + Some(Control::ExprForLoop(ExprForLoop { pat, expr, body, .. - }) => { - append_children.extend(quote! { - for #pat in #expr { - ::hirola::prelude::GenericNode::append_child( - &element, - &#body.inner_element(), - ); + })) => { + if let Expr::Cast(ExprCast { ty, expr, .. }) = expr.as_ref() { + match ty.as_ref() { + &Type::Infer(_) => { + append_children.extend(quote! { + let template = { + let props = ::hirola::prelude::IndexedProps { + iterable: #expr, + template: move | #pat | { + #body + } + }; + let indexed = ::hirola::prelude::Indexed { + props + }; + Box::new(indexed) + }; + }); + } + &Type::Path(ref path) => { + let ident = Ident::new("SignalVec", Span::call_site()); + if path.path.is_ident(&ident) { + append_children.extend(quote! { + let template = { + let props = ::hirola::prelude::IndexedProps { + iterable: #expr, + template: move | #pat | { + #body + } + }; + let indexed = ::hirola::prelude::Indexed { + props + }; + Box::new(indexed) + }; + }); + } else { + append_children.extend( + syn::Error::new( + ty.span(), + "expected SignalVec or _", + ) + .to_compile_error(), + ); + } + } + _ => { + append_children.extend( + syn::Error::new(ty.span(), "expected SignalVec or _") + .to_compile_error(), + ); + } } - }); + } else { + append_children.extend(quote! { + for #pat in #expr { + ::hirola::prelude::Dom::append_child( + &mut template, + #body, + ).unwrap(); + } + }); + } } + Some(Control::ExprIf(ExprIf { + cond, + then_branch, + else_branch, + .. + })) => { + let (_, else_branch) = else_branch.unwrap(); + + if let Expr::Cast(ExprCast { ty, expr, .. }) = cond.as_ref() { + match ty.as_ref() { + &Type::Infer(_) => { + append_children.extend(quote! { + let mut template = { + let switch = ::hirola::prelude::Switch { + signal: #expr, + renderer: |res| { + if res { + #then_branch + } else { + #else_branch + } + } + }; + Box::new(switch) + }; + }); + } + &Type::Path(ref path) => { + let ident = Ident::new("Signal", Span::call_site()); + if path.path.is_ident(&ident) { + append_children.extend(quote! { + let mut template = { + let switch = ::hirola::prelude::Switch { + signal: #expr, + renderer: |res| { + if res { + #then_branch + } else { + #else_branch + } + } + }; + Box::new(switch) + }; + }); + } else { + append_children.extend( + syn::Error::new(ty.span(), "expected Signal or _") + .to_compile_error(), + ); + } + } + _ => { + append_children.extend( + syn::Error::new( + ty.span(), + "expected Signal, SignalVec or _", + ) + .to_compile_error(), + ); + } + } + } else { + append_children.extend(quote! { + ::hirola::prelude::Dom::append_render( + &mut template, + #[allow(unused_braces)] + #block, + ); + }); + } + } + + Some(Control::ExprMatch(ExprMatch { expr, arms, .. })) => match *expr { + Expr::Await(fut) => { + let fut = fut.base; + append_children.extend(quote! { + let suspense = { + + let suspense = ::hirola::prelude::Suspense { + future: Box::pin(#fut), + template: Box::new(move |res| { + match res { + #(#arms)* + } + }) + }; + suspense + }; + ::hirola::prelude::Dom::append_render( + &mut template, + suspense + ); + + }); + } + _ => { + append_children.extend(quote! { + ::hirola::prelude::Dom::append_render( + &mut template, + #[allow(unused_braces)] + #block, + ); + }); + } + }, _ => { append_children.extend(quote! { - ::hirola::prelude::GenericNode::append_render( - &element, - ::std::boxed::Box::new(move || { - #[allow(unused_braces)] - ::std::boxed::Box::new(#expr) - }), + ::hirola::prelude::Dom::append_render( + &mut template, + #[allow(unused_braces)] + #block, ); }); } }, - _ => { - return quote! { - compile_error!("Unexpected missing block for NodeType::Block") - } + NodeBlock::Invalid { body, .. } => { + return syn::Error::new(body.span(), "Invalid block").to_compile_error() } }, _ => { @@ -276,32 +431,32 @@ fn children_to_tokens(children: Vec) -> TokenStream { } } } - } else { - // tokens.extend(quote! { - // let #receiver = Vec::new(); - // }); } let quoted = quote! { - // let element = #tag_name; - // #(#set_attributes)* - // #(#set_event_listeners)* - // #(#set_noderefs)* #(#append_children)* - // element + }; tokens.extend(quoted); tokens } -fn braced_for_loop(expr: &ExprBlock) -> Option<&ExprForLoop> { - let len = expr.block.stmts.len(); +enum Control { + ExprForLoop(ExprForLoop), + ExprIf(ExprIf), + ExprMatch(ExprMatch), +} + +fn braced_for_control(block: &Block) -> Option { + let len = block.stmts.len(); if len != 1 { None } else { - let stmt = &expr.block.stmts[0]; + let stmt = &block.stmts[0]; match stmt { - Stmt::Expr(Expr::ForLoop(expr)) => Some(expr), + Stmt::Expr(Expr::ForLoop(expr), _) => Some(Control::ExprForLoop(expr.clone())), + Stmt::Expr(Expr::If(expr), _) => Some(Control::ExprIf(expr.clone())), + Stmt::Expr(Expr::Match(expr), _) => Some(Control::ExprMatch(expr.clone())), _ => None, } } @@ -325,7 +480,7 @@ pub fn html(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let output = to_token_stream(input); let quoted = quote! { - ::hirola::prelude::TemplateResult::new(::std::convert::Into::<_>::into(#output)) + ::hirola::prelude::Dom::from(#output) }; quoted.into() } diff --git a/crates/hirola-macros/tests/component.rs b/crates/hirola-macros/tests/component.rs new file mode 100644 index 0000000..dfdc1e0 --- /dev/null +++ b/crates/hirola-macros/tests/component.rs @@ -0,0 +1,41 @@ +use hirola::prelude::*; +use hirola_macros::html; + +#[component] +fn MyComponent() -> Dom { + let world = "planet"; + html! { +

    {world}

    + } +} + +#[component] +fn MyComponentWithProps(world: &'static str) -> Dom { + html! { +

    {world}

    + } +} + +#[test] +fn it_renders_component() { + let result = render_to_string({ + html! { + <> + + + } + }); + assert_eq!("

    planet

    ", result); +} + +#[test] +fn it_renders_component_with_props() { + let result = render_to_string({ + html! { + <> + + + } + }); + assert_eq!("

    hirola

    ", result); +} diff --git a/crates/hirola-macros/tests/lib.rs b/crates/hirola-macros/tests/lib.rs index d297b2b..eb6e236 100644 --- a/crates/hirola-macros/tests/lib.rs +++ b/crates/hirola-macros/tests/lib.rs @@ -1,20 +1,12 @@ +use hirola::prelude::*; use hirola_macros::html; -use hirola_core::TemplateResult; - -// fn entry(entry: u8) -> String { -// html_to_string! { -//
  • {entry}
  • -// } -// } #[test] -fn test() { +fn it_works() { let world = "planet"; - - assert_eq!( - html! { -

    {world}

    - }, - TemplateResult::empty() - ); + let template = html! { +

    {world}

    + }; + let result = render_to_string(template); + assert_eq!("

    planet

    ", result); } diff --git a/examples/canvas/src/main.rs b/examples/canvas/src/main.rs index ee8e986..51ed9ae 100644 --- a/examples/canvas/src/main.rs +++ b/examples/canvas/src/main.rs @@ -3,7 +3,7 @@ use tool::SignTool; mod tool; -fn signature_pad(_app: &HirolaApp) -> Dom { +fn signature_pad(_app: &App) -> Dom { let canvas = NodeRef::new(); let tool = SignTool::new(canvas.clone()); @@ -15,7 +15,7 @@ fn signature_pad(_app: &HirolaApp) -> Dom { }); let mouse_move = tool.callback(|tool, e| { - if *tool.is_mouse_clicked.get() && *tool.is_mouse_in_canvas.get() { + if tool.is_mouse_clicked.get() && tool.is_mouse_in_canvas.get() { tool.update_position(e); tool.draw(); } @@ -50,7 +50,5 @@ fn main() { let document = window.document().unwrap(); let body = document.body().unwrap(); - let app = HirolaApp::new(); - - app.mount(&body, signature_pad); + hirola::prelude::render_to(signature_pad, &body); } diff --git a/examples/canvas/src/tool.rs b/examples/canvas/src/tool.rs index 2c24e2a..0f11ebc 100644 --- a/examples/canvas/src/tool.rs +++ b/examples/canvas/src/tool.rs @@ -6,24 +6,24 @@ use web_sys::MouseEvent; #[derive(Clone)] pub struct SignTool { - pub(crate) is_mouse_clicked: Signal, - pub(crate) is_mouse_in_canvas: Signal, - pub(crate) prev_x: Signal, - pub(crate) cur_x: Signal, - pub(crate) prev_y: Signal, - pub(crate) cur_y: Signal, + pub(crate) is_mouse_clicked: Mutable, + pub(crate) is_mouse_in_canvas: Mutable, + pub(crate) prev_x: Mutable, + pub(crate) cur_x: Mutable, + pub(crate) prev_y: Mutable, + pub(crate) cur_y: Mutable, pub(crate) canvas: NodeRef, } impl SignTool { pub fn new(canvas: NodeRef) -> Self { SignTool { - is_mouse_clicked: Signal::new(false), - is_mouse_in_canvas: Signal::new(false), - prev_x: Signal::new(0), - cur_x: Signal::new(0), - prev_y: Signal::new(0), - cur_y: Signal::new(0), + is_mouse_clicked: Mutable::new(false), + is_mouse_in_canvas: Mutable::new(false), + prev_x: Mutable::new(0), + cur_x: Mutable::new(0), + prev_y: Mutable::new(0), + cur_y: Mutable::new(0), canvas, } } @@ -36,12 +36,23 @@ impl SignTool { .inner_element() .dyn_into::() .unwrap(); - self.prev_x.set(*self.cur_x.get()); - self.prev_y.set(*self.cur_y.get()); + self.prev_x.set(self.cur_x.get()); + self.prev_y.set(self.cur_y.get()); self.cur_x.set(e.client_x() - canvas.offset_left()); self.cur_y.set(e.client_y() - canvas.offset_top()); } + pub fn callback(&self, f: F) -> Box + where + F: Fn(Self, E) + 'static, + { + let state = self.clone(); + let cb = move |e: E| { + f(state.clone(), e); + }; + Box::new(cb) + } + pub fn draw(&self) { let canvas = self .canvas @@ -58,13 +69,11 @@ impl SignTool { .unwrap(); context.begin_path(); - context.move_to((*self.prev_x.get()).into(), (*self.prev_y.get()).into()); - context.line_to((*self.cur_x.get()).into(), (*self.cur_y.get()).into()); + context.move_to((self.prev_x.get()).into(), (self.prev_y.get()).into()); + context.line_to((self.cur_x.get()).into(), (self.cur_y.get()).into()); context.set_stroke_style(&JsValue::from_str("black")); context.set_line_width(2.0); context.stroke(); context.close_path(); } } - -impl State for SignTool {} diff --git a/examples/counter/Cargo.toml b/examples/counter/Cargo.toml index 8cecf95..e4382c6 100644 --- a/examples/counter/Cargo.toml +++ b/examples/counter/Cargo.toml @@ -6,26 +6,9 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -hirola = { path = "../../", features=["form"]} -wasm-bindgen = {version = "0.2"} - -[dependencies.web-sys] -features = [ - "console", - "Comment", - "Document", - "DocumentFragment", - "Element", - "Event", - "HtmlElement", - "Node", - "Text", - "Window", -] - -version = "0.3" +hirola = { path = "../../", features = ["dom", "app"]} [dev-dependencies] wasm-bindgen-test = "0.3.0" -hirola = { path = "../../", default-features=false, features =["router", "global-state", "ssr"]} \ No newline at end of file +hirola = { path = "../../", default-features=false, features =["dom"]} \ No newline at end of file diff --git a/examples/counter/index.html b/examples/counter/index.html index 9bfe26e..391d52c 100644 --- a/examples/counter/index.html +++ b/examples/counter/index.html @@ -1,11 +1,9 @@ - - - Hirola Counter - - - + + + + Hirola Counter + + + \ No newline at end of file diff --git a/examples/counter/src/main.rs b/examples/counter/src/main.rs index c725702..d3f1c20 100644 --- a/examples/counter/src/main.rs +++ b/examples/counter/src/main.rs @@ -1,32 +1,28 @@ +use std::fmt::Display; use hirola::prelude::*; +use hirola::signal::Mutable; -fn counter(_app: &HirolaApp) -> Dom { - let count = Signal::new(0); - html! { -
    - - {count.get()} -
    - - } +#[component] +pub fn Menu(title: &'static str, children: R) -> Dom { + Dom::new() } -fn main() { - let window = web_sys::window().unwrap(); - let document = window.document().unwrap(); - let body = document.body().unwrap(); - let app = HirolaApp::new(); - app.mount(&body, counter); -} -#[cfg(test)] -mod tests { - use super::*; - use wasm_bindgen_test::*; - #[wasm_bindgen_test] - fn counter_renders() { - let app = HirolaApp::new(); - let res = app.render_to_string(counter); - assert_eq!("
    0
    ", &res); +fn counter() -> Dom { + let count = Mutable::new(0i32); + let decrement = count.callback(|s| *s.lock_mut() -= 1); + let increment = count.callback(|s| *s.lock_mut() += 1); + html! { + <> + + {count} + +
    + } } + +fn main() { + let root = render(counter()).unwrap(); + std::mem::forget(root); +} diff --git a/examples/docs/Cargo.toml b/examples/docs/Cargo.toml index 7683168..fb86cfd 100644 --- a/examples/docs/Cargo.toml +++ b/examples/docs/Cargo.toml @@ -6,19 +6,11 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -hirola = { path = "../../", features =["router", "global-state"]} -wasm-bindgen = {version = "0.2"} - - -[dependencies.web-sys] -features = [ - "Window", - "DomTokenList", - "HtmlInputElement", -] - -version = "0.3" - -[dev-dependencies] -wasm-bindgen-test = "0.3.0" -hirola = { path = "../../", default-features=false, features =["router", "global-state", "ssr"]} \ No newline at end of file +hirola = { path = "../../", default-features = false, features = [ + "app", + "ssr", +] } +glob = "0.3.1" +comrak = { version = "0.18", features = ["syntect"] } +fronma = "0.2" +serde = { version = "1", features = ["derive"] } diff --git a/examples/docs/Trunk.toml b/examples/docs/Trunk.toml deleted file mode 100644 index f21ed92..0000000 --- a/examples/docs/Trunk.toml +++ /dev/null @@ -1,11 +0,0 @@ -[build] -# The index HTML file to drive the bundling process. -target = "index.html" -# Build in release mode. -release = true -# The output dir for all final assets. -dist = "../../public" -# The public URL from which assets are to be served. -public_url = "/" -# Whether to include hash values in the output file names. -filehash = true \ No newline at end of file diff --git a/examples/docs/index.html b/examples/docs/index.html deleted file mode 100644 index abcaf50..0000000 --- a/examples/docs/index.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - - Hirola Documentation in Hirola - - - - - - - diff --git a/examples/docs/src/components/code_preview.rs b/examples/docs/src/components/code_preview.rs deleted file mode 100644 index fac6c5f..0000000 --- a/examples/docs/src/components/code_preview.rs +++ /dev/null @@ -1,28 +0,0 @@ -use hirola::prelude::*; -use wasm_bindgen::prelude::*; -use web_sys::Element; - -#[wasm_bindgen] -extern "C" { - /// Highlight.js - #[wasm_bindgen(js_namespace = hljs)] - fn highlightElement(element: Element); -} - -/// mixin to highlight code -fn highlight_code<'a>(_example_name: &'a str) -> Box () + 'a> { - let cb = move |node: DomNode| { - let element = node.unchecked_into::(); - highlightElement(element); - }; - Box::new(cb) -} - -#[component] -pub fn CodePreview<'a>(code: &'a str, file: &'a str) -> Dom { - let file = file.to_string(); - let code = code.to_string(); - html! { -
    {code.clone()}
    - } -} diff --git a/examples/docs/src/components/example.rs b/examples/docs/src/components/example.rs deleted file mode 100644 index e69de29..0000000 diff --git a/examples/docs/src/components/mod.rs b/examples/docs/src/components/mod.rs index 87e9cad..618ba87 100644 --- a/examples/docs/src/components/mod.rs +++ b/examples/docs/src/components/mod.rs @@ -1,7 +1,3 @@ #![allow(non_snake_case)] - -pub mod code_preview; -pub mod example; pub mod logo; -pub mod seo_title; pub mod side_bar; diff --git a/examples/docs/src/components/seo_title.rs b/examples/docs/src/components/seo_title.rs deleted file mode 100644 index ea85d7b..0000000 --- a/examples/docs/src/components/seo_title.rs +++ /dev/null @@ -1,13 +0,0 @@ -use std::fmt::Display; - -use hirola::prelude::*; - -#[component] -pub fn SeoTitle<'a, T: Display + ?Sized>(title: &'a T) -> Dom { - web_sys::window() - .unwrap() - .document() - .unwrap() - .set_title(&format!("{title}")); - Dom::empty() -} diff --git a/examples/docs/src/components/side_bar.rs b/examples/docs/src/components/side_bar.rs index 7bfbc32..2dfda6a 100644 --- a/examples/docs/src/components/side_bar.rs +++ b/examples/docs/src/components/side_bar.rs @@ -1,101 +1,128 @@ use hirola::prelude::*; #[component] -pub fn SideBar(router: Router) -> Dom { +pub fn SideBar() -> Dom { html! {
      -
    • - "Home" -
    • -
    • - "Basics" - -
    • -
    • - "Inbuilt Mixins" - } } diff --git a/examples/docs/src/main.rs b/examples/docs/src/main.rs index 8e94838..3d9a9df 100644 --- a/examples/docs/src/main.rs +++ b/examples/docs/src/main.rs @@ -1,160 +1,235 @@ mod components; -mod pages; +// mod markdown; +// mod pages; + +use std::{fs::File, path::PathBuf}; use components::logo::HirolaLogo; +use comrak::{markdown_to_html_with_plugins, ComrakPlugins}; use hirola::prelude::*; -use pages::{ - async_page, event_handling_page, extending_page, forms_page, getting_started_page, home, - inner_mixins, mixins_page, reactivity_page, router_page, ssr_page, state_page, templating_page, - testing_page, -}; use crate::components::side_bar::SideBar; +use serde::Deserialize; -// macro_rules! make_example { - -// ($jsx:expr)=>{ +use comrak::plugins::syntect::SyntectAdapter; -// { -// // { -// // html! { -// //
      {stringify!($jsx)}
      -// // } -// // }; -// html! { -//
      -//
      -//                 {std::stringify!($jsx)}
      -//                 
      -//
      -// } -// } -// } -// } - -fn docs(app: &HirolaApp) -> Dom { - let router = app.data::().unwrap().clone(); - let app = app.clone(); +#[allow(dead_code)] +#[derive(Debug, Deserialize)] +struct Seo { + title: String, + date: Option, + tags: Vec, + summary: String, + draft: bool, +} +fn with_layout(seo: Seo) -> Dom { html! { -
      -
      -
      - - -
      - -
      -
      - -
      - -
      -
      -
      -
      - - -
      + + + {seo.title.clone()} " | Hirola documentation" + + + + + + + + + + + + + + + + + + + + -
      - {router.render(&app)} -
      -
      -
      + + +
      +
      +
      + + +
      + +
      +
      + +
      + +
      +
      +
      +
      + + +
      + +
      + "__MARKDOWN_CONTENT_HERE__" + +
      +
      +
      + + } } fn main() { - let window = web_sys::window().unwrap(); - let document = window.document().unwrap(); - let body = document.body().unwrap(); - - let mut app = HirolaApp::new(); - let mut router = Router::new(); - router.add("/", home); - router.add("/basics/getting-started", getting_started_page); - router.add("/basics/reactivity", reactivity_page); - router.add("/basics/templating", templating_page); - router.add("/basics/mixins", mixins_page); - router.add("/basics/events", event_handling_page); - - router.add("/mixins/:mixin", inner_mixins); - - router.add("/advanced/testing", testing_page); - router.add("/advanced/ssr", ssr_page); - router.add("/advanced/async", async_page); - router.add("/advanced/extending", extending_page); + use glob::glob; + for entry in glob("src/pages/**/*.md").expect("Failed to read glob pattern") { + match entry { + Ok(path) => { + let (content, seo) = markdown_page(&path); + let mut layout = "".to_string(); + layout.extend(render_to_string(with_layout(seo)).chars()); + let html_path = path + .to_string_lossy() + .replace("src/pages", "dist") + .replace(".md", ".html"); + std::fs::create_dir_all("dist/basics").unwrap(); + std::fs::create_dir_all("dist/advanced").unwrap(); + std::fs::create_dir_all("dist/plugins").unwrap(); + let _file = File::create(&html_path).unwrap(); + std::fs::write( + &html_path, + layout.replace("__MARKDOWN_CONTENT_HERE__", &content), + ) + .unwrap(); + } + Err(e) => println!("{:?}", e), + } + } +} - router.add("/plugins/form", forms_page); - router.add("/plugins/router", router_page); - router.add("/plugins/state", state_page); +fn markdown_page(path: &PathBuf) -> (String, Seo) { + let adapter = SyntectAdapter::new("InspiredGitHub"); + use comrak::ComrakOptions; + let markdown = std::fs::read_to_string(path).unwrap(); + let mut options = ComrakOptions::default(); + let mut plugins = ComrakPlugins::default(); - app.extend(router); - app.mount(&body, docs); + options.extension.front_matter_delimiter = Some("---".to_owned()); + plugins.render.codefence_syntax_highlighter = Some(&adapter); + let data = fronma::parser::parse::(&markdown) + .expect(&format!("in file: {}", path.to_string_lossy())); + let res = markdown_to_html_with_plugins(&data.body, &options, &plugins); + (res, data.headers) } diff --git a/examples/docs/src/pages/advanced/async.md b/examples/docs/src/pages/advanced/async.md new file mode 100644 index 0000000..922ac20 --- /dev/null +++ b/examples/docs/src/pages/advanced/async.md @@ -0,0 +1,71 @@ +--- +title: Async handling with hirola. +date: "2023-01-21" +tags: ["rust", "hirola", "basics", "starter"] +summary: We are going to learn how to handle event handling using hirola +draft: false +--- + +# Async handling + +## Using Suspense + +Hirola allows some async handling via `wasm-bindgen-futures`. +Consider this example: + +```rust +async fn fetcher() -> Result { + let window = web_sys::window().unwrap(); + + let mut opts = RequestInit::new(); + opts.method("GET"); + let url = format!("https://jsonplaceholder.typicode.com/users"); + let request = Request::new_with_str_and_init(&url, &opts)?; + + let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?; + let resp: Response = resp_value.dyn_into()?; + let json = resp.json()?; + let json = JsFuture::from(json).await?; + let users: Users = json.into_serde().unwrap(); + Ok(users) +} +``` + +You can mount this future on the dom: + +```rust +html!{ + {match fetcher().suspend().await { + Loading => html! {
      "Loading..."
      }, + Ready(Ok(users)) => { + html! { +
        + {for user in users { + html! {
      • {user.name}
      • } + }} +
      + } + }, + Ready(Err(err)) => html! {
      "An error occurred: " {err.to_string()}
      } + } + } +} +``` + +## Using side effects + +Side effects are futures that may never complete. +Any future can be used as a side-effect, and is dropped when it is complete or the dom item attached to it is dropped. + +```rust +let effect = async { + loop { + //...... + } +}; + +html! { +
      +} + +``` diff --git a/examples/docs/src/pages/advanced/ssr.md b/examples/docs/src/pages/advanced/ssr.md new file mode 100644 index 0000000..0bebb0a --- /dev/null +++ b/examples/docs/src/pages/advanced/ssr.md @@ -0,0 +1,20 @@ +--- +title: Server Side Rendering +date: "2023-01-21" +tags: ["rust", "hirola", "basics", "starter"] +summary: Hirola supports basic server side rendering +draft: false +--- + +# Server Side Rendering + +Hirola supports basic server side rendering with the feature `ssr`. + +## Example +```rust +fn main(){ + let app = HirolaApp::new(); + let res = app.render_to_string(counter); + assert_eq!("
      0
      ", &res); +} +``` \ No newline at end of file diff --git a/examples/docs/src/pages/advanced/testing.md b/examples/docs/src/pages/advanced/testing.md new file mode 100644 index 0000000..32f8fd5 --- /dev/null +++ b/examples/docs/src/pages/advanced/testing.md @@ -0,0 +1,50 @@ +--- +title: Testing an app built with hirola. +date: "2023-01-21" +tags: ["rust", "hirola", "basics", "starter"] +summary: Testing on hirola is based on wasm-bindgen-test. +draft: false +--- + +# Testing + +Testing on hirola is based on wasm-bindgen-test. + +> The wasm-bindgen-test crate is an experimental test harness for Rust programs compiled to wasm.[→ Read more about testing on wasm32-unknown-unknown with wasm-bindgen-test](https://rustwasm.github.io/wasm-bindgen/wasm-bindgen-test/index.html) + +## Example + +A testing example can be seen in the counter example + +```rust +use hirola::prelude::*; +fn counter() -> Dom { + let count = Mutable::new(0); + html! { +
      + + {count} +
      + } +} +fn main() { + hirola::render(counter()).unwrap(); +} + +#[cfg(test)] +mod tests { + use super::*; + use wasm_bindgen_test::*; + #[wasm_bindgen_test] + fn counter_renders() { + let res = hirola::render_to_string(counter); + assert_eq!("
      0
      ", &res); + } +} +``` + +Tests can be run with wasmpack + +`wasm-pack test --node` + +Testing is still a work in progress diff --git a/examples/docs/src/pages/async_handling.rs b/examples/docs/src/pages/async_handling.rs deleted file mode 100644 index 8d235d6..0000000 --- a/examples/docs/src/pages/async_handling.rs +++ /dev/null @@ -1,46 +0,0 @@ -use crate::components::code_preview::CodePreview; -use crate::components::seo_title::SeoTitle; -use hirola::prelude::*; - -pub fn async_page(_app: &HirolaApp) -> Dom { - html! { -
      - - -

      "Async handling"

      -

      "Hirola allows some async handling via ""async"" feature."

      -

      "Example"

      - Result { - let window = web_sys::window().unwrap(); - - let mut opts = RequestInit::new(); - opts.method("GET"); - let url = format!("https://jsonplaceholder.typicode.com/users"); - let request = Request::new_with_str_and_init(&url, &opts)?; - - let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?; - let resp: Response = resp_value.dyn_into()?; - let json = resp.json()?; - let json = JsFuture::from(json).await?; - let users: Users = json.into_serde().unwrap(); - Ok(users) -} - -fn fetch_users(_app: &HirolaApp) -> Dom { - let users: AsyncResult = use_async(fetcher()); - html! { -
      - {if users.get().is_none() { - html!{ -
      "Loading..."
      - } - } else { - ...... -"# - file="main.rs" - /> -
      - } -} diff --git a/examples/docs/src/pages/basics/events.md b/examples/docs/src/pages/basics/events.md new file mode 100644 index 0000000..dafade7 --- /dev/null +++ b/examples/docs/src/pages/basics/events.md @@ -0,0 +1,28 @@ +--- +title: Event Handling with hirola. +date: "2023-01-21" +tags: ["rust", "hirola", "basics", "starter"] +summary: We are going to learn how to handle event handling using hirola +draft: false +--- + +# Event Handling + +Hirola uses an `on:` binding style + +> Hirola uses mounts events to web_sys::Element under the hood, so you should be able to use any valid eventhandler.[→ Read more about Events on MDN](https://developer.mozilla.org/en-US/docs/Web/Events) + +## Example + +```rust +html! { + +} + +``` diff --git a/examples/docs/src/pages/basics/getting-started.md b/examples/docs/src/pages/basics/getting-started.md new file mode 100644 index 0000000..10cc429 --- /dev/null +++ b/examples/docs/src/pages/basics/getting-started.md @@ -0,0 +1,68 @@ +--- +title: Getting started with hirola. +date: '2023-01-21' +tags: ['rust', 'hirola', 'basics', 'starter'] +summary: We are going to create a simple counter program using hirola +draft: false +--- + +# Prerequisites + +Before getting started with `hirola` we are going to assume that you have the following tools installed: + +- rust +- cargo +- trunk + +# Getting Started + +We are going to create a simple counter program. + +`cargo new counter` + +With a new project, we need to create an index file which is the entry point and required by trunk + +`cd counter` + +Create an **index.html** in the root of counter. Add the contents below + +```html + + + + + Hirola Counter + + +``` + +Lets add some code to **src/main.rs** + +```rust,no_run +use hirola::prelude::*; + +fn counter() -> Dom { + let count = Mutable::new(0i32); + let decrement = count.callback(|s| *s.lock_mut() -= 1); + let increment = count.callback(|s| *s.lock_mut() += 1); + html! { + <> + + {count} + + + } +} + +fn main() { + let root = render(counter()).unwrap(); + // We prevent the root from being dropped + std::mem::forget(root); +} +``` + +Now lets run our project + +`trunk serve` + +You should be able to get counter running. diff --git a/examples/docs/src/pages/basics/mixins.md b/examples/docs/src/pages/basics/mixins.md new file mode 100644 index 0000000..599e101 --- /dev/null +++ b/examples/docs/src/pages/basics/mixins.md @@ -0,0 +1,33 @@ +--- +title: Mixin Handling with hirola. +date: "2023-01-21" +tags: ["rust", "hirola", "basics", "starter"] +summary: We are going to learn how to handle mixins using hirola +draft: false +--- + +# Mixins + +## Example + +Mixins allow developers to extend functionality by attaching it to a dom node. + +```rust +use web_sys::Element; +/// Mixin that controls tailwind opacity based on a bool signal +fn opacity<'a>(signal: &'a Mutable) -> Box () + 'a> { + let signal = signal.clone(); + let cb = move |node: DomNode| { + let element = node.unchecked_into::(); + let signal = signal.clone(); + if *signal.get() { + element.class_list().add_1("opacity-100").unwrap(); + element.class_list().remove_1("opacity-0").unwrap(); + } else { + element.class_list().add_1("opacity-0").unwrap(); + element.class_list().remove_1("opacity-100").unwrap(); + } + }; + Box::new(cb) +} +``` diff --git a/examples/docs/src/pages/basics/reactivity.md b/examples/docs/src/pages/basics/reactivity.md new file mode 100644 index 0000000..42dcd15 --- /dev/null +++ b/examples/docs/src/pages/basics/reactivity.md @@ -0,0 +1,39 @@ +--- +title: Reactivity with hirola. +date: "2023-01-21" +tags: ["rust", "hirola", "basics", "starter"] +summary: We are going to learn how to handle reactivity using hirola +draft: false +--- + +# Reactivity + +Hirola offers reactivity via futures-signals using mainly `Mutable`, `MutableVec` and `MutableBtreeMap`. Once a signal is updated, these changes are propagated to the dom. + +> Hirola uses frp-signals reactivity under the hood to provide these functions.[→ Read more about frp reactivity](https://crates.io/crates/futures-signals) + +## Reactive Signal + +```rust +use hirola_core::prelude::*; +let count = Mutable::new(0i32); +assert_eq!(count.get(), 0); + +count.set(1); +assert_eq!(count.get(), 1); +``` + +## Subscribing + +Subscribing is done via polling a future: + +```rust +use hirola_core::prelude::*; +let state = Mutable::new(0); +assert_eq!(state.get(), 0); +state.to_signal().for_each(|value| { + // do something with new value +}); +/// later +state.set(1); +``` diff --git a/examples/docs/src/pages/basics/state-management.md b/examples/docs/src/pages/basics/state-management.md new file mode 100644 index 0000000..9c938d6 --- /dev/null +++ b/examples/docs/src/pages/basics/state-management.md @@ -0,0 +1,26 @@ +--- +title: State Management with hirola +date: '2023-01-21' +tags: ['rust', 'hirola', 'basics', 'starter'] +summary: We are going to look at how hirola handles state management +draft: false +--- + + +# State Management + +Hirola allows basic state management using `app` feature. + +## Getting started + +```rs +let window = web_sys::window().unwrap(); +let document = window.document().unwrap(); +let body = document.body().unwrap(); +let todos = MutableVec::new(); +let mut app = App::new(todos); +/// Add routes +app.mount(); +``` + +With that you can access the state from the current route. diff --git a/examples/docs/src/pages/basics/templating.md b/examples/docs/src/pages/basics/templating.md new file mode 100644 index 0000000..2a2c5ae --- /dev/null +++ b/examples/docs/src/pages/basics/templating.md @@ -0,0 +1,82 @@ +--- +title: Templating with hirola. +date: '2023-01-21' +tags: ['rust', 'hirola', 'basics', 'starter'] +summary: We are going to look at how hirola handles iteration and conditional flow +draft: false +--- + +# Templating + +Hirola uses rsx which is an implementation of jsx in rust. This also means it inherits all the caveats. + +## Iteration + +Looping through an array of values is important to any framework. Frameworks like react provide a key mechanism to improve on the rerendering. + +## Basic + +If you are iterating over a non-signal iterator, you can use the normal for-loop + +### Example + +```rust +{for i in 0..5 { + html! { +
        +
      • {i}
      • +
      + } +}} +``` + +
      +- 0 + +- 1 + +- 2 + +- 3 + +- 4 +
      + +## With Signals + +Sometimes, you are working with a signal and want to react to changes on the ui. You can use Keyed and Indexed + +### Keyed + +```rust +todo!(); +``` + +### Indexed + +```rust +
        + {colors + .signal_vec() + .render_map(|item| { + html! {
      • {item}
      • } + }) + } +
      +``` + +## Components + +One can write components as functions starting with uppercase and add a `component` proc attribute + +```rust +#[component] +fn Todo(router: Router) -> Dom { + html! { +
      + } +} +html! { + +} +``` diff --git a/examples/docs/src/pages/event_handling.rs b/examples/docs/src/pages/event_handling.rs deleted file mode 100644 index 90983d2..0000000 --- a/examples/docs/src/pages/event_handling.rs +++ /dev/null @@ -1,47 +0,0 @@ -use crate::components::code_preview::CodePreview; -use crate::components::seo_title::SeoTitle; -use hirola::prelude::*; - -pub fn event_handling_page(_app: &HirolaApp) -> Dom { - html! { -
      - -

      "Event Handling"

      -

      "Hirola uses an ""on:"" binding style"

      -
      -

      "Hirola uses mounts events to web_sys::Element under the hood, so you should be able to use any valid eventhandler." - "→ Read more about Events on MDN" -

      -
      -

      "Example"

      - - "Click Me" - -}"# - file="src/main.rs" /> - -
      - { - - html! { - - } - } -
      - - -
      - } -} diff --git a/examples/docs/src/pages/extending.rs b/examples/docs/src/pages/extending.rs deleted file mode 100644 index d98abac..0000000 --- a/examples/docs/src/pages/extending.rs +++ /dev/null @@ -1,24 +0,0 @@ -use crate::components::seo_title::SeoTitle; -use hirola::prelude::*; - -pub fn extending_page(_app: &HirolaApp) -> Dom { - html! { -
      - - -

      "Extending"

      -

      "Hirola supports extending via mixins and can be enabled by feature""mixins""."

      -

      "Some rules about mixins"

      -
        -
      • "Mixins are executed sequentially"
      • -
      • "Mixins should be the last attributes on a dom attrubutes"
      • -
      • "A good mixin receives a signal and interacts with the bound node"
      • -
      -
      -

      "Hirola recommends you package your plugin and publish it to crates.io with the hirola- prefix. The hirola-form plugin is a good example." - "→ See example on docs.rs" -

      -
      -
      - } -} diff --git a/examples/docs/src/pages/getting_started.rs b/examples/docs/src/pages/getting_started.rs deleted file mode 100644 index 7266853..0000000 --- a/examples/docs/src/pages/getting_started.rs +++ /dev/null @@ -1,68 +0,0 @@ -use hirola::prelude::*; - -use crate::components::code_preview::CodePreview; -use crate::components::seo_title::SeoTitle; - -const INDEX: &str = r#" - - - - Hirola Counter - - -"#; - -pub fn getting_started_page(_app: &HirolaApp) -> Dom { - html! { -
      - - -

      "Prerequisites"

      -

      - "Before getting started with" - "hirola" - " we are going to assume that you have the following tools installed:" -

      -
        -
      • "Rust"
      • -
      • "Cargo"
      • -
      • "Trunk"
      • -
      -

      "Getting Started"

      -

      "We are going to create a simple counter program."

      - - "cargo new counter" - -

      "With a new project, we need to create an index file which is the entry point and required by trunk"

      - - "cd counter" - -

      "Create an ""index.html"" in the root of counter. Add the contents below"

      - - -

      "Lets add some code to ""src/main.rs"

      - -

      "Now lets run our project"

      - - "trunk serve" - -

      "You should be able to get counter running."

      -

      "Try it out"

      -
      - { - let count = Signal::new(0); - html! { -
      - - {count.get()} -
      - } - } -
      -

      "We can also test our project using wasm-pack"

      - - "wasm-pack test --node" - -
      - } -} diff --git a/examples/docs/src/pages/index.md b/examples/docs/src/pages/index.md new file mode 100644 index 0000000..e208046 --- /dev/null +++ b/examples/docs/src/pages/index.md @@ -0,0 +1,52 @@ +--- +title: Hirola - A KISS Rust frontend framework +date: '2023-01-21' +tags: ['rust', 'hirola', 'basics', 'starter'] +summary: Hirola is a frontend framework for Rust that is focused on simplicity and predictability +draft: false +--- + +# What is Hirola? + +**Hirola** is a frontend framework for Rust that is focused on simplicity and predictability. + +## Goals + +- Keep it simple. +- Make it easy to read, extend and share code. Mixins and components are kept simple and \*macro-free. +- Basic state management that is easily extensible +- Familiarity. Uses rsx which is very similar to jsx. + +## Example + +```rust +use std::fmt::Display; +use hirola::prelude::*; +use hirola::signal::Mutable; + +fn counter() -> Dom { + let count = Mutable::new(0i32); + let decrement = count.callback(|s| *s.lock_mut() -= 1); + let increment = count.callback(|s| *s.lock_mut() += 1); + html! { + <> + + {count} + + + } +} + +fn main() { + let root = render(counter()).unwrap(); + // We prevent the root from being dropped + std::mem::forget(root); +} +``` + +## Features + +- **`serde`**— Enables serialization of state +- **`ssr`**— Enables server side rendering 🚧 +- **`router`**— Enables Isomorphic Routing +- **`form`**— Enables form mixins and utilities 🚧 diff --git a/examples/docs/src/pages/mixins.rs b/examples/docs/src/pages/mixins.rs deleted file mode 100644 index 79c0c95..0000000 --- a/examples/docs/src/pages/mixins.rs +++ /dev/null @@ -1,292 +0,0 @@ -use std::str::FromStr; - -use hirola::prelude::mixins::{model::input, rhtml, show, text}; -use hirola::prelude::*; -use wasm_bindgen::JsCast; -use web_sys::{Element, Event, HtmlInputElement}; - -use crate::components::code_preview::CodePreview; -use crate::components::seo_title::SeoTitle; - -fn opacity<'a>(signal: &'a Signal) -> Box () + 'a> { - let signal = signal.clone(); - let cb = move |node: DomNode| { - let element = node.unchecked_into::(); - let signal = signal.clone(); - create_effect(move || { - if *signal.get() { - element.class_list().add_1("opacity-100").unwrap(); - element.class_list().remove_1("opacity-0").unwrap(); - } else { - element.class_list().add_1("opacity-0").unwrap(); - element.class_list().remove_1("opacity-100").unwrap(); - } - }) - }; - Box::new(cb) -} - -pub fn mixins_page(_app: &HirolaApp) -> Dom { - html! { -
      - -

      "Mixins"

      -

      "Mixins are ways of sharing and extending code in hirola."

      -

      "Hirola is highly inspired by Alpine.js, and mixins can be considered similar to directives"

      -

      "Mixins can be very powerful in applying DRY techniques. Lets start simple and create a mixin that controls tailwinds opacity."

      -

      "Example"

      - (signal: &'a Signal) -> Box () + 'a> { - let signal = signal.clone(); - let cb = move |node: DomNode| { - let element = node.unchecked_into::(); - let signal = signal.clone(); - create_effect(move || { - if *signal.get() { - element.class_list().add_1("opacity-100").unwrap(); - element.class_list().remove_1("opacity-0").unwrap(); - } else { - element.class_list().add_1("opacity-0").unwrap(); - element.class_list().remove_1("opacity-100").unwrap(); - } - }) - }; - Box::new(cb) -} - -let is_shown = Signal::new(true); - -let toggle = is_shown.mut_callback(|show, _e| !show); - -html! { -
      -
      - -
      -} - -"# - file="main.rs" - /> -
      - { - - - let is_shown = Signal::new(true); - let toggle = is_shown.mut_callback(|show, _e| !show); - html! { -
      - -
      - - -
      - } - } - -
      - - -
      - } -} - -pub fn inner_mixins(_app: &HirolaApp) -> Dom { - let router: &Router = _app.data().unwrap(); - let param = router.param("mixin").unwrap_or(format!("404")); - let mixin = InbuiltMixin::from_str(¶m).unwrap(); - let title = format!("Mixin - mixin:{} | Hirola", param); - html! { -
      - -

      {format!("mixin:{}", param)}

      - { - match mixin { - InbuiltMixin::Show => { - html! { -
      -

      "A css-powered mixin that toggles display based on a signal"

      -

      "Example"

      - - - "I am shown" -
      - } -"# - file="main.rs" - /> -
      - { - let shown = Signal::new(true); - html! { -
      - - "I am shown" -
      - } - } -
      -
      - } - }, - InbuiltMixin::Text => { - html! { -
      -

      "A text mixin that binds a signal to an element's textContent"

      -

      "Example"

      - () - .unwrap(); - input.value() -}); -html! { -
      - - -
      -} -"# - file="main.rs" - /> -
      - { - let message = Signal::new(format!("Hello Hirola")); - let handle_change = message.mut_callback(|_cur, e: Event| { - let input = e - .current_target() - .unwrap() - .dyn_into::() - .unwrap(); - input.value() - }); - html! { -
      - - -
      - } - } -
      -
      - } - - }, - InbuiltMixin::RHtml => { - html! { -
      -

      "A mixin that allows setting raw html"

      -

      "Example"

      - - -
      -} -"# - file="main.rs" - /> -
      - { - let message = "Hello Hirola"; - html! { -
      - -
      - } - } -
      -
      - } - - }, - InbuiltMixin::Model => { - html! { -
      -

      "A mixin that makes two-way binding on a signal and form element"

      -

      "Example"

      - - - -
      -} -"# - file="main.rs" - /> -
      - { - let message = Signal::new(format!("Hello Hirola")); - html! { -
      - - -
      - } - } -
      -
      - } - - - }, - _ => { - html! { -

      "TODO"

      - } - } - } - } -
      - } -} - -#[derive(Debug)] -enum InbuiltMixin { - Show, - Text, - RHtml, - Model, - Transition, - Ignore, - If, -} - -impl FromStr for InbuiltMixin { - type Err = (); - fn from_str(input: &str) -> Result { - match input { - "show" => Ok(InbuiltMixin::Show), - "text" => Ok(InbuiltMixin::Text), - "rhtml" => Ok(InbuiltMixin::RHtml), - "model" => Ok(InbuiltMixin::Model), - "transition" => Ok(InbuiltMixin::Transition), - "ignore" => Ok(InbuiltMixin::Ignore), - "if" => Ok(InbuiltMixin::If), - _ => Err(()), - } - } -} diff --git a/examples/docs/src/pages/mod.rs b/examples/docs/src/pages/mod.rs deleted file mode 100644 index aa9fb67..0000000 --- a/examples/docs/src/pages/mod.rs +++ /dev/null @@ -1,112 +0,0 @@ -mod async_handling; -mod event_handling; -mod extending; -mod form; -mod getting_started; -mod mixins; -mod reactivity; -mod router; -mod ssr; -mod state; -mod templating; -mod testing; - -use hirola::prelude::*; - -pub use async_handling::async_page; -pub use event_handling::event_handling_page; -pub use extending::extending_page; -pub use form::forms_page; -pub use getting_started::getting_started_page; -pub use mixins::inner_mixins; -pub use mixins::mixins_page; -pub use reactivity::reactivity_page; -pub use router::router_page; -pub use ssr::ssr_page; -pub use state::state_page; -pub use templating::templating_page; -pub use testing::testing_page; - -use crate::components::code_preview::CodePreview; - -pub fn home(_: &HirolaApp) -> Dom { - html! { -
      -

      "What is Hirola?"

      -

      "Hirola"" is an un-opinionated Rust web framework that is focused on simplicity and predictability."

      -

      "Goals"

      -
        -
      • "Keep it simple. Most Rust web frameworks have a huge learning curve and verbose syntaxes. We yearn to minimize these."
      • -
      • "Make it easy to read, extend and share code. Mixins and components are kept simple and macro-free."
      • -
      • "No Context. You can choose passing props down, and/or use the global-state if routing. You can write hook-like functions though."
      • -
      • "Familiality. Uses rsx which is very similar to JSX."
      • -
      -

      "Example"

      - Dom { - let state = Signal::new(99); - let decerement = state.mut_callback(|count, _| *count - 1); - let incerement = state.mut_callback(|count, _| *count + 1); - - html! { -
      - - - -
      - } -} - -fn main() { - let window = web_sys::window().unwrap(); - let document = window.document().unwrap(); - let body = document.body().unwrap(); - - let app = HirolaApp::new(); - app.mount(&body, counter); -}"# - file="main.rs"/> - -
      - { - let state = Signal::new(99); - let decerement = state.mut_callback(|count, _| *count - 1); - let incerement = state.mut_callback(|count, _| *count + 1); - - html! { -
      - - - -
      - } - } -
      -

      "Features"

      -
        -
      • -

        "serde" "— Enables serialization of state"

        -
      • -
      • -

        "ssr" "— Enables server side rendering 🚧"

        -
      • -
      • -

        "router" "— Enables Isomorphic Routing"

        -
      • -
      • -

        "global-state" "— Enables global state management"

        -
      • -
      • -

        "async" "— Enables async utilities 🚧"

        -
      • -
      • -

        "form" "— Enables form mixins and utilities 🚧"

        -
      • -
      - -
      - } -} diff --git a/examples/docs/src/pages/form.rs b/examples/docs/src/pages/plugins/form.md similarity index 59% rename from examples/docs/src/pages/form.rs rename to examples/docs/src/pages/plugins/form.md index 3f2049c..09489c8 100644 --- a/examples/docs/src/pages/form.rs +++ b/examples/docs/src/pages/plugins/form.md @@ -1,18 +1,19 @@ -use crate::components::code_preview::CodePreview; -use crate::components::seo_title::SeoTitle; -use hirola::prelude::*; +--- +title: Form handling with hirola. +date: "2023-01-21" +tags: ["rust", "hirola", "basics", "starter"] +summary: We are going to learn how to handle forms using hirola +draft: false +--- -pub fn forms_page(_app: &HirolaApp) -> Dom { - html! { -
      - +# Forms + +Hirola is un-opinionated in form management. It should be pretty easy to roll out your own. To enable the inbuilt form management use the feature flag `form`. -

      "Forms"

      -

      "Hirola is un-opinionated in form management. It should be pretty easy to roll out your own. To enable the inbuilt form management use the feature flag ""form"

      -

      "Getting started"

      - Dom { ...... } -"# - file="src/main.rs" - /> -
      - } -} +``` diff --git a/examples/docs/src/pages/plugins/router.md b/examples/docs/src/pages/plugins/router.md new file mode 100644 index 0000000..e9f7c30 --- /dev/null +++ b/examples/docs/src/pages/plugins/router.md @@ -0,0 +1,18 @@ +--- +title: Route handling with hirola. +date: "2023-01-21" +tags: ["rust", "hirola", "basics", "starter"] +summary: We are going to learn how to handle route handling using hirola +draft: false +--- + +# Router + +Hirola is un-opinionated in route management. It should be pretty easy to roll out your own. To enable the inbuilt router use the feature flag `app`. + +```rust +let mut app = App::new(()); +app.route("/", home); +app.route("/todo/:id", todo_view); +app.mount(); +``` diff --git a/examples/docs/src/pages/reactivity.rs b/examples/docs/src/pages/reactivity.rs deleted file mode 100644 index 254f5a4..0000000 --- a/examples/docs/src/pages/reactivity.rs +++ /dev/null @@ -1,49 +0,0 @@ -use crate::components::code_preview::CodePreview; -use crate::components::seo_title::SeoTitle; -use hirola::prelude::*; - -pub fn reactivity_page(_app: &HirolaApp) -> Dom { - html! { -
      - -

      "Reactivity"

      -

      - r#"Hirola offers reactivity via a primitive called signal and an effect called create_effect. Once a signal is updated, these changes are propagated to the dom."# -

      -
      -

      "Hirola uses a fork of maple(now sycamore) reactivity engine under the hood to provide these functions." - "→ Read more about sycamore reactivity primitives" -

      -
      -

      "Reactive Signal"

      - - - -

      "Signal is pretty similar to useState in react or Alpine.reactive"

      -

      "Subscribing"

      -

      "Subscribing is done via create_effect"

      - - -
      - } -} diff --git a/examples/docs/src/pages/router.rs b/examples/docs/src/pages/router.rs deleted file mode 100644 index 13c94a2..0000000 --- a/examples/docs/src/pages/router.rs +++ /dev/null @@ -1,31 +0,0 @@ -use crate::components::code_preview::CodePreview; -use crate::components::seo_title::SeoTitle; -use hirola::prelude::*; - -pub fn router_page(_app: &HirolaApp) -> Dom { - html! { -
      - - -

      "Router"

      -

      "Hirola is un-opinionated in route management. It should be pretty easy to roll out your own. To enable the inbuilt router use the feature flag ""router"

      -

      "Getting started"

      - -
      - } -} diff --git a/examples/docs/src/pages/ssr.rs b/examples/docs/src/pages/ssr.rs deleted file mode 100644 index 2d21bdf..0000000 --- a/examples/docs/src/pages/ssr.rs +++ /dev/null @@ -1,24 +0,0 @@ -use crate::components::code_preview::CodePreview; -use crate::components::seo_title::SeoTitle; -use hirola::prelude::*; - -pub fn ssr_page(_app: &HirolaApp) -> Dom { - html! { -
      - -

      "Server Side Rendering"

      -

      "Hirola supports basic server side rendering with the feature ""ssr""."

      -

      "Example"

      - 0
      ", &res); -} -"# - file="main.rs" - /> -
      - } -} diff --git a/examples/docs/src/pages/state.rs b/examples/docs/src/pages/state.rs deleted file mode 100644 index 67896c3..0000000 --- a/examples/docs/src/pages/state.rs +++ /dev/null @@ -1,35 +0,0 @@ -use crate::components::code_preview::CodePreview; -use crate::components::seo_title::SeoTitle; -use hirola::prelude::*; - -pub fn state_page(_app: &HirolaApp) -> Dom { - html! { -
      - - -

      "State Management"

      -

      "Hirola is un-opinionated in state management. It should be pretty easy to roll out your own. To enable the global state management use the feature flag ""global-state"

      -

      "Getting started"

      - -
      - } -} diff --git a/examples/docs/src/pages/templating.rs b/examples/docs/src/pages/templating.rs deleted file mode 100644 index 71d2163..0000000 --- a/examples/docs/src/pages/templating.rs +++ /dev/null @@ -1,92 +0,0 @@ -use crate::components::seo_title::SeoTitle; -use crate::pages::CodePreview; -use hirola::prelude::*; - -pub fn templating_page(_app: &HirolaApp) -> Dom { - html! { -
      - -

      "Templating"

      -

      "Hirola uses rsx which is an implementation of jsx in rust. This also means it inherits all the caveats."

      -

      "Iteration"

      -

      "Looping through an array of values is important to any framework. Frameworks like react provide a key mechanism to improve on the rerendering."

      -

      "Basic"

      -

      "If you are iterating over a non-signal iterator, you can use the normal for-loop"

      -

      "Example"

      - -
    • {i}
    • -
    - } -}} -" - file="src/main.rs" - /> -
    - {for i in 0..5 { - html! { -
      -
    • {i}
    • -
    - } - }} -
    -

    "With Signal"

    -

    "Sometimes, you are working with a signal and want to react to changes on the ui. You can use Keyed and Indexed"

    -

    "Keyed"

    - - } - }, - key: |item| item.get().title - } - } -/> -" - file="src/main.rs" - /> -

    "Indexed"

    - - } - }, - } - } -/> -" - file="src/main.rs" - /> -

    "Components"

    -

    "One can write components as functions starting with uppercase"

    - -
    - } -} diff --git a/examples/docs/src/pages/testing.rs b/examples/docs/src/pages/testing.rs deleted file mode 100644 index 468b241..0000000 --- a/examples/docs/src/pages/testing.rs +++ /dev/null @@ -1,24 +0,0 @@ -use crate::components::code_preview::CodePreview; -use crate::components::seo_title::SeoTitle; -use hirola::prelude::*; - -pub fn testing_page(_app: &HirolaApp) -> Dom { - html! { -
    - -

    "Testing"

    -

    "Testing on hirola is based on wasm-bindgen-test."

    -
    -

    "The wasm-bindgen-test crate is an experimental test harness for Rust programs compiled to wasm." - "→ Read more about testing on wasm32-unknown-unknown with wasm-bindgen-test" -

    -
    -

    "Example"

    -

    "A testing example can be seen in the counter example"

    - -

    "Tests can be run with wasmpack"

    - "wasm-pack test --node" -

    "Testing is still a work in progress"

    -
    - } -} diff --git a/examples/fake-api/Cargo.toml b/examples/fake-api/Cargo.toml index fd2302f..49b050c 100644 --- a/examples/fake-api/Cargo.toml +++ b/examples/fake-api/Cargo.toml @@ -6,20 +6,11 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -hirola = { path = "../../", features= ["async"]} -wasm-bindgen = { version = "0.2.82", features = ["serde-serialize"] } -js-sys = "0.3.59" -wasm-bindgen-futures = "0.4.32" +anyhow = "1" +hirola = { path = "../../" } serde = { version = "1.0.80", features = ["derive"] } -serde_json = "1" +reqwasm = "0.5.0" [dependencies.web-sys] version = "0.3.4" -features = [ - 'Headers', - 'Request', - 'RequestInit', - 'RequestMode', - 'Response', - 'Window', -] +features = ['Window'] diff --git a/examples/fake-api/index.html b/examples/fake-api/index.html index db6c44c..155dc00 100644 --- a/examples/fake-api/index.html +++ b/examples/fake-api/index.html @@ -3,9 +3,5 @@ Hirola Fake API - diff --git a/examples/fake-api/src/main.rs b/examples/fake-api/src/main.rs index 649872b..5667440 100644 --- a/examples/fake-api/src/main.rs +++ b/examples/fake-api/src/main.rs @@ -1,54 +1,45 @@ mod model; - use hirola::prelude::*; use model::Users; -use wasm_bindgen::JsCast; -use wasm_bindgen::JsValue; -use wasm_bindgen_futures::JsFuture; -use web_sys::{Request, RequestInit, Response}; - -async fn fetcher() -> Result { - let window = web_sys::window().unwrap(); - - let mut opts = RequestInit::new(); - opts.method("GET"); - let url = format!("https://jsonplaceholder.typicode.com/users"); - let request = Request::new_with_str_and_init(&url, &opts)?; - - let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?; - let resp: Response = resp_value.dyn_into()?; - let json = resp.json()?; - let json = JsFuture::from(json).await?; - let users: Users = json.into_serde().unwrap(); - Ok(users) +use reqwasm::http::Request; +use anyhow::bail; + +async fn user_fetcher() -> anyhow::Result { + let request = Request::get("https://jsonplaceholder.typicode.com/users"); + let response = request.send().await?; + if response.status() == 200 { + return Ok(response.json().await?); + } else { + bail!( + "Failed with status {}, {}", + response.status(), + response.text().await? + ) + } } -fn fetch_users(_app: &HirolaApp) -> Dom { - let users: AsyncResult = use_async(fetcher()); - +fn fetch_users() -> Dom { html! { -
    - {if users.get().is_none() { - html!{ -
    "Loading..."
    - } - } else { - let users = &*users.get(); - let users = users.clone().unwrap(); - +
    +

    "Users"

    + {match user_fetcher().suspend().await { + Loading => { + html! {
    "Loading..."
    } + } + Ready(Ok(users)) => { html! { -
    - {for user in users.unwrap() { - html! { -
    - {user.name.clone()} -
    - } - }} -
    +
      + {for user in users { + html! {
    • {user.name}
    • } + }} +
    } - }} -
    + } + Ready(Err(err)) => { + html! {
    "An error occurred: " {err.to_string()}
    } + } + }} +
    } } @@ -57,6 +48,7 @@ fn main() { let document = window.document().unwrap(); let body = document.body().unwrap(); - let app = HirolaApp::new(); - app.mount(&body, fetch_users); + let dom = render_to(fetch_users(), &body).unwrap(); + + std::mem::forget(dom); } diff --git a/examples/form/Cargo.toml b/examples/form/Cargo.toml index 9670721..6930c00 100644 --- a/examples/form/Cargo.toml +++ b/examples/form/Cargo.toml @@ -6,8 +6,8 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -hirola = { path = "../../", features=["form"]} -wasm-bindgen = {version = "0.2", features= ["serde-serialize"]} +hirola = { path = "../../", features = ["form", "dom"] } +wasm-bindgen = { version = "0.2", features = ["serde-serialize"] } validator = "0.10" validator_derive = "0.10" serde = { version = "1.0.80", features = ["derive"] } @@ -30,7 +30,7 @@ features = [ "HtmlTextAreaElement", "HtmlSelectElement", "HtmlFormElement", - "FormData" + "FormData", ] version = "0.3" diff --git a/examples/form/src/main.rs b/examples/form/src/main.rs index 09dc1ba..bdcf9d9 100644 --- a/examples/form/src/main.rs +++ b/examples/form/src/main.rs @@ -2,11 +2,11 @@ extern crate validator_derive; use serde::{Deserialize, Serialize}; -use validator::Validate; +use validator::{Validate, ValidationErrors}; use hirola::{ - form::{Bind, FormHandler}, - prelude::{mixins::text, *}, + form::{Bind, Form, FormHandler}, + prelude::*, }; use web_sys::{Event, HtmlInputElement}; @@ -22,25 +22,38 @@ struct Login { remember: String, } -#[component] -fn InnerComponent(bind: Bind) -> Dom { - let increment = bind.callback(move |bind, _e: Event| { - let value = bind.get_value(); - bind.set_value(value + 1) - }); - - let bind = bind.clone(); +impl hirola::form::Validate for Login { + type Error = ValidationErrors; + fn validate(&self) -> Result<(), Self::Error> { + Validate::validate(&self) + } - html! { - <> - "Counter" - - {bind.get_value()} - + fn errors(&self) -> std::collections::HashMap<&'static str, String> { + Validate::validate(&self) + .unwrap_err() + .errors() + .into_iter() + .map(|(key, value)| (*key, format!("{value:?}"))) + .collect() } } -fn form_demo(_app: &HirolaApp) -> Dom { +// #[component] +// fn InnerComponent(bind: Bind) -> Dom { +// let increment = bind.callback(move |bind, _e: Event| { +// let value = bind.get_value(); +// bind.set_value(value.get() + 1) +// }); +// html! { +// <> +// "Counter" +// +// +// +// } +// } + +fn form_demo() -> Dom { let form = FormHandler::new(Login { email: "example@gmail.com".to_string(), password: String::new(), @@ -52,36 +65,46 @@ fn form_demo(_app: &HirolaApp) -> Dom { + ref=form.node_ref() + on:submit=|e| e.prevent_default() + >
    - + ()} - /> - + /> + // +
    + class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300" + > + "Your password" + ()} />
    - ("count")} /> +
    @@ -92,20 +115,21 @@ fn form_demo(_app: &HirolaApp) -> Dom { value="" class="w-4 h-4 bg-gray-50 rounded border border-gray-300 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800" required="" - // mixin:form={&form.register::()} + mixin:form={&form.register::()} />
    } @@ -115,7 +139,5 @@ fn main() { let window = web_sys::window().unwrap(); let document = window.document().unwrap(); let body = document.body().unwrap(); - - let app = HirolaApp::new(); - app.mount(&body, form_demo); + std::mem::forget(render_to(form_demo(), &body)); } diff --git a/examples/mixin/src/main.rs b/examples/mixin/src/main.rs index b362713..c0c2266 100644 --- a/examples/mixin/src/main.rs +++ b/examples/mixin/src/main.rs @@ -9,10 +9,10 @@ fn x_html<'a>(text: &'a str) -> Box () + 'a> { Box::new(cb) } -fn mixin_demo(_app: &HirolaApp) -> Dom { +fn mixin_demo() -> Dom { let raw = "calebporzio"; - let is_shown = Signal::new(true); - let toggle = is_shown.mut_callback(|show, _e| !show); + let is_shown = Mutable::new(true); + let toggle = is_shown.update_with(|show, _e| !show); html! {
    Dom { "width": "max-content"; "margin-left": "auto"; "margin-right": "auto"; - "margin-top": {if *is_shown.get() { "5px" } else { "10px" }}; + "margin-top": {if is_shown.get() { "5px" } else { "10px" }}; } @media "(orientation: landscape)" { @@ -67,7 +67,5 @@ fn main() { let window = web_sys::window().unwrap(); let document = window.document().unwrap(); let body = document.body().unwrap(); - - let app = HirolaApp::new(); - app.mount(&body, mixin_demo); + render_to(mixin_demo, &body); } diff --git a/examples/todo/Cargo.toml b/examples/todo/Cargo.toml index b92da4f..89bbf14 100644 --- a/examples/todo/Cargo.toml +++ b/examples/todo/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -hirola = { path = "../../", features =["router", "global-state"]} +hirola = { path = "../../", features =["app"]} wasm-bindgen = {version = "0.2"} [dependencies.web-sys] diff --git a/examples/todo/src/main.rs b/examples/todo/src/main.rs index 00e98dc..153706d 100644 --- a/examples/todo/src/main.rs +++ b/examples/todo/src/main.rs @@ -1,4 +1,5 @@ use hirola::prelude::*; +use hirola::signal::Mutable; use wasm_bindgen::JsCast; use web_sys::window; use web_sys::HtmlInputElement; @@ -11,14 +12,14 @@ struct Todo { } #[component] -fn TodoCard(todo: StateHandle, router: Router, todos: Signal>>) { +fn TodoCard(todo: ReadOnlyMutable, router: Router, todos: Mutable>>) { let todo = (&*todo).clone().get(); let id = todo.id.clone(); let href = format!("/todo/{}", id); let title = todo.title.clone(); let tl = title.clone(); - let on_remove = todos.callback(move |todos, _e| { + let on_remove = todos.update(move |todos, _e| { let index = todos .get() .iter() @@ -34,7 +35,7 @@ fn TodoCard(todo: StateHandle, router: Router, todos: Signal - "View" + "Dom" } } -fn colors(_app: &HirolaApp) -> Dom { - let colors = Signal::new( - vec!["Red", "Green", "Blue"] - .iter() - .map(|c| Signal::new(c.to_string())) - .collect::>>(), - ); - let add_new = colors.callback(move |colors, _e: Event| { - colors.push(Signal::new("New Color".to_string())); - }); - +#[component] +fn MyComponentWithProps(world: &'static str) -> Dom { html! { - <> - - - - - - +

    {world}

    } } @@ -75,6 +53,7 @@ fn main() { let document = window.document().unwrap(); let body = document.body().unwrap(); - let app = HirolaApp::new(); - app.mount(&body, colors); + let dom = render_to(colors(), &body).unwrap(); + + std::mem::forget(dom); } diff --git a/src/lib.rs b/src/lib.rs index 7e94c6f..4996ca7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,51 +5,35 @@ unreachable_pub )] #![cfg_attr(docsrs, feature(doc_cfg))] -//! # Hirola API Documentation -//! **Hirola** is an un-opinionated and extensible web framework for that is focused on simplicity and predictability. -//! -//! ## Example -//! ```rust,no_run -//! use hirola::prelude::*; -//! -//! fn counter(_: &HirolaApp) -> Dom { -//! let state = Signal::new(99); -//! let decerement = state.mut_callback(|count, _| *count - 1); -//! let incerement = state.mut_callback(|count, _| *count + 1); -//! -//! html! { -//!
    -//! -//! -//! -//!
    -//! } -//! } -//! -//! fn main() { -//! let window = web_sys::window().unwrap(); -//! let document = window.document().unwrap(); -//! let body = document.body().unwrap(); -//! -//! let app = HirolaApp::new(); -//! app.mount(&body, counter); -//! } -//! ``` -//! -//! +#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))] //! ## Features #![cfg_attr( feature = "docsrs", cfg_attr(doc, doc = ::document_features::document_features!()) )] -//! Hirola is derived from a fork of [maple reactivity core](https://github.com/lukechu10/maple). -/// The defaults from core +/// The defaults imports pub mod prelude { - pub use super::*; pub use hirola_core::prelude::*; } +/// Exposing single item signal +pub mod signal { + pub use hirola_core::prelude::signal::*; +} + +/// Exposing vec signal +pub mod signal_vec { + pub use hirola_core::prelude::signal_vec::*; +} + +/// App introduces state and routing management. +/// Use if you need to store data shared between routes or just routing. +#[cfg(feature = "app")] +pub mod app { + pub use hirola_core::app::*; +} + /// Include form mixins and utilities #[cfg(feature = "form")] #[cfg_attr(docsrs, doc(cfg(feature = "form")))] diff --git a/tests/browser.rs b/tests/browser.rs index 3e44857..41fe64c 100644 --- a/tests/browser.rs +++ b/tests/browser.rs @@ -1,65 +1,70 @@ #![allow(unused_variables)] - use hirola::prelude::*; +use hirola_core::dom_test_utils::next_tick; +use wasm_bindgen::JsCast; use wasm_bindgen_test::*; +use web_sys::{Element, Node}; wasm_bindgen_test_configure!(run_in_browser); -#[wasm_bindgen_test] -fn app_renders() { - let app = HirolaApp::new(); - fn test_app(app: &HirolaApp) -> Dom { - html! { - "Test" - } - } - let res = app.render_to_string(test_app); - assert_eq!("Test", &res); +fn body() -> Node { + let doc = web_sys::window().unwrap().document().unwrap(); + let element = doc.create_element("div").unwrap().into(); + element } -#[wasm_bindgen_test] -fn router_renders() { - let mut app = HirolaApp::new(); - let mut router = Router::new(); - router.add("/", |_| { - html! { -
    "Main"
    - } - }); - app.extend(router); - - fn test_app(app: &HirolaApp) -> Dom { - let router: &Router = app.data().unwrap(); - router.push("/"); - router.render(app) - } - let res = app.render_to_string(test_app); - assert_eq!("
    Main
    ", &res); +fn inner_html(element: &Node) -> String { + let element = element.dyn_ref::().unwrap(); + element.inner_html() } #[wasm_bindgen_test] fn router_pushes() { - let mut app = HirolaApp::new(); - let mut router = Router::new(); - router.add("/", |_| { + let mut app: App<()> = App::new(()); + app.route("/", |_| { html! {
    "Main"
    } }); - router.add("/page", |_| { + app.route("/page", |_| { html! {
    "Page"
    } }); + let router = app.router().clone(); + let node = body(); + let root = app.mount_to(&node); + assert_eq!("
    Main
    ", inner_html(&node)); + router.push("/page"); - app.extend(router.clone()); + next_tick(move || { + assert_eq!("
    Page
    ", inner_html(&node)); + }); +} - fn test_app(app: &HirolaApp) -> Dom { - let router: &Router = app.data().unwrap(); - router.push("/page"); - router.render(app) +#[wasm_bindgen_test] +fn app_renders() { + let mut app: App<()> = App::new(()); + fn test_app(app: &App<()>) -> Dom { + html! { + "Test" + } } + let node = &body(); + app.route("/", test_app); + app.mount_to(&node); + assert_eq!("Test", inner_html(&node)); +} - let res = app.render_to_string(test_app); - assert_eq!("
    Page
    ", &res); +#[wasm_bindgen_test] +fn router_renders() { + let mut app: App<()> = App::new(()); + app.route("/", |_| { + html! { +
    "Main"
    + } + }); + let node = &body(); + let dom = app.mount_to(&node); + assert_eq!("
    Main
    ", inner_html(&node)); }