From d6a294c04305cf065cecad379bdd03986376b27d Mon Sep 17 00:00:00 2001 From: gyoder <70408179+gyoder@users.noreply.github.com> Date: Sat, 21 Jun 2025 23:29:00 -0600 Subject: [PATCH 01/25] started htmlua-parser --- .gitignore | 21 + Cargo.lock | 1078 +++++++++++++++++++++++++++++++++++++ cargo.toml | 3 + htmlua-parser/Cargo.toml | 10 + htmlua-parser/src/lib.rs | 109 ++++ htmlua-server/Cargo.toml | 6 + htmlua-server/src/main.rs | 3 + 7 files changed, 1230 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 cargo.toml create mode 100644 htmlua-parser/Cargo.toml create mode 100644 htmlua-parser/src/lib.rs create mode 100644 htmlua-server/Cargo.toml create mode 100644 htmlua-server/src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ad67955 --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +# Generated by Cargo +# will have compiled files and executables +debug +target + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +# Generated by cargo mutants +# Contains mutation testing data +**/mutants.out*/ + +# RustRover +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..04bec2a --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1078 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" + +[[package]] +name = "bstr" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cc" +version = "1.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cssparser" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "754b69d351cdc2d8ee09ae203db831e005560fc6030da058f86ad60c92a9cb0a" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "matches", + "phf 0.8.0", + "proc-macro2", + "quote", + "smallvec", + "syn 1.0.109", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.104", +] + +[[package]] +name = "derive_more" +version = "0.99.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.104", +] + +[[package]] +name = "dtoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "env_home" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" + +[[package]] +name = "erased-serde" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e004d887f51fcb9fef17317a2f3525c887d8aa3f4f50fed920816a688284a5b7" +dependencies = [ + "serde", + "typeid", +] + +[[package]] +name = "errno" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "html5ever" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7" +dependencies = [ + "log", + "mac", + "markup5ever", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "htmlua-parser" +version = "0.1.0" +dependencies = [ + "anyhow", + "kuchikiki", + "mlua", + "tendril", +] + +[[package]] +name = "htmlua-server" +version = "0.1.0" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + +[[package]] +name = "kuchikiki" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e4755b7b995046f510a7520c42b2fed58b77bd94d5a87a8eb43d2fd126da8" +dependencies = [ + "cssparser", + "html5ever", + "indexmap", + "matches", + "selectors", +] + +[[package]] +name = "libc" +version = "0.2.174" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" + +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "lock_api" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "lua-src" +version = "547.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edaf29e3517b49b8b746701e5648ccb5785cde1c119062cbabbc5d5cd115e42" +dependencies = [ + "cc", +] + +[[package]] +name = "luajit-src" +version = "210.5.12+a4f56a4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3a8e7962a5368d5f264d045a5a255e90f9aa3fc1941ae15a8d2940d42cac671" +dependencies = [ + "cc", + "which", +] + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "markup5ever" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016" +dependencies = [ + "log", + "phf 0.10.1", + "phf_codegen 0.10.0", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "mlua" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1f5f8fbebc7db5f671671134b9321c4b9aa9adeafccfd9a8c020ae45c6a35d0" +dependencies = [ + "bstr", + "either", + "erased-serde", + "mlua-sys", + "num-traits", + "parking_lot", + "rustc-hash", + "rustversion", + "serde", + "serde-value", +] + +[[package]] +name = "mlua-sys" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "380c1f7e2099cafcf40e51d3a9f20a346977587aa4d012eae1f043149a728a93" +dependencies = [ + "cc", + "cfg-if", + "lua-src", + "luajit-src", + "pkg-config", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "parking_lot" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "phf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +dependencies = [ + "phf_macros", + "phf_shared 0.8.0", + "proc-macro-hack", +] + +[[package]] +name = "phf" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +dependencies = [ + "phf_shared 0.10.0", +] + +[[package]] +name = "phf_codegen" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", +] + +[[package]] +name = "phf_codegen" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", +] + +[[package]] +name = "phf_generator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +dependencies = [ + "phf_shared 0.8.0", + "rand 0.7.3", +] + +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared 0.10.0", + "rand 0.8.5", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared 0.11.3", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6fde18ff429ffc8fe78e2bf7f8b7a5a5a6e2a8b58bc5a9ac69198bbda9189c" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher 0.3.11", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher 0.3.11", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher 1.0.1", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[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 = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", + "rand_pcg", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "redox_syscall" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" +dependencies = [ + "bitflags 2.9.1", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +dependencies = [ + "bitflags 2.9.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustversion" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "selectors" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df320f1889ac4ba6bc0cdc9c9af7af4bd64bb927bccdf32d81140dc1f9be12fe" +dependencies = [ + "bitflags 1.3.2", + "cssparser", + "derive_more", + "fxhash", + "log", + "matches", + "phf 0.8.0", + "phf_codegen 0.8.0", + "precomputed-hash", + "servo_arc", + "smallvec", + "thin-slice", +] + +[[package]] +name = "semver" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float", + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "servo_arc" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98238b800e0d1576d8b6e3de32827c2d74bee68bb97748dcf5071fb53965432" +dependencies = [ + "nodrop", + "stable_deref_trait", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.11.3", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "thin-slice" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "which" +version = "7.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d643ce3fd3e5b54854602a080f34fb10ab75e0b813ee32d00ca2b44fa74762" +dependencies = [ + "either", + "env_home", + "rustix", + "winsafe", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" +dependencies = [ + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "winsafe" +version = "0.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" + +[[package]] +name = "zerocopy" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] diff --git a/cargo.toml b/cargo.toml new file mode 100644 index 0000000..89e71bf --- /dev/null +++ b/cargo.toml @@ -0,0 +1,3 @@ +[workspace] +members = ["htmlua-parser", "htmlua-server"] +resolver = "2" diff --git a/htmlua-parser/Cargo.toml b/htmlua-parser/Cargo.toml new file mode 100644 index 0000000..8a18618 --- /dev/null +++ b/htmlua-parser/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "htmlua-parser" +version = "0.1.0" +edition = "2024" + +[dependencies] +anyhow = "1.0.98" +kuchikiki = "0.8.2" +mlua = { version = "0.10.5", features = ["lua54", "vendored", "serialize"] } +tendril = "0.4.3" diff --git a/htmlua-parser/src/lib.rs b/htmlua-parser/src/lib.rs new file mode 100644 index 0000000..80a21dd --- /dev/null +++ b/htmlua-parser/src/lib.rs @@ -0,0 +1,109 @@ +#![warn(clippy::pedantic)] + +use std::cell::RefCell; +use std::fmt::Write; +use std::rc::Rc; + +use anyhow::Result; +use anyhow::anyhow; +use kuchikiki::{ + NodeRef, + traits::TendrilSink, +}; +use mlua::{Lua, Table}; +use tendril::fmt; +use tendril::{Atomicity, Tendril}; + +fn create_luahtml_stdlib(l: &Lua, stdout: &Rc>) -> mlua::Result { + let t = l.create_table()?; + + // This cannot be the best way to do this + + let stdout_println = stdout.clone(); + t.set( + "println", + l.create_function(move |_, text: String| { + let mut stdout_ref = stdout_println.borrow_mut(); + writeln!(stdout_ref, "{text}").map_err(mlua::Error::external)?; + Ok(()) + })?, + )?; + + let stdout_print = stdout.clone(); + t.set( + "print", + l.create_function(move |_, text: String| { + let mut stdout_ref = stdout_print.borrow_mut(); + write!(stdout_ref, "{text}").map_err(mlua::Error::external)?; + Ok(()) + })?, + )?; + Ok(t) +} + +pub fn parse>>(to_parse: T) -> Result +where + tendril::Tendril: std::convert::From>, +{ + let document = kuchikiki::parse_html().one(to_parse.into()); + let lua = Lua::new(); + let globals = lua.globals(); + + let stdout: Rc> = Rc::new(RefCell::new(String::new())); + + let htmlua_table = create_luahtml_stdlib(&lua, &stdout) + .map_err(|e| anyhow::anyhow!("Failed to create Lua stdlib: {}", e))?; + + globals + .set("htmlua", htmlua_table) + .map_err(|e| anyhow::anyhow!("Failed to set global: {}", e))?; + + let lua_elements: Vec<_> = match document.select("lua") { + Ok(e) => e.collect(), + Err(()) => return Err(anyhow!("Unable to find Lua")), + }; + + for node in lua_elements { + if let Some(text_node) = node.as_node().first_child() { + if let Some(lua_code) = text_node.as_text() { + stdout.borrow_mut().clear(); + lua.load(lua_code.borrow().as_str()) + .exec() + .map_err(|e| anyhow::anyhow!("Failed to execute Lua: {}", e))?; + node.as_node() + .insert_before(NodeRef::new_text(stdout.borrow().as_str())); + node.as_node().detach(); + } + } + } + + Ok(document) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn base() -> Result<()> { + let page = r#" + + + + Basic HTML Page + + +

Hello World

+

This is a paragraph.

+ + htmlua.println("Test from Lua!") + + + "#; + let d = parse(page)?; + let text = d.select_first("span").unwrap().as_node().text_contents(); + assert_eq!(text, "Test from Lua!\n"); + assert!(d.select_first("lua").is_err()); + Ok(()) + } +} diff --git a/htmlua-server/Cargo.toml b/htmlua-server/Cargo.toml new file mode 100644 index 0000000..13df255 --- /dev/null +++ b/htmlua-server/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "htmlua-server" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/htmlua-server/src/main.rs b/htmlua-server/src/main.rs new file mode 100644 index 0000000..e7a11a9 --- /dev/null +++ b/htmlua-server/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} From 013317c13c6389dd41ba79f4d244ff5a230df28c Mon Sep 17 00:00:00 2001 From: gyoder <70408179+gyoder@users.noreply.github.com> Date: Mon, 23 Jun 2025 17:42:36 -0600 Subject: [PATCH 02/25] applied suggestions from @kdkasad --- cargo.toml => Cargo.toml | 2 ++ htmlua-parser/Cargo.toml | 4 ++++ htmlua-parser/src/lib.rs | 24 +++++++++++------------- 3 files changed, 17 insertions(+), 13 deletions(-) rename cargo.toml => Cargo.toml (97%) diff --git a/cargo.toml b/Cargo.toml similarity index 97% rename from cargo.toml rename to Cargo.toml index 89e71bf..931e4cc 100644 --- a/cargo.toml +++ b/Cargo.toml @@ -1,3 +1,5 @@ [workspace] members = ["htmlua-parser", "htmlua-server"] resolver = "2" + + diff --git a/htmlua-parser/Cargo.toml b/htmlua-parser/Cargo.toml index 8a18618..b611fde 100644 --- a/htmlua-parser/Cargo.toml +++ b/htmlua-parser/Cargo.toml @@ -8,3 +8,7 @@ anyhow = "1.0.98" kuchikiki = "0.8.2" mlua = { version = "0.10.5", features = ["lua54", "vendored", "serialize"] } tendril = "0.4.3" + + +[lints.clippy] +pedantic = { level = "warn", priority = -1 } diff --git a/htmlua-parser/src/lib.rs b/htmlua-parser/src/lib.rs index 80a21dd..dcbd4b1 100644 --- a/htmlua-parser/src/lib.rs +++ b/htmlua-parser/src/lib.rs @@ -1,5 +1,3 @@ -#![warn(clippy::pedantic)] - use std::cell::RefCell; use std::fmt::Write; use std::rc::Rc; @@ -24,8 +22,7 @@ fn create_luahtml_stdlib(l: &Lua, stdout: &Rc>) -> mlua::Result< "println", l.create_function(move |_, text: String| { let mut stdout_ref = stdout_println.borrow_mut(); - writeln!(stdout_ref, "{text}").map_err(mlua::Error::external)?; - Ok(()) + writeln!(stdout_ref, "{text}").map_err(mlua::Error::external) })?, )?; @@ -34,15 +31,17 @@ fn create_luahtml_stdlib(l: &Lua, stdout: &Rc>) -> mlua::Result< "print", l.create_function(move |_, text: String| { let mut stdout_ref = stdout_print.borrow_mut(); - write!(stdout_ref, "{text}").map_err(mlua::Error::external)?; - Ok(()) + write!(stdout_ref, "{text}").map_err(mlua::Error::external) })?, )?; Ok(t) } -pub fn parse>>(to_parse: T) -> Result +pub fn parse(to_parse: T) -> Result where + F: fmt::Format, + A: Atomicity, + T: Into>, tendril::Tendril: std::convert::From>, { let document = kuchikiki::parse_html().one(to_parse.into()); @@ -52,11 +51,11 @@ where let stdout: Rc> = Rc::new(RefCell::new(String::new())); let htmlua_table = create_luahtml_stdlib(&lua, &stdout) - .map_err(|e| anyhow::anyhow!("Failed to create Lua stdlib: {}", e))?; + .map_err(|e| anyhow!("Failed to create Lua stdlib: {}", e))?; globals .set("htmlua", htmlua_table) - .map_err(|e| anyhow::anyhow!("Failed to set global: {}", e))?; + .map_err(|e| anyhow!("Failed to set global: {}", e))?; let lua_elements: Vec<_> = match document.select("lua") { Ok(e) => e.collect(), @@ -69,7 +68,7 @@ where stdout.borrow_mut().clear(); lua.load(lua_code.borrow().as_str()) .exec() - .map_err(|e| anyhow::anyhow!("Failed to execute Lua: {}", e))?; + .map_err(|e| anyhow!("Failed to execute Lua: {}", e))?; node.as_node() .insert_before(NodeRef::new_text(stdout.borrow().as_str())); node.as_node().detach(); @@ -85,7 +84,7 @@ mod tests { use super::*; #[test] - fn base() -> Result<()> { + fn base() { let page = r#" @@ -100,10 +99,9 @@ mod tests { "#; - let d = parse(page)?; + let d = parse(page).unwrap(); let text = d.select_first("span").unwrap().as_node().text_contents(); assert_eq!(text, "Test from Lua!\n"); assert!(d.select_first("lua").is_err()); - Ok(()) } } From 0c33e46b3055defe2baf37ce609cf6f1a7e2b370 Mon Sep 17 00:00:00 2001 From: gyoder <70408179+gyoder@users.noreply.github.com> Date: Mon, 23 Jun 2025 20:04:08 -0600 Subject: [PATCH 03/25] added template expanding --- htmlua-parser/src/lib.rs | 88 ++++++++++++++++--- htmlua-parser/tests/components/comp1.html | 1 + .../tests/components/comp2_level1.html | 4 + .../tests/components/comp2_level2.html | 1 + 4 files changed, 82 insertions(+), 12 deletions(-) create mode 100644 htmlua-parser/tests/components/comp1.html create mode 100644 htmlua-parser/tests/components/comp2_level1.html create mode 100644 htmlua-parser/tests/components/comp2_level2.html diff --git a/htmlua-parser/src/lib.rs b/htmlua-parser/src/lib.rs index dcbd4b1..bf62208 100644 --- a/htmlua-parser/src/lib.rs +++ b/htmlua-parser/src/lib.rs @@ -1,5 +1,7 @@ use std::cell::RefCell; use std::fmt::Write; +use std::fs::read_to_string; +use std::path::PathBuf; use std::rc::Rc; use anyhow::Result; @@ -9,8 +11,7 @@ use kuchikiki::{ traits::TendrilSink, }; use mlua::{Lua, Table}; -use tendril::fmt; -use tendril::{Atomicity, Tendril}; +use tendril::Atomicity; fn create_luahtml_stdlib(l: &Lua, stdout: &Rc>) -> mlua::Result
{ let t = l.create_table()?; @@ -37,14 +38,7 @@ fn create_luahtml_stdlib(l: &Lua, stdout: &Rc>) -> mlua::Result< Ok(t) } -pub fn parse(to_parse: T) -> Result -where - F: fmt::Format, - A: Atomicity, - T: Into>, - tendril::Tendril: std::convert::From>, -{ - let document = kuchikiki::parse_html().one(to_parse.into()); +pub fn execute_lua(document: NodeRef) -> Result { let lua = Lua::new(); let globals = lua.globals(); @@ -79,12 +73,32 @@ where Ok(document) } +pub fn expand_template(document: NodeRef, component_path: &PathBuf) -> Result { + for i in document.select("include").map_err(|()| anyhow!("Error finding include"))? { + let attrs = match i.as_node().as_element() { + Some(e) => e.attributes.borrow(), + None => continue, + }; + if let Some(include_path) = attrs.get("path") { + let mut item_path = component_path.clone(); + item_path.push(include_path); + println!("{item_path:?}"); + let component_text = read_to_string(item_path)?; + let new_node = kuchikiki::parse_html().one(component_text); + let replaced_node = expand_template(new_node, component_path)?; + i.as_node().insert_before(replaced_node); + i.as_node().detach(); + } + } + Ok(document) +} + #[cfg(test)] mod tests { use super::*; #[test] - fn base() { + fn basic_lua() { let page = r#" @@ -99,9 +113,59 @@ mod tests { "#; - let d = parse(page).unwrap(); + let document = kuchikiki::parse_html().one(page); + let d = execute_lua(document).unwrap(); let text = d.select_first("span").unwrap().as_node().text_contents(); assert_eq!(text, "Test from Lua!\n"); assert!(d.select_first("lua").is_err()); } + + #[test] + fn basic_include() { + let page = r#" + + + + Basic HTML Page + + +

Hello World

+

This is a paragraph.

+ + + htmlua.println("Test from Lua!") + + + "#; + let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + p.push("tests/components"); + let document = kuchikiki::parse_html().one(page); + let d = expand_template(document, &p).unwrap(); + let text = d.select_first("span").unwrap().as_node().text_contents(); + assert_eq!(text, "included1"); + assert!(d.select_first("include").is_err()); + } + + #[test] + fn recursive_include() { + let page = r#" + + + + Basic HTML Page + + +

Hello World

+ + + "#; + let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + p.push("tests/components"); + let document = kuchikiki::parse_html().one(page); + let d = expand_template(document, &p).unwrap(); + let text = d.select_first("span").unwrap().as_node().text_contents(); + assert_eq!(text, "included_twice"); + assert!(d.select_first("include").is_err()); + } + } diff --git a/htmlua-parser/tests/components/comp1.html b/htmlua-parser/tests/components/comp1.html new file mode 100644 index 0000000..516d00e --- /dev/null +++ b/htmlua-parser/tests/components/comp1.html @@ -0,0 +1 @@ +included1 diff --git a/htmlua-parser/tests/components/comp2_level1.html b/htmlua-parser/tests/components/comp2_level1.html new file mode 100644 index 0000000..086a939 --- /dev/null +++ b/htmlua-parser/tests/components/comp2_level1.html @@ -0,0 +1,4 @@ +
+

test

+ +
diff --git a/htmlua-parser/tests/components/comp2_level2.html b/htmlua-parser/tests/components/comp2_level2.html new file mode 100644 index 0000000..5a8bced --- /dev/null +++ b/htmlua-parser/tests/components/comp2_level2.html @@ -0,0 +1 @@ +included_twice From 93cee588074981d33bd0da54aaa2e9c0311c3d87 Mon Sep 17 00:00:00 2001 From: gyoder <70408179+gyoder@users.noreply.github.com> Date: Wed, 25 Jun 2025 20:21:37 -0600 Subject: [PATCH 04/25] added include elements --- htmlua-parser/src/lib.rs | 80 +++++++++++++++++-- .../tests/components/with_include1.html | 7 ++ 2 files changed, 81 insertions(+), 6 deletions(-) create mode 100644 htmlua-parser/tests/components/with_include1.html diff --git a/htmlua-parser/src/lib.rs b/htmlua-parser/src/lib.rs index bf62208..d5b291f 100644 --- a/htmlua-parser/src/lib.rs +++ b/htmlua-parser/src/lib.rs @@ -11,7 +11,6 @@ use kuchikiki::{ traits::TendrilSink, }; use mlua::{Lua, Table}; -use tendril::Atomicity; fn create_luahtml_stdlib(l: &Lua, stdout: &Rc>) -> mlua::Result
{ let t = l.create_table()?; @@ -73,7 +72,21 @@ pub fn execute_lua(document: NodeRef) -> Result { Ok(document) } -pub fn expand_template(document: NodeRef, component_path: &PathBuf) -> Result { +pub fn expand_template(document: NodeRef, component_path: &PathBuf, include_from: Option<&NodeRef>) -> Result { + if let Some(from_node) = include_from { + + + for i in document.select("includeelement").map_err(|()| anyhow!("Error finding includeelement"))? { + if let Some(name) = i.attributes.borrow().get("name") { + let exported = from_node.select_first(format!("exportelement.{name}").as_str()).map_err(|()| anyhow!("Error finding exportelement"))?; + exported.as_node().children().for_each(|c| i.as_node().insert_after(c)); + } + } + while let Ok(i) = document.select_first("includeelement") { + i.as_node().detach(); + } + + } for i in document.select("include").map_err(|()| anyhow!("Error finding include"))? { let attrs = match i.as_node().as_element() { Some(e) => e.attributes.borrow(), @@ -82,10 +95,9 @@ pub fn expand_template(document: NodeRef, component_path: &PathBuf) -> Result + + + Basic HTML Page + + +

Hello World

+

This is a paragraph.

+ + htmlua.println("Test from Lua!") + +
+ htmlua.println("test 2") +
+ + "#; + let document = kuchikiki::parse_html().one(page); + let d = execute_lua(document).unwrap(); + let text = d.select_first("#ta").unwrap().as_node().text_contents(); + assert_eq!(text, "Test from Lua!\n"); + let text = d.select_first("#tb").unwrap().as_node().text_contents(); + assert_eq!(text, "test 2\n"); + assert!(d.select_first("lua").is_err()); + } + #[test] fn basic_include() { let page = r#" @@ -140,7 +180,7 @@ mod tests { let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR")); p.push("tests/components"); let document = kuchikiki::parse_html().one(page); - let d = expand_template(document, &p).unwrap(); + let d = expand_template(document, &p, None).unwrap(); let text = d.select_first("span").unwrap().as_node().text_contents(); assert_eq!(text, "included1"); assert!(d.select_first("include").is_err()); @@ -162,10 +202,38 @@ mod tests { let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR")); p.push("tests/components"); let document = kuchikiki::parse_html().one(page); - let d = expand_template(document, &p).unwrap(); + let d = expand_template(document, &p, None).unwrap(); let text = d.select_first("span").unwrap().as_node().text_contents(); assert_eq!(text, "included_twice"); assert!(d.select_first("include").is_err()); } + #[test] + fn export_element_include() { + let page = r#" + + + + Basic HTML Page + + +

Hello World

+ + element 1 + element 2 + + + "#; + let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + p.push("tests/components"); + let document = kuchikiki::parse_html().one(page); + let d = expand_template(document, &p, None).unwrap(); + let text = d.select_first("#ta").unwrap().as_node().text_contents(); + assert_eq!(text, "element 1"); + let text = d.select_first("#tb").unwrap().as_node().text_contents(); + assert_eq!(text, "element 2"); + assert!(d.select_first("exportelement").is_err()); + assert!(d.select_first("includeelement").is_err()); + } + } diff --git a/htmlua-parser/tests/components/with_include1.html b/htmlua-parser/tests/components/with_include1.html new file mode 100644 index 0000000..4902bf1 --- /dev/null +++ b/htmlua-parser/tests/components/with_include1.html @@ -0,0 +1,7 @@ +
+ +
+
+ +
+
From e02572711822188ef7fd4f57a2c7262bf0496d8f Mon Sep 17 00:00:00 2001 From: gyoder <70408179+gyoder@users.noreply.github.com> Date: Sat, 5 Jul 2025 16:13:17 -0600 Subject: [PATCH 05/25] added apache2 cgi script --- Cargo.lock | 8 ++ Cargo.toml | 2 +- htmlua-apache-cgi/Cargo.toml | 8 ++ htmlua-apache-cgi/src/main.rs | 32 +++++ htmlua-parser/src/lib.rs | 239 +--------------------------------- htmlua-parser/src/render.rs | 239 ++++++++++++++++++++++++++++++++++ htmlua-parser/src/serve.rs | 39 ++++++ 7 files changed, 329 insertions(+), 238 deletions(-) create mode 100644 htmlua-apache-cgi/Cargo.toml create mode 100644 htmlua-apache-cgi/src/main.rs create mode 100644 htmlua-parser/src/render.rs create mode 100644 htmlua-parser/src/serve.rs diff --git a/Cargo.lock b/Cargo.lock index 04bec2a..17554c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -211,6 +211,14 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "htmlua-apache-cgi" +version = "0.1.0" +dependencies = [ + "htmlua-parser", + "kuchikiki", +] + [[package]] name = "htmlua-parser" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 931e4cc..f64ae5e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["htmlua-parser", "htmlua-server"] +members = [ "htmlua-apache-cgi","htmlua-parser", "htmlua-server"] resolver = "2" diff --git a/htmlua-apache-cgi/Cargo.toml b/htmlua-apache-cgi/Cargo.toml new file mode 100644 index 0000000..7a8052d --- /dev/null +++ b/htmlua-apache-cgi/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "htmlua-apache-cgi" +version = "0.1.0" +edition = "2024" + +[dependencies] +htmlua-parser = { path = "../htmlua-parser" } +kuchikiki = "0.8.2" diff --git a/htmlua-apache-cgi/src/main.rs b/htmlua-apache-cgi/src/main.rs new file mode 100644 index 0000000..ce86dc6 --- /dev/null +++ b/htmlua-apache-cgi/src/main.rs @@ -0,0 +1,32 @@ +use std::env; +use std::fs; +use std::fs::read_to_string; +use std::path::{Path, PathBuf}; +use std::str::FromStr; +use htmlua_parser::serve::serve_content; +use kuchikiki::{ + NodeRef, + traits::TendrilSink, +}; + +fn main() { + println!("Content-Type: text/html\n"); + + let request_uri = env::var("PATH_INFO").unwrap_or_else(|_| "".to_string()); + + let page = serve_content(request_uri.as_str()).unwrap(); + + + // Output the final expanded HTML + println!("{page}"); +} + +// Simple HTML escape function +fn html_escape(input: &str) -> String { + input + .replace('&', "&") + .replace('<', "<") + .replace('>', ">") + .replace('"', """) + .replace('\'', "'") +} diff --git a/htmlua-parser/src/lib.rs b/htmlua-parser/src/lib.rs index d5b291f..75aac9c 100644 --- a/htmlua-parser/src/lib.rs +++ b/htmlua-parser/src/lib.rs @@ -1,239 +1,4 @@ -use std::cell::RefCell; -use std::fmt::Write; -use std::fs::read_to_string; -use std::path::PathBuf; -use std::rc::Rc; -use anyhow::Result; -use anyhow::anyhow; -use kuchikiki::{ - NodeRef, - traits::TendrilSink, -}; -use mlua::{Lua, Table}; +pub mod render; +pub mod serve; -fn create_luahtml_stdlib(l: &Lua, stdout: &Rc>) -> mlua::Result
{ - let t = l.create_table()?; - - // This cannot be the best way to do this - - let stdout_println = stdout.clone(); - t.set( - "println", - l.create_function(move |_, text: String| { - let mut stdout_ref = stdout_println.borrow_mut(); - writeln!(stdout_ref, "{text}").map_err(mlua::Error::external) - })?, - )?; - - let stdout_print = stdout.clone(); - t.set( - "print", - l.create_function(move |_, text: String| { - let mut stdout_ref = stdout_print.borrow_mut(); - write!(stdout_ref, "{text}").map_err(mlua::Error::external) - })?, - )?; - Ok(t) -} - -pub fn execute_lua(document: NodeRef) -> Result { - let lua = Lua::new(); - let globals = lua.globals(); - - let stdout: Rc> = Rc::new(RefCell::new(String::new())); - - let htmlua_table = create_luahtml_stdlib(&lua, &stdout) - .map_err(|e| anyhow!("Failed to create Lua stdlib: {}", e))?; - - globals - .set("htmlua", htmlua_table) - .map_err(|e| anyhow!("Failed to set global: {}", e))?; - - let lua_elements: Vec<_> = match document.select("lua") { - Ok(e) => e.collect(), - Err(()) => return Err(anyhow!("Unable to find Lua")), - }; - - for node in lua_elements { - if let Some(text_node) = node.as_node().first_child() { - if let Some(lua_code) = text_node.as_text() { - stdout.borrow_mut().clear(); - lua.load(lua_code.borrow().as_str()) - .exec() - .map_err(|e| anyhow!("Failed to execute Lua: {}", e))?; - node.as_node() - .insert_before(NodeRef::new_text(stdout.borrow().as_str())); - node.as_node().detach(); - } - } - } - - Ok(document) -} - -pub fn expand_template(document: NodeRef, component_path: &PathBuf, include_from: Option<&NodeRef>) -> Result { - if let Some(from_node) = include_from { - - - for i in document.select("includeelement").map_err(|()| anyhow!("Error finding includeelement"))? { - if let Some(name) = i.attributes.borrow().get("name") { - let exported = from_node.select_first(format!("exportelement.{name}").as_str()).map_err(|()| anyhow!("Error finding exportelement"))?; - exported.as_node().children().for_each(|c| i.as_node().insert_after(c)); - } - } - while let Ok(i) = document.select_first("includeelement") { - i.as_node().detach(); - } - - } - for i in document.select("include").map_err(|()| anyhow!("Error finding include"))? { - let attrs = match i.as_node().as_element() { - Some(e) => e.attributes.borrow(), - None => continue, - }; - if let Some(include_path) = attrs.get("path") { - let mut item_path = component_path.clone(); - item_path.push(include_path); - let component_text = read_to_string(item_path)?; - let new_node = kuchikiki::parse_html().one(component_text); - let replaced_node = expand_template(new_node, component_path, Some(i.as_node()))?; - i.as_node().insert_before(replaced_node); - i.as_node().detach(); - } - } - Ok(document) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn basic_lua() { - let page = r#" - - - - Basic HTML Page - - -

Hello World

-

This is a paragraph.

- - htmlua.println("Test from Lua!") - - - "#; - let document = kuchikiki::parse_html().one(page); - let d = execute_lua(document).unwrap(); - let text = d.select_first("span").unwrap().as_node().text_contents(); - assert_eq!(text, "Test from Lua!\n"); - assert!(d.select_first("lua").is_err()); - } - - #[test] - fn multiple_lua() { - let page = r#" - - - - Basic HTML Page - - -

Hello World

-

This is a paragraph.

- - htmlua.println("Test from Lua!") - -
- htmlua.println("test 2") -
- - "#; - let document = kuchikiki::parse_html().one(page); - let d = execute_lua(document).unwrap(); - let text = d.select_first("#ta").unwrap().as_node().text_contents(); - assert_eq!(text, "Test from Lua!\n"); - let text = d.select_first("#tb").unwrap().as_node().text_contents(); - assert_eq!(text, "test 2\n"); - assert!(d.select_first("lua").is_err()); - } - - #[test] - fn basic_include() { - let page = r#" - - - - Basic HTML Page - - -

Hello World

-

This is a paragraph.

- - - htmlua.println("Test from Lua!") - - - "#; - let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - p.push("tests/components"); - let document = kuchikiki::parse_html().one(page); - let d = expand_template(document, &p, None).unwrap(); - let text = d.select_first("span").unwrap().as_node().text_contents(); - assert_eq!(text, "included1"); - assert!(d.select_first("include").is_err()); - } - - #[test] - fn recursive_include() { - let page = r#" - - - - Basic HTML Page - - -

Hello World

- - - "#; - let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - p.push("tests/components"); - let document = kuchikiki::parse_html().one(page); - let d = expand_template(document, &p, None).unwrap(); - let text = d.select_first("span").unwrap().as_node().text_contents(); - assert_eq!(text, "included_twice"); - assert!(d.select_first("include").is_err()); - } - - #[test] - fn export_element_include() { - let page = r#" - - - - Basic HTML Page - - -

Hello World

- - element 1 - element 2 - - - "#; - let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - p.push("tests/components"); - let document = kuchikiki::parse_html().one(page); - let d = expand_template(document, &p, None).unwrap(); - let text = d.select_first("#ta").unwrap().as_node().text_contents(); - assert_eq!(text, "element 1"); - let text = d.select_first("#tb").unwrap().as_node().text_contents(); - assert_eq!(text, "element 2"); - assert!(d.select_first("exportelement").is_err()); - assert!(d.select_first("includeelement").is_err()); - } - -} diff --git a/htmlua-parser/src/render.rs b/htmlua-parser/src/render.rs new file mode 100644 index 0000000..d5b291f --- /dev/null +++ b/htmlua-parser/src/render.rs @@ -0,0 +1,239 @@ +use std::cell::RefCell; +use std::fmt::Write; +use std::fs::read_to_string; +use std::path::PathBuf; +use std::rc::Rc; + +use anyhow::Result; +use anyhow::anyhow; +use kuchikiki::{ + NodeRef, + traits::TendrilSink, +}; +use mlua::{Lua, Table}; + +fn create_luahtml_stdlib(l: &Lua, stdout: &Rc>) -> mlua::Result
{ + let t = l.create_table()?; + + // This cannot be the best way to do this + + let stdout_println = stdout.clone(); + t.set( + "println", + l.create_function(move |_, text: String| { + let mut stdout_ref = stdout_println.borrow_mut(); + writeln!(stdout_ref, "{text}").map_err(mlua::Error::external) + })?, + )?; + + let stdout_print = stdout.clone(); + t.set( + "print", + l.create_function(move |_, text: String| { + let mut stdout_ref = stdout_print.borrow_mut(); + write!(stdout_ref, "{text}").map_err(mlua::Error::external) + })?, + )?; + Ok(t) +} + +pub fn execute_lua(document: NodeRef) -> Result { + let lua = Lua::new(); + let globals = lua.globals(); + + let stdout: Rc> = Rc::new(RefCell::new(String::new())); + + let htmlua_table = create_luahtml_stdlib(&lua, &stdout) + .map_err(|e| anyhow!("Failed to create Lua stdlib: {}", e))?; + + globals + .set("htmlua", htmlua_table) + .map_err(|e| anyhow!("Failed to set global: {}", e))?; + + let lua_elements: Vec<_> = match document.select("lua") { + Ok(e) => e.collect(), + Err(()) => return Err(anyhow!("Unable to find Lua")), + }; + + for node in lua_elements { + if let Some(text_node) = node.as_node().first_child() { + if let Some(lua_code) = text_node.as_text() { + stdout.borrow_mut().clear(); + lua.load(lua_code.borrow().as_str()) + .exec() + .map_err(|e| anyhow!("Failed to execute Lua: {}", e))?; + node.as_node() + .insert_before(NodeRef::new_text(stdout.borrow().as_str())); + node.as_node().detach(); + } + } + } + + Ok(document) +} + +pub fn expand_template(document: NodeRef, component_path: &PathBuf, include_from: Option<&NodeRef>) -> Result { + if let Some(from_node) = include_from { + + + for i in document.select("includeelement").map_err(|()| anyhow!("Error finding includeelement"))? { + if let Some(name) = i.attributes.borrow().get("name") { + let exported = from_node.select_first(format!("exportelement.{name}").as_str()).map_err(|()| anyhow!("Error finding exportelement"))?; + exported.as_node().children().for_each(|c| i.as_node().insert_after(c)); + } + } + while let Ok(i) = document.select_first("includeelement") { + i.as_node().detach(); + } + + } + for i in document.select("include").map_err(|()| anyhow!("Error finding include"))? { + let attrs = match i.as_node().as_element() { + Some(e) => e.attributes.borrow(), + None => continue, + }; + if let Some(include_path) = attrs.get("path") { + let mut item_path = component_path.clone(); + item_path.push(include_path); + let component_text = read_to_string(item_path)?; + let new_node = kuchikiki::parse_html().one(component_text); + let replaced_node = expand_template(new_node, component_path, Some(i.as_node()))?; + i.as_node().insert_before(replaced_node); + i.as_node().detach(); + } + } + Ok(document) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn basic_lua() { + let page = r#" + + + + Basic HTML Page + + +

Hello World

+

This is a paragraph.

+ + htmlua.println("Test from Lua!") + + + "#; + let document = kuchikiki::parse_html().one(page); + let d = execute_lua(document).unwrap(); + let text = d.select_first("span").unwrap().as_node().text_contents(); + assert_eq!(text, "Test from Lua!\n"); + assert!(d.select_first("lua").is_err()); + } + + #[test] + fn multiple_lua() { + let page = r#" + + + + Basic HTML Page + + +

Hello World

+

This is a paragraph.

+ + htmlua.println("Test from Lua!") + +
+ htmlua.println("test 2") +
+ + "#; + let document = kuchikiki::parse_html().one(page); + let d = execute_lua(document).unwrap(); + let text = d.select_first("#ta").unwrap().as_node().text_contents(); + assert_eq!(text, "Test from Lua!\n"); + let text = d.select_first("#tb").unwrap().as_node().text_contents(); + assert_eq!(text, "test 2\n"); + assert!(d.select_first("lua").is_err()); + } + + #[test] + fn basic_include() { + let page = r#" + + + + Basic HTML Page + + +

Hello World

+

This is a paragraph.

+ + + htmlua.println("Test from Lua!") + + + "#; + let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + p.push("tests/components"); + let document = kuchikiki::parse_html().one(page); + let d = expand_template(document, &p, None).unwrap(); + let text = d.select_first("span").unwrap().as_node().text_contents(); + assert_eq!(text, "included1"); + assert!(d.select_first("include").is_err()); + } + + #[test] + fn recursive_include() { + let page = r#" + + + + Basic HTML Page + + +

Hello World

+ + + "#; + let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + p.push("tests/components"); + let document = kuchikiki::parse_html().one(page); + let d = expand_template(document, &p, None).unwrap(); + let text = d.select_first("span").unwrap().as_node().text_contents(); + assert_eq!(text, "included_twice"); + assert!(d.select_first("include").is_err()); + } + + #[test] + fn export_element_include() { + let page = r#" + + + + Basic HTML Page + + +

Hello World

+ + element 1 + element 2 + + + "#; + let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + p.push("tests/components"); + let document = kuchikiki::parse_html().one(page); + let d = expand_template(document, &p, None).unwrap(); + let text = d.select_first("#ta").unwrap().as_node().text_contents(); + assert_eq!(text, "element 1"); + let text = d.select_first("#tb").unwrap().as_node().text_contents(); + assert_eq!(text, "element 2"); + assert!(d.select_first("exportelement").is_err()); + assert!(d.select_first("includeelement").is_err()); + } + +} diff --git a/htmlua-parser/src/serve.rs b/htmlua-parser/src/serve.rs new file mode 100644 index 0000000..6bbf73a --- /dev/null +++ b/htmlua-parser/src/serve.rs @@ -0,0 +1,39 @@ +use std::env; +use std::fs; +use std::fs::read_to_string; +use std::path::{Path, PathBuf}; +use std::str::FromStr; +use super::render::execute_lua; +use super::render::expand_template; +use anyhow::Result; +use kuchikiki::{ + NodeRef, + traits::TendrilSink, +}; + +pub fn serve_content(request_uri: &str) -> Result{ + // TODO: read from config + let pages = PathBuf::from("/var/www/htmlua/pages"); + let components = PathBuf::from("/var/www/htmlua/components"); + + let safe_path = Path::new(request_uri).strip_prefix("/")?; + let page_path = pages.join(safe_path); + + let page_string = read_to_string(&page_path)?; + + let doc = kuchikiki::parse_html().one(page_string); + let full_doc = expand_template(doc, &components, None)?; + + let executed_doc = execute_lua(full_doc)?; + + Ok(executed_doc.to_string()) +} + +fn html_escape(input: &str) -> String { + input + .replace('&', "&") + .replace('<', "<") + .replace('>', ">") + .replace('"', """) + .replace('\'', "'") +} From 7e896c1397a6d2ccbac31a63852b6ed0fbd32a8d Mon Sep 17 00:00:00 2001 From: gyoder <70408179+gyoder@users.noreply.github.com> Date: Sat, 5 Jul 2025 16:13:17 -0600 Subject: [PATCH 06/25] added apache2 cgi script --- Cargo.lock | 8 ++ Cargo.toml | 2 +- htmlua-apache-cgi/Cargo.toml | 8 ++ htmlua-apache-cgi/src/main.rs | 32 +++++ htmlua-parser/src/lib.rs | 239 +--------------------------------- htmlua-parser/src/render.rs | 239 ++++++++++++++++++++++++++++++++++ htmlua-parser/src/serve.rs | 39 ++++++ 7 files changed, 329 insertions(+), 238 deletions(-) create mode 100644 htmlua-apache-cgi/Cargo.toml create mode 100644 htmlua-apache-cgi/src/main.rs create mode 100644 htmlua-parser/src/render.rs create mode 100644 htmlua-parser/src/serve.rs diff --git a/Cargo.lock b/Cargo.lock index 04bec2a..17554c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -211,6 +211,14 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "htmlua-apache-cgi" +version = "0.1.0" +dependencies = [ + "htmlua-parser", + "kuchikiki", +] + [[package]] name = "htmlua-parser" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 931e4cc..f64ae5e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["htmlua-parser", "htmlua-server"] +members = [ "htmlua-apache-cgi","htmlua-parser", "htmlua-server"] resolver = "2" diff --git a/htmlua-apache-cgi/Cargo.toml b/htmlua-apache-cgi/Cargo.toml new file mode 100644 index 0000000..7a8052d --- /dev/null +++ b/htmlua-apache-cgi/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "htmlua-apache-cgi" +version = "0.1.0" +edition = "2024" + +[dependencies] +htmlua-parser = { path = "../htmlua-parser" } +kuchikiki = "0.8.2" diff --git a/htmlua-apache-cgi/src/main.rs b/htmlua-apache-cgi/src/main.rs new file mode 100644 index 0000000..ce86dc6 --- /dev/null +++ b/htmlua-apache-cgi/src/main.rs @@ -0,0 +1,32 @@ +use std::env; +use std::fs; +use std::fs::read_to_string; +use std::path::{Path, PathBuf}; +use std::str::FromStr; +use htmlua_parser::serve::serve_content; +use kuchikiki::{ + NodeRef, + traits::TendrilSink, +}; + +fn main() { + println!("Content-Type: text/html\n"); + + let request_uri = env::var("PATH_INFO").unwrap_or_else(|_| "".to_string()); + + let page = serve_content(request_uri.as_str()).unwrap(); + + + // Output the final expanded HTML + println!("{page}"); +} + +// Simple HTML escape function +fn html_escape(input: &str) -> String { + input + .replace('&', "&") + .replace('<', "<") + .replace('>', ">") + .replace('"', """) + .replace('\'', "'") +} diff --git a/htmlua-parser/src/lib.rs b/htmlua-parser/src/lib.rs index d5b291f..75aac9c 100644 --- a/htmlua-parser/src/lib.rs +++ b/htmlua-parser/src/lib.rs @@ -1,239 +1,4 @@ -use std::cell::RefCell; -use std::fmt::Write; -use std::fs::read_to_string; -use std::path::PathBuf; -use std::rc::Rc; -use anyhow::Result; -use anyhow::anyhow; -use kuchikiki::{ - NodeRef, - traits::TendrilSink, -}; -use mlua::{Lua, Table}; +pub mod render; +pub mod serve; -fn create_luahtml_stdlib(l: &Lua, stdout: &Rc>) -> mlua::Result
{ - let t = l.create_table()?; - - // This cannot be the best way to do this - - let stdout_println = stdout.clone(); - t.set( - "println", - l.create_function(move |_, text: String| { - let mut stdout_ref = stdout_println.borrow_mut(); - writeln!(stdout_ref, "{text}").map_err(mlua::Error::external) - })?, - )?; - - let stdout_print = stdout.clone(); - t.set( - "print", - l.create_function(move |_, text: String| { - let mut stdout_ref = stdout_print.borrow_mut(); - write!(stdout_ref, "{text}").map_err(mlua::Error::external) - })?, - )?; - Ok(t) -} - -pub fn execute_lua(document: NodeRef) -> Result { - let lua = Lua::new(); - let globals = lua.globals(); - - let stdout: Rc> = Rc::new(RefCell::new(String::new())); - - let htmlua_table = create_luahtml_stdlib(&lua, &stdout) - .map_err(|e| anyhow!("Failed to create Lua stdlib: {}", e))?; - - globals - .set("htmlua", htmlua_table) - .map_err(|e| anyhow!("Failed to set global: {}", e))?; - - let lua_elements: Vec<_> = match document.select("lua") { - Ok(e) => e.collect(), - Err(()) => return Err(anyhow!("Unable to find Lua")), - }; - - for node in lua_elements { - if let Some(text_node) = node.as_node().first_child() { - if let Some(lua_code) = text_node.as_text() { - stdout.borrow_mut().clear(); - lua.load(lua_code.borrow().as_str()) - .exec() - .map_err(|e| anyhow!("Failed to execute Lua: {}", e))?; - node.as_node() - .insert_before(NodeRef::new_text(stdout.borrow().as_str())); - node.as_node().detach(); - } - } - } - - Ok(document) -} - -pub fn expand_template(document: NodeRef, component_path: &PathBuf, include_from: Option<&NodeRef>) -> Result { - if let Some(from_node) = include_from { - - - for i in document.select("includeelement").map_err(|()| anyhow!("Error finding includeelement"))? { - if let Some(name) = i.attributes.borrow().get("name") { - let exported = from_node.select_first(format!("exportelement.{name}").as_str()).map_err(|()| anyhow!("Error finding exportelement"))?; - exported.as_node().children().for_each(|c| i.as_node().insert_after(c)); - } - } - while let Ok(i) = document.select_first("includeelement") { - i.as_node().detach(); - } - - } - for i in document.select("include").map_err(|()| anyhow!("Error finding include"))? { - let attrs = match i.as_node().as_element() { - Some(e) => e.attributes.borrow(), - None => continue, - }; - if let Some(include_path) = attrs.get("path") { - let mut item_path = component_path.clone(); - item_path.push(include_path); - let component_text = read_to_string(item_path)?; - let new_node = kuchikiki::parse_html().one(component_text); - let replaced_node = expand_template(new_node, component_path, Some(i.as_node()))?; - i.as_node().insert_before(replaced_node); - i.as_node().detach(); - } - } - Ok(document) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn basic_lua() { - let page = r#" - - - - Basic HTML Page - - -

Hello World

-

This is a paragraph.

- - htmlua.println("Test from Lua!") - - - "#; - let document = kuchikiki::parse_html().one(page); - let d = execute_lua(document).unwrap(); - let text = d.select_first("span").unwrap().as_node().text_contents(); - assert_eq!(text, "Test from Lua!\n"); - assert!(d.select_first("lua").is_err()); - } - - #[test] - fn multiple_lua() { - let page = r#" - - - - Basic HTML Page - - -

Hello World

-

This is a paragraph.

- - htmlua.println("Test from Lua!") - -
- htmlua.println("test 2") -
- - "#; - let document = kuchikiki::parse_html().one(page); - let d = execute_lua(document).unwrap(); - let text = d.select_first("#ta").unwrap().as_node().text_contents(); - assert_eq!(text, "Test from Lua!\n"); - let text = d.select_first("#tb").unwrap().as_node().text_contents(); - assert_eq!(text, "test 2\n"); - assert!(d.select_first("lua").is_err()); - } - - #[test] - fn basic_include() { - let page = r#" - - - - Basic HTML Page - - -

Hello World

-

This is a paragraph.

- - - htmlua.println("Test from Lua!") - - - "#; - let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - p.push("tests/components"); - let document = kuchikiki::parse_html().one(page); - let d = expand_template(document, &p, None).unwrap(); - let text = d.select_first("span").unwrap().as_node().text_contents(); - assert_eq!(text, "included1"); - assert!(d.select_first("include").is_err()); - } - - #[test] - fn recursive_include() { - let page = r#" - - - - Basic HTML Page - - -

Hello World

- - - "#; - let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - p.push("tests/components"); - let document = kuchikiki::parse_html().one(page); - let d = expand_template(document, &p, None).unwrap(); - let text = d.select_first("span").unwrap().as_node().text_contents(); - assert_eq!(text, "included_twice"); - assert!(d.select_first("include").is_err()); - } - - #[test] - fn export_element_include() { - let page = r#" - - - - Basic HTML Page - - -

Hello World

- - element 1 - element 2 - - - "#; - let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - p.push("tests/components"); - let document = kuchikiki::parse_html().one(page); - let d = expand_template(document, &p, None).unwrap(); - let text = d.select_first("#ta").unwrap().as_node().text_contents(); - assert_eq!(text, "element 1"); - let text = d.select_first("#tb").unwrap().as_node().text_contents(); - assert_eq!(text, "element 2"); - assert!(d.select_first("exportelement").is_err()); - assert!(d.select_first("includeelement").is_err()); - } - -} diff --git a/htmlua-parser/src/render.rs b/htmlua-parser/src/render.rs new file mode 100644 index 0000000..d5b291f --- /dev/null +++ b/htmlua-parser/src/render.rs @@ -0,0 +1,239 @@ +use std::cell::RefCell; +use std::fmt::Write; +use std::fs::read_to_string; +use std::path::PathBuf; +use std::rc::Rc; + +use anyhow::Result; +use anyhow::anyhow; +use kuchikiki::{ + NodeRef, + traits::TendrilSink, +}; +use mlua::{Lua, Table}; + +fn create_luahtml_stdlib(l: &Lua, stdout: &Rc>) -> mlua::Result
{ + let t = l.create_table()?; + + // This cannot be the best way to do this + + let stdout_println = stdout.clone(); + t.set( + "println", + l.create_function(move |_, text: String| { + let mut stdout_ref = stdout_println.borrow_mut(); + writeln!(stdout_ref, "{text}").map_err(mlua::Error::external) + })?, + )?; + + let stdout_print = stdout.clone(); + t.set( + "print", + l.create_function(move |_, text: String| { + let mut stdout_ref = stdout_print.borrow_mut(); + write!(stdout_ref, "{text}").map_err(mlua::Error::external) + })?, + )?; + Ok(t) +} + +pub fn execute_lua(document: NodeRef) -> Result { + let lua = Lua::new(); + let globals = lua.globals(); + + let stdout: Rc> = Rc::new(RefCell::new(String::new())); + + let htmlua_table = create_luahtml_stdlib(&lua, &stdout) + .map_err(|e| anyhow!("Failed to create Lua stdlib: {}", e))?; + + globals + .set("htmlua", htmlua_table) + .map_err(|e| anyhow!("Failed to set global: {}", e))?; + + let lua_elements: Vec<_> = match document.select("lua") { + Ok(e) => e.collect(), + Err(()) => return Err(anyhow!("Unable to find Lua")), + }; + + for node in lua_elements { + if let Some(text_node) = node.as_node().first_child() { + if let Some(lua_code) = text_node.as_text() { + stdout.borrow_mut().clear(); + lua.load(lua_code.borrow().as_str()) + .exec() + .map_err(|e| anyhow!("Failed to execute Lua: {}", e))?; + node.as_node() + .insert_before(NodeRef::new_text(stdout.borrow().as_str())); + node.as_node().detach(); + } + } + } + + Ok(document) +} + +pub fn expand_template(document: NodeRef, component_path: &PathBuf, include_from: Option<&NodeRef>) -> Result { + if let Some(from_node) = include_from { + + + for i in document.select("includeelement").map_err(|()| anyhow!("Error finding includeelement"))? { + if let Some(name) = i.attributes.borrow().get("name") { + let exported = from_node.select_first(format!("exportelement.{name}").as_str()).map_err(|()| anyhow!("Error finding exportelement"))?; + exported.as_node().children().for_each(|c| i.as_node().insert_after(c)); + } + } + while let Ok(i) = document.select_first("includeelement") { + i.as_node().detach(); + } + + } + for i in document.select("include").map_err(|()| anyhow!("Error finding include"))? { + let attrs = match i.as_node().as_element() { + Some(e) => e.attributes.borrow(), + None => continue, + }; + if let Some(include_path) = attrs.get("path") { + let mut item_path = component_path.clone(); + item_path.push(include_path); + let component_text = read_to_string(item_path)?; + let new_node = kuchikiki::parse_html().one(component_text); + let replaced_node = expand_template(new_node, component_path, Some(i.as_node()))?; + i.as_node().insert_before(replaced_node); + i.as_node().detach(); + } + } + Ok(document) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn basic_lua() { + let page = r#" + + + + Basic HTML Page + + +

Hello World

+

This is a paragraph.

+ + htmlua.println("Test from Lua!") + + + "#; + let document = kuchikiki::parse_html().one(page); + let d = execute_lua(document).unwrap(); + let text = d.select_first("span").unwrap().as_node().text_contents(); + assert_eq!(text, "Test from Lua!\n"); + assert!(d.select_first("lua").is_err()); + } + + #[test] + fn multiple_lua() { + let page = r#" + + + + Basic HTML Page + + +

Hello World

+

This is a paragraph.

+ + htmlua.println("Test from Lua!") + +
+ htmlua.println("test 2") +
+ + "#; + let document = kuchikiki::parse_html().one(page); + let d = execute_lua(document).unwrap(); + let text = d.select_first("#ta").unwrap().as_node().text_contents(); + assert_eq!(text, "Test from Lua!\n"); + let text = d.select_first("#tb").unwrap().as_node().text_contents(); + assert_eq!(text, "test 2\n"); + assert!(d.select_first("lua").is_err()); + } + + #[test] + fn basic_include() { + let page = r#" + + + + Basic HTML Page + + +

Hello World

+

This is a paragraph.

+ + + htmlua.println("Test from Lua!") + + + "#; + let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + p.push("tests/components"); + let document = kuchikiki::parse_html().one(page); + let d = expand_template(document, &p, None).unwrap(); + let text = d.select_first("span").unwrap().as_node().text_contents(); + assert_eq!(text, "included1"); + assert!(d.select_first("include").is_err()); + } + + #[test] + fn recursive_include() { + let page = r#" + + + + Basic HTML Page + + +

Hello World

+ + + "#; + let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + p.push("tests/components"); + let document = kuchikiki::parse_html().one(page); + let d = expand_template(document, &p, None).unwrap(); + let text = d.select_first("span").unwrap().as_node().text_contents(); + assert_eq!(text, "included_twice"); + assert!(d.select_first("include").is_err()); + } + + #[test] + fn export_element_include() { + let page = r#" + + + + Basic HTML Page + + +

Hello World

+ + element 1 + element 2 + + + "#; + let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + p.push("tests/components"); + let document = kuchikiki::parse_html().one(page); + let d = expand_template(document, &p, None).unwrap(); + let text = d.select_first("#ta").unwrap().as_node().text_contents(); + assert_eq!(text, "element 1"); + let text = d.select_first("#tb").unwrap().as_node().text_contents(); + assert_eq!(text, "element 2"); + assert!(d.select_first("exportelement").is_err()); + assert!(d.select_first("includeelement").is_err()); + } + +} diff --git a/htmlua-parser/src/serve.rs b/htmlua-parser/src/serve.rs new file mode 100644 index 0000000..6bbf73a --- /dev/null +++ b/htmlua-parser/src/serve.rs @@ -0,0 +1,39 @@ +use std::env; +use std::fs; +use std::fs::read_to_string; +use std::path::{Path, PathBuf}; +use std::str::FromStr; +use super::render::execute_lua; +use super::render::expand_template; +use anyhow::Result; +use kuchikiki::{ + NodeRef, + traits::TendrilSink, +}; + +pub fn serve_content(request_uri: &str) -> Result{ + // TODO: read from config + let pages = PathBuf::from("/var/www/htmlua/pages"); + let components = PathBuf::from("/var/www/htmlua/components"); + + let safe_path = Path::new(request_uri).strip_prefix("/")?; + let page_path = pages.join(safe_path); + + let page_string = read_to_string(&page_path)?; + + let doc = kuchikiki::parse_html().one(page_string); + let full_doc = expand_template(doc, &components, None)?; + + let executed_doc = execute_lua(full_doc)?; + + Ok(executed_doc.to_string()) +} + +fn html_escape(input: &str) -> String { + input + .replace('&', "&") + .replace('<', "<") + .replace('>', ">") + .replace('"', """) + .replace('\'', "'") +} From 88b22b4e774c00344f8dad2d9eda6e1ff0491a6e Mon Sep 17 00:00:00 2001 From: gyoder <70408179+gyoder@users.noreply.github.com> Date: Sat, 5 Jul 2025 16:17:58 -0600 Subject: [PATCH 07/25] cleaned up --- .rustfmt.toml | 11 +++++++++++ htmlua-apache-cgi/src/main.rs | 23 +--------------------- htmlua-parser/src/lib.rs | 2 -- htmlua-parser/src/render.rs | 37 ++++++++++++++++++----------------- htmlua-parser/src/serve.rs | 23 ++++------------------ 5 files changed, 35 insertions(+), 61 deletions(-) create mode 100644 .rustfmt.toml diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 0000000..21ebfc1 --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1,11 @@ +unstable_features = true + +max_width = 120 +fn_params_layout = "Compressed" +fn_single_line = true +fn_call_width = 120 +where_single_line = true +brace_style = "PreferSameLine" +blank_lines_upper_bound = 5 +combine_control_expr = false +wrap_comments = false diff --git a/htmlua-apache-cgi/src/main.rs b/htmlua-apache-cgi/src/main.rs index ce86dc6..ab91cb1 100644 --- a/htmlua-apache-cgi/src/main.rs +++ b/htmlua-apache-cgi/src/main.rs @@ -1,32 +1,11 @@ -use std::env; -use std::fs; -use std::fs::read_to_string; -use std::path::{Path, PathBuf}; -use std::str::FromStr; use htmlua_parser::serve::serve_content; -use kuchikiki::{ - NodeRef, - traits::TendrilSink, -}; +use std::env; fn main() { println!("Content-Type: text/html\n"); let request_uri = env::var("PATH_INFO").unwrap_or_else(|_| "".to_string()); - let page = serve_content(request_uri.as_str()).unwrap(); - - // Output the final expanded HTML println!("{page}"); } - -// Simple HTML escape function -fn html_escape(input: &str) -> String { - input - .replace('&', "&") - .replace('<', "<") - .replace('>', ">") - .replace('"', """) - .replace('\'', "'") -} diff --git a/htmlua-parser/src/lib.rs b/htmlua-parser/src/lib.rs index 75aac9c..03e66bd 100644 --- a/htmlua-parser/src/lib.rs +++ b/htmlua-parser/src/lib.rs @@ -1,4 +1,2 @@ - pub mod render; pub mod serve; - diff --git a/htmlua-parser/src/render.rs b/htmlua-parser/src/render.rs index d5b291f..58a8259 100644 --- a/htmlua-parser/src/render.rs +++ b/htmlua-parser/src/render.rs @@ -6,10 +6,7 @@ use std::rc::Rc; use anyhow::Result; use anyhow::anyhow; -use kuchikiki::{ - NodeRef, - traits::TendrilSink, -}; +use kuchikiki::{NodeRef, traits::TendrilSink}; use mlua::{Lua, Table}; fn create_luahtml_stdlib(l: &Lua, stdout: &Rc>) -> mlua::Result
{ @@ -43,8 +40,8 @@ pub fn execute_lua(document: NodeRef) -> Result { let stdout: Rc> = Rc::new(RefCell::new(String::new())); - let htmlua_table = create_luahtml_stdlib(&lua, &stdout) - .map_err(|e| anyhow!("Failed to create Lua stdlib: {}", e))?; + let htmlua_table = + create_luahtml_stdlib(&lua, &stdout).map_err(|e| anyhow!("Failed to create Lua stdlib: {}", e))?; globals .set("htmlua", htmlua_table) @@ -74,20 +71,25 @@ pub fn execute_lua(document: NodeRef) -> Result { pub fn expand_template(document: NodeRef, component_path: &PathBuf, include_from: Option<&NodeRef>) -> Result { if let Some(from_node) = include_from { - - - for i in document.select("includeelement").map_err(|()| anyhow!("Error finding includeelement"))? { + for i in document + .select("includeelement") + .map_err(|()| anyhow!("Error finding includeelement"))? + { if let Some(name) = i.attributes.borrow().get("name") { - let exported = from_node.select_first(format!("exportelement.{name}").as_str()).map_err(|()| anyhow!("Error finding exportelement"))?; + let exported = from_node + .select_first(format!("exportelement.{name}").as_str()) + .map_err(|()| anyhow!("Error finding exportelement"))?; exported.as_node().children().for_each(|c| i.as_node().insert_after(c)); } } while let Ok(i) = document.select_first("includeelement") { i.as_node().detach(); } - } - for i in document.select("include").map_err(|()| anyhow!("Error finding include"))? { + for i in document + .select("include") + .map_err(|()| anyhow!("Error finding include"))? + { let attrs = match i.as_node().as_element() { Some(e) => e.attributes.borrow(), None => continue, @@ -125,7 +127,7 @@ mod tests { "#; - let document = kuchikiki::parse_html().one(page); + let document = kuchikiki::parse_html().one(page); let d = execute_lua(document).unwrap(); let text = d.select_first("span").unwrap().as_node().text_contents(); assert_eq!(text, "Test from Lua!\n"); @@ -151,7 +153,7 @@ mod tests { "#; - let document = kuchikiki::parse_html().one(page); + let document = kuchikiki::parse_html().one(page); let d = execute_lua(document).unwrap(); let text = d.select_first("#ta").unwrap().as_node().text_contents(); assert_eq!(text, "Test from Lua!\n"); @@ -179,7 +181,7 @@ mod tests { "#; let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR")); p.push("tests/components"); - let document = kuchikiki::parse_html().one(page); + let document = kuchikiki::parse_html().one(page); let d = expand_template(document, &p, None).unwrap(); let text = d.select_first("span").unwrap().as_node().text_contents(); assert_eq!(text, "included1"); @@ -201,7 +203,7 @@ mod tests { "#; let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR")); p.push("tests/components"); - let document = kuchikiki::parse_html().one(page); + let document = kuchikiki::parse_html().one(page); let d = expand_template(document, &p, None).unwrap(); let text = d.select_first("span").unwrap().as_node().text_contents(); assert_eq!(text, "included_twice"); @@ -226,7 +228,7 @@ mod tests { "#; let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR")); p.push("tests/components"); - let document = kuchikiki::parse_html().one(page); + let document = kuchikiki::parse_html().one(page); let d = expand_template(document, &p, None).unwrap(); let text = d.select_first("#ta").unwrap().as_node().text_contents(); assert_eq!(text, "element 1"); @@ -235,5 +237,4 @@ mod tests { assert!(d.select_first("exportelement").is_err()); assert!(d.select_first("includeelement").is_err()); } - } diff --git a/htmlua-parser/src/serve.rs b/htmlua-parser/src/serve.rs index 6bbf73a..1dfd306 100644 --- a/htmlua-parser/src/serve.rs +++ b/htmlua-parser/src/serve.rs @@ -1,17 +1,11 @@ -use std::env; -use std::fs; -use std::fs::read_to_string; -use std::path::{Path, PathBuf}; -use std::str::FromStr; use super::render::execute_lua; use super::render::expand_template; use anyhow::Result; -use kuchikiki::{ - NodeRef, - traits::TendrilSink, -}; +use kuchikiki::traits::TendrilSink; +use std::fs::read_to_string; +use std::path::{Path, PathBuf}; -pub fn serve_content(request_uri: &str) -> Result{ +pub fn serve_content(request_uri: &str) -> Result { // TODO: read from config let pages = PathBuf::from("/var/www/htmlua/pages"); let components = PathBuf::from("/var/www/htmlua/components"); @@ -28,12 +22,3 @@ pub fn serve_content(request_uri: &str) -> Result{ Ok(executed_doc.to_string()) } - -fn html_escape(input: &str) -> String { - input - .replace('&', "&") - .replace('<', "<") - .replace('>', ">") - .replace('"', """) - .replace('\'', "'") -} From 94267c681f8086d118a4e07a626b94c517d2648b Mon Sep 17 00:00:00 2001 From: gyoder <70408179+gyoder@users.noreply.github.com> Date: Sat, 5 Jul 2025 18:36:26 -0600 Subject: [PATCH 08/25] added test --- htmlua-parser/src/render.rs | 31 +++++++++++++++++++++++ htmlua-parser/src/serve.rs | 2 +- htmlua-parser/tests/components/comp1.html | 2 +- 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/htmlua-parser/src/render.rs b/htmlua-parser/src/render.rs index 58a8259..d50e934 100644 --- a/htmlua-parser/src/render.rs +++ b/htmlua-parser/src/render.rs @@ -188,6 +188,37 @@ mod tests { assert!(d.select_first("include").is_err()); } + #[test] + fn basic_lua_include() { + let page = r#" + + + + Basic HTML Page + + +

Hello World

+

This is a paragraph.

+ + htmlua.println("Test from Lua!") + + + + "#; + let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + p.push("tests/components"); + let document = kuchikiki::parse_html().one(page); + let d = execute_lua(expand_template(document, &p, None).unwrap()).unwrap(); + let text = d.select_first("span").unwrap().as_node().text_contents(); + println!("{}", d.to_string()); + assert_eq!(text, "Test from Lua!\n"); + assert!(d.select_first("lua").is_err()); + let text = d.select_first("#inc1").unwrap().as_node().text_contents(); + assert_eq!(text, "included1"); + assert!(d.select_first("include").is_err()); + + } + #[test] fn recursive_include() { let page = r#" diff --git a/htmlua-parser/src/serve.rs b/htmlua-parser/src/serve.rs index 1dfd306..134cc13 100644 --- a/htmlua-parser/src/serve.rs +++ b/htmlua-parser/src/serve.rs @@ -16,8 +16,8 @@ pub fn serve_content(request_uri: &str) -> Result { let page_string = read_to_string(&page_path)?; let doc = kuchikiki::parse_html().one(page_string); - let full_doc = expand_template(doc, &components, None)?; + let full_doc = expand_template(doc, &components, None)?; let executed_doc = execute_lua(full_doc)?; Ok(executed_doc.to_string()) diff --git a/htmlua-parser/tests/components/comp1.html b/htmlua-parser/tests/components/comp1.html index 516d00e..4110604 100644 --- a/htmlua-parser/tests/components/comp1.html +++ b/htmlua-parser/tests/components/comp1.html @@ -1 +1 @@ -included1 +included1 From 9a0038dacde3bcf1a10de51278d72f0f322c3817 Mon Sep 17 00:00:00 2001 From: gyoder <70408179+gyoder@users.noreply.github.com> Date: Sat, 5 Jul 2025 21:33:25 -0600 Subject: [PATCH 09/25] fixed weird wrapping --- htmlua-parser/src/render.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/htmlua-parser/src/render.rs b/htmlua-parser/src/render.rs index d50e934..fe0b64f 100644 --- a/htmlua-parser/src/render.rs +++ b/htmlua-parser/src/render.rs @@ -9,7 +9,7 @@ use anyhow::anyhow; use kuchikiki::{NodeRef, traits::TendrilSink}; use mlua::{Lua, Table}; -fn create_luahtml_stdlib(l: &Lua, stdout: &Rc>) -> mlua::Result
{ +fn create_htmlua_stdlib(l: &Lua, stdout: &Rc>) -> mlua::Result
{ let t = l.create_table()?; // This cannot be the best way to do this @@ -41,7 +41,7 @@ pub fn execute_lua(document: NodeRef) -> Result { let stdout: Rc> = Rc::new(RefCell::new(String::new())); let htmlua_table = - create_luahtml_stdlib(&lua, &stdout).map_err(|e| anyhow!("Failed to create Lua stdlib: {}", e))?; + create_htmlua_stdlib(&lua, &stdout).map_err(|e| anyhow!("Failed to create Lua stdlib: {}", e))?; globals .set("htmlua", htmlua_table) @@ -98,8 +98,9 @@ pub fn expand_template(document: NodeRef, component_path: &PathBuf, include_from let mut item_path = component_path.clone(); item_path.push(include_path); let component_text = read_to_string(item_path)?; - let new_node = kuchikiki::parse_html().one(component_text); - let replaced_node = expand_template(new_node, component_path, Some(i.as_node()))?; + let new_doc = kuchikiki::parse_html().one(component_text); + let new_node = new_doc.select_first("body").map_err(|()| anyhow!("Failed to get body"))?; + let replaced_node = expand_template(new_node.as_node().clone(), component_path, Some(i.as_node()))?; i.as_node().insert_before(replaced_node); i.as_node().detach(); } From 8e25531af188b89acd9f3283b6925f06789248af Mon Sep 17 00:00:00 2001 From: Quin Date: Sat, 5 Jul 2025 22:04:27 -0600 Subject: [PATCH 10/25] Added support for . Closes #4. --- Cargo.lock | 41 ++++++++++++++++++++++++ htmlua-parser/Cargo.toml | 1 + htmlua-parser/src/render.rs | 63 ++++++++++++++++++++++++++++++++----- htmlua-parser/src/serve.rs | 17 +++++----- 4 files changed, 105 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 17554c8..b13312c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -169,6 +169,15 @@ dependencies = [ "byteorder", ] +[[package]] +name = "getopts" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cba6ae63eb948698e300f645f87c70f76630d505f23b8907cf1e193ee85048c1" +dependencies = [ + "unicode-width", +] + [[package]] name = "getrandom" version = "0.1.16" @@ -226,6 +235,7 @@ dependencies = [ "anyhow", "kuchikiki", "mlua", + "pulldown-cmark", "tendril", ] @@ -572,6 +582,25 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "pulldown-cmark" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0" +dependencies = [ + "bitflags 2.9.1", + "getopts", + "memchr", + "pulldown-cmark-escape", + "unicase", +] + +[[package]] +name = "pulldown-cmark-escape" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" + [[package]] name = "quote" version = "1.0.40" @@ -877,12 +906,24 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + [[package]] name = "unicode-ident" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "unicode-width" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" + [[package]] name = "utf-8" version = "0.7.6" diff --git a/htmlua-parser/Cargo.toml b/htmlua-parser/Cargo.toml index b611fde..c3cbe07 100644 --- a/htmlua-parser/Cargo.toml +++ b/htmlua-parser/Cargo.toml @@ -7,6 +7,7 @@ edition = "2024" anyhow = "1.0.98" kuchikiki = "0.8.2" mlua = { version = "0.10.5", features = ["lua54", "vendored", "serialize"] } +pulldown-cmark = "0.13.0" tendril = "0.4.3" diff --git a/htmlua-parser/src/render.rs b/htmlua-parser/src/render.rs index 58a8259..21f66ba 100644 --- a/htmlua-parser/src/render.rs +++ b/htmlua-parser/src/render.rs @@ -1,13 +1,9 @@ -use std::cell::RefCell; -use std::fmt::Write; -use std::fs::read_to_string; -use std::path::PathBuf; -use std::rc::Rc; +use std::{cell::RefCell, fmt::Write, fs::read_to_string, path::PathBuf, rc::Rc}; -use anyhow::Result; -use anyhow::anyhow; +use anyhow::{Result, anyhow}; use kuchikiki::{NodeRef, traits::TendrilSink}; use mlua::{Lua, Table}; +use pulldown_cmark::{Options, Parser, html}; fn create_luahtml_stdlib(l: &Lua, stdout: &Rc>) -> mlua::Result
{ let t = l.create_table()?; @@ -69,6 +65,30 @@ pub fn execute_lua(document: NodeRef) -> Result { Ok(document) } +pub fn process_markdown(document: NodeRef) -> Result { + let markdown_elements: Vec<_> = match document.select("markdown") { + Ok(e) => e.collect(), + Err(()) => return Err(anyhow!("Unable to find markdown elements")), + }; + for node in markdown_elements { + if let Some(text_node) = node.as_node().first_child() { + if let Some(markdown_text) = text_node.as_text() { + let borrowed_text = markdown_text.borrow(); + let parser = Parser::new_ext(&borrowed_text, Options::all()); + let mut html_output = String::new(); + html::push_html(&mut html_output, parser); + let html_fragment = kuchikiki::parse_html().one(html_output); + for child in html_fragment.children() { + node.as_node().insert_before(child); + } + // Remove the original markdown node. + node.as_node().detach(); + } + } + } + Ok(document) +} + pub fn expand_template(document: NodeRef, component_path: &PathBuf, include_from: Option<&NodeRef>) -> Result { if let Some(from_node) = include_from { for i in document @@ -237,4 +257,33 @@ mod tests { assert!(d.select_first("exportelement").is_err()); assert!(d.select_first("includeelement").is_err()); } + + #[test] + fn basic_markdown() { + let page = r#" + + + + Markdown Test + + +

Hello World

+
+ +# This is a heading +This is a paragraph with **bold text** and *italic text*. + +- Item 1 +- Item 2 +- Item 3 + +
+ + "#; + let document = kuchikiki::parse_html().one(page); + let d = process_markdown(document).unwrap(); + assert!(d.select_first("markdown").is_err()); + assert!(d.select_first("p").is_ok()); + assert!(d.select_first("ul").is_ok()); + } } diff --git a/htmlua-parser/src/serve.rs b/htmlua-parser/src/serve.rs index 1dfd306..eb9a043 100644 --- a/htmlua-parser/src/serve.rs +++ b/htmlua-parser/src/serve.rs @@ -1,24 +1,21 @@ -use super::render::execute_lua; -use super::render::expand_template; +use super::render::{execute_lua, expand_template, process_markdown}; use anyhow::Result; use kuchikiki::traits::TendrilSink; -use std::fs::read_to_string; -use std::path::{Path, PathBuf}; +use std::{ + fs::read_to_string, + path::{Path, PathBuf}, +}; pub fn serve_content(request_uri: &str) -> Result { // TODO: read from config let pages = PathBuf::from("/var/www/htmlua/pages"); let components = PathBuf::from("/var/www/htmlua/components"); - let safe_path = Path::new(request_uri).strip_prefix("/")?; let page_path = pages.join(safe_path); - let page_string = read_to_string(&page_path)?; - let doc = kuchikiki::parse_html().one(page_string); let full_doc = expand_template(doc, &components, None)?; - - let executed_doc = execute_lua(full_doc)?; - + let markdown_doc = process_markdown(full_doc)?; + let executed_doc = execute_lua(markdown_doc)?; Ok(executed_doc.to_string()) } From cd5c12ff211a70f9d7f086abcf537220f7023ae4 Mon Sep 17 00:00:00 2001 From: Quin Date: Sat, 5 Jul 2025 22:28:16 -0600 Subject: [PATCH 11/25] Add basic syntax highlighting support with syntect. Closes #5. --- htmlua-parser/Cargo.toml | 1 + htmlua-parser/src/render.rs | 89 +++++++++++++++++++++++++++++++++++-- htmlua-parser/src/serve.rs | 5 ++- 3 files changed, 89 insertions(+), 6 deletions(-) diff --git a/htmlua-parser/Cargo.toml b/htmlua-parser/Cargo.toml index c3cbe07..1e27f09 100644 --- a/htmlua-parser/Cargo.toml +++ b/htmlua-parser/Cargo.toml @@ -8,6 +8,7 @@ anyhow = "1.0.98" kuchikiki = "0.8.2" mlua = { version = "0.10.5", features = ["lua54", "vendored", "serialize"] } pulldown-cmark = "0.13.0" +syntect = "5.2.0" tendril = "0.4.3" diff --git a/htmlua-parser/src/render.rs b/htmlua-parser/src/render.rs index eb50c2c..396df4f 100644 --- a/htmlua-parser/src/render.rs +++ b/htmlua-parser/src/render.rs @@ -4,6 +4,13 @@ use anyhow::{Result, anyhow}; use kuchikiki::{NodeRef, traits::TendrilSink}; use mlua::{Lua, Table}; use pulldown_cmark::{Options, Parser, html}; +use syntect::{ + easy::HighlightLines, + highlighting::{Style, ThemeSet}, + html::{IncludeBackground, styled_line_to_highlighted_html}, + parsing::SyntaxSet, + util::LinesWithEndings, +}; fn create_htmlua_stdlib(l: &Lua, stdout: &Rc>) -> mlua::Result
{ let t = l.create_table()?; @@ -119,7 +126,9 @@ pub fn expand_template(document: NodeRef, component_path: &PathBuf, include_from item_path.push(include_path); let component_text = read_to_string(item_path)?; let new_doc = kuchikiki::parse_html().one(component_text); - let new_node = new_doc.select_first("body").map_err(|()| anyhow!("Failed to get body"))?; + let new_node = new_doc + .select_first("body") + .map_err(|()| anyhow!("Failed to get body"))?; let replaced_node = expand_template(new_node.as_node().clone(), component_path, Some(i.as_node()))?; i.as_node().insert_before(replaced_node); i.as_node().detach(); @@ -128,6 +137,49 @@ pub fn expand_template(document: NodeRef, component_path: &PathBuf, include_from Ok(document) } +pub fn process_syntax_highlighting(document: NodeRef) -> Result { + let ps = SyntaxSet::load_defaults_newlines(); + let ts = ThemeSet::load_defaults(); + let syntax_elements: Vec<_> = match document.select("syntaxhighlight") { + Ok(e) => e.collect(), + Err(()) => return Err(anyhow!("Unable to find syntaxhighlight elements")), + }; + for node in syntax_elements { + let attrs = match node.as_node().as_element() { + Some(e) => e.attributes.borrow(), + None => continue, + }; + let language = attrs.get("lang").unwrap_or("text"); + let theme_name = attrs.get("theme").unwrap_or("base16-ocean.dark"); + if let Some(text_node) = node.as_node().first_child() { + if let Some(code_text) = text_node.as_text() { + let syntax = ps + .find_syntax_by_extension(language) + .or_else(|| ps.find_syntax_by_name(language)) + .unwrap_or_else(|| ps.find_syntax_plain_text()); + let theme = &ts.themes[theme_name]; + let mut h = HighlightLines::new(syntax, theme); + let mut html_output = String::new(); + write!(html_output, r#"
"#)?;
+                html_output.push_str("");
+                for line in LinesWithEndings::from(&code_text.borrow()) {
+                    let ranges: Vec<(Style, &str)> = h.highlight_line(line, &ps).unwrap();
+                    let escaped = styled_line_to_highlighted_html(&ranges[..], IncludeBackground::No).unwrap();
+                    html_output.push_str(&escaped);
+                }
+                html_output.push_str("
"); + let html_fragment = kuchikiki::parse_html().one(html_output); + for child in html_fragment.children() { + node.as_node().insert_before(child); + } + // Remove the original syntaxhighlight node. + node.as_node().detach(); + } + } + } + Ok(document) +} + #[cfg(test)] mod tests { use super::*; @@ -237,7 +289,6 @@ mod tests { let text = d.select_first("#inc1").unwrap().as_node().text_contents(); assert_eq!(text, "included1"); assert!(d.select_first("include").is_err()); - } #[test] @@ -292,7 +343,7 @@ mod tests { #[test] fn basic_markdown() { - let page = r#" + let page = r" @@ -311,11 +362,41 @@ This is a paragraph with **bold text** and *italic text*. - "#; + "; let document = kuchikiki::parse_html().one(page); let d = process_markdown(document).unwrap(); assert!(d.select_first("markdown").is_err()); assert!(d.select_first("p").is_ok()); assert!(d.select_first("ul").is_ok()); } + + #[test] + fn basic_syntax_highlighting() { + let page = r#" + + + + Syntax Highlighting Test + + +

Code Example

+ +fn main() { + println!("Hello, world!"); + let x = 42; + let y = "test"; +} + + + "#; + let document = kuchikiki::parse_html().one(page); + let d = process_syntax_highlighting(document).unwrap(); + assert!(d.select_first("syntaxhighlight").is_err()); + assert!(d.select_first("pre").is_ok()); + assert!(d.select_first("code").is_ok()); + let pre = d.select_first("pre").unwrap(); + let attrs = pre.attributes.borrow(); + assert_eq!(attrs.get("data-lang"), Some("rust")); + assert!(attrs.get("class").unwrap().contains("syntax-highlight")); + } } diff --git a/htmlua-parser/src/serve.rs b/htmlua-parser/src/serve.rs index eb9a043..5b63e3d 100644 --- a/htmlua-parser/src/serve.rs +++ b/htmlua-parser/src/serve.rs @@ -1,4 +1,4 @@ -use super::render::{execute_lua, expand_template, process_markdown}; +use super::render::{execute_lua, expand_template, process_markdown, process_syntax_highlighting}; use anyhow::Result; use kuchikiki::traits::TendrilSink; use std::{ @@ -16,6 +16,7 @@ pub fn serve_content(request_uri: &str) -> Result { let doc = kuchikiki::parse_html().one(page_string); let full_doc = expand_template(doc, &components, None)?; let markdown_doc = process_markdown(full_doc)?; - let executed_doc = execute_lua(markdown_doc)?; + let highlighted_doc = process_syntax_highlighting(markdown_doc)?; + let executed_doc = execute_lua(highlighted_doc)?; Ok(executed_doc.to_string()) } From 3f8616e8b08b7eeec94b9019fd3f4ca1216ecb7d Mon Sep 17 00:00:00 2001 From: Quin Date: Sat, 5 Jul 2025 22:28:26 -0600 Subject: [PATCH 12/25] Add basic syntax highlighting support with syntect. Closes #5. --- Cargo.lock | 305 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 300 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b13312c..5bfc908 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "anyhow" version = "1.0.98" @@ -14,6 +20,21 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -44,9 +65,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cc" -version = "1.2.27" +version = "1.2.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc" +checksum = "5c1599538de2394445747c8cf7935946e3cc27e9625f889d979bfb2aaf569362" dependencies = [ "shlex", ] @@ -63,6 +84,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + [[package]] name = "cssparser" version = "0.27.2" @@ -71,7 +101,7 @@ checksum = "754b69d351cdc2d8ee09ae203db831e005560fc6030da058f86ad60c92a9cb0a" dependencies = [ "cssparser-macros", "dtoa-short", - "itoa", + "itoa 0.4.8", "matches", "phf 0.8.0", "proc-macro2", @@ -90,6 +120,15 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "deranged" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", +] + [[package]] name = "derive_more" version = "0.99.20" @@ -130,6 +169,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + [[package]] name = "erased-serde" version = "0.4.6" @@ -150,6 +195,22 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "flate2" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +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 = "futf" version = "0.1.5" @@ -206,6 +267,12 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" + [[package]] name = "html5ever" version = "0.26.0" @@ -236,6 +303,7 @@ dependencies = [ "kuchikiki", "mlua", "pulldown-cmark", + "syntect", "tendril", ] @@ -250,7 +318,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", - "hashbrown", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +dependencies = [ + "equivalent", + "hashbrown 0.15.4", ] [[package]] @@ -259,6 +337,12 @@ version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + [[package]] name = "kuchikiki" version = "0.8.2" @@ -267,7 +351,7 @@ checksum = "f29e4755b7b995046f510a7520c42b2fed58b77bd94d5a87a8eb43d2fd126da8" dependencies = [ "cssparser", "html5ever", - "indexmap", + "indexmap 1.9.3", "matches", "selectors", ] @@ -278,6 +362,12 @@ version = "0.2.174" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +[[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.9.4" @@ -351,6 +441,15 @@ version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + [[package]] name = "mlua" version = "0.10.5" @@ -394,6 +493,12 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-traits" version = "0.2.19" @@ -403,6 +508,34 @@ dependencies = [ "autocfg", ] +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "onig" +version = "6.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "336b9c63443aceef14bea841b899035ae3abe89b7c486aaf4c5bd8aafedac3f0" +dependencies = [ + "bitflags 2.9.1", + "libc", + "once_cell", + "onig_sys", +] + +[[package]] +name = "onig_sys" +version = "69.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f86c6eef3d6df15f23bcfb6af487cbd2fed4e5581d58d5bf1f5f8b7f6727dc" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "ordered-float" version = "2.10.1" @@ -552,6 +685,25 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "plist" +version = "1.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "546b279bf0638ee811d9e47de2ca5b66575a543035d79fdf83959dd2f5c3b4c3" +dependencies = [ + "base64", + "indexmap 2.10.0", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -601,6 +753,15 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" +[[package]] +name = "quick-xml" +version = "0.37.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.40" @@ -700,6 +861,12 @@ dependencies = [ "bitflags 2.9.1", ] +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + [[package]] name = "rustc-hash" version = "2.1.1" @@ -734,6 +901,21 @@ version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -796,6 +978,18 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa 1.0.15", + "memchr", + "ryu", + "serde", +] + [[package]] name = "servo_arc" version = "0.1.1" @@ -883,6 +1077,28 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syntect" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874dcfa363995604333cf947ae9f751ca3af4522c60886774c4963943b4746b1" +dependencies = [ + "bincode", + "bitflags 1.3.2", + "flate2", + "fnv", + "once_cell", + "onig", + "plist", + "regex-syntax", + "serde", + "serde_derive", + "serde_json", + "thiserror", + "walkdir", + "yaml-rust", +] + [[package]] name = "tendril" version = "0.4.3" @@ -900,6 +1116,57 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "time" +version = "0.3.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +dependencies = [ + "deranged", + "itoa 1.0.15", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + +[[package]] +name = "time-macros" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "typeid" version = "1.0.3" @@ -930,6 +1197,16 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "wasi" version = "0.9.0+wasi-snapshot-preview1" @@ -954,6 +1231,15 @@ dependencies = [ "winsafe", ] +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "windows-sys" version = "0.59.0" @@ -1106,6 +1392,15 @@ version = "0.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" +[[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 = "zerocopy" version = "0.8.26" From f9b2e74b4525eabb5ea7d1df84b4e570194429b8 Mon Sep 17 00:00:00 2001 From: gyoder <70408179+gyoder@users.noreply.github.com> Date: Mon, 7 Jul 2025 11:33:26 -0600 Subject: [PATCH 13/25] Fixed document vs fragment parsing --- Cargo.lock | 70 ++++++++++++++++++++++++++++++++++-- htmlua-parser/Cargo.toml | 2 ++ htmlua-parser/src/helpers.rs | 32 +++++++++++++++++ htmlua-parser/src/lib.rs | 1 + htmlua-parser/src/render.rs | 14 ++++---- htmlua-parser/src/serve.rs | 8 ++--- 6 files changed, 113 insertions(+), 14 deletions(-) create mode 100644 htmlua-parser/src/helpers.rs diff --git a/Cargo.lock b/Cargo.lock index 17554c8..385bc4d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -205,12 +205,23 @@ checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7" dependencies = [ "log", "mac", - "markup5ever", + "markup5ever 0.11.0", "proc-macro2", "quote", "syn 1.0.109", ] +[[package]] +name = "html5ever" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55d958c2f74b664487a2035fe1dadb032c48718a03b63f3ab0b8537db8549ed4" +dependencies = [ + "log", + "markup5ever 0.35.0", + "match_token", +] + [[package]] name = "htmlua-apache-cgi" version = "0.1.0" @@ -224,7 +235,9 @@ name = "htmlua-parser" version = "0.1.0" dependencies = [ "anyhow", + "html5ever 0.35.0", "kuchikiki", + "markup5ever 0.11.0", "mlua", "tendril", ] @@ -256,7 +269,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f29e4755b7b995046f510a7520c42b2fed58b77bd94d5a87a8eb43d2fd126da8" dependencies = [ "cssparser", - "html5ever", + "html5ever 0.26.0", "indexmap", "matches", "selectors", @@ -329,6 +342,28 @@ dependencies = [ "tendril", ] +[[package]] +name = "markup5ever" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "311fe69c934650f8f19652b3946075f0fc41ad8757dbb68f1ca14e7900ecc1c3" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + +[[package]] +name = "match_token" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac84fd3f360fcc43dc5f5d186f02a94192761a080e8bc58621ad4d12296a58cf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "matches" version = "0.1.10" @@ -445,6 +480,15 @@ dependencies = [ "phf_shared 0.10.0", ] +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_shared 0.11.3", +] + [[package]] name = "phf_codegen" version = "0.8.0" @@ -465,6 +509,16 @@ dependencies = [ "phf_shared 0.10.0", ] +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", +] + [[package]] name = "phf_generator" version = "0.8.0" @@ -901,6 +955,18 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "web_atoms" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57ffde1dc01240bdf9992e3205668b235e59421fd085e8a317ed98da0178d414" +dependencies = [ + "phf 0.11.3", + "phf_codegen 0.11.3", + "string_cache", + "string_cache_codegen", +] + [[package]] name = "which" version = "7.0.3" diff --git a/htmlua-parser/Cargo.toml b/htmlua-parser/Cargo.toml index b611fde..c3ecb0f 100644 --- a/htmlua-parser/Cargo.toml +++ b/htmlua-parser/Cargo.toml @@ -5,7 +5,9 @@ edition = "2024" [dependencies] anyhow = "1.0.98" +html5ever = "0.35.0" kuchikiki = "0.8.2" +markup5ever = "0.11.0" mlua = { version = "0.10.5", features = ["lua54", "vendored", "serialize"] } tendril = "0.4.3" diff --git a/htmlua-parser/src/helpers.rs b/htmlua-parser/src/helpers.rs new file mode 100644 index 0000000..90439f7 --- /dev/null +++ b/htmlua-parser/src/helpers.rs @@ -0,0 +1,32 @@ +use std::{fs::File, io::{BufRead, BufReader, Read, Seek, SeekFrom}, path::PathBuf}; + +use anyhow::Result; +use kuchikiki::NodeRef; +use markup5ever::{LocalName, Namespace, QualName}; +use tendril::TendrilSink; + + + + +pub fn read_doc_from_file(path: PathBuf) -> Result { + + let mut file = File::open(path)?; + let mut reader = BufReader::new(&mut file); + + let mut first_line = String::new(); + reader.read_line(&mut first_line)?; + let first_line_trimmed = first_line.trim(); + + let is_whole_doc = first_line_trimmed.eq_ignore_ascii_case(""); + + file.seek(SeekFrom::Start(0))?; + let mut page_string = String::new(); + file.read_to_string(&mut page_string)?; + + if is_whole_doc { + Ok(kuchikiki::parse_html().one(page_string)) + } else { + let ctx_name = QualName::new(None, Namespace::from("http://www.w3.org/1999/xhtml"), LocalName::from("div")); + Ok(kuchikiki::parse_fragment(ctx_name, Vec::new()).one(page_string)) + } +} diff --git a/htmlua-parser/src/lib.rs b/htmlua-parser/src/lib.rs index 03e66bd..cd87be2 100644 --- a/htmlua-parser/src/lib.rs +++ b/htmlua-parser/src/lib.rs @@ -1,2 +1,3 @@ pub mod render; pub mod serve; +pub mod helpers; diff --git a/htmlua-parser/src/render.rs b/htmlua-parser/src/render.rs index fe0b64f..1ea99a0 100644 --- a/htmlua-parser/src/render.rs +++ b/htmlua-parser/src/render.rs @@ -1,14 +1,15 @@ use std::cell::RefCell; use std::fmt::Write; -use std::fs::read_to_string; use std::path::PathBuf; use std::rc::Rc; use anyhow::Result; use anyhow::anyhow; -use kuchikiki::{NodeRef, traits::TendrilSink}; +use kuchikiki::NodeRef; use mlua::{Lua, Table}; +use crate::helpers::read_doc_from_file; + fn create_htmlua_stdlib(l: &Lua, stdout: &Rc>) -> mlua::Result
{ let t = l.create_table()?; @@ -79,13 +80,14 @@ pub fn expand_template(document: NodeRef, component_path: &PathBuf, include_from let exported = from_node .select_first(format!("exportelement.{name}").as_str()) .map_err(|()| anyhow!("Error finding exportelement"))?; - exported.as_node().children().for_each(|c| i.as_node().insert_after(c)); + exported.as_node().children().rev().for_each(|c| i.as_node().insert_after(c)); } } while let Ok(i) = document.select_first("includeelement") { i.as_node().detach(); } } + for i in document .select("include") .map_err(|()| anyhow!("Error finding include"))? @@ -97,10 +99,8 @@ pub fn expand_template(document: NodeRef, component_path: &PathBuf, include_from if let Some(include_path) = attrs.get("path") { let mut item_path = component_path.clone(); item_path.push(include_path); - let component_text = read_to_string(item_path)?; - let new_doc = kuchikiki::parse_html().one(component_text); - let new_node = new_doc.select_first("body").map_err(|()| anyhow!("Failed to get body"))?; - let replaced_node = expand_template(new_node.as_node().clone(), component_path, Some(i.as_node()))?; + let new_node = read_doc_from_file(item_path)?; + let replaced_node = expand_template(new_node, component_path, Some(i.as_node()))?; i.as_node().insert_before(replaced_node); i.as_node().detach(); } diff --git a/htmlua-parser/src/serve.rs b/htmlua-parser/src/serve.rs index 134cc13..370d2e9 100644 --- a/htmlua-parser/src/serve.rs +++ b/htmlua-parser/src/serve.rs @@ -1,8 +1,8 @@ +use crate::helpers::read_doc_from_file; + use super::render::execute_lua; use super::render::expand_template; use anyhow::Result; -use kuchikiki::traits::TendrilSink; -use std::fs::read_to_string; use std::path::{Path, PathBuf}; pub fn serve_content(request_uri: &str) -> Result { @@ -13,9 +13,7 @@ pub fn serve_content(request_uri: &str) -> Result { let safe_path = Path::new(request_uri).strip_prefix("/")?; let page_path = pages.join(safe_path); - let page_string = read_to_string(&page_path)?; - - let doc = kuchikiki::parse_html().one(page_string); + let doc = read_doc_from_file(page_path)?; let full_doc = expand_template(doc, &components, None)?; let executed_doc = execute_lua(full_doc)?; From 3d50b12df4bd1e81248256893bd9a83498a0dae9 Mon Sep 17 00:00:00 2001 From: gyoder <70408179+gyoder@users.noreply.github.com> Date: Mon, 7 Jul 2025 12:36:28 -0600 Subject: [PATCH 14/25] Added custom theme loading --- htmlua-parser/src/render.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/htmlua-parser/src/render.rs b/htmlua-parser/src/render.rs index 396df4f..55047fc 100644 --- a/htmlua-parser/src/render.rs +++ b/htmlua-parser/src/render.rs @@ -139,11 +139,16 @@ pub fn expand_template(document: NodeRef, component_path: &PathBuf, include_from pub fn process_syntax_highlighting(document: NodeRef) -> Result { let ps = SyntaxSet::load_defaults_newlines(); - let ts = ThemeSet::load_defaults(); + let mut ts = ThemeSet::load_defaults(); let syntax_elements: Vec<_> = match document.select("syntaxhighlight") { Ok(e) => e.collect(), Err(()) => return Err(anyhow!("Unable to find syntaxhighlight elements")), }; + if !syntax_elements.is_empty() { + // TODO: read from config + let themes = PathBuf::from("/var/www/htmlua/themes"); + ts.add_from_folder(themes)?; + } for node in syntax_elements { let attrs = match node.as_node().as_element() { Some(e) => e.attributes.borrow(), @@ -163,8 +168,8 @@ pub fn process_syntax_highlighting(document: NodeRef) -> Result { write!(html_output, r#"
"#)?;
                 html_output.push_str("");
                 for line in LinesWithEndings::from(&code_text.borrow()) {
-                    let ranges: Vec<(Style, &str)> = h.highlight_line(line, &ps).unwrap();
-                    let escaped = styled_line_to_highlighted_html(&ranges[..], IncludeBackground::No).unwrap();
+                    let ranges: Vec<(Style, &str)> = h.highlight_line(line, &ps)?;
+                    let escaped = styled_line_to_highlighted_html(&ranges[..], IncludeBackground::No)?;
                     html_output.push_str(&escaped);
                 }
                 html_output.push_str("
"); From 88c262fe64cfd0e4385fc4e06f1c4f33054afa63 Mon Sep 17 00:00:00 2001 From: gyoder <70408179+gyoder@users.noreply.github.com> Date: Tue, 8 Jul 2025 11:44:13 -0600 Subject: [PATCH 15/25] bug: ignores error if theme folder does not exist --- Cargo.lock | 2 +- htmlua-parser/src/render.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 118ff7a..8096bb3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -363,7 +363,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f29e4755b7b995046f510a7520c42b2fed58b77bd94d5a87a8eb43d2fd126da8" dependencies = [ "cssparser", - "html5ever", + "html5ever 0.26.0", "indexmap 1.9.3", "matches", "selectors", diff --git a/htmlua-parser/src/render.rs b/htmlua-parser/src/render.rs index 35c37f4..2e4ed87 100644 --- a/htmlua-parser/src/render.rs +++ b/htmlua-parser/src/render.rs @@ -146,7 +146,7 @@ pub fn process_syntax_highlighting(document: NodeRef) -> Result { if !syntax_elements.is_empty() { // TODO: read from config let themes = PathBuf::from("/var/www/htmlua/themes"); - ts.add_from_folder(themes)?; + let _ = ts.add_from_folder(themes); } for node in syntax_elements { let attrs = match node.as_node().as_element() { From c8ffbb2af0eae39b897ff3b1b6c8263181fdd8d4 Mon Sep 17 00:00:00 2001 From: gyoder <70408179+gyoder@users.noreply.github.com> Date: Tue, 8 Jul 2025 12:25:04 -0600 Subject: [PATCH 16/25] bug: multiple includes now works --- htmlua-parser/src/render.rs | 37 ++++++++++++++++++-- htmlua-parser/tests/components/multi1_1.html | 1 + htmlua-parser/tests/components/multi1_2.html | 2 ++ htmlua-parser/tests/components/multi1_3.html | 1 + 4 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 htmlua-parser/tests/components/multi1_1.html create mode 100644 htmlua-parser/tests/components/multi1_2.html create mode 100644 htmlua-parser/tests/components/multi1_3.html diff --git a/htmlua-parser/src/render.rs b/htmlua-parser/src/render.rs index 2e4ed87..6444d60 100644 --- a/htmlua-parser/src/render.rs +++ b/htmlua-parser/src/render.rs @@ -119,6 +119,7 @@ pub fn expand_template(document: NodeRef, component_path: &PathBuf, include_from for i in document .select("include") .map_err(|()| anyhow!("Error finding include"))? + .collect::>() { let attrs = match i.as_node().as_element() { Some(e) => e.attributes.borrow(), @@ -129,10 +130,13 @@ pub fn expand_template(document: NodeRef, component_path: &PathBuf, include_from item_path.push(include_path); let new_node = read_doc_from_file(item_path)?; let replaced_node = expand_template(new_node, component_path, Some(i.as_node()))?; - i.as_node().insert_before(replaced_node); - i.as_node().detach(); + replaced_node.select_first("html").map_err(|()| anyhow!("Error finding html"))?.as_node().children().rev().for_each(|c| i.as_node().insert_after(c)); + } } + while let Ok(i) = document.select_first("include") { + i.as_node().detach(); + } Ok(document) } @@ -265,6 +269,35 @@ mod tests { assert!(d.select_first("include").is_err()); } + #[test] + fn multiple_include() { + let page = r#" + + + + Basic HTML Page + + +

Hello World

+

This is a paragraph.

+ + + + + "#; + let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + p.push("tests/components"); + let document = kuchikiki::parse_html().one(page); + let d = expand_template(document, &p, None).unwrap(); + let text = d.select_first("#inc1").unwrap().as_node().text_contents(); + assert_eq!(text, "included1"); + let text = d.select_first("#inc2").unwrap().as_node().text_contents(); + assert_eq!(text, "included2"); + let text = d.select_first("#inc3").unwrap().as_node().text_contents(); + assert_eq!(text, "included3"); + assert!(d.select_first("include").is_err()); + } + #[test] fn basic_lua_include() { let page = r#" diff --git a/htmlua-parser/tests/components/multi1_1.html b/htmlua-parser/tests/components/multi1_1.html new file mode 100644 index 0000000..4110604 --- /dev/null +++ b/htmlua-parser/tests/components/multi1_1.html @@ -0,0 +1 @@ +included1 diff --git a/htmlua-parser/tests/components/multi1_2.html b/htmlua-parser/tests/components/multi1_2.html new file mode 100644 index 0000000..0799d2f --- /dev/null +++ b/htmlua-parser/tests/components/multi1_2.html @@ -0,0 +1,2 @@ + +included2 diff --git a/htmlua-parser/tests/components/multi1_3.html b/htmlua-parser/tests/components/multi1_3.html new file mode 100644 index 0000000..05b221c --- /dev/null +++ b/htmlua-parser/tests/components/multi1_3.html @@ -0,0 +1 @@ +included3 From 4144fa2cba9732c1cee39b44b38f3752aefc4c03 Mon Sep 17 00:00:00 2001 From: gyoder <70408179+gyoder@users.noreply.github.com> Date: Fri, 11 Jul 2025 12:19:26 -0600 Subject: [PATCH 17/25] Added Basic HTTP stdlib --- Cargo.lock | 1203 ++++++++++++++++++++++++++++ cspell.json | 10 + htmlua-parser/Cargo.toml | 5 +- htmlua-parser/src/htmlua_stdlib.rs | 151 ++++ htmlua-parser/src/lib.rs | 1 + htmlua-parser/src/render.rs | 25 +- 6 files changed, 1370 insertions(+), 25 deletions(-) create mode 100644 cspell.json create mode 100644 htmlua-parser/src/htmlua_stdlib.rs diff --git a/Cargo.lock b/Cargo.lock index 8096bb3..da92710 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + [[package]] name = "adler2" version = "2.0.1" @@ -14,12 +23,33 @@ version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "backtrace" +version = "0.3.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + [[package]] name = "base64" version = "0.22.1" @@ -57,12 +87,24 @@ dependencies = [ "serde", ] +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + [[package]] name = "cc" version = "1.2.29" @@ -84,6 +126,22 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "crc32fast" version = "1.4.2" @@ -142,6 +200,17 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "dtoa" version = "1.0.10" @@ -163,6 +232,15 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "env_home" version = "0.1.0" @@ -195,6 +273,12 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "flate2" version = "1.1.2" @@ -211,6 +295,30 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + [[package]] name = "futf" version = "0.1.5" @@ -221,6 +329,56 @@ dependencies = [ "new_debug_unreachable", ] +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + [[package]] name = "fxhash" version = "0.2.1" @@ -261,6 +419,43 @@ dependencies = [ "wasi 0.11.1+wasi-snapshot-preview1", ] +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "h2" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17da50a276f1e01e0ba6c029e47b7100754904ee8a278f886546e98575380785" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.10.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -316,6 +511,9 @@ dependencies = [ "markup5ever 0.11.0", "mlua", "pulldown-cmark", + "reqwest", + "serde", + "serde_json", "syntect", "tendril", ] @@ -324,6 +522,231 @@ dependencies = [ name = "htmlua-server" version = "0.1.0" +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa 1.0.15", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "itoa 1.0.15", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f66d5bd4c6f02bf0542fad85d626775bab9258cf795a4256dcaf3161114d1df" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -344,6 +767,33 @@ dependencies = [ "hashbrown 0.15.4", ] +[[package]] +name = "io-uring" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013" +dependencies = [ + "bitflags 2.9.1", + "cfg-if", + "libc", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "itoa" version = "0.4.8" @@ -356,6 +806,16 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + [[package]] name = "kuchikiki" version = "0.8.2" @@ -387,6 +847,12 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + [[package]] name = "lock_api" version = "0.4.13" @@ -476,6 +942,12 @@ version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -485,12 +957,24 @@ dependencies = [ "adler2", ] +[[package]] +name = "mio" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", +] + [[package]] name = "mlua" version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1f5f8fbebc7db5f671671134b9321c4b9aa9adeafccfd9a8c020ae45c6a35d0" dependencies = [ + "anyhow", "bstr", "either", "erased-serde", @@ -516,6 +1000,23 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "new_debug_unreachable" version = "1.0.6" @@ -543,6 +1044,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -571,6 +1081,50 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "openssl" +version = "0.10.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" +dependencies = [ + "bitflags 2.9.1", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "ordered-float" version = "2.10.1" @@ -603,6 +1157,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + [[package]] name = "phf" version = "0.8.0" @@ -733,6 +1293,18 @@ dependencies = [ "siphasher 1.0.1", ] +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[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.32" @@ -752,6 +1324,15 @@ dependencies = [ "time", ] +[[package]] +name = "potential_utf" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +dependencies = [ + "zerovec", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -825,6 +1406,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "rand" version = "0.7.3" @@ -921,6 +1508,68 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "reqwest" +version = "0.12.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" + [[package]] name = "rustc-hash" version = "2.1.1" @@ -949,6 +1598,39 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "rustls" +version = "0.23.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2491382039b29b9b11ff08b76ff6c97cf287671dbb74f0be44bda389fffe9bd1" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.21" @@ -970,12 +1652,44 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.9.1", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "selectors" version = "0.22.0" @@ -1044,6 +1758,18 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa 1.0.15", + "ryu", + "serde", +] + [[package]] name = "servo_arc" version = "0.1.1" @@ -1072,12 +1798,28 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" +[[package]] +name = "slab" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" + [[package]] name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -1109,6 +1851,12 @@ dependencies = [ "quote", ] +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "1.0.109" @@ -1131,6 +1879,26 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "syntect" version = "5.2.0" @@ -1153,6 +1921,40 @@ dependencies = [ "yaml-rust", ] +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.9.1", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +dependencies = [ + "fastrand", + "getrandom 0.3.3", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + [[package]] name = "tendril" version = "0.4.3" @@ -1221,6 +2023,136 @@ dependencies = [ "time-core", ] +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.46.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17" +dependencies = [ + "backtrace", + "bytes", + "io-uring", + "libc", + "mio", + "pin-project-lite", + "slab", + "socket2", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags 2.9.1", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "typeid" version = "1.0.3" @@ -1245,12 +2177,41 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + [[package]] name = "utf-8" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "walkdir" version = "2.5.0" @@ -1261,6 +2222,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.9.0+wasi-snapshot-preview1" @@ -1273,6 +2243,96 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn 2.0.104", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "web_atoms" version = "0.1.3" @@ -1306,6 +2366,50 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-registry" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.59.0" @@ -1458,6 +2562,21 @@ version = "0.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.9.1", +] + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + [[package]] name = "yaml-rust" version = "0.4.5" @@ -1467,6 +2586,30 @@ dependencies = [ "linked-hash-map", ] +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.8.26" @@ -1486,3 +2629,63 @@ dependencies = [ "quote", "syn 2.0.104", ] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] diff --git a/cspell.json b/cspell.json new file mode 100644 index 0000000..e8c260e --- /dev/null +++ b/cspell.json @@ -0,0 +1,10 @@ +{ + "words": [ + "htmlua", + "headermap", + "reqwest" + ], + "userWords": [ + "mlua" + ] +} \ No newline at end of file diff --git a/htmlua-parser/Cargo.toml b/htmlua-parser/Cargo.toml index 28416b7..83769af 100644 --- a/htmlua-parser/Cargo.toml +++ b/htmlua-parser/Cargo.toml @@ -8,8 +8,11 @@ anyhow = "1.0.98" html5ever = "0.35.0" kuchikiki = "0.8.2" markup5ever = "0.11.0" -mlua = { version = "0.10.5", features = ["lua54", "vendored", "serialize"] } +mlua = { version = "0.10.5", features = ["lua54", "vendored", "serialize", "anyhow"] } pulldown-cmark = "0.13.0" +reqwest = { version = "0.12.22", features = ["blocking", "json"] } +serde = { version = "1.0.219", features = ["derive"] } +serde_json = "1.0.140" syntect = "5.2.0" tendril = "0.4.3" diff --git a/htmlua-parser/src/htmlua_stdlib.rs b/htmlua-parser/src/htmlua_stdlib.rs new file mode 100644 index 0000000..3c27f26 --- /dev/null +++ b/htmlua-parser/src/htmlua_stdlib.rs @@ -0,0 +1,151 @@ +use std::{cell::{LazyCell, RefCell}, collections::HashMap, fmt::Write, rc::Rc}; + +use anyhow::anyhow; +use mlua::{prelude::*, Error, Lua, Table}; +use reqwest::{blocking::{Client, Response}, header::HeaderMap}; +use serde::{Deserialize, Serialize}; +use serde_json::to_value; + + +pub fn create_htmlua_stdlib(l: &Lua, stdout: &Rc>) -> mlua::Result
{ + let t = l.create_table()?; + + // This cannot be the best way to do this + + let stdout_println = stdout.clone(); + t.set( + "println", + l.create_function(move |_, text: String| { + let mut stdout_ref = stdout_println.borrow_mut(); + writeln!(stdout_ref, "{text}").map_err(mlua::Error::external) + })?, + )?; + + let stdout_print = stdout.clone(); + t.set( + "print", + l.create_function(move |_, text: String| { + let mut stdout_ref = stdout_print.borrow_mut(); + write!(stdout_ref, "{text}").map_err(mlua::Error::external) + })?, + )?; + + t.set("http", create_http_lib(l)?)?; + Ok(t) +} + +fn create_http_lib(l: &Lua) -> mlua::Result
{ + let t = l.create_table()?; + let http_client: Rc> = Rc::new(LazyCell::new(|| Client::builder() + .timeout(std::time::Duration::from_secs(3)) + .user_agent("htmlua/0.1.0") + .build().unwrap() + )); + + + let client = http_client.clone(); + t.set( + "get", + l.create_function(move |_, url: String| { + let res = client.get(url).send().map_err(|e| Error::RuntimeError(e.to_string()))?; + let lua_res: LuaHttpResponse = TryFrom::try_from(res)?; + Ok(lua_res) + })? + )?; + + let client = http_client.clone(); + t.set( + "post", + l.create_function(move |_, url: String| { + let res = client.post(url).send().map_err(|e| Error::RuntimeError(e.to_string()))?; + let lua_res: LuaHttpResponse = TryFrom::try_from(res)?; + Ok(lua_res) + })? + )?; + + let client = http_client.clone(); + t.set( + "get_with_data", + l.create_function(move |_, (url, data): (String, HashMap)| { + let res = client.get(url).query(&data).send().map_err(|e| Error::RuntimeError(e.to_string()))?; + let lua_res: LuaHttpResponse = TryFrom::try_from(res)?; + Ok(lua_res) + })? + )?; + + let client = http_client.clone(); + t.set( + "post_with_data_form", + l.create_function(move |_, (url, data): (String, HashMap)| { + let res = client.post(url).form(&data).send().map_err(|e| Error::RuntimeError(e.to_string()))?; + let lua_res: LuaHttpResponse = TryFrom::try_from(res)?; + Ok(lua_res) + })? + )?; + + let client = http_client.clone(); + t.set( + "post_with_data_json", + l.create_function(move |_, (url, data): (String, HashMap)| { + let res = client.post(url).json(&data).send().map_err(|e| Error::RuntimeError(e.to_string()))?; + let lua_res: LuaHttpResponse = TryFrom::try_from(res)?; + Ok(lua_res) + })? + )?; + //TODO: all http + + t.set( + "decode_json", + l.create_function(move |l, text: String| { + let table: serde_json::Value = serde_json::from_str(&text).map_err(|e| Error::RuntimeError(e.to_string()))?; + Ok(l.to_value(&table)) + })? + )?; + + Ok(t) +} + + +#[derive(Serialize, Deserialize)] +struct LuaHttpResponse { + headers: HashMap, + body: String, + status: u16, +} + +impl TryFrom for LuaHttpResponse { + fn try_from(value: Response) -> Result { + Ok(LuaHttpResponse { headers: headermap_to_hashmap(value.headers()), status: value.status().as_u16(), body: value.text().map_err(|e| Error::RuntimeError(e.to_string()))?, }) + } + + type Error = Error; +} + +fn headermap_to_hashmap(headers: &HeaderMap) -> HashMap { + let mut map = HashMap::new(); + + for (name, value) in headers { + if let Ok(value_str) = value.to_str() { + map.insert(name.as_str().to_string(), value_str.to_string()); + } + } + + map +} + +impl IntoLua for LuaHttpResponse { + fn into_lua(self, lua: &Lua) -> LuaResult { + let table = lua.create_table()?; + + table.set("status", self.status)?; + table.set("body", self.body)?; + + let headers_table = lua.create_table()?; + for (key, value) in self.headers { + headers_table.set(key, value)?; + } + table.set("headers", headers_table)?; + + Ok(LuaValue::Table(table)) + } +} diff --git a/htmlua-parser/src/lib.rs b/htmlua-parser/src/lib.rs index cd87be2..be272dd 100644 --- a/htmlua-parser/src/lib.rs +++ b/htmlua-parser/src/lib.rs @@ -1,3 +1,4 @@ pub mod render; pub mod serve; pub mod helpers; +pub mod htmlua_stdlib; diff --git a/htmlua-parser/src/render.rs b/htmlua-parser/src/render.rs index 6444d60..1bb043b 100644 --- a/htmlua-parser/src/render.rs +++ b/htmlua-parser/src/render.rs @@ -12,32 +12,9 @@ use syntect::{ util::LinesWithEndings, }; -use crate::helpers::read_doc_from_file; +use crate::{helpers::read_doc_from_file, htmlua_stdlib::create_htmlua_stdlib}; -fn create_htmlua_stdlib(l: &Lua, stdout: &Rc>) -> mlua::Result
{ - let t = l.create_table()?; - // This cannot be the best way to do this - - let stdout_println = stdout.clone(); - t.set( - "println", - l.create_function(move |_, text: String| { - let mut stdout_ref = stdout_println.borrow_mut(); - writeln!(stdout_ref, "{text}").map_err(mlua::Error::external) - })?, - )?; - - let stdout_print = stdout.clone(); - t.set( - "print", - l.create_function(move |_, text: String| { - let mut stdout_ref = stdout_print.borrow_mut(); - write!(stdout_ref, "{text}").map_err(mlua::Error::external) - })?, - )?; - Ok(t) -} pub fn execute_lua(document: NodeRef) -> Result { let lua = Lua::new(); From 38be7b73656d70e21118e1a8b7b6b21c8c20a849 Mon Sep 17 00:00:00 2001 From: Quin Date: Fri, 11 Jul 2025 16:55:49 -0600 Subject: [PATCH 18/25] Basic toml config support. --- Cargo.lock | 129 ++++++++++++++++++++++++++++++++++- htmlua-parser/Cargo.toml | 4 +- htmlua-parser/src/config.rs | 98 ++++++++++++++++++++++++++ htmlua-parser/src/helpers.rs | 10 +-- htmlua-parser/src/lib.rs | 3 +- htmlua-parser/src/render.rs | 28 +++++--- htmlua-parser/src/serve.rs | 33 +++++---- 7 files changed, 274 insertions(+), 31 deletions(-) create mode 100644 htmlua-parser/src/config.rs diff --git a/Cargo.lock b/Cargo.lock index 8096bb3..1449110 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -142,6 +142,27 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.60.2", +] + [[package]] name = "dtoa" version = "1.0.10" @@ -311,13 +332,16 @@ name = "htmlua-parser" version = "0.1.0" dependencies = [ "anyhow", + "dirs", "html5ever 0.35.0", "kuchikiki", "markup5ever 0.11.0", "mlua", "pulldown-cmark", + "serde", "syntect", "tendril", + "toml", ] [[package]] @@ -375,6 +399,16 @@ version = "0.2.174" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +[[package]] +name = "libredox" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1580801010e535496706ba011c15f8532df6b42297d2e471fec38ceadd8c0638" +dependencies = [ + "bitflags 2.9.1", + "libc", +] + [[package]] name = "linked-hash-map" version = "0.5.6" @@ -571,6 +605,12 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "ordered-float" version = "2.10.1" @@ -915,6 +955,17 @@ dependencies = [ "bitflags 2.9.1", ] +[[package]] +name = "redox_users" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" +dependencies = [ + "getrandom 0.2.16", + "libredox", + "thiserror 2.0.12", +] + [[package]] name = "regex-syntax" version = "0.8.5" @@ -1044,6 +1095,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83" +dependencies = [ + "serde", +] + [[package]] name = "servo_arc" version = "0.1.1" @@ -1148,7 +1208,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "thiserror", + "thiserror 1.0.69", "walkdir", "yaml-rust", ] @@ -1176,7 +1236,16 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", ] [[package]] @@ -1190,6 +1259,17 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "time" version = "0.3.41" @@ -1221,6 +1301,45 @@ dependencies = [ "time-core", ] +[[package]] +name = "toml" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed0aee96c12fa71097902e0bb061a5e1ebd766a6636bb605ba401c45c1650eac" +dependencies = [ + "indexmap 2.10.0", + "serde", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_parser" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97200572db069e74c512a14117b296ba0a80a30123fbbb5aa1f4a348f639ca30" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64" + [[package]] name = "typeid" version = "1.0.3" @@ -1452,6 +1571,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +[[package]] +name = "winnow" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" + [[package]] name = "winsafe" version = "0.0.19" diff --git a/htmlua-parser/Cargo.toml b/htmlua-parser/Cargo.toml index 28416b7..9e5018e 100644 --- a/htmlua-parser/Cargo.toml +++ b/htmlua-parser/Cargo.toml @@ -5,14 +5,16 @@ edition = "2024" [dependencies] anyhow = "1.0.98" +dirs = "6.0.0" html5ever = "0.35.0" kuchikiki = "0.8.2" markup5ever = "0.11.0" mlua = { version = "0.10.5", features = ["lua54", "vendored", "serialize"] } pulldown-cmark = "0.13.0" +serde = { version = "1.0.219", features = ["derive"] } syntect = "5.2.0" tendril = "0.4.3" - +toml = "0.9.2" [lints.clippy] pedantic = { level = "warn", priority = -1 } diff --git a/htmlua-parser/src/config.rs b/htmlua-parser/src/config.rs new file mode 100644 index 0000000..70ea7a8 --- /dev/null +++ b/htmlua-parser/src/config.rs @@ -0,0 +1,98 @@ +use anyhow::{Context, Result}; +use serde::{Deserialize, Serialize}; +use std::{fs, path::PathBuf}; + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct Config { + pub paths: PathConfig, + pub server: ServerConfig, + pub syntax_highlighting: SyntaxConfig, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct PathConfig { + pub pages: PathBuf, + pub components: PathBuf, + pub themes: PathBuf, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct ServerConfig { + pub host: String, + pub port: u16, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct SyntaxConfig { + pub default_theme: String, + pub load_custom_themes: bool, +} + +impl Default for Config { + fn default() -> Self { + Self { + paths: PathConfig { + pages: PathBuf::from("/var/www/htmlua/pages"), + components: PathBuf::from("/var/www/htmlua/components"), + themes: PathBuf::from("/var/www/htmlua/themes"), + }, + server: ServerConfig { + host: "127.0.0.1".to_string(), + port: 8080, + }, + syntax_highlighting: SyntaxConfig { + default_theme: "base16-ocean.dark".to_string(), + load_custom_themes: true, + }, + } + } +} + +impl Config { + pub fn load() -> Result { + let config_path = Self::get_config_path(); + if config_path.exists() { + let config_content = fs::read_to_string(&config_path) + .with_context(|| format!("Failed to read config file: {}", config_path.display()))?; + let config: Config = toml::from_str(&config_content) + .with_context(|| format!("Failed to parse config file: {}", config_path.display()))?; + Ok(config) + } else { + let default_config = Config::default(); + if let Some(parent) = config_path.parent() { + fs::create_dir_all(parent) + .with_context(|| format!("Failed to create config directory: {}", parent.display()))?; + } + let config_content = + toml::to_string_pretty(&default_config).context("Failed to serialize default config")?; + fs::write(&config_path, config_content) + .with_context(|| format!("Failed to write default config to: {}", config_path.display()))?; + Ok(default_config) + } + } + + fn get_config_path() -> PathBuf { + if cfg!(windows) { + dirs::config_dir() + .unwrap_or_else(|| PathBuf::from("C:\\ProgramData")) + .join("htmlua") + .join("config.toml") + } else { + PathBuf::from("/etc/htmlua.toml") + } + } + + pub fn save(&self) -> Result<()> { + let config_path = Self::get_config_path(); + if let Some(parent) = config_path.parent() { + fs::create_dir_all(parent) + .with_context(|| format!("Failed to create config directory: {}", parent.display()))?; + } + let config_content = toml::to_string_pretty(self).context("Failed to serialize config")?; + fs::write(&config_path, config_content) + .with_context(|| format!("Failed to write config to: {}", config_path.display()))?; + Ok(()) + } + + pub fn config_file_path() -> PathBuf { Self::get_config_path() } +} diff --git a/htmlua-parser/src/helpers.rs b/htmlua-parser/src/helpers.rs index 90439f7..bd99f81 100644 --- a/htmlua-parser/src/helpers.rs +++ b/htmlua-parser/src/helpers.rs @@ -1,15 +1,15 @@ -use std::{fs::File, io::{BufRead, BufReader, Read, Seek, SeekFrom}, path::PathBuf}; +use std::{ + fs::File, + io::{BufRead, BufReader, Read, Seek, SeekFrom}, + path::PathBuf, +}; use anyhow::Result; use kuchikiki::NodeRef; use markup5ever::{LocalName, Namespace, QualName}; use tendril::TendrilSink; - - - pub fn read_doc_from_file(path: PathBuf) -> Result { - let mut file = File::open(path)?; let mut reader = BufReader::new(&mut file); diff --git a/htmlua-parser/src/lib.rs b/htmlua-parser/src/lib.rs index cd87be2..525486e 100644 --- a/htmlua-parser/src/lib.rs +++ b/htmlua-parser/src/lib.rs @@ -1,3 +1,4 @@ +pub mod config; +pub mod helpers; pub mod render; pub mod serve; -pub mod helpers; diff --git a/htmlua-parser/src/render.rs b/htmlua-parser/src/render.rs index 6444d60..9360f16 100644 --- a/htmlua-parser/src/render.rs +++ b/htmlua-parser/src/render.rs @@ -1,4 +1,4 @@ -use std::{cell::RefCell, fmt::Write, fs::read_to_string, path::PathBuf, rc::Rc}; +use std::{cell::RefCell, fmt::Write, path::PathBuf, rc::Rc}; use anyhow::{Result, anyhow}; use kuchikiki::{NodeRef, traits::TendrilSink}; @@ -12,7 +12,7 @@ use syntect::{ util::LinesWithEndings, }; -use crate::helpers::read_doc_from_file; +use crate::{helpers::read_doc_from_file, serve::get_config}; fn create_htmlua_stdlib(l: &Lua, stdout: &Rc>) -> mlua::Result
{ let t = l.create_table()?; @@ -108,7 +108,11 @@ pub fn expand_template(document: NodeRef, component_path: &PathBuf, include_from let exported = from_node .select_first(format!("exportelement.{name}").as_str()) .map_err(|()| anyhow!("Error finding exportelement"))?; - exported.as_node().children().rev().for_each(|c| i.as_node().insert_after(c)); + exported + .as_node() + .children() + .rev() + .for_each(|c| i.as_node().insert_after(c)); } } while let Ok(i) = document.select_first("includeelement") { @@ -130,8 +134,13 @@ pub fn expand_template(document: NodeRef, component_path: &PathBuf, include_from item_path.push(include_path); let new_node = read_doc_from_file(item_path)?; let replaced_node = expand_template(new_node, component_path, Some(i.as_node()))?; - replaced_node.select_first("html").map_err(|()| anyhow!("Error finding html"))?.as_node().children().rev().for_each(|c| i.as_node().insert_after(c)); - + replaced_node + .select_first("html") + .map_err(|()| anyhow!("Error finding html"))? + .as_node() + .children() + .rev() + .for_each(|c| i.as_node().insert_after(c)); } } while let Ok(i) = document.select_first("include") { @@ -141,16 +150,15 @@ pub fn expand_template(document: NodeRef, component_path: &PathBuf, include_from } pub fn process_syntax_highlighting(document: NodeRef) -> Result { + let config = get_config(); let ps = SyntaxSet::load_defaults_newlines(); let mut ts = ThemeSet::load_defaults(); let syntax_elements: Vec<_> = match document.select("syntaxhighlight") { Ok(e) => e.collect(), Err(()) => return Err(anyhow!("Unable to find syntaxhighlight elements")), }; - if !syntax_elements.is_empty() { - // TODO: read from config - let themes = PathBuf::from("/var/www/htmlua/themes"); - let _ = ts.add_from_folder(themes); + if !syntax_elements.is_empty() && config.syntax_highlighting.load_custom_themes { + let _ = ts.add_from_folder(&config.paths.themes); } for node in syntax_elements { let attrs = match node.as_node().as_element() { @@ -158,7 +166,7 @@ pub fn process_syntax_highlighting(document: NodeRef) -> Result { None => continue, }; let language = attrs.get("lang").unwrap_or("text"); - let theme_name = attrs.get("theme").unwrap_or("base16-ocean.dark"); + let theme_name = attrs.get("theme").unwrap_or(&config.syntax_highlighting.default_theme); if let Some(text_node) = node.as_node().first_child() { if let Some(code_text) = text_node.as_text() { let syntax = ps diff --git a/htmlua-parser/src/serve.rs b/htmlua-parser/src/serve.rs index 06de0ea..9821210 100644 --- a/htmlua-parser/src/serve.rs +++ b/htmlua-parser/src/serve.rs @@ -1,20 +1,29 @@ -use crate::helpers::read_doc_from_file; - -use super::render::{execute_lua, expand_template, process_markdown, process_syntax_highlighting}; +use crate::{ + config::Config, + helpers::read_doc_from_file, + render::{execute_lua, expand_template, process_markdown, process_syntax_highlighting}, +}; use anyhow::Result; -use std::path::{Path, PathBuf} -; +use std::{path::Path, sync::OnceLock}; + +static CONFIG: OnceLock = OnceLock::new(); + +pub fn get_config() -> &'static Config { + CONFIG.get_or_init(|| { + Config::load().unwrap_or_else(|e| { + eprintln!("Warning: Failed to load config: {}", e); + eprintln!("Using default configuration"); + Config::default() + }) + }) +} pub fn serve_content(request_uri: &str) -> Result { - // TODO: read from config - let pages = PathBuf::from("/var/www/htmlua/pages"); - let components = PathBuf::from("/var/www/htmlua/components"); + let config = get_config(); let safe_path = Path::new(request_uri).strip_prefix("/")?; - let page_path = pages.join(safe_path); - + let page_path = config.paths.pages.join(safe_path); let doc = read_doc_from_file(page_path)?; - - let full_doc = expand_template(doc, &components, None)?; + let full_doc = expand_template(doc, &config.paths.components, None)?; let markdown_doc = process_markdown(full_doc)?; let highlighted_doc = process_syntax_highlighting(markdown_doc)?; let executed_doc = execute_lua(highlighted_doc)?; From 085aa5a1f553d4acb2acc7ac7d74690e9326a3a5 Mon Sep 17 00:00:00 2001 From: gyoder <70408179+gyoder@users.noreply.github.com> Date: Sat, 12 Jul 2025 21:51:05 -0600 Subject: [PATCH 19/25] added full request and tests --- Cargo.lock | 119 +++++++++++++++++++ htmlua-parser/Cargo.toml | 1 + htmlua-parser/src/helpers.rs | 7 +- htmlua-parser/src/htmlua_stdlib.rs | 119 +++++++++++++++---- htmlua-parser/src/lib.rs | 4 +- htmlua-parser/src/render.rs | 177 +++++++++++++++++++++++++++-- htmlua-parser/src/serve.rs | 3 +- 7 files changed, 392 insertions(+), 38 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index da92710..2ae966a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,15 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "anyhow" version = "1.0.98" @@ -84,6 +93,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" dependencies = [ "memchr", + "regex-automata", "serde", ] @@ -151,6 +161,21 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + [[package]] name = "cssparser" version = "0.27.2" @@ -329,6 +354,20 @@ dependencies = [ "new_debug_unreachable", ] +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.31" @@ -351,6 +390,17 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "futures-sink" version = "0.3.31" @@ -369,8 +419,10 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ + "futures-channel", "futures-core", "futures-io", + "futures-macro", "futures-sink", "futures-task", "memchr", @@ -507,6 +559,7 @@ version = "0.1.0" dependencies = [ "anyhow", "html5ever 0.35.0", + "httptest", "kuchikiki", "markup5ever 0.11.0", "mlua", @@ -562,6 +615,36 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "httptest" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde82de3ef9bd882493c6a5edbc3363ad928925b30ccecc0f2ddeb42601b3021" +dependencies = [ + "bstr", + "bytes", + "crossbeam-channel", + "form_urlencoded", + "futures", + "http", + "http-body-util", + "hyper", + "hyper-util", + "log", + "once_cell", + "regex", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", +] + [[package]] name = "hyper" version = "1.6.0" @@ -575,6 +658,7 @@ dependencies = [ "http", "http-body", "httparse", + "httpdate", "itoa 1.0.15", "pin-project-lite", "smallvec", @@ -1502,6 +1586,29 @@ dependencies = [ "bitflags 2.9.1", ] +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + [[package]] name = "regex-syntax" version = "0.8.5" @@ -2047,9 +2154,21 @@ dependencies = [ "pin-project-lite", "slab", "socket2", + "tokio-macros", "windows-sys 0.52.0", ] +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "tokio-native-tls" version = "0.3.1" diff --git a/htmlua-parser/Cargo.toml b/htmlua-parser/Cargo.toml index 83769af..4ee6be3 100644 --- a/htmlua-parser/Cargo.toml +++ b/htmlua-parser/Cargo.toml @@ -6,6 +6,7 @@ edition = "2024" [dependencies] anyhow = "1.0.98" html5ever = "0.35.0" +httptest = "0.16.3" kuchikiki = "0.8.2" markup5ever = "0.11.0" mlua = { version = "0.10.5", features = ["lua54", "vendored", "serialize", "anyhow"] } diff --git a/htmlua-parser/src/helpers.rs b/htmlua-parser/src/helpers.rs index 90439f7..512ebb7 100644 --- a/htmlua-parser/src/helpers.rs +++ b/htmlua-parser/src/helpers.rs @@ -1,4 +1,8 @@ -use std::{fs::File, io::{BufRead, BufReader, Read, Seek, SeekFrom}, path::PathBuf}; +use std::{ + fs::File, + io::{BufRead, BufReader, Read, Seek, SeekFrom}, + path::PathBuf, +}; use anyhow::Result; use kuchikiki::NodeRef; @@ -9,7 +13,6 @@ use tendril::TendrilSink; pub fn read_doc_from_file(path: PathBuf) -> Result { - let mut file = File::open(path)?; let mut reader = BufReader::new(&mut file); diff --git a/htmlua-parser/src/htmlua_stdlib.rs b/htmlua-parser/src/htmlua_stdlib.rs index 3c27f26..d36620e 100644 --- a/htmlua-parser/src/htmlua_stdlib.rs +++ b/htmlua-parser/src/htmlua_stdlib.rs @@ -1,10 +1,19 @@ -use std::{cell::{LazyCell, RefCell}, collections::HashMap, fmt::Write, rc::Rc}; +use std::{ + cell::{LazyCell, RefCell}, + collections::HashMap, + fmt::Write, + rc::Rc, + str::FromStr, + time::Duration, +}; -use anyhow::anyhow; -use mlua::{prelude::*, Error, Lua, Table}; -use reqwest::{blocking::{Client, Response}, header::HeaderMap}; +use mlua::{Error, Lua, Table, prelude::*}; +use reqwest::{ + Method, Url, + blocking::{Client, Response}, + header::HeaderMap, +}; use serde::{Deserialize, Serialize}; -use serde_json::to_value; pub fn create_htmlua_stdlib(l: &Lua, stdout: &Rc>) -> mlua::Result
{ @@ -34,13 +43,16 @@ pub fn create_htmlua_stdlib(l: &Lua, stdout: &Rc>) -> mlua::Resu Ok(t) } +#[allow(clippy::too_many_lines)] fn create_http_lib(l: &Lua) -> mlua::Result
{ let t = l.create_table()?; - let http_client: Rc> = Rc::new(LazyCell::new(|| Client::builder() + let http_client: Rc> = Rc::new(LazyCell::new(|| { + Client::builder() .timeout(std::time::Duration::from_secs(3)) .user_agent("htmlua/0.1.0") - .build().unwrap() - )); + .build() + .unwrap() + })); let client = http_client.clone(); @@ -50,56 +62,116 @@ fn create_http_lib(l: &Lua) -> mlua::Result
{ let res = client.get(url).send().map_err(|e| Error::RuntimeError(e.to_string()))?; let lua_res: LuaHttpResponse = TryFrom::try_from(res)?; Ok(lua_res) - })? + })?, )?; let client = http_client.clone(); t.set( "post", l.create_function(move |_, url: String| { - let res = client.post(url).send().map_err(|e| Error::RuntimeError(e.to_string()))?; + let res = client + .post(url) + .send() + .map_err(|e| Error::RuntimeError(e.to_string()))?; let lua_res: LuaHttpResponse = TryFrom::try_from(res)?; Ok(lua_res) - })? + })?, )?; let client = http_client.clone(); t.set( "get_with_data", l.create_function(move |_, (url, data): (String, HashMap)| { - let res = client.get(url).query(&data).send().map_err(|e| Error::RuntimeError(e.to_string()))?; + let res = client + .get(url) + .query(&data) + .send() + .map_err(|e| Error::RuntimeError(e.to_string()))?; let lua_res: LuaHttpResponse = TryFrom::try_from(res)?; Ok(lua_res) - })? + })?, )?; let client = http_client.clone(); t.set( "post_with_data_form", l.create_function(move |_, (url, data): (String, HashMap)| { - let res = client.post(url).form(&data).send().map_err(|e| Error::RuntimeError(e.to_string()))?; + let res = client + .post(url) + .form(&data) + .send() + .map_err(|e| Error::RuntimeError(e.to_string()))?; let lua_res: LuaHttpResponse = TryFrom::try_from(res)?; Ok(lua_res) - })? + })?, )?; let client = http_client.clone(); t.set( "post_with_data_json", l.create_function(move |_, (url, data): (String, HashMap)| { - let res = client.post(url).json(&data).send().map_err(|e| Error::RuntimeError(e.to_string()))?; + let res = client + .post(url) + .json(&data) + .send() + .map_err(|e| Error::RuntimeError(e.to_string()))?; let lua_res: LuaHttpResponse = TryFrom::try_from(res)?; Ok(lua_res) - })? + })?, + )?; + + let client = http_client.clone(); + t.set( + "request", + l.create_function(move |_, table: mlua::Table| { + let mut request = client.request( + Method::from_bytes(table.get::("method")?.as_bytes()) + .map_err(|e| Error::RuntimeError(e.to_string()))?, + Url::from_str(table.get::("url")?.as_str()).map_err(|e| Error::RuntimeError(e.to_string()))?, + ); + + if let Ok(header_tbl) = table.get::("headers") { + request = header_tbl + .pairs::() + .filter_map(std::result::Result::ok) + .fold(request, |req, (k, v)| req.header(k, v)); + } + + if let Ok(basic_auth) = table.get::("basic_auth") { + request = request + .basic_auth(basic_auth.get::("username")?, basic_auth.get::("password").ok()); + } + + if let Ok(bearer_auth) = table.get::("bearer_auth") { + request = request.bearer_auth(bearer_auth.get::("token")?); + } + + if let Ok(body) = table.get::("body") { + request = request.body(body); + } + + if let Ok(json) = table.get::("json") { + request = request.json(&json); + } + + if let Ok(timeout) = table.get::("timeout") { + request = request.timeout(Duration::from_secs(timeout)); + } + + + let res = request.send().map_err(|e| Error::RuntimeError(e.to_string()))?; + let lua_res: LuaHttpResponse = TryFrom::try_from(res)?; + Ok(lua_res) + })?, )?; - //TODO: all http t.set( "decode_json", l.create_function(move |l, text: String| { - let table: serde_json::Value = serde_json::from_str(&text).map_err(|e| Error::RuntimeError(e.to_string()))?; + let table: serde_json::Value = + serde_json::from_str(&text).map_err(|e| Error::RuntimeError(e.to_string()))?; Ok(l.to_value(&table)) - })? + })?, )?; Ok(t) @@ -115,7 +187,11 @@ struct LuaHttpResponse { impl TryFrom for LuaHttpResponse { fn try_from(value: Response) -> Result { - Ok(LuaHttpResponse { headers: headermap_to_hashmap(value.headers()), status: value.status().as_u16(), body: value.text().map_err(|e| Error::RuntimeError(e.to_string()))?, }) + Ok(LuaHttpResponse { + headers: headermap_to_hashmap(value.headers()), + status: value.status().as_u16(), + body: value.text().map_err(|e| Error::RuntimeError(e.to_string()))?, + }) } type Error = Error; @@ -133,6 +209,7 @@ fn headermap_to_hashmap(headers: &HeaderMap) -> HashMap { map } + impl IntoLua for LuaHttpResponse { fn into_lua(self, lua: &Lua) -> LuaResult { let table = lua.create_table()?; diff --git a/htmlua-parser/src/lib.rs b/htmlua-parser/src/lib.rs index be272dd..062fcb4 100644 --- a/htmlua-parser/src/lib.rs +++ b/htmlua-parser/src/lib.rs @@ -1,4 +1,4 @@ -pub mod render; -pub mod serve; pub mod helpers; pub mod htmlua_stdlib; +pub mod render; +pub mod serve; diff --git a/htmlua-parser/src/render.rs b/htmlua-parser/src/render.rs index 1bb043b..a92e7c5 100644 --- a/htmlua-parser/src/render.rs +++ b/htmlua-parser/src/render.rs @@ -1,8 +1,13 @@ -use std::{cell::RefCell, fmt::Write, fs::read_to_string, path::PathBuf, rc::Rc}; +use std::{ + cell::{LazyCell, RefCell}, + fmt::Write, + path::PathBuf, + rc::Rc, +}; use anyhow::{Result, anyhow}; use kuchikiki::{NodeRef, traits::TendrilSink}; -use mlua::{Lua, Table}; +use mlua::Lua; use pulldown_cmark::{Options, Parser, html}; use syntect::{ easy::HighlightLines, @@ -15,20 +20,22 @@ use syntect::{ use crate::{helpers::read_doc_from_file, htmlua_stdlib::create_htmlua_stdlib}; - -pub fn execute_lua(document: NodeRef) -> Result { +fn build_lua_with_stdout(stdout: &Rc>) -> Result { let lua = Lua::new(); let globals = lua.globals(); - let stdout: Rc> = Rc::new(RefCell::new(String::new())); - - let htmlua_table = - create_htmlua_stdlib(&lua, &stdout).map_err(|e| anyhow!("Failed to create Lua stdlib: {}", e))?; + let htmlua_table = create_htmlua_stdlib(&lua, stdout).map_err(|e| anyhow!("Failed to create Lua stdlib: {}", e))?; globals .set("htmlua", htmlua_table) .map_err(|e| anyhow!("Failed to set global: {}", e))?; + Ok(lua) +} + +pub fn execute_lua(document: NodeRef) -> Result { + let stdout: Rc> = Rc::new(RefCell::new(String::new())); + let lua = LazyCell::new(|| build_lua_with_stdout(&stdout).unwrap_or_default()); let lua_elements: Vec<_> = match document.select("lua") { Ok(e) => e.collect(), Err(()) => return Err(anyhow!("Unable to find Lua")), @@ -85,7 +92,11 @@ pub fn expand_template(document: NodeRef, component_path: &PathBuf, include_from let exported = from_node .select_first(format!("exportelement.{name}").as_str()) .map_err(|()| anyhow!("Error finding exportelement"))?; - exported.as_node().children().rev().for_each(|c| i.as_node().insert_after(c)); + exported + .as_node() + .children() + .rev() + .for_each(|c| i.as_node().insert_after(c)); } } while let Ok(i) = document.select_first("includeelement") { @@ -107,8 +118,13 @@ pub fn expand_template(document: NodeRef, component_path: &PathBuf, include_from item_path.push(include_path); let new_node = read_doc_from_file(item_path)?; let replaced_node = expand_template(new_node, component_path, Some(i.as_node()))?; - replaced_node.select_first("html").map_err(|()| anyhow!("Error finding html"))?.as_node().children().rev().for_each(|c| i.as_node().insert_after(c)); - + replaced_node + .select_first("html") + .map_err(|()| anyhow!("Error finding html"))? + .as_node() + .children() + .rev() + .for_each(|c| i.as_node().insert_after(c)); } } while let Ok(i) = document.select_first("include") { @@ -167,6 +183,8 @@ pub fn process_syntax_highlighting(document: NodeRef) -> Result { #[cfg(test)] mod tests { + use httptest::{Expectation, ServerPool, matchers::*, responders::*}; + use super::*; #[test] @@ -413,4 +431,141 @@ fn main() { assert_eq!(attrs.get("data-lang"), Some("rust")); assert!(attrs.get("class").unwrap().contains("syntax-highlight")); } + + static SERVER_POOL: ServerPool = ServerPool::new(2); + + #[test] + fn basic_get() { + let server = SERVER_POOL.get_server(); + + server.expect( + Expectation::matching(request::method_path("GET", "/test/1")).respond_with(status_code(200).body("ret")), + ); + + let stdout = Rc::new(RefCell::new(String::new())); + let lua = build_lua_with_stdout(&stdout).unwrap(); + let code = format!("htmlua.print(htmlua.http.get(\"{}\").body)", server.url("/test/1")); + lua.load(code).exec().unwrap(); + assert_eq!(stdout.borrow().as_str(), "ret"); + } + + #[test] + fn basic_post() { + let server = SERVER_POOL.get_server(); + + server.expect( + Expectation::matching(request::method_path("POST", "/test/1")).respond_with(status_code(200).body("ret")), + ); + + let stdout = Rc::new(RefCell::new(String::new())); + let lua = build_lua_with_stdout(&stdout).unwrap(); + let code = format!("htmlua.print(htmlua.http.post(\"{}\").body)", server.url("/test/1")); + lua.load(code).exec().unwrap(); + assert_eq!(stdout.borrow().as_str(), "ret"); + } + + #[test] + fn get_with_data() { + let server = SERVER_POOL.get_server(); + + server.expect( + Expectation::matching(request::query(url_decoded(contains(("a", "b"))))) + .respond_with(status_code(200).body("ret")), + ); + + let stdout = Rc::new(RefCell::new(String::new())); + let lua = build_lua_with_stdout(&stdout).unwrap(); + let code = format!( + " + data = {{}} + data.a = \"b\" + htmlua.print(htmlua.http.get_with_data(\"{}\", data).body) + ", + server.url("/test/1") + ); + lua.load(code).exec().unwrap(); + assert_eq!(stdout.borrow().as_str(), "ret"); + } + + #[test] + fn post_with_data_form() { + let server = SERVER_POOL.get_server(); + + server.expect( + Expectation::matching(all_of![request::method_path("POST", "/test/1"), request::body("a=b")]) + .respond_with(status_code(200).body("ret")), + ); + + let stdout = Rc::new(RefCell::new(String::new())); + let lua = build_lua_with_stdout(&stdout).unwrap(); + let code = format!( + " + data = {{}} + data.a = \"b\" + htmlua.print(htmlua.http.post_with_data_form(\"{}\", data).body) + ", + server.url("/test/1") + ); + lua.load(code).exec().unwrap(); + assert_eq!(stdout.borrow().as_str(), "ret"); + } + + #[test] + fn post_with_data_json() { + let server = SERVER_POOL.get_server(); + + server.expect( + Expectation::matching(all_of![request::method_path("POST", "/test/1"), request::body(r#"{"a":"b"}"#)]) + .respond_with(status_code(200).body("ret")), + ); + + let stdout = Rc::new(RefCell::new(String::new())); + let lua = build_lua_with_stdout(&stdout).unwrap(); + let code = format!( + " + data = {{}} + data.a = \"b\" + htmlua.print(htmlua.http.post_with_data_json(\"{}\", data).body) + ", + server.url("/test/1") + ); + lua.load(code).exec().unwrap(); + assert_eq!(stdout.borrow().as_str(), "ret"); + } + + #[test] + fn table_request() { + let server = SERVER_POOL.get_server(); + + server.expect( + Expectation::matching(all_of![ + request::method_path("GET", "/test/1"), + request::body("cool"), + request::headers(contains(("testhdr", "val"))), + request::headers(contains(("authorization", "Basic dXNlcjpwYXNz"))), + ]) + .respond_with(status_code(200).body("ret")), + ); + + let stdout = Rc::new(RefCell::new(String::new())); + let lua = build_lua_with_stdout(&stdout).unwrap(); + let code = format!( + " + req = {{}} + req.url = \"{}\" + req.method = \"GET\" + req.headers = {{}} + req.headers.testhdr = \"val\" + req.basic_auth = {{}} + req.basic_auth.username = \"user\" + req.basic_auth.password = \"pass\" + req.body = \"cool\" + + htmlua.print(htmlua.http.request(req).body) + ", + server.url("/test/1") + ); + lua.load(code).exec().unwrap(); + assert_eq!(stdout.borrow().as_str(), "ret"); + } } diff --git a/htmlua-parser/src/serve.rs b/htmlua-parser/src/serve.rs index 06de0ea..cfb3811 100644 --- a/htmlua-parser/src/serve.rs +++ b/htmlua-parser/src/serve.rs @@ -2,8 +2,7 @@ use crate::helpers::read_doc_from_file; use super::render::{execute_lua, expand_template, process_markdown, process_syntax_highlighting}; use anyhow::Result; -use std::path::{Path, PathBuf} -; +use std::path::{Path, PathBuf}; pub fn serve_content(request_uri: &str) -> Result { // TODO: read from config From 8dfaf0b386c7b2cceaae929d7c81e6c2c2d59ad9 Mon Sep 17 00:00:00 2001 From: gyoder <70408179+gyoder@users.noreply.github.com> Date: Sat, 12 Jul 2025 22:54:46 -0600 Subject: [PATCH 20/25] Added footnote support --- htmlua-parser/src/render.rs | 66 ++++++++++++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/htmlua-parser/src/render.rs b/htmlua-parser/src/render.rs index 9360f16..d0fd029 100644 --- a/htmlua-parser/src/render.rs +++ b/htmlua-parser/src/render.rs @@ -1,7 +1,8 @@ use std::{cell::RefCell, fmt::Write, path::PathBuf, rc::Rc}; -use anyhow::{Result, anyhow}; +use anyhow::{anyhow, Result}; use kuchikiki::{NodeRef, traits::TendrilSink}; +use markup5ever::{LocalName, Namespace, QualName}; use mlua::{Lua, Table}; use pulldown_cmark::{Options, Parser, html}; use syntect::{ @@ -196,6 +197,29 @@ pub fn process_syntax_highlighting(document: NodeRef) -> Result { Ok(document) } +pub fn generate_footnotes(document: NodeRef) -> Result { + let Ok(footnote_container) = document.select_first("footnotecontainer") else { return Ok(document) }; + + let ctx_name = QualName::new(None, Namespace::from("http://www.w3.org/1999/xhtml"), LocalName::from("div")); + for (i, footnote) in document.select("footnote").map_err(|()| anyhow!("Failed to get footnote"))?.enumerate() { + let i = i + 1; + let fn_text = footnote.text_contents(); + let sup_tag = kuchikiki::parse_fragment(ctx_name.clone(), Vec::new()).one( + format!("{i}") + ).select_first("a").map_err(|()| anyhow!("parse err"))?; + footnote.as_node().insert_after(sup_tag.as_node().clone()); + let text_tag = kuchikiki::parse_fragment(ctx_name.clone(), Vec::new()).one( + format!("

{i}: {fn_text}

") + ).select_first("p").map_err(|()| anyhow!("parse err"))?; + footnote_container.as_node().insert_before(text_tag.as_node().clone()); + } + while let Ok(i) = document.select_first("footnote") { + i.as_node().detach(); + } + footnote_container.as_node().detach(); + Ok(document) +} + #[cfg(test)] mod tests { use super::*; @@ -386,6 +410,46 @@ mod tests { assert!(d.select_first("includeelement").is_err()); } + #[test] + fn footnotes() { + let page = r" + + + + Basic HTML Page + + +
+

asdf

+

asdfum actually

+

asdf

+

asdfno

+
+
+ +
+ + "; + let document = kuchikiki::parse_html().one(page); + let d = generate_footnotes(document).unwrap(); + let sup1 = d.select_first("#ft-sup-1").unwrap(); + assert_eq!(sup1.attributes.borrow().get("title").unwrap(), "um actually"); + assert_eq!(sup1.text_contents(), "1"); + + let sup2 = d.select_first("#ft-sup-2").unwrap(); + assert_eq!(sup2.attributes.borrow().get("title").unwrap(), "no"); + assert_eq!(sup2.text_contents(), "2"); + + let ft_text_1 = d.select_first("#ft-text-1").unwrap(); + assert!(ft_text_1.text_contents().contains("um actually")); + + let ft_text_2 = d.select_first("#ft-text-2").unwrap(); + assert!(ft_text_2.text_contents().contains("no")); + + assert!(d.select_first("footnote").is_err()); + assert!(d.select_first("footnotecontainer").is_err()); + } + #[test] fn basic_markdown() { let page = r" From 3632dd5e09dcc1f8d09335c1144541ee1ab5ed63 Mon Sep 17 00:00:00 2001 From: Quin Date: Sat, 12 Jul 2025 23:54:21 -0600 Subject: [PATCH 21/25] Add import sorting options to .rustfmt.toml. --- .rustfmt.toml | 2 ++ htmlua-apache-cgi/src/main.rs | 3 ++- htmlua-parser/src/config.rs | 3 ++- htmlua-parser/src/lib.rs | 2 +- htmlua-parser/src/render.rs | 26 +++++++++++++++++--------- htmlua-parser/src/serve.rs | 6 ++++-- 6 files changed, 28 insertions(+), 14 deletions(-) diff --git a/.rustfmt.toml b/.rustfmt.toml index 21ebfc1..25b733d 100644 --- a/.rustfmt.toml +++ b/.rustfmt.toml @@ -9,3 +9,5 @@ brace_style = "PreferSameLine" blank_lines_upper_bound = 5 combine_control_expr = false wrap_comments = false +group_imports = "StdExternalCrate" +imports_granularity = "Crate" diff --git a/htmlua-apache-cgi/src/main.rs b/htmlua-apache-cgi/src/main.rs index ab91cb1..5998ff6 100644 --- a/htmlua-apache-cgi/src/main.rs +++ b/htmlua-apache-cgi/src/main.rs @@ -1,6 +1,7 @@ -use htmlua_parser::serve::serve_content; use std::env; +use htmlua_parser::serve::serve_content; + fn main() { println!("Content-Type: text/html\n"); diff --git a/htmlua-parser/src/config.rs b/htmlua-parser/src/config.rs index 70ea7a8..ffe1320 100644 --- a/htmlua-parser/src/config.rs +++ b/htmlua-parser/src/config.rs @@ -1,6 +1,7 @@ +use std::{fs, path::PathBuf}; + use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; -use std::{fs, path::PathBuf}; #[derive(Debug, Deserialize, Serialize, Clone)] pub struct Config { diff --git a/htmlua-parser/src/lib.rs b/htmlua-parser/src/lib.rs index 2dc831c..d2d5599 100644 --- a/htmlua-parser/src/lib.rs +++ b/htmlua-parser/src/lib.rs @@ -1,5 +1,5 @@ +pub mod config; pub mod helpers; pub mod htmlua_stdlib; -pub mod config; pub mod render; pub mod serve; diff --git a/htmlua-parser/src/render.rs b/htmlua-parser/src/render.rs index cc56eda..7046db8 100644 --- a/htmlua-parser/src/render.rs +++ b/htmlua-parser/src/render.rs @@ -5,7 +5,7 @@ use std::{ rc::Rc, }; -use anyhow::{anyhow, Result}; +use anyhow::{Result, anyhow}; use kuchikiki::{NodeRef, traits::TendrilSink}; use markup5ever::{LocalName, Namespace, QualName}; use mlua::{Lua, Table}; @@ -182,19 +182,27 @@ pub fn process_syntax_highlighting(document: NodeRef) -> Result { } pub fn generate_footnotes(document: NodeRef) -> Result { - let Ok(footnote_container) = document.select_first("footnotecontainer") else { return Ok(document) }; + let Ok(footnote_container) = document.select_first("footnotecontainer") else { + return Ok(document); + }; let ctx_name = QualName::new(None, Namespace::from("http://www.w3.org/1999/xhtml"), LocalName::from("div")); - for (i, footnote) in document.select("footnote").map_err(|()| anyhow!("Failed to get footnote"))?.enumerate() { + for (i, footnote) in document + .select("footnote") + .map_err(|()| anyhow!("Failed to get footnote"))? + .enumerate() + { let i = i + 1; let fn_text = footnote.text_contents(); - let sup_tag = kuchikiki::parse_fragment(ctx_name.clone(), Vec::new()).one( - format!("{i}") - ).select_first("a").map_err(|()| anyhow!("parse err"))?; + let sup_tag = kuchikiki::parse_fragment(ctx_name.clone(), Vec::new()) + .one(format!("{i}")) + .select_first("a") + .map_err(|()| anyhow!("parse err"))?; footnote.as_node().insert_after(sup_tag.as_node().clone()); - let text_tag = kuchikiki::parse_fragment(ctx_name.clone(), Vec::new()).one( - format!("

{i}: {fn_text}

") - ).select_first("p").map_err(|()| anyhow!("parse err"))?; + let text_tag = kuchikiki::parse_fragment(ctx_name.clone(), Vec::new()) + .one(format!("

{i}: {fn_text}

")) + .select_first("p") + .map_err(|()| anyhow!("parse err"))?; footnote_container.as_node().insert_before(text_tag.as_node().clone()); } while let Ok(i) = document.select_first("footnote") { diff --git a/htmlua-parser/src/serve.rs b/htmlua-parser/src/serve.rs index 9821210..715bfcc 100644 --- a/htmlua-parser/src/serve.rs +++ b/htmlua-parser/src/serve.rs @@ -1,10 +1,12 @@ +use std::{path::Path, sync::OnceLock}; + +use anyhow::Result; + use crate::{ config::Config, helpers::read_doc_from_file, render::{execute_lua, expand_template, process_markdown, process_syntax_highlighting}, }; -use anyhow::Result; -use std::{path::Path, sync::OnceLock}; static CONFIG: OnceLock = OnceLock::new(); From 17759c3210a88a1babbde650e565442ce169a1c6 Mon Sep 17 00:00:00 2001 From: Quin Date: Sat, 12 Jul 2025 23:56:50 -0600 Subject: [PATCH 22/25] Sort imports with the help of rustfmt. --- .rustfmt.toml | 2 ++ htmlua-apache-cgi/src/main.rs | 3 ++- htmlua-parser/src/config.rs | 3 ++- htmlua-parser/src/lib.rs | 2 +- htmlua-parser/src/render.rs | 26 +++++++++++++++++--------- htmlua-parser/src/serve.rs | 6 ++++-- 6 files changed, 28 insertions(+), 14 deletions(-) diff --git a/.rustfmt.toml b/.rustfmt.toml index 21ebfc1..25b733d 100644 --- a/.rustfmt.toml +++ b/.rustfmt.toml @@ -9,3 +9,5 @@ brace_style = "PreferSameLine" blank_lines_upper_bound = 5 combine_control_expr = false wrap_comments = false +group_imports = "StdExternalCrate" +imports_granularity = "Crate" diff --git a/htmlua-apache-cgi/src/main.rs b/htmlua-apache-cgi/src/main.rs index ab91cb1..5998ff6 100644 --- a/htmlua-apache-cgi/src/main.rs +++ b/htmlua-apache-cgi/src/main.rs @@ -1,6 +1,7 @@ -use htmlua_parser::serve::serve_content; use std::env; +use htmlua_parser::serve::serve_content; + fn main() { println!("Content-Type: text/html\n"); diff --git a/htmlua-parser/src/config.rs b/htmlua-parser/src/config.rs index 70ea7a8..ffe1320 100644 --- a/htmlua-parser/src/config.rs +++ b/htmlua-parser/src/config.rs @@ -1,6 +1,7 @@ +use std::{fs, path::PathBuf}; + use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; -use std::{fs, path::PathBuf}; #[derive(Debug, Deserialize, Serialize, Clone)] pub struct Config { diff --git a/htmlua-parser/src/lib.rs b/htmlua-parser/src/lib.rs index 2dc831c..d2d5599 100644 --- a/htmlua-parser/src/lib.rs +++ b/htmlua-parser/src/lib.rs @@ -1,5 +1,5 @@ +pub mod config; pub mod helpers; pub mod htmlua_stdlib; -pub mod config; pub mod render; pub mod serve; diff --git a/htmlua-parser/src/render.rs b/htmlua-parser/src/render.rs index cc56eda..7046db8 100644 --- a/htmlua-parser/src/render.rs +++ b/htmlua-parser/src/render.rs @@ -5,7 +5,7 @@ use std::{ rc::Rc, }; -use anyhow::{anyhow, Result}; +use anyhow::{Result, anyhow}; use kuchikiki::{NodeRef, traits::TendrilSink}; use markup5ever::{LocalName, Namespace, QualName}; use mlua::{Lua, Table}; @@ -182,19 +182,27 @@ pub fn process_syntax_highlighting(document: NodeRef) -> Result { } pub fn generate_footnotes(document: NodeRef) -> Result { - let Ok(footnote_container) = document.select_first("footnotecontainer") else { return Ok(document) }; + let Ok(footnote_container) = document.select_first("footnotecontainer") else { + return Ok(document); + }; let ctx_name = QualName::new(None, Namespace::from("http://www.w3.org/1999/xhtml"), LocalName::from("div")); - for (i, footnote) in document.select("footnote").map_err(|()| anyhow!("Failed to get footnote"))?.enumerate() { + for (i, footnote) in document + .select("footnote") + .map_err(|()| anyhow!("Failed to get footnote"))? + .enumerate() + { let i = i + 1; let fn_text = footnote.text_contents(); - let sup_tag = kuchikiki::parse_fragment(ctx_name.clone(), Vec::new()).one( - format!("{i}") - ).select_first("a").map_err(|()| anyhow!("parse err"))?; + let sup_tag = kuchikiki::parse_fragment(ctx_name.clone(), Vec::new()) + .one(format!("{i}")) + .select_first("a") + .map_err(|()| anyhow!("parse err"))?; footnote.as_node().insert_after(sup_tag.as_node().clone()); - let text_tag = kuchikiki::parse_fragment(ctx_name.clone(), Vec::new()).one( - format!("

{i}: {fn_text}

") - ).select_first("p").map_err(|()| anyhow!("parse err"))?; + let text_tag = kuchikiki::parse_fragment(ctx_name.clone(), Vec::new()) + .one(format!("

{i}: {fn_text}

")) + .select_first("p") + .map_err(|()| anyhow!("parse err"))?; footnote_container.as_node().insert_before(text_tag.as_node().clone()); } while let Ok(i) = document.select_first("footnote") { diff --git a/htmlua-parser/src/serve.rs b/htmlua-parser/src/serve.rs index 9821210..715bfcc 100644 --- a/htmlua-parser/src/serve.rs +++ b/htmlua-parser/src/serve.rs @@ -1,10 +1,12 @@ +use std::{path::Path, sync::OnceLock}; + +use anyhow::Result; + use crate::{ config::Config, helpers::read_doc_from_file, render::{execute_lua, expand_template, process_markdown, process_syntax_highlighting}, }; -use anyhow::Result; -use std::{path::Path, sync::OnceLock}; static CONFIG: OnceLock = OnceLock::new(); From a07d893e6e137cc3641312e95b191a018c8643db Mon Sep 17 00:00:00 2001 From: gyoder <70408179+gyoder@users.noreply.github.com> Date: Mon, 21 Jul 2025 12:26:51 -0600 Subject: [PATCH 23/25] fixed parser bug --- cspell.json | 4 +++- htmlua-parser/src/helpers.rs | 5 ++-- htmlua-parser/src/render.rs | 23 ++++++++++++++++++- .../tests/components/include_base.html | 9 ++++++++ 4 files changed, 37 insertions(+), 4 deletions(-) create mode 100644 htmlua-parser/tests/components/include_base.html diff --git a/cspell.json b/cspell.json index e8c260e..44ee262 100644 --- a/cspell.json +++ b/cspell.json @@ -2,7 +2,9 @@ "words": [ "htmlua", "headermap", - "reqwest" + "reqwest", + "includeelement", + "exportelement" ], "userWords": [ "mlua" diff --git a/htmlua-parser/src/helpers.rs b/htmlua-parser/src/helpers.rs index bd99f81..3db0443 100644 --- a/htmlua-parser/src/helpers.rs +++ b/htmlua-parser/src/helpers.rs @@ -6,9 +6,10 @@ use std::{ use anyhow::Result; use kuchikiki::NodeRef; -use markup5ever::{LocalName, Namespace, QualName}; +use markup5ever::{ns, LocalName, Namespace, namespace_url, QualName}; use tendril::TendrilSink; + pub fn read_doc_from_file(path: PathBuf) -> Result { let mut file = File::open(path)?; let mut reader = BufReader::new(&mut file); @@ -26,7 +27,7 @@ pub fn read_doc_from_file(path: PathBuf) -> Result { if is_whole_doc { Ok(kuchikiki::parse_html().one(page_string)) } else { - let ctx_name = QualName::new(None, Namespace::from("http://www.w3.org/1999/xhtml"), LocalName::from("div")); + let ctx_name = QualName::new(None, ns!(), LocalName::from("div")); Ok(kuchikiki::parse_fragment(ctx_name, Vec::new()).one(page_string)) } } diff --git a/htmlua-parser/src/render.rs b/htmlua-parser/src/render.rs index 7046db8..e141020 100644 --- a/htmlua-parser/src/render.rs +++ b/htmlua-parser/src/render.rs @@ -215,6 +215,7 @@ pub fn generate_footnotes(document: NodeRef) -> Result { #[cfg(test)] mod tests { use httptest::{Expectation, ServerPool, matchers::*, responders::*}; + use markup5ever::{ns, namespace_url}; use super::*; @@ -346,7 +347,6 @@ mod tests { let document = kuchikiki::parse_html().one(page); let d = execute_lua(expand_template(document, &p, None).unwrap()).unwrap(); let text = d.select_first("span").unwrap().as_node().text_contents(); - println!("{}", d.to_string()); assert_eq!(text, "Test from Lua!\n"); assert!(d.select_first("lua").is_err()); let text = d.select_first("#inc1").unwrap().as_node().text_contents(); @@ -404,6 +404,27 @@ mod tests { assert!(d.select_first("includeelement").is_err()); } + #[test] + fn include_base() { + let page = r#" + + title_test + body_el + + "#; + let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + p.push("tests/components"); + let ctx_name = QualName::new(None, ns!(html), LocalName::from("div")); + let document = kuchikiki::parse_fragment(ctx_name, Vec::new()).one(page); + let d = expand_template(document, &p, None).unwrap(); + let text = d.select_first("head").unwrap().as_node().select_first("title").unwrap().as_node().text_contents(); + assert_eq!(text, "title_test"); + let text = d.select_first("#tb").unwrap().as_node().text_contents(); + assert_eq!(text, "body_el"); + assert!(d.select_first("exportelement").is_err()); + assert!(d.select_first("includeelement").is_err()); + } + #[test] fn footnotes() { let page = r" diff --git a/htmlua-parser/tests/components/include_base.html b/htmlua-parser/tests/components/include_base.html new file mode 100644 index 0000000..fa86fe6 --- /dev/null +++ b/htmlua-parser/tests/components/include_base.html @@ -0,0 +1,9 @@ + + + + + +

Hello World

+ + + From bfa5d30ae4cd56804093fefd0d91f5f18116d0bc Mon Sep 17 00:00:00 2001 From: gyoder <70408179+gyoder@users.noreply.github.com> Date: Mon, 21 Jul 2025 12:35:51 -0600 Subject: [PATCH 24/25] fixed parser bug --- htmlua-parser/src/config.rs | 1 + htmlua-parser/src/helpers.rs | 2 +- htmlua-parser/src/render.rs | 13 ++++++++++--- htmlua-parser/src/serve.rs | 10 ++++++---- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/htmlua-parser/src/config.rs b/htmlua-parser/src/config.rs index ffe1320..de734ed 100644 --- a/htmlua-parser/src/config.rs +++ b/htmlua-parser/src/config.rs @@ -95,5 +95,6 @@ impl Config { Ok(()) } + #[must_use] pub fn config_file_path() -> PathBuf { Self::get_config_path() } } diff --git a/htmlua-parser/src/helpers.rs b/htmlua-parser/src/helpers.rs index 3db0443..2992f70 100644 --- a/htmlua-parser/src/helpers.rs +++ b/htmlua-parser/src/helpers.rs @@ -6,7 +6,7 @@ use std::{ use anyhow::Result; use kuchikiki::NodeRef; -use markup5ever::{ns, LocalName, Namespace, namespace_url, QualName}; +use markup5ever::{LocalName, QualName, namespace_url, ns}; use tendril::TendrilSink; diff --git a/htmlua-parser/src/render.rs b/htmlua-parser/src/render.rs index e141020..6a70315 100644 --- a/htmlua-parser/src/render.rs +++ b/htmlua-parser/src/render.rs @@ -8,7 +8,7 @@ use std::{ use anyhow::{Result, anyhow}; use kuchikiki::{NodeRef, traits::TendrilSink}; use markup5ever::{LocalName, Namespace, QualName}; -use mlua::{Lua, Table}; +use mlua::Lua; use pulldown_cmark::{Options, Parser, html}; use syntect::{ easy::HighlightLines, @@ -215,7 +215,7 @@ pub fn generate_footnotes(document: NodeRef) -> Result { #[cfg(test)] mod tests { use httptest::{Expectation, ServerPool, matchers::*, responders::*}; - use markup5ever::{ns, namespace_url}; + use markup5ever::{namespace_url, ns}; use super::*; @@ -417,7 +417,14 @@ mod tests { let ctx_name = QualName::new(None, ns!(html), LocalName::from("div")); let document = kuchikiki::parse_fragment(ctx_name, Vec::new()).one(page); let d = expand_template(document, &p, None).unwrap(); - let text = d.select_first("head").unwrap().as_node().select_first("title").unwrap().as_node().text_contents(); + let text = d + .select_first("head") + .unwrap() + .as_node() + .select_first("title") + .unwrap() + .as_node() + .text_contents(); assert_eq!(text, "title_test"); let text = d.select_first("#tb").unwrap().as_node().text_contents(); assert_eq!(text, "body_el"); diff --git a/htmlua-parser/src/serve.rs b/htmlua-parser/src/serve.rs index 715bfcc..375e885 100644 --- a/htmlua-parser/src/serve.rs +++ b/htmlua-parser/src/serve.rs @@ -1,6 +1,6 @@ use std::{path::Path, sync::OnceLock}; -use anyhow::Result; +use anyhow::{Result, anyhow}; use crate::{ config::Config, @@ -13,7 +13,7 @@ static CONFIG: OnceLock = OnceLock::new(); pub fn get_config() -> &'static Config { CONFIG.get_or_init(|| { Config::load().unwrap_or_else(|e| { - eprintln!("Warning: Failed to load config: {}", e); + eprintln!("Warning: Failed to load config: {e}"); eprintln!("Using default configuration"); Config::default() }) @@ -24,8 +24,10 @@ pub fn serve_content(request_uri: &str) -> Result { let config = get_config(); let safe_path = Path::new(request_uri).strip_prefix("/")?; let page_path = config.paths.pages.join(safe_path); - let doc = read_doc_from_file(page_path)?; - let full_doc = expand_template(doc, &config.paths.components, None)?; + let doc = read_doc_from_file(page_path)? + .select_first("html") + .map_err(|()| anyhow!("failed to read doc"))?; + let full_doc = expand_template(doc.as_node().to_owned(), &config.paths.components, None)?; let markdown_doc = process_markdown(full_doc)?; let highlighted_doc = process_syntax_highlighting(markdown_doc)?; let executed_doc = execute_lua(highlighted_doc)?; From 02132764068aa51ac98628d988dff15255308df0 Mon Sep 17 00:00:00 2001 From: Grace Yoder Date: Fri, 17 Jul 2026 19:54:15 -0600 Subject: [PATCH 25/25] feat: zig rewrite --- .crow/build.yml | 63 ++++++++++++++++ .crow/release.yml | 23 ++++++ .gitignore | 19 +++++ LICENSE | 19 +++++ README.md | 9 +++ build.zig | 112 ++++++++++++++++++++++++++++ build.zig.zon | 24 ++++++ examples/a.htmlua | 3 + examples/config.zon | 4 + examples/main.htmlua | 21 ++++++ src/cgi.zig | 79 ++++++++++++++++++++ src/main.zig | 59 +++++++++++++++ src/root.zig | 76 +++++++++++++++++++ src/tags.zig | 4 + src/tags/Export.zig | 52 +++++++++++++ src/tags/Include.zig | 58 +++++++++++++++ src/tags/Syntax.zig | 169 +++++++++++++++++++++++++++++++++++++++++++ src/tags/Use.zig | 48 ++++++++++++ src/utils.zig | 23 ++++++ 19 files changed, 865 insertions(+) create mode 100644 .crow/build.yml create mode 100644 .crow/release.yml create mode 100755 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100755 build.zig create mode 100755 build.zig.zon create mode 100755 examples/a.htmlua create mode 100755 examples/config.zon create mode 100755 examples/main.htmlua create mode 100755 src/cgi.zig create mode 100755 src/main.zig create mode 100755 src/root.zig create mode 100755 src/tags.zig create mode 100755 src/tags/Export.zig create mode 100755 src/tags/Include.zig create mode 100755 src/tags/Syntax.zig create mode 100755 src/tags/Use.zig create mode 100755 src/utils.zig diff --git a/.crow/build.yml b/.crow/build.yml new file mode 100644 index 0000000..9163a4d --- /dev/null +++ b/.crow/build.yml @@ -0,0 +1,63 @@ +when: + - event: tag + +clone: + git: + image: codefloe.com/crow-plugins/clone:1.1.0 + settings: + tags: true + +variables: + - &zig_image 'ghcr.io/rust-cross/cargo-zigbuild:0.23' + +steps: + - name: build-linux-x86_64 + image: *zig_image + commands: + # - zig build -Dtarget=x86_64-linux-musl -Doptimize=ReleaseSafe --prefix zig-out-linux-x86_64 + - zig build -Dtarget=x86_64-linux-musl --prefix zig-out-linux-x86_64 + - cp zig-out-linux-x86_64/bin/htmlua htmlua-linux-x86_64 + - cp zig-out-linux-x86_64/bin/htmlua-cgi htmlua-cgi-linux-x86_64 + + - name: build-linux-aarch64 + image: *zig_image + commands: + # - zig build -Dtarget=aarch64-linux-musl -Doptimize=ReleaseSafe --prefix zig-out-linux-aarch64 + - zig build -Dtarget=aarch64-linux-musl --prefix zig-out-linux-aarch64 + - cp zig-out-linux-aarch64/bin/htmlua htmlua-linux-aarch64 + - cp zig-out-linux-aarch64/bin/htmlua-cgi htmlua-cgi-linux-aarch64 + + - name: build-macos-x86_64 + image: *zig_image + commands: + # - zig build -Dtarget=x86_64-macos -Doptimize=ReleaseSafe --prefix zig-out-macos-x86_64 --sysroot /opt/MacOSX11.3.sdk + - zig build -Dtarget=x86_64-macos --prefix zig-out-macos-x86_64 --sysroot /opt/MacOSX11.3.sdk + - cp zig-out-macos-x86_64/bin/htmlua htmlua-macos-x86_64 + - cp zig-out-macos-x86_64/bin/htmlua-cgi htmlua-cgi-macos-x86_64 + + - name: build-macos-aarch64 + image: *zig_image + commands: + # - zig build -Dtarget=aarch64-macos -Doptimize=ReleaseSafe --prefix zig-out-macos-aarch64 --sysroot /opt/MacOSX11.3.sdk + - zig build -Dtarget=aarch64-macos --prefix zig-out-macos-aarch64 --sysroot /opt/MacOSX11.3.sdk + - cp zig-out-macos-aarch64/bin/htmlua htmlua-macos-aarch64 + - cp zig-out-macos-aarch64/bin/htmlua-cgi htmlua-cgi-macos-aarch64 + + + - name: release + image: woodpeckerci/plugin-release + depends_on: [build-linux-x86_64, build-linux-aarch64, build-macos-x86_64, build-macos-aarch64] + settings: + api_key: + from_secret: forgejo_token + files: + - htmlua-linux-x86_64 + - htmlua-linux-aarch64 + - htmlua-macos-x86_64 + - htmlua-macos-aarch64 + - htmlua-cgi-linux-x86_64 + - htmlua-cgi-linux-aarch64 + - htmlua-cgi-macos-x86_64 + - htmlua-cgi-macos-aarch64 + checksum: sha256 + file_exists: overwrite diff --git a/.crow/release.yml b/.crow/release.yml new file mode 100644 index 0000000..3770f56 --- /dev/null +++ b/.crow/release.yml @@ -0,0 +1,23 @@ +# This file is a bit of a hack in order to run the real release CI +when: + - event: push + branch: ${CI_REPO_DEFAULT_BRANCH} + +clone: + git: + image: codefloe.com/crow-plugins/clone:1.1.0 + settings: + tags: true + + +steps: + - name: retag-latest + image: alpine:3.20 + environment: + FORGEJO_TOKEN: + from_secret: forgejo_token + commands: + - apk add --no-cache git + - git remote set-url origin "https://oauth2:$${FORGEJO_TOKEN}@$${CI_FORGE_URL#https://}/$${CI_REPO}.git" + - git tag -f latest + - git push -f origin latest diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..8493d22 --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +# This file is for zig-specific build artifacts. +# If you have OS-specific or editor-specific files to ignore, +# such as *.swp or .DS_Store, put those in your global +# ~/.gitignore and put this in your ~/.gitconfig: +# +# [core] +# excludesfile = ~/.gitignore +# +# Cheers! +# -andrewrk + +.zig-cache/ +zig-out/ +zig-pkg/ +/release/ +/debug/ +/build/ +/build-*/ +/docgen_tmp/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5c5954d --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright 2026 Grace Yoder + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the “Software”), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..9232026 --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# HTMLua + +### An HTML templating engine with (not yet reimplemented) Lua scripting abilities + +See in action at [grace.pink](https://grace.pink/index.htmlua) + +This is currently an incomplete rewrite. If you want to have the old rust +version you are able to checkout the `rust` tag. + diff --git a/build.zig b/build.zig new file mode 100755 index 0000000..42d6c20 --- /dev/null +++ b/build.zig @@ -0,0 +1,112 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const ezts = b.dependency("eazy_zig_tree_sitter", .{ + .target = target, + .optimize = optimize, + .use_all_langs = true, + .use_third_party = true, + }); + + const ts = b.dependency("tree_sitter", .{ + .target = target, + .optimize = optimize, + }); + + const mod = b.addModule("htmlua", .{ + .root_source_file = b.path("src/root.zig"), + .target = target, + .imports = &.{ + .{ .name = "easy-zig-tree-sitter", .module = ezts.module("eazy_zig_tree_sitter") }, + .{ .name = "tree-sitter", .module = ts.module("tree_sitter") }, + }, + }); + + // Debug bin + const exe = b.addExecutable(.{ + .name = "htmlua", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + .imports = &.{ + .{ .name = "htmlua", .module = mod }, + }, + }), + }); + + b.installArtifact(exe); + + const run_step = b.step("run", "Run the app"); + + const run_cmd = b.addRunArtifact(exe); + run_step.dependOn(&run_cmd.step); + + run_cmd.step.dependOn(b.getInstallStep()); + + if (b.args) |args| { + run_cmd.addArgs(args); + } + + // CGI bin + const cgi = b.addExecutable(.{ + .name = "htmlua-cgi", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/cgi.zig"), + .target = target, + .optimize = optimize, + .imports = &.{ + .{ .name = "htmlua", .module = mod }, + }, + }), + }); + + b.installArtifact(cgi); + + const mod_tests = b.addTest(.{ + .root_module = mod, + }); + + const run_mod_tests = b.addRunArtifact(mod_tests); + + const exe_tests = b.addTest(.{ + .root_module = exe.root_module, + }); + + const run_exe_tests = b.addRunArtifact(exe_tests); + + const test_step = b.step("test", "Run tests"); + test_step.dependOn(&run_mod_tests.step); + test_step.dependOn(&run_exe_tests.step); + + // https://zigtools.org/zls/guides/build-on-save/ + const exe_check = b.addExecutable(.{ + .name = "htmlua", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + .imports = &.{ + .{ .name = "htmlua", .module = mod }, + }, + }), + }); + const cgi_check = b.addExecutable(.{ + .name = "htmlua-cgi", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/cgi.zig"), + .target = target, + .optimize = optimize, + .imports = &.{ + .{ .name = "htmlua", .module = mod }, + }, + }), + }); + + const check = b.step("check", "Check if htmlua compiles"); + check.dependOn(&exe_check.step); + check.dependOn(&cgi_check.step); +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100755 index 0000000..297ddc1 --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,24 @@ +.{ + .name = .htmlua, + .version = "1.0.0-alpha", + .fingerprint = 0xb94bab7ca04adb0c, + .minimum_zig_version = "0.16.0", + .dependencies = .{ + .tree_sitter = .{ + .url = "git+https://github.com/tree-sitter/zig-tree-sitter.git#b562510b5563dcf3b7fcac4a5fe9d60834c84667", + .hash = "tree_sitter-0.26.0-8heIf-vDAQDh0HhAAUVwVZFmLrwSKMZl5gRaVFqtas0W", + }, + .eazy_zig_tree_sitter = .{ + .url = "git+https://git.gae.moe/grace/eazy-zig-tree-sitter.git#8d1fa80a0e225282f607a1e2b2504cfdbf427cfa", + .hash = "eazy_zig_tree_sitter-0.0.0-QEABqToEAwCwlPVt4G7QS6E6vicz4SISg8N93xRr8t8K", + }, + }, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + "examples", + "LICENSE", + "README.md", + }, +} diff --git a/examples/a.htmlua b/examples/a.htmlua new file mode 100755 index 0000000..4857b50 --- /dev/null +++ b/examples/a.htmlua @@ -0,0 +1,3 @@ +

included file!

+ + diff --git a/examples/config.zon b/examples/config.zon new file mode 100755 index 0000000..2335fbe --- /dev/null +++ b/examples/config.zon @@ -0,0 +1,4 @@ +.{ + .component_path = "./examples", + .template_path = "./examples", +} diff --git a/examples/main.htmlua b/examples/main.htmlua new file mode 100755 index 0000000..d7827c2 --- /dev/null +++ b/examples/main.htmlua @@ -0,0 +1,21 @@ + + + + + main + + + +

hello

+ + +

exported from main!

+
+ +

exported from main but again!

+
+
+ + + + diff --git a/src/cgi.zig b/src/cgi.zig new file mode 100755 index 0000000..849ef8c --- /dev/null +++ b/src/cgi.zig @@ -0,0 +1,79 @@ +const std = @import("std"); +const Io = std.Io; +const File = Io.File; +const Allocator = std.mem.Allocator; +const ArenaAllocator = std.heap.ArenaAllocator; +const ArrayList = std.ArrayList; +const StringArrayHashMap = std.StringArrayHashMapUnmanaged; + +const HTMLua = @import("htmlua"); + +const Config = struct { + component_path: []const u8, + template_path: []const u8, +}; + +var component_dir: Io.Dir = undefined; + +pub fn main(init: std.process.Init) !void { + const io = init.io; + + var arena = std.heap.ArenaAllocator.init(init.gpa); + defer arena.deinit(); + const alloc = arena.allocator(); + + var args = init.minimal.args.iterate(); + + const cwd = std.Io.Dir.cwd(); + + if (!args.skip()) unreachable; + const config_file_path = args.next().?; + const config_buf = try cwd.readFileAllocOptions(io, config_file_path, alloc, .unlimited, std.mem.Alignment.@"1", '\x00'); + const config = try std.zon.parse.fromSliceAlloc( + Config, + alloc, + config_buf, + null, + .{}, + ); + + component_dir = try cwd.openDir(io, config.component_path, .{}); + const template_dir = try cwd.openDir(io, config.template_path, .{}); + + var state: HTMLua.Parser = .{ + .tags = &[_]HTMLua.ParserTag{ + HTMLua.tags.Include, + HTMLua.tags.Export, + HTMLua.tags.Use, + HTMLua.tags.Syntax, + }, + .tag_stack = try ArrayList([]const u8).initCapacity(alloc, 256), + .exported_parts = try StringArrayHashMap([]const u8).init(alloc, &[0][]const u8{}, &[0][]const u8{}), + .component_resolver = struct { + pub fn resolve(arena_inner: *ArenaAllocator, io_inner: Io, path: []const u8) anyerror!*Io.Reader { + const read_buff = try arena_inner.allocator().alloc(u8, 16384); + const clean_path = try HTMLua.utils.sanitizePath(arena_inner.allocator(), path); + var file = try component_dir.openFile(io_inner, clean_path, .{ .mode = .read_only }); + const reader = try arena_inner.allocator().create(File.Reader); + reader.* = file.reader(io_inner, read_buff); + return &reader.interface; + } + }.resolve, + }; + + const request_path = init.environ_map.get("PATH_INFO").?; + const clean_request_path = try HTMLua.utils.sanitizePath(alloc, request_path); + const file = try template_dir.openFile(io, clean_request_path, .{ .mode = .read_only }); + defer file.close(io); + + const in_buf = try alloc.alloc(u8, 16384); + var in = file.reader(io, in_buf); + + const out_buf = try alloc.alloc(u8, 16384); + var out = File.stdout().writer(io, out_buf); + + try out.interface.writeAll("Content-type: text/html\n"); + try HTMLua.parseTemplate(&arena, io, &state, &in.interface, &out.interface); + + try out.flush(); +} diff --git a/src/main.zig b/src/main.zig new file mode 100755 index 0000000..18b8872 --- /dev/null +++ b/src/main.zig @@ -0,0 +1,59 @@ +const std = @import("std"); +const Io = std.Io; +const File = Io.File; +const Allocator = std.mem.Allocator; +const ArenaAllocator = std.heap.ArenaAllocator; +const ArrayList = std.ArrayList; +const StringArrayHashMap = std.StringArrayHashMapUnmanaged; + +const HTMLua = @import("root.zig"); + +pub fn main(init: std.process.Init) !void { + const io = init.io; + + var arena = std.heap.ArenaAllocator.init(init.gpa); + defer arena.deinit(); + const alloc = arena.allocator(); + + var state: HTMLua.Parser = .{ + .component_resolver = &resolver, + .tags = &[_]HTMLua.ParserTag{ + HTMLua.tags.Include, + HTMLua.tags.Export, + HTMLua.tags.Use, + }, + .tag_stack = try ArrayList([]const u8).initCapacity(alloc, 256), + .exported_parts = try StringArrayHashMap([]const u8).init(alloc, &[0][]const u8{}, &[0][]const u8{}), + }; + + // I dont like this but this is temporary so its ok + const args = try init.minimal.args.toSlice(alloc); + const file_path = if (args.len != 1) args[1] else null; + const file = if (file_path) |fp| + try Io.Dir.cwd().openFile(io, fp, .{ .mode = .read_only }) + else + null; + defer if (file) |f| f.close(io); + + const in_buf = try alloc.alloc(u8, 1024); + var in = + if (file) |f| f.reader(io, in_buf) else File.stdin().reader(io, in_buf); + + const out_buf = try alloc.alloc(u8, 1024); + var out = File.stdout().writer(io, out_buf); + + try HTMLua.parseTemplate(&arena, io, &state, &in.interface, &out.interface); + + try out.flush(); +} + +// https://ziggit.dev/t/zig-0-15-1-reader-writer-dont-make-copies-of-fieldparentptr-based-interfaces/11719 +fn resolver(arena: *ArenaAllocator, io: Io, path: []const u8) anyerror!*Io.Reader { + const read_buff = try arena.allocator().alloc(u8, 1024); + const cwd = std.Io.Dir.cwd(); + + var file = try cwd.openFile(io, path, .{ .mode = .read_only }); + const reader = try arena.allocator().create(Io.File.Reader); + reader.* = file.reader(io, read_buff); + return &reader.interface; +} diff --git a/src/root.zig b/src/root.zig new file mode 100755 index 0000000..3ddfe78 --- /dev/null +++ b/src/root.zig @@ -0,0 +1,76 @@ +const std = @import("std"); +const Io = std.Io; +const Allocator = std.mem.Allocator; +const ArenaAllocator = std.heap.ArenaAllocator; +const SplitIterator = std.mem.SplitIterator; +const DelimiterType = std.mem.DelimiterType; +const ArrayList = std.ArrayList; +const StringArrayHashMap = std.StringArrayHashMapUnmanaged; + +pub const tags = @import("tags.zig"); +pub const utils = @import("utils.zig"); + +pub const ParserFn = + fn ( + arena: *ArenaAllocator, + io: Io, + state: *Parser, + reader: *Io.Reader, + writer: *Io.Writer, + attrs: *SplitIterator(u8, DelimiterType.scalar), + ) anyerror!void; + +pub const ParserTag = struct { + name: []const u8, + parse_fn: *const ParserFn, +}; + +pub const Parser = struct { + component_resolver: *const fn (alloc: *ArenaAllocator, io: Io, path: []const u8) anyerror!*Io.Reader, + exported_parts: StringArrayHashMap([]const u8), + tags: []const ParserTag, + tag_stack: ArrayList([]const u8), +}; + +pub fn parseTemplate(arena: *ArenaAllocator, io: Io, state: *Parser, reader: *Io.Reader, writer: *Io.Writer) !void { + outer: while (true) { + _ = reader.streamDelimiter(writer, '<') catch |err| { + if (err == Io.Reader.Error.EndOfStream) break else return err; + }; + // consume '<' + _ = try reader.takeByte(); + if (try reader.peekByte() == '/') { + // consume '/' + _ = try reader.takeByte(); + const last_tag = state.tag_stack.getLastOrNull() orelse ""; + // if reached closing tag, send execution back up the stack + if (std.mem.eql(u8, last_tag, try reader.peekDelimiterExclusive('>'))) { + // consume closing tag + _ = try reader.takeDelimiterInclusive('>'); + return; + } else { + try writer.writeAll("') catch |err| { + if (err == error.StreamTooLong) { + try writer.writeByte('<'); + break :outer; + } else return err; + }; + var it = std.mem.splitScalar(u8, inside, ' '); + const tag_name = it.first(); + for (state.tags) |tag| { + if (std.mem.eql(u8, tag.name, tag_name) == true) { + // consume '>' + _ = try reader.takeByte(); + try tag.parse_fn(arena, io, state, reader, writer, &it); + continue :outer; + } + } + + try writer.writeByte('<'); + try writer.writeAll(inside); + } +} diff --git a/src/tags.zig b/src/tags.zig new file mode 100755 index 0000000..3f4169e --- /dev/null +++ b/src/tags.zig @@ -0,0 +1,4 @@ +pub const Include = @import("tags/Include.zig").Include; +pub const Export = @import("tags/Export.zig").Export; +pub const Use = @import("tags/Use.zig").Use; +pub const Syntax = @import("tags/Syntax.zig").Syntax; diff --git a/src/tags/Export.zig b/src/tags/Export.zig new file mode 100755 index 0000000..4b64048 --- /dev/null +++ b/src/tags/Export.zig @@ -0,0 +1,52 @@ +const std = @import("std"); +const Io = std.Io; +const Allocator = std.mem.Allocator; +const ArenaAllocator = std.heap.ArenaAllocator; +const SplitIterator = std.mem.SplitIterator; +const DelimiterType = std.mem.DelimiterType; + +const parser = @import("../root.zig"); +const Parser = parser.Parser; + +pub const ExportTagError = error{ + NoPartAttribute, +}; + +pub const Export: parser.ParserTag = .{ + .name = "export", + .parse_fn = &parse, +}; + +fn parse( + arena: *ArenaAllocator, + io: Io, + state: *Parser, + reader: *Io.Reader, + writer: *Io.Writer, + attrs: *SplitIterator(u8, DelimiterType.scalar), +) anyerror!void { + _ = writer; + + const raw_part_attr: []const u8 = while (attrs.next()) |attr| { + var it = std.mem.splitScalar(u8, attr, '='); + if (std.mem.eql(u8, it.first(), "part")) { + break it.next() orelse return ExportTagError.NoPartAttribute; + } + } else return ExportTagError.NoPartAttribute; + + const part = try arena.allocator().dupe(u8, raw_part_attr); + + var part_buffer = try Io.Writer.Allocating.initCapacity(arena.allocator(), 1024); + + try state.tag_stack.append(arena.allocator(), "export"); + try parser.parseTemplate( + arena, + io, + state, + reader, + &part_buffer.writer, + ); + _ = state.tag_stack.pop(); + + try state.exported_parts.put(arena.allocator(), part, try part_buffer.toOwnedSlice()); +} diff --git a/src/tags/Include.zig b/src/tags/Include.zig new file mode 100755 index 0000000..87f0b3f --- /dev/null +++ b/src/tags/Include.zig @@ -0,0 +1,58 @@ +const std = @import("std"); +const Io = std.Io; +const Allocator = std.mem.Allocator; +const ArenaAllocator = std.heap.ArenaAllocator; +const SplitIterator = std.mem.SplitIterator; +const DelimiterType = std.mem.DelimiterType; + +const parser = @import("../root.zig"); +const Parser = parser.Parser; + +pub const IncludeTagError = error{ + NoPathAttribute, + PathOpenFailure, +}; + +pub const Include: parser.ParserTag = .{ + .name = "include", + .parse_fn = &parse, +}; + +fn parse( + arena: *ArenaAllocator, + io: Io, + state: *Parser, + reader: *Io.Reader, + writer: *Io.Writer, + attrs: *SplitIterator(u8, DelimiterType.scalar), +) anyerror!void { + const raw_path_attr: []const u8 = while (attrs.next()) |attr| { + var it = std.mem.splitScalar(u8, attr, '='); + if (std.mem.eql(u8, it.first(), "path")) { + const raw_path = it.next() orelse return IncludeTagError.NoPathAttribute; + if (raw_path.len == 0) return IncludeTagError.NoPathAttribute; + if ((raw_path[0] == '"' and raw_path[raw_path.len - 1] == '"') or + (raw_path[0] == '\'' and raw_path[raw_path.len - 1] == '\'')) + { + if (raw_path.len <= 2) return IncludeTagError.NoPathAttribute; + break raw_path[1 .. raw_path.len - 1]; + } else { + break raw_path; + } + } + } else return IncludeTagError.NoPathAttribute; + + const path = try arena.allocator().dupe(u8, raw_path_attr); + + // parse through to gather tags + try state.tag_stack.append(arena.allocator(), "include"); + try parser.parseTemplate(arena, io, state, reader, writer); + _ = state.tag_stack.pop(); + + const included_reader = state.component_resolver(arena, io, path) catch { + std.log.err("Include Tag: failed to open file {s}", .{path}); + return IncludeTagError.PathOpenFailure; + }; + + try parser.parseTemplate(arena, io, state, included_reader, writer); +} diff --git a/src/tags/Syntax.zig b/src/tags/Syntax.zig new file mode 100755 index 0000000..03930b1 --- /dev/null +++ b/src/tags/Syntax.zig @@ -0,0 +1,169 @@ +const std = @import("std"); +const Io = std.Io; +const Allocator = std.mem.Allocator; +const ArenaAllocator = std.heap.ArenaAllocator; +const SplitIterator = std.mem.SplitIterator; +const DelimiterType = std.mem.DelimiterType; + +const ezts = @import("easy-zig-tree-sitter"); +const ts = @import("tree-sitter"); + +const parser = @import("../root.zig"); +const Parser = parser.Parser; + +pub const SyntaxTagError = error{InvalidLanguage}; +pub const Syntax: parser.ParserTag = .{ + .name = "syntax", + .parse_fn = &parse, +}; + +fn parse( + arena: *ArenaAllocator, + io: Io, + state: *Parser, + reader: *Io.Reader, + writer: *Io.Writer, + attrs: *SplitIterator(u8, DelimiterType.scalar), +) anyerror!void { + _ = state; + _ = io; + + const raw_lang_attr: []const u8 = while (attrs.next()) |attr| { + var it = std.mem.splitScalar(u8, attr, '='); + if (std.mem.eql(u8, it.first(), "lang")) { + break it.next() orelse ""; + } + } else ""; + + const lang_key = try arena.allocator().dupe(u8, raw_lang_attr); + + _ = try reader.takeDelimiterInclusive('\n'); + const first_line = try reader.peekDelimiterInclusive('\n'); + const whitespace_chr = for (first_line, 0..) |chr, idx| { + if (!(chr == ' ' or chr == '\t')) break idx; + } else 0; + + var code = try std.ArrayList(u8).initCapacity(arena.allocator(), 1024); + + while (true) { + const line = try reader.takeDelimiterInclusive('\n'); + if (std.mem.indexOf(u8, line, "")) |idx| { + try code.appendSlice(arena.allocator(), line[0..idx]); + // TODO: tags at the end will break the parser + break; + } + const whitespace_lem = @min(line.len, whitespace_chr); + + if (std.mem.countScalar(u8, line[0..whitespace_lem], ' ') + std.mem.countScalar(u8, line[0..whitespace_lem], '\t') < whitespace_lem) { + try code.appendSlice(arena.allocator(), line); + } else { + try code.appendSlice(arena.allocator(), line[whitespace_lem..line.len]); + } + } + + // Alignment issues? + // ts.setAllocator(arena.allocator()); + // TODO: TS Thread Mutex + + // TODO: null handeling + var hlp = HLParser.create(lang_key, code.items) orelse { + std.log.warn("Failed to make Parser for {s}", .{lang_key}); + return; + }; + defer hlp.destroy(); + var next_hl = hlp.nextHL(); + + var end_stack = try std.ArrayList(u32).initCapacity(arena.allocator(), 1024); + var code_idx: u32 = 0; + + try writer.writeAll("
");
+
+    while (code_idx != code.items.len) {
+        var next_idx: u32 = @intCast(code.items.len);
+        if (end_stack.getLastOrNull()) |last| {
+            if (last == code_idx) {
+                try writer.writeAll("");
+                _ = end_stack.pop();
+                continue;
+            } else if (last < next_idx) {
+                next_idx = last;
+            }
+        }
+        if (next_hl) |hl| {
+            if (hl.start == code_idx) {
+                try writer.writeAll("");
+                try end_stack.append(arena.allocator(), hl.end);
+                next_hl = hlp.nextHL();
+                continue;
+            } else if (hl.start < next_idx) {
+                next_idx = hl.start;
+            }
+        }
+        try writer.writeAll(code.items[code_idx..next_idx]);
+        code_idx = next_idx;
+    }
+
+    try writer.writeAll("
"); +} + +const HLGroup = struct { + name: []const u8, + start: u32, + end: u32, +}; + +const HLParser = struct { + lang: *ts.Language, + tree: *ts.Tree, + query: *ts.Query, + cursor: *ts.QueryCursor, + fn create(lang_key: []const u8, code: []const u8) ?HLParser { + const lang = ezts.getLang(lang_key) orelse return null; + const query_string = ezts.getQuery(lang_key, "highlights") orelse return null; + + const ps = ts.Parser.create(); + + ps.setLanguage(lang) catch return null; + const tree = ps.parseString(code, null) orelse return null; + + var error_offset: u32 = 0; + const query = ts.Query.create(lang, query_string, &error_offset) catch return null; + + const cursor = ts.QueryCursor.create(); + cursor.exec(query, tree.rootNode()); + + return .{ + .lang = lang, + .tree = tree, + .query = query, + .cursor = cursor, + }; + } + + fn destroy(self: *HLParser) void { + self.cursor.destroy(); + self.query.destroy(); + self.tree.destroy(); + self.lang.destroy(); + } + + fn nextHL(self: *HLParser) ?HLGroup { + if (self.cursor.nextCapture()) |data| { + const idx, const match = data; + const c = match.captures[idx]; + + return .{ + .start = c.node.startByte(), + .end = c.node.endByte(), + .name = self.query.captureNameForId(c.index) orelse return null, + }; + } else return null; + } +}; diff --git a/src/tags/Use.zig b/src/tags/Use.zig new file mode 100755 index 0000000..6ba1b1b --- /dev/null +++ b/src/tags/Use.zig @@ -0,0 +1,48 @@ +const std = @import("std"); +const Io = std.Io; +const Allocator = std.mem.Allocator; +const ArenaAllocator = std.heap.ArenaAllocator; +const SplitIterator = std.mem.SplitIterator; +const DelimiterType = std.mem.DelimiterType; + +const parser = @import("../root.zig"); +const Parser = parser.Parser; + +pub const UseTagError = error{ NoPartAttribute, NoPartExported }; + +pub const Use: parser.ParserTag = .{ + .name = "use", + .parse_fn = &parse, +}; + +fn parse( + arena: *ArenaAllocator, + io: Io, + state: *Parser, + reader: *Io.Reader, + writer: *Io.Writer, + attrs: *SplitIterator(u8, DelimiterType.scalar), +) anyerror!void { + _ = io; + + const raw_part_attr: []const u8 = while (attrs.next()) |attr| { + var it = std.mem.splitScalar(u8, attr, '='); + if (std.mem.eql(u8, it.first(), "part")) { + break it.next() orelse return UseTagError.NoPartAttribute; + } + } else return UseTagError.NoPartAttribute; + + const part = try arena.allocator().dupe(u8, raw_part_attr); + + const part_text = state.exported_parts.get(part) orelse return UseTagError.NoPartExported; + + var part_reader = Io.Reader.fixed(part_text); + + _ = try part_reader.streamRemaining(writer); + + while (true) { + _ = try reader.discardDelimiterInclusive('<'); + const inside = try reader.takeDelimiterInclusive('>'); + if (std.mem.eql(u8, "/use>", inside)) return; + } +} diff --git a/src/utils.zig b/src/utils.zig new file mode 100755 index 0000000..ae03d76 --- /dev/null +++ b/src/utils.zig @@ -0,0 +1,23 @@ +const std = @import("std"); +const Io = std.Io; +const File = Io.File; +const Allocator = std.mem.Allocator; +const ArenaAllocator = std.heap.ArenaAllocator; +const ArrayList = std.ArrayList; +const StringArrayHashMap = std.StringArrayHashMapUnmanaged; + +pub fn sanitizePath(alloc: Allocator, path: []const u8) ![]const u8 { + const clean = try alloc.alloc(u8, path.len); + var list = ArrayList(u8).initBuffer(clean); + var pit = std.mem.splitScalar(u8, path, '/'); + while (pit.next()) |part| { + if (part.len == 0) continue; + if (std.mem.eql(u8, part, "..")) continue; + // This should never actually allocate + try list.appendSlice(alloc, part); + try list.append(alloc, '/'); + } + const new_len = list.items.len - 1; + _ = alloc.resize(clean, new_len); + return clean[0..new_len]; +}