page rewrite
Some checks failed
CI / Check typos (push) Successful in 1m3s
CI / Check links (push) Failing after 1m14s
CI / Clippy (push) Successful in 1m43s
CI / Build and test (push) Successful in 1m31s
CI / Build container (push) Successful in 1m45s
CI / Deploy on waypoint (push) Failing after 1m23s
Some checks failed
CI / Check typos (push) Successful in 1m3s
CI / Check links (push) Failing after 1m14s
CI / Clippy (push) Successful in 1m43s
CI / Build and test (push) Successful in 1m31s
CI / Build container (push) Successful in 1m45s
CI / Deploy on waypoint (push) Failing after 1m23s
This commit is contained in:
374
Cargo.lock
generated
374
Cargo.lock
generated
@@ -394,6 +394,12 @@ version = "1.24.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4"
|
checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "byteorder"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "byteorder-lite"
|
name = "byteorder-lite"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -693,6 +699,29 @@ dependencies = [
|
|||||||
"typenum",
|
"typenum",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cssparser"
|
||||||
|
version = "0.35.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4e901edd733a1472f944a45116df3f846f54d37e67e68640ac8bb69689aca2aa"
|
||||||
|
dependencies = [
|
||||||
|
"cssparser-macros",
|
||||||
|
"dtoa-short",
|
||||||
|
"itoa",
|
||||||
|
"phf 0.11.3",
|
||||||
|
"smallvec",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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.108",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "deranged"
|
name = "deranged"
|
||||||
version = "0.5.5"
|
version = "0.5.5"
|
||||||
@@ -737,6 +766,26 @@ dependencies = [
|
|||||||
"syn 2.0.108",
|
"syn 2.0.108",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "derive_more"
|
||||||
|
version = "2.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678"
|
||||||
|
dependencies = [
|
||||||
|
"derive_more-impl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "derive_more-impl"
|
||||||
|
version = "2.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.108",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "diff"
|
name = "diff"
|
||||||
version = "0.1.13"
|
version = "0.1.13"
|
||||||
@@ -812,6 +861,27 @@ version = "1.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
|
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
|
||||||
|
|
||||||
|
[[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 = "ego-tree"
|
||||||
|
version = "0.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b2972feb8dffe7bc8c5463b1dacda1b0dfbed3710e50f977d965429692d74cd8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "either"
|
name = "either"
|
||||||
version = "1.15.0"
|
version = "1.15.0"
|
||||||
@@ -883,6 +953,16 @@ version = "1.0.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "errno"
|
||||||
|
version = "0.3.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "exr"
|
name = "exr"
|
||||||
version = "1.73.0"
|
version = "1.73.0"
|
||||||
@@ -909,6 +989,12 @@ dependencies = [
|
|||||||
"regex-syntax",
|
"regex-syntax",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fastrand"
|
||||||
|
version = "2.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fax"
|
name = "fax"
|
||||||
version = "0.2.6"
|
version = "0.2.6"
|
||||||
@@ -961,12 +1047,6 @@ version = "1.0.7"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "foldhash"
|
|
||||||
version = "0.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "form_urlencoded"
|
name = "form_urlencoded"
|
||||||
version = "1.2.2"
|
version = "1.2.2"
|
||||||
@@ -986,6 +1066,31 @@ dependencies = [
|
|||||||
"syn 2.0.108",
|
"syn 2.0.108",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[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 = "futures"
|
||||||
|
version = "0.3.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
|
||||||
|
dependencies = [
|
||||||
|
"futures-channel",
|
||||||
|
"futures-core",
|
||||||
|
"futures-executor",
|
||||||
|
"futures-io",
|
||||||
|
"futures-sink",
|
||||||
|
"futures-task",
|
||||||
|
"futures-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-channel"
|
name = "futures-channel"
|
||||||
version = "0.3.31"
|
version = "0.3.31"
|
||||||
@@ -1002,6 +1107,17 @@ version = "0.3.31"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-executor"
|
||||||
|
version = "0.3.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"futures-task",
|
||||||
|
"futures-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-io"
|
name = "futures-io"
|
||||||
version = "0.3.31"
|
version = "0.3.31"
|
||||||
@@ -1037,6 +1153,7 @@ version = "0.3.31"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-io",
|
"futures-io",
|
||||||
"futures-macro",
|
"futures-macro",
|
||||||
@@ -1048,6 +1165,15 @@ dependencies = [
|
|||||||
"slab",
|
"slab",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fxhash"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "generic-array"
|
name = "generic-array"
|
||||||
version = "0.14.9"
|
version = "0.14.9"
|
||||||
@@ -1058,6 +1184,15 @@ dependencies = [
|
|||||||
"version_check",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getopts"
|
||||||
|
version = "0.2.24"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-width 0.2.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "getrandom"
|
name = "getrandom"
|
||||||
version = "0.2.16"
|
version = "0.2.16"
|
||||||
@@ -1171,11 +1306,6 @@ name = "hashbrown"
|
|||||||
version = "0.16.0"
|
version = "0.16.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
|
checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
|
||||||
dependencies = [
|
|
||||||
"allocator-api2",
|
|
||||||
"equivalent",
|
|
||||||
"foldhash",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "heck"
|
name = "heck"
|
||||||
@@ -1212,6 +1342,17 @@ dependencies = [
|
|||||||
"utf8-width",
|
"utf8-width",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "html5ever"
|
||||||
|
version = "0.35.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "55d958c2f74b664487a2035fe1dadb032c48718a03b63f3ab0b8537db8549ed4"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"markup5ever",
|
||||||
|
"match_token",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http"
|
name = "http"
|
||||||
version = "1.3.1"
|
version = "1.3.1"
|
||||||
@@ -1681,6 +1822,12 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "linux-raw-sys"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "litemap"
|
name = "litemap"
|
||||||
version = "0.8.1"
|
version = "0.8.1"
|
||||||
@@ -1727,21 +1874,18 @@ dependencies = [
|
|||||||
"imgref",
|
"imgref",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "lru"
|
|
||||||
version = "0.16.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "96051b46fc183dc9cd4a223960ef37b9af631b55191852a8274bfef064cda20f"
|
|
||||||
dependencies = [
|
|
||||||
"hashbrown 0.16.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lru-slab"
|
name = "lru-slab"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mac"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "macro-sass"
|
name = "macro-sass"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
@@ -1760,7 +1904,7 @@ dependencies = [
|
|||||||
"argparse",
|
"argparse",
|
||||||
"const_format",
|
"const_format",
|
||||||
"derivative",
|
"derivative",
|
||||||
"derive_more",
|
"derive_more 0.99.20",
|
||||||
"downcast-rs",
|
"downcast-rs",
|
||||||
"entities",
|
"entities",
|
||||||
"html-escape",
|
"html-escape",
|
||||||
@@ -1774,6 +1918,28 @@ dependencies = [
|
|||||||
"unicode-general-category",
|
"unicode-general-category",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[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.108",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "matchers"
|
name = "matchers"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -2085,6 +2251,26 @@ version = "1.70.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ormc-scrape"
|
||||||
|
version = "0.0.1"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"axum",
|
||||||
|
"base64",
|
||||||
|
"clap",
|
||||||
|
"futures",
|
||||||
|
"reqwest",
|
||||||
|
"scraper",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tempfile",
|
||||||
|
"tokio",
|
||||||
|
"toolbox",
|
||||||
|
"tracing",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "owo-colors"
|
name = "owo-colors"
|
||||||
version = "4.2.3"
|
version = "4.2.3"
|
||||||
@@ -2097,15 +2283,13 @@ version = "0.0.1"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
"chrono",
|
"chrono",
|
||||||
"libservice",
|
|
||||||
"lru",
|
|
||||||
"maud",
|
"maud",
|
||||||
"parking_lot",
|
|
||||||
"pixel-transform",
|
"pixel-transform",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_urlencoded",
|
||||||
"tokio",
|
"tokio",
|
||||||
"toolbox",
|
"toolbox",
|
||||||
"tower-http",
|
"tower",
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2163,6 +2347,16 @@ dependencies = [
|
|||||||
"phf_shared 0.13.1",
|
"phf_shared 0.13.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf_codegen"
|
||||||
|
version = "0.11.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a"
|
||||||
|
dependencies = [
|
||||||
|
"phf_generator",
|
||||||
|
"phf_shared 0.11.3",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "phf_generator"
|
name = "phf_generator"
|
||||||
version = "0.11.3"
|
version = "0.11.3"
|
||||||
@@ -2283,6 +2477,12 @@ dependencies = [
|
|||||||
"zerocopy",
|
"zerocopy",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "precomputed-hash"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pretty_assertions"
|
name = "pretty_assertions"
|
||||||
version = "1.4.1"
|
version = "1.4.1"
|
||||||
@@ -2829,6 +3029,19 @@ dependencies = [
|
|||||||
"semver",
|
"semver",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustix"
|
||||||
|
version = "1.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"errno",
|
||||||
|
"libc",
|
||||||
|
"linux-raw-sys",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls"
|
name = "rustls"
|
||||||
version = "0.23.34"
|
version = "0.23.34"
|
||||||
@@ -2897,6 +3110,40 @@ version = "1.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scraper"
|
||||||
|
version = "0.24.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e5f3a24d916e78954af99281a455168d4a9515d65eca99a18da1b813689c4ad9"
|
||||||
|
dependencies = [
|
||||||
|
"cssparser",
|
||||||
|
"ego-tree",
|
||||||
|
"getopts",
|
||||||
|
"html5ever",
|
||||||
|
"precomputed-hash",
|
||||||
|
"selectors",
|
||||||
|
"tendril",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "selectors"
|
||||||
|
version = "0.31.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5685b6ae43bfcf7d2e7dfcfb5d8e8f61b46442c902531e41a32a9a8bf0ee0fb6"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"cssparser",
|
||||||
|
"derive_more 2.0.1",
|
||||||
|
"fxhash",
|
||||||
|
"log",
|
||||||
|
"new_debug_unreachable",
|
||||||
|
"phf 0.11.3",
|
||||||
|
"phf_codegen",
|
||||||
|
"precomputed-hash",
|
||||||
|
"servo_arc",
|
||||||
|
"smallvec",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "semver"
|
name = "semver"
|
||||||
version = "1.0.27"
|
version = "1.0.27"
|
||||||
@@ -3012,9 +3259,19 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
"toml 0.9.8",
|
"toml 0.9.8",
|
||||||
"toolbox",
|
"toolbox",
|
||||||
|
"tower-http",
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "servo_arc"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930"
|
||||||
|
dependencies = [
|
||||||
|
"stable_deref_trait",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha2"
|
name = "sha2"
|
||||||
version = "0.10.9"
|
version = "0.10.9"
|
||||||
@@ -3130,6 +3387,31 @@ dependencies = [
|
|||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[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",
|
||||||
|
"phf_shared 0.11.3",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strsim"
|
name = "strsim"
|
||||||
version = "0.11.1"
|
version = "0.11.1"
|
||||||
@@ -3329,6 +3611,30 @@ version = "0.12.16"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
|
checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tempfile"
|
||||||
|
version = "3.23.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16"
|
||||||
|
dependencies = [
|
||||||
|
"fastrand",
|
||||||
|
"getrandom 0.3.4",
|
||||||
|
"once_cell",
|
||||||
|
"rustix",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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]]
|
[[package]]
|
||||||
name = "term"
|
name = "term"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
@@ -3914,6 +4220,12 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utf-8"
|
||||||
|
version = "0.7.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "utf8-width"
|
name = "utf8-width"
|
||||||
version = "0.1.7"
|
version = "0.1.7"
|
||||||
@@ -4134,6 +4446,18 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[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",
|
||||||
|
"string_cache",
|
||||||
|
"string_cache_codegen",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webpage"
|
name = "webpage"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
|
|||||||
@@ -80,6 +80,8 @@ service-webpage = { path = "crates/service/service-webpage" }
|
|||||||
#
|
#
|
||||||
axum = { version = "0.8.6", features = ["macros", "multipart"] }
|
axum = { version = "0.8.6", features = ["macros", "multipart"] }
|
||||||
tower-http = { version = "0.6.6", features = ["trace", "compression-full"] }
|
tower-http = { version = "0.6.6", features = ["trace", "compression-full"] }
|
||||||
|
tower = { version = "0.5.2" }
|
||||||
|
serde_urlencoded = { version = "0.7.1" }
|
||||||
utoipa = "5.4.0"
|
utoipa = "5.4.0"
|
||||||
utoipa-swagger-ui = { version = "9.0.2", features = [
|
utoipa-swagger-ui = { version = "9.0.2", features = [
|
||||||
"axum",
|
"axum",
|
||||||
@@ -93,6 +95,7 @@ emojis = "0.8.0"
|
|||||||
reqwest = { version = "0.12.24", default-features = false, features = [
|
reqwest = { version = "0.12.24", default-features = false, features = [
|
||||||
"http2",
|
"http2",
|
||||||
"rustls-tls",
|
"rustls-tls",
|
||||||
|
"rustls-tls-webpki-roots", # Need to recompile to update
|
||||||
"cookies",
|
"cookies",
|
||||||
"gzip",
|
"gzip",
|
||||||
"stream",
|
"stream",
|
||||||
@@ -144,6 +147,9 @@ lru = "0.16.2"
|
|||||||
parking_lot = "0.12.5"
|
parking_lot = "0.12.5"
|
||||||
lazy_static = "1.5.0"
|
lazy_static = "1.5.0"
|
||||||
image = "0.25.8"
|
image = "0.25.8"
|
||||||
|
scraper = "0.24.0"
|
||||||
|
futures = "0.3.31"
|
||||||
|
tempfile = "3.23.0"
|
||||||
|
|
||||||
# md_* test utilities
|
# md_* test utilities
|
||||||
prettydiff = "0.9.0"
|
prettydiff = "0.9.0"
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ workspace = true
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
toolbox = { workspace = true }
|
toolbox = { workspace = true }
|
||||||
libservice = { workspace = true }
|
|
||||||
pixel-transform = { workspace = true }
|
pixel-transform = { workspace = true }
|
||||||
|
|
||||||
axum = { workspace = true }
|
axum = { workspace = true }
|
||||||
@@ -17,7 +16,6 @@ tokio = { workspace = true }
|
|||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
maud = { workspace = true }
|
maud = { workspace = true }
|
||||||
chrono = { workspace = true }
|
chrono = { workspace = true }
|
||||||
parking_lot = { workspace = true }
|
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
lru = { workspace = true }
|
tower = { workspace = true }
|
||||||
tower-http = { workspace = true }
|
serde_urlencoded = { workspace = true }
|
||||||
|
|||||||
1
crates/lib/page/htmx/htmx-2.0.8.min.js
vendored
Normal file
1
crates/lib/page/htmx/htmx-2.0.8.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
11
crates/lib/page/htmx/json-enc-1.9.12.js
Normal file
11
crates/lib/page/htmx/json-enc-1.9.12.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
htmx.defineExtension('json-enc', {
|
||||||
|
onEvent: function (name, evt) {
|
||||||
|
if (name === "htmx:configRequest") {
|
||||||
|
evt.detail.headers['Content-Type'] = "application/json";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
encodeParameters: function (xhr, parameters, elt) {
|
||||||
|
xhr.overrideMimeType('text/json');
|
||||||
|
return (JSON.stringify(parameters));
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -1,8 +1,25 @@
|
|||||||
mod servable;
|
//! A web stack for embedded uis.
|
||||||
pub use servable::*;
|
//!
|
||||||
|
//! Featuring:
|
||||||
|
//! - htmx
|
||||||
|
//! - axum
|
||||||
|
//! - rust
|
||||||
|
//! - and maud
|
||||||
|
|
||||||
mod requestcontext;
|
pub mod servable;
|
||||||
pub use requestcontext::*;
|
|
||||||
|
|
||||||
mod server;
|
mod types;
|
||||||
pub use server::*;
|
pub use types::*;
|
||||||
|
|
||||||
|
mod route;
|
||||||
|
pub use route::*;
|
||||||
|
|
||||||
|
pub const HTMX_2_0_8: servable::StaticAsset = servable::StaticAsset {
|
||||||
|
bytes: include_str!("../htmx/htmx-2.0.8.min.js").as_bytes(),
|
||||||
|
mime: toolbox::mime::MimeType::Javascript,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const EXT_JSON_1_19_12: servable::StaticAsset = servable::StaticAsset {
|
||||||
|
bytes: include_str!("../htmx/json-enc-1.9.12.js").as_bytes(),
|
||||||
|
mime: toolbox::mime::MimeType::Javascript,
|
||||||
|
};
|
||||||
|
|||||||
275
crates/lib/page/src/route.rs
Normal file
275
crates/lib/page/src/route.rs
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
use axum::{
|
||||||
|
Router,
|
||||||
|
body::Body,
|
||||||
|
http::{HeaderMap, HeaderValue, Method, Request, StatusCode, header},
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
};
|
||||||
|
use chrono::TimeDelta;
|
||||||
|
use std::{
|
||||||
|
collections::{BTreeMap, HashMap},
|
||||||
|
convert::Infallible,
|
||||||
|
net::SocketAddr,
|
||||||
|
pin::Pin,
|
||||||
|
sync::Arc,
|
||||||
|
task::{Context, Poll},
|
||||||
|
time::Instant,
|
||||||
|
};
|
||||||
|
use toolbox::mime::MimeType;
|
||||||
|
use tower::Service;
|
||||||
|
use tracing::trace;
|
||||||
|
|
||||||
|
use crate::{ClientInfo, RenderContext, Rendered, RenderedBody, servable::Servable};
|
||||||
|
|
||||||
|
struct Default404 {}
|
||||||
|
impl Servable for Default404 {
|
||||||
|
fn head<'a>(
|
||||||
|
&'a self,
|
||||||
|
_ctx: &'a RenderContext,
|
||||||
|
) -> Pin<Box<dyn Future<Output = Rendered<()>> + 'a + Send + Sync>> {
|
||||||
|
Box::pin(async {
|
||||||
|
return Rendered {
|
||||||
|
code: StatusCode::NOT_FOUND,
|
||||||
|
body: (),
|
||||||
|
ttl: Some(TimeDelta::days(1)),
|
||||||
|
immutable: true,
|
||||||
|
headers: HeaderMap::new(),
|
||||||
|
mime: Some(MimeType::Html),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render<'a>(
|
||||||
|
&'a self,
|
||||||
|
ctx: &'a RenderContext,
|
||||||
|
) -> Pin<Box<dyn Future<Output = Rendered<RenderedBody>> + 'a + Send + Sync>> {
|
||||||
|
Box::pin(async { self.head(ctx).await.with_body(RenderedBody::Empty) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A set of related [Servable]s under one route.
|
||||||
|
///
|
||||||
|
/// Use as follows:
|
||||||
|
/// ```ignore
|
||||||
|
///
|
||||||
|
/// // Add compression, for example.
|
||||||
|
/// // Also consider CORS and timeout.
|
||||||
|
/// let compression: CompressionLayer = CompressionLayer::new()
|
||||||
|
/// .br(true)
|
||||||
|
/// .deflate(true)
|
||||||
|
/// .gzip(true)
|
||||||
|
/// .zstd(true)
|
||||||
|
/// .compress_when(DefaultPredicate::new());
|
||||||
|
///
|
||||||
|
/// let route = ServableRoute::new()
|
||||||
|
/// .add_page(
|
||||||
|
/// "/page",
|
||||||
|
/// StaticAsset {
|
||||||
|
/// bytes: "I am a page".as_bytes(),
|
||||||
|
/// mime: MimeType::Text,
|
||||||
|
/// },
|
||||||
|
/// );
|
||||||
|
///
|
||||||
|
/// Router::new()
|
||||||
|
/// .nest_service("/", route)
|
||||||
|
/// .layer(compression.clone());
|
||||||
|
/// ```
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ServableRoute {
|
||||||
|
pages: Arc<HashMap<String, Arc<dyn Servable>>>,
|
||||||
|
notfound: Arc<dyn Servable>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ServableRoute {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
pages: Arc::new(HashMap::new()),
|
||||||
|
notfound: Arc::new(Default404 {}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set this server's "not found" page
|
||||||
|
pub fn with_404<S: Servable + 'static>(mut self, page: S) -> Self {
|
||||||
|
self.notfound = Arc::new(page);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a page to this server at the given route.
|
||||||
|
/// - panics if route does not start with a `/`, ends with a `/`, or contains `//`.
|
||||||
|
/// - urls are normalized, routes that violate this condition will never be served.
|
||||||
|
/// - `/` is an exception, it is valid.
|
||||||
|
/// - panics if called after this service is started
|
||||||
|
/// - overwrites existing pages
|
||||||
|
pub fn add_page<S: Servable + 'static>(mut self, route: impl Into<String>, page: S) -> Self {
|
||||||
|
let route = route.into();
|
||||||
|
|
||||||
|
if !route.starts_with("/") {
|
||||||
|
panic!("route must start with /")
|
||||||
|
};
|
||||||
|
|
||||||
|
if route.ends_with("/") && route != "/" {
|
||||||
|
panic!("route must not end with /")
|
||||||
|
};
|
||||||
|
|
||||||
|
if route.contains("//") {
|
||||||
|
panic!("route must not contain //")
|
||||||
|
};
|
||||||
|
|
||||||
|
#[expect(clippy::expect_used)]
|
||||||
|
Arc::get_mut(&mut self.pages)
|
||||||
|
.expect("add_pages called after service was started")
|
||||||
|
.insert(route, Arc::new(page));
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convenience method.
|
||||||
|
/// Turns this service into a router.
|
||||||
|
///
|
||||||
|
/// Equivalent to:
|
||||||
|
/// ```ignore
|
||||||
|
/// Router::new().fallback_service(self)
|
||||||
|
/// ```
|
||||||
|
pub fn into_router<T: Clone + Send + Sync + 'static>(self) -> Router<T> {
|
||||||
|
Router::new().fallback_service(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// MARK: impl Service
|
||||||
|
//
|
||||||
|
|
||||||
|
impl Service<Request<Body>> for ServableRoute {
|
||||||
|
type Response = Response;
|
||||||
|
type Error = Infallible;
|
||||||
|
type Future =
|
||||||
|
Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send + 'static>>;
|
||||||
|
|
||||||
|
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||||
|
Poll::Ready(Ok(()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn call(&mut self, req: Request<Body>) -> Self::Future {
|
||||||
|
if req.method() != Method::GET && req.method() != Method::HEAD {
|
||||||
|
let mut headers = HeaderMap::with_capacity(1);
|
||||||
|
headers.insert(header::ACCEPT, HeaderValue::from_static("GET,HEAD"));
|
||||||
|
return Box::pin(async {
|
||||||
|
Ok((StatusCode::METHOD_NOT_ALLOWED, headers).into_response())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let pages = self.pages.clone();
|
||||||
|
let notfound = self.notfound.clone();
|
||||||
|
Box::pin(async move {
|
||||||
|
let addr = req.extensions().get::<SocketAddr>().copied();
|
||||||
|
let route = req.uri().path().to_owned();
|
||||||
|
let headers = req.headers().clone();
|
||||||
|
let query: BTreeMap<String, String> =
|
||||||
|
serde_urlencoded::from_str(req.uri().query().unwrap_or("")).unwrap_or_default();
|
||||||
|
|
||||||
|
let start = Instant::now();
|
||||||
|
let client_info = ClientInfo::from_headers(&headers);
|
||||||
|
let ua = headers
|
||||||
|
.get("user-agent")
|
||||||
|
.and_then(|x| x.to_str().ok())
|
||||||
|
.unwrap_or("");
|
||||||
|
|
||||||
|
trace!(
|
||||||
|
message = "Serving route",
|
||||||
|
route,
|
||||||
|
addr = ?addr,
|
||||||
|
user_agent = ua,
|
||||||
|
device_type = ?client_info.device_type
|
||||||
|
);
|
||||||
|
|
||||||
|
// Normalize url with redirect
|
||||||
|
if (route.ends_with('/') && route != "/") || route.contains("//") {
|
||||||
|
let mut new_route = route.clone();
|
||||||
|
while new_route.contains("//") {
|
||||||
|
new_route = new_route.replace("//", "/");
|
||||||
|
}
|
||||||
|
let new_route = new_route.trim_matches('/');
|
||||||
|
|
||||||
|
trace!(
|
||||||
|
message = "Redirecting",
|
||||||
|
route,
|
||||||
|
new_route,
|
||||||
|
addr = ?addr,
|
||||||
|
user_agent = ua,
|
||||||
|
device_type = ?client_info.device_type
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut headers = HeaderMap::with_capacity(1);
|
||||||
|
match HeaderValue::from_str(&format!("/{new_route}")) {
|
||||||
|
Ok(x) => headers.append(header::LOCATION, x),
|
||||||
|
Err(_) => return Ok(StatusCode::BAD_REQUEST.into_response()),
|
||||||
|
};
|
||||||
|
return Ok((StatusCode::PERMANENT_REDIRECT, headers).into_response());
|
||||||
|
}
|
||||||
|
|
||||||
|
let ctx = RenderContext {
|
||||||
|
client_info,
|
||||||
|
route,
|
||||||
|
query,
|
||||||
|
};
|
||||||
|
|
||||||
|
let page = pages.get(&ctx.route).unwrap_or(¬found);
|
||||||
|
let mut rend = match req.method() == Method::HEAD {
|
||||||
|
true => page.head(&ctx).await.with_body(RenderedBody::Empty),
|
||||||
|
false => page.render(&ctx).await,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Tweak headers
|
||||||
|
{
|
||||||
|
if !rend.headers.contains_key(header::CACHE_CONTROL) {
|
||||||
|
let max_age = rend.ttl.map(|x| x.num_seconds()).unwrap_or(1).max(1);
|
||||||
|
|
||||||
|
let mut value = String::new();
|
||||||
|
if rend.immutable {
|
||||||
|
value.push_str("immutable, ");
|
||||||
|
}
|
||||||
|
|
||||||
|
value.push_str("public, ");
|
||||||
|
value.push_str(&format!("max-age={}, ", max_age));
|
||||||
|
|
||||||
|
#[expect(clippy::unwrap_used)]
|
||||||
|
rend.headers.insert(
|
||||||
|
header::CACHE_CONTROL,
|
||||||
|
HeaderValue::from_str(value.trim().trim_end_matches(',')).unwrap(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !rend.headers.contains_key("Accept-CH") {
|
||||||
|
rend.headers
|
||||||
|
.insert("Accept-CH", HeaderValue::from_static("Sec-CH-UA-Mobile"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if !rend.headers.contains_key(header::CONTENT_TYPE)
|
||||||
|
&& let Some(mime) = &rend.mime
|
||||||
|
{
|
||||||
|
#[expect(clippy::unwrap_used)]
|
||||||
|
rend.headers.insert(
|
||||||
|
header::CONTENT_TYPE,
|
||||||
|
HeaderValue::from_str(&mime.to_string()).unwrap(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trace!(
|
||||||
|
message = "Served route",
|
||||||
|
route = ctx.route,
|
||||||
|
addr = ?addr,
|
||||||
|
user_agent = ua,
|
||||||
|
device_type = ?client_info.device_type,
|
||||||
|
time_ns = start.elapsed().as_nanos()
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(match rend.body {
|
||||||
|
RenderedBody::Markup(m) => (rend.code, rend.headers, m.0).into_response(),
|
||||||
|
RenderedBody::Static(d) => (rend.code, rend.headers, d).into_response(),
|
||||||
|
RenderedBody::Bytes(d) => (rend.code, rend.headers, d).into_response(),
|
||||||
|
RenderedBody::String(s) => (rend.code, rend.headers, s).into_response(),
|
||||||
|
RenderedBody::Empty => (rend.code, rend.headers).into_response(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ use std::{pin::Pin, str::FromStr};
|
|||||||
use toolbox::mime::MimeType;
|
use toolbox::mime::MimeType;
|
||||||
use tracing::{error, trace};
|
use tracing::{error, trace};
|
||||||
|
|
||||||
use crate::{Rendered, RenderedBody, RequestContext, Servable};
|
use crate::{RenderContext, Rendered, RenderedBody, servable::Servable};
|
||||||
|
|
||||||
pub struct StaticAsset {
|
pub struct StaticAsset {
|
||||||
pub bytes: &'static [u8],
|
pub bytes: &'static [u8],
|
||||||
@@ -13,10 +13,69 @@ pub struct StaticAsset {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Servable for StaticAsset {
|
impl Servable for StaticAsset {
|
||||||
|
fn head<'a>(
|
||||||
|
&'a self,
|
||||||
|
ctx: &'a RenderContext,
|
||||||
|
) -> Pin<Box<dyn Future<Output = Rendered<()>> + 'a + Send + Sync>> {
|
||||||
|
Box::pin(async {
|
||||||
|
let ttl = Some(TimeDelta::days(30));
|
||||||
|
let is_image = TransformerChain::mime_is_image(&self.mime);
|
||||||
|
|
||||||
|
let transform = match (is_image, ctx.query.get("t")) {
|
||||||
|
(false, _) | (_, None) => None,
|
||||||
|
|
||||||
|
(true, Some(x)) => match TransformerChain::from_str(x) {
|
||||||
|
Ok(x) => Some(x),
|
||||||
|
Err(_err) => {
|
||||||
|
return Rendered {
|
||||||
|
code: StatusCode::BAD_REQUEST,
|
||||||
|
body: (),
|
||||||
|
ttl,
|
||||||
|
immutable: true,
|
||||||
|
|
||||||
|
headers: HeaderMap::new(),
|
||||||
|
mime: None,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
match transform {
|
||||||
|
Some(transform) => {
|
||||||
|
return Rendered {
|
||||||
|
code: StatusCode::OK,
|
||||||
|
body: (),
|
||||||
|
ttl,
|
||||||
|
immutable: true,
|
||||||
|
|
||||||
|
headers: HeaderMap::new(),
|
||||||
|
mime: Some(
|
||||||
|
transform
|
||||||
|
.output_mime(&self.mime)
|
||||||
|
.unwrap_or(self.mime.clone()),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
None => {
|
||||||
|
return Rendered {
|
||||||
|
code: StatusCode::OK,
|
||||||
|
body: (),
|
||||||
|
ttl,
|
||||||
|
immutable: true,
|
||||||
|
|
||||||
|
headers: HeaderMap::new(),
|
||||||
|
mime: Some(self.mime.clone()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn render<'a>(
|
fn render<'a>(
|
||||||
&'a self,
|
&'a self,
|
||||||
ctx: &'a RequestContext,
|
ctx: &'a RenderContext,
|
||||||
) -> Pin<Box<dyn Future<Output = crate::Rendered> + 'a + Send + Sync>> {
|
) -> Pin<Box<dyn Future<Output = Rendered<RenderedBody>> + 'a + Send + Sync>> {
|
||||||
Box::pin(async {
|
Box::pin(async {
|
||||||
let ttl = Some(TimeDelta::days(30));
|
let ttl = Some(TimeDelta::days(30));
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,26 @@
|
|||||||
pub mod asset;
|
mod asset;
|
||||||
pub mod page;
|
pub use asset::*;
|
||||||
pub mod redirect;
|
|
||||||
|
mod page;
|
||||||
|
pub use page::*;
|
||||||
|
|
||||||
|
mod redirect;
|
||||||
|
pub use redirect::*;
|
||||||
|
|
||||||
|
/// Something that may be served over http.
|
||||||
|
pub trait Servable: Send + Sync {
|
||||||
|
/// Return the same response as [Servable::render], but with an empty body.
|
||||||
|
/// Used to respond to `HEAD` requests.
|
||||||
|
fn head<'a>(
|
||||||
|
&'a self,
|
||||||
|
ctx: &'a crate::RenderContext,
|
||||||
|
) -> std::pin::Pin<Box<dyn Future<Output = crate::Rendered<()>> + 'a + Send + Sync>>;
|
||||||
|
|
||||||
|
/// Render this page
|
||||||
|
fn render<'a>(
|
||||||
|
&'a self,
|
||||||
|
ctx: &'a crate::RenderContext,
|
||||||
|
) -> std::pin::Pin<
|
||||||
|
Box<dyn Future<Output = crate::Rendered<crate::RenderedBody>> + 'a + Send + Sync>,
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
use axum::http::{HeaderMap, StatusCode};
|
use axum::http::{HeaderMap, StatusCode};
|
||||||
use chrono::TimeDelta;
|
use chrono::TimeDelta;
|
||||||
use maud::{Markup, Render, html};
|
use maud::{DOCTYPE, Markup, PreEscaped, html};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::pin::Pin;
|
use std::{pin::Pin, sync::Arc};
|
||||||
use toolbox::mime::MimeType;
|
use toolbox::mime::MimeType;
|
||||||
|
|
||||||
use crate::{Rendered, RenderedBody, RequestContext, Servable};
|
use crate::{RenderContext, Rendered, RenderedBody, servable::Servable};
|
||||||
|
|
||||||
//
|
//
|
||||||
// MARK: metadata
|
// MARK: metadata
|
||||||
@@ -17,7 +17,6 @@ pub struct PageMetadata {
|
|||||||
pub author: Option<String>,
|
pub author: Option<String>,
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
pub image: Option<String>,
|
pub image: Option<String>,
|
||||||
pub backlinks: Option<bool>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for PageMetadata {
|
impl Default for PageMetadata {
|
||||||
@@ -27,42 +26,15 @@ impl Default for PageMetadata {
|
|||||||
author: None,
|
author: None,
|
||||||
description: None,
|
description: None,
|
||||||
image: None,
|
image: None,
|
||||||
backlinks: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render for PageMetadata {
|
|
||||||
fn render(&self) -> Markup {
|
|
||||||
let empty = String::new();
|
|
||||||
let title = &self.title;
|
|
||||||
let author = &self.author.as_ref().unwrap_or(&empty);
|
|
||||||
let description = &self.description.as_ref().unwrap_or(&empty);
|
|
||||||
let image = &self.image.as_ref().unwrap_or(&empty);
|
|
||||||
|
|
||||||
html !(
|
|
||||||
meta property="og:site_name" content=(title) {}
|
|
||||||
meta name="title" content=(title) {}
|
|
||||||
meta property="og:title" content=(title) {}
|
|
||||||
meta property="twitter:title" content=(title) {}
|
|
||||||
|
|
||||||
meta name="author" content=(author) {}
|
|
||||||
|
|
||||||
meta name="description" content=(description) {}
|
|
||||||
meta property="og:description" content=(description) {}
|
|
||||||
meta property="twitter:description" content=(description) {}
|
|
||||||
|
|
||||||
meta content=(image) property="og:image" {}
|
|
||||||
link rel="shortcut icon" href=(image) type="image/x-icon" {}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// MARK: page
|
// MARK: page
|
||||||
//
|
//
|
||||||
|
|
||||||
// Some HTML
|
#[derive(Clone)]
|
||||||
pub struct Page {
|
pub struct Page {
|
||||||
pub meta: PageMetadata,
|
pub meta: PageMetadata,
|
||||||
pub immutable: bool,
|
pub immutable: bool,
|
||||||
@@ -79,47 +51,101 @@ pub struct Page {
|
|||||||
/// or the contents of a wrapper element (defined in the page server struct).
|
/// or the contents of a wrapper element (defined in the page server struct).
|
||||||
///
|
///
|
||||||
/// This closure must never return `<html>` or `<head>`.
|
/// This closure must never return `<html>` or `<head>`.
|
||||||
pub generate_html: Box<
|
pub generate_html: Arc<
|
||||||
dyn Send
|
dyn Send
|
||||||
+ Sync
|
+ Sync
|
||||||
|
+ 'static
|
||||||
+ for<'a> Fn(
|
+ for<'a> Fn(
|
||||||
&'a Page,
|
&'a Page,
|
||||||
&'a RequestContext,
|
&'a RenderContext,
|
||||||
) -> Pin<Box<dyn Future<Output = Markup> + 'a + Send + Sync>>,
|
) -> Pin<Box<dyn Future<Output = Markup> + Send + Sync + 'a>>,
|
||||||
>,
|
>,
|
||||||
|
|
||||||
pub response_code: StatusCode,
|
pub response_code: StatusCode,
|
||||||
|
|
||||||
|
pub scripts_inline: Vec<String>,
|
||||||
|
pub scripts_linked: Vec<String>,
|
||||||
|
pub styles_linked: Vec<String>,
|
||||||
|
pub styles_inline: Vec<String>,
|
||||||
|
|
||||||
|
/// `name`, `content` for extra `<meta>` tags
|
||||||
|
pub extra_meta: Vec<(String, String)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Page {
|
impl Default for Page {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Page {
|
Page {
|
||||||
|
// No cache by default
|
||||||
|
html_ttl: None,
|
||||||
|
immutable: false,
|
||||||
|
|
||||||
meta: Default::default(),
|
meta: Default::default(),
|
||||||
html_ttl: Some(TimeDelta::days(1)),
|
generate_html: Arc::new(|_, _| Box::pin(async { html!() })),
|
||||||
generate_html: Box::new(|_, _| Box::pin(async { html!() })),
|
|
||||||
immutable: true,
|
|
||||||
response_code: StatusCode::OK,
|
response_code: StatusCode::OK,
|
||||||
|
scripts_inline: Vec::new(),
|
||||||
|
scripts_linked: Vec::new(),
|
||||||
|
styles_inline: Vec::new(),
|
||||||
|
styles_linked: Vec::new(),
|
||||||
|
extra_meta: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Page {
|
impl Page {
|
||||||
pub async fn generate_html(&self, ctx: &RequestContext) -> Markup {
|
pub async fn generate_html(&self, ctx: &RenderContext) -> Markup {
|
||||||
(self.generate_html)(self, ctx).await
|
(self.generate_html)(self, ctx).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn immutable(mut self, immutable: bool) -> Self {
|
||||||
|
self.immutable = immutable;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn html_ttl(mut self, html_ttl: Option<TimeDelta>) -> Self {
|
||||||
|
self.html_ttl = html_ttl;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn response_code(mut self, response_code: StatusCode) -> Self {
|
||||||
|
self.response_code = response_code;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_script_inline(mut self, script: impl Into<String>) -> Self {
|
||||||
|
self.scripts_inline.push(script.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_script_linked(mut self, url: impl Into<String>) -> Self {
|
||||||
|
self.scripts_linked.push(url.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_style_inline(mut self, style: impl Into<String>) -> Self {
|
||||||
|
self.styles_inline.push(style.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_style_linked(mut self, url: impl Into<String>) -> Self {
|
||||||
|
self.styles_linked.push(url.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_extra_meta(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
|
||||||
|
self.extra_meta.push((key.into(), value.into()));
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Servable for Page {
|
impl Servable for Page {
|
||||||
fn render<'a>(
|
fn head<'a>(
|
||||||
&'a self,
|
&'a self,
|
||||||
ctx: &'a RequestContext,
|
_ctx: &'a RenderContext,
|
||||||
) -> Pin<Box<dyn Future<Output = crate::Rendered> + 'a + Send + Sync>> {
|
) -> Pin<Box<dyn Future<Output = Rendered<()>> + 'a + Send + Sync>> {
|
||||||
Box::pin(async {
|
Box::pin(async {
|
||||||
let html = self.generate_html(ctx).await;
|
|
||||||
|
|
||||||
return Rendered {
|
return Rendered {
|
||||||
code: self.response_code,
|
code: self.response_code,
|
||||||
body: RenderedBody::Markup(html),
|
body: (),
|
||||||
ttl: self.html_ttl,
|
ttl: self.html_ttl,
|
||||||
immutable: self.immutable,
|
immutable: self.immutable,
|
||||||
headers: HeaderMap::new(),
|
headers: HeaderMap::new(),
|
||||||
@@ -127,4 +153,157 @@ impl Servable for Page {
|
|||||||
};
|
};
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'a>(
|
||||||
|
&'a self,
|
||||||
|
ctx: &'a RenderContext,
|
||||||
|
) -> Pin<Box<dyn Future<Output = Rendered<RenderedBody>> + 'a + Send + Sync>> {
|
||||||
|
Box::pin(async {
|
||||||
|
let inner_html = self.generate_html(ctx).await;
|
||||||
|
|
||||||
|
let html = html! {
|
||||||
|
(DOCTYPE)
|
||||||
|
html {
|
||||||
|
head {
|
||||||
|
meta charset="UTF-8";
|
||||||
|
meta name="viewport" content="width=device-width, initial-scale=1,user-scalable=no";
|
||||||
|
meta content="text/html; charset=UTF-8" http-equiv="content-type";
|
||||||
|
meta property="og:type" content="website";
|
||||||
|
@for (name, content) in &self.extra_meta {
|
||||||
|
meta name=(name) content=(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Metadata
|
||||||
|
//
|
||||||
|
title { (PreEscaped(self.meta.title.clone())) }
|
||||||
|
meta property="og:site_name" content=(self.meta.title);
|
||||||
|
meta name="title" content=(self.meta.title);
|
||||||
|
meta property="og:title" content=(self.meta.title);
|
||||||
|
meta property="twitter:title" content=(self.meta.title);
|
||||||
|
|
||||||
|
@if let Some(author) = &self.meta.author {
|
||||||
|
meta name="author" content=(author);
|
||||||
|
}
|
||||||
|
|
||||||
|
@if let Some(desc) = &self.meta.description {
|
||||||
|
meta name="description" content=(desc);
|
||||||
|
meta property="og:description" content=(desc);
|
||||||
|
meta property="twitter:description" content=(desc);
|
||||||
|
}
|
||||||
|
|
||||||
|
@if let Some(image) = &self.meta.image {
|
||||||
|
meta content=(image) property="og:image";
|
||||||
|
link rel="shortcut icon" href=(image) type="image/x-icon";
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Scripts & styles
|
||||||
|
//
|
||||||
|
@for script in &self.scripts_linked {
|
||||||
|
script src=(script) {}
|
||||||
|
}
|
||||||
|
@for style in &self.styles_linked {
|
||||||
|
link rel="stylesheet" type="text/css" href=(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
@for script in &self.scripts_inline {
|
||||||
|
script { (PreEscaped(script)) }
|
||||||
|
}
|
||||||
|
@for style in &self.styles_inline {
|
||||||
|
style { (PreEscaped(style)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body { main { (inner_html) } }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return self.head(ctx).await.with_body(RenderedBody::Markup(html));
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// MARK: template
|
||||||
|
//
|
||||||
|
|
||||||
|
pub struct PageTemplate {
|
||||||
|
pub immutable: bool,
|
||||||
|
pub html_ttl: Option<TimeDelta>,
|
||||||
|
pub response_code: StatusCode,
|
||||||
|
|
||||||
|
pub scripts_inline: &'static [&'static str],
|
||||||
|
pub scripts_linked: &'static [&'static str],
|
||||||
|
pub styles_inline: &'static [&'static str],
|
||||||
|
pub styles_linked: &'static [&'static str],
|
||||||
|
pub extra_meta: &'static [(&'static str, &'static str)],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for PageTemplate {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::const_default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PageTemplate {
|
||||||
|
pub const fn const_default() -> Self {
|
||||||
|
Self {
|
||||||
|
html_ttl: Some(TimeDelta::days(1)),
|
||||||
|
immutable: true,
|
||||||
|
response_code: StatusCode::OK,
|
||||||
|
|
||||||
|
scripts_inline: &[],
|
||||||
|
scripts_linked: &[],
|
||||||
|
styles_inline: &[],
|
||||||
|
styles_linked: &[],
|
||||||
|
extra_meta: &[],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new page using this template,
|
||||||
|
/// with the given metadata and renderer.
|
||||||
|
pub fn derive<
|
||||||
|
R: Send
|
||||||
|
+ Sync
|
||||||
|
+ 'static
|
||||||
|
+ for<'a> Fn(
|
||||||
|
&'a Page,
|
||||||
|
&'a RenderContext,
|
||||||
|
) -> Pin<Box<dyn Future<Output = Markup> + Send + Sync + 'a>>,
|
||||||
|
>(
|
||||||
|
&self,
|
||||||
|
meta: PageMetadata,
|
||||||
|
generate_html: R,
|
||||||
|
) -> Page {
|
||||||
|
Page {
|
||||||
|
meta,
|
||||||
|
immutable: self.immutable,
|
||||||
|
html_ttl: self.html_ttl,
|
||||||
|
response_code: self.response_code,
|
||||||
|
|
||||||
|
scripts_inline: self
|
||||||
|
.scripts_inline
|
||||||
|
.iter()
|
||||||
|
.map(|x| (*x).to_owned())
|
||||||
|
.collect(),
|
||||||
|
|
||||||
|
scripts_linked: self
|
||||||
|
.scripts_linked
|
||||||
|
.iter()
|
||||||
|
.map(|x| (*x).to_owned())
|
||||||
|
.collect(),
|
||||||
|
|
||||||
|
styles_inline: self.styles_inline.iter().map(|x| (*x).to_owned()).collect(),
|
||||||
|
styles_linked: self.styles_linked.iter().map(|x| (*x).to_owned()).collect(),
|
||||||
|
|
||||||
|
extra_meta: self
|
||||||
|
.extra_meta
|
||||||
|
.iter()
|
||||||
|
.map(|(a, b)| ((*a).to_owned(), (*b).to_owned()))
|
||||||
|
.collect(),
|
||||||
|
|
||||||
|
generate_html: Arc::new(generate_html),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use axum::http::{
|
|||||||
header::{self, InvalidHeaderValue},
|
header::{self, InvalidHeaderValue},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{Rendered, RenderedBody, RequestContext, Servable};
|
use crate::{RenderContext, Rendered, RenderedBody, servable::Servable};
|
||||||
|
|
||||||
pub struct Redirect {
|
pub struct Redirect {
|
||||||
to: HeaderValue,
|
to: HeaderValue,
|
||||||
@@ -20,10 +20,10 @@ impl Redirect {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Servable for Redirect {
|
impl Servable for Redirect {
|
||||||
fn render<'a>(
|
fn head<'a>(
|
||||||
&'a self,
|
&'a self,
|
||||||
_ctx: &'a RequestContext,
|
_ctx: &'a RenderContext,
|
||||||
) -> Pin<Box<dyn Future<Output = Rendered> + 'a + Send + Sync>> {
|
) -> Pin<Box<dyn Future<Output = Rendered<()>> + 'a + Send + Sync>> {
|
||||||
Box::pin(async {
|
Box::pin(async {
|
||||||
let mut headers = HeaderMap::with_capacity(1);
|
let mut headers = HeaderMap::with_capacity(1);
|
||||||
headers.append(header::LOCATION, self.to.clone());
|
headers.append(header::LOCATION, self.to.clone());
|
||||||
@@ -31,11 +31,18 @@ impl Servable for Redirect {
|
|||||||
return Rendered {
|
return Rendered {
|
||||||
code: StatusCode::PERMANENT_REDIRECT,
|
code: StatusCode::PERMANENT_REDIRECT,
|
||||||
headers,
|
headers,
|
||||||
body: RenderedBody::Empty,
|
body: (),
|
||||||
ttl: None,
|
ttl: None,
|
||||||
immutable: true,
|
immutable: true,
|
||||||
mime: None,
|
mime: None,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render<'a>(
|
||||||
|
&'a self,
|
||||||
|
ctx: &'a RenderContext,
|
||||||
|
) -> Pin<Box<dyn Future<Output = Rendered<RenderedBody>> + 'a + Send + Sync>> {
|
||||||
|
Box::pin(async { self.head(ctx).await.with_body(RenderedBody::Empty) })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,324 +0,0 @@
|
|||||||
use axum::{
|
|
||||||
Router,
|
|
||||||
extract::{ConnectInfo, Path, Query, State},
|
|
||||||
http::{HeaderMap, HeaderValue, StatusCode, header},
|
|
||||||
response::{IntoResponse, Response},
|
|
||||||
routing::get,
|
|
||||||
};
|
|
||||||
use chrono::{DateTime, TimeDelta, Utc};
|
|
||||||
use libservice::ServiceConnectInfo;
|
|
||||||
use lru::LruCache;
|
|
||||||
use maud::Markup;
|
|
||||||
use parking_lot::Mutex;
|
|
||||||
use std::{
|
|
||||||
collections::{BTreeMap, HashMap},
|
|
||||||
num::NonZero,
|
|
||||||
pin::Pin,
|
|
||||||
sync::Arc,
|
|
||||||
time::Instant,
|
|
||||||
};
|
|
||||||
use toolbox::mime::MimeType;
|
|
||||||
use tower_http::compression::{CompressionLayer, DefaultPredicate};
|
|
||||||
use tracing::trace;
|
|
||||||
|
|
||||||
use crate::{ClientInfo, RequestContext};
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub enum RenderedBody {
|
|
||||||
Markup(Markup),
|
|
||||||
Static(&'static [u8]),
|
|
||||||
Bytes(Vec<u8>),
|
|
||||||
String(String),
|
|
||||||
Empty,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Rendered {
|
|
||||||
pub code: StatusCode,
|
|
||||||
pub headers: HeaderMap,
|
|
||||||
pub body: RenderedBody,
|
|
||||||
pub mime: Option<MimeType>,
|
|
||||||
|
|
||||||
/// How long to cache this response.
|
|
||||||
/// If none, don't cache.
|
|
||||||
pub ttl: Option<TimeDelta>,
|
|
||||||
pub immutable: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait Servable: Send + Sync {
|
|
||||||
fn render<'a>(
|
|
||||||
&'a self,
|
|
||||||
ctx: &'a RequestContext,
|
|
||||||
) -> Pin<Box<dyn Future<Output = Rendered> + 'a + Send + Sync>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Default404 {}
|
|
||||||
impl Servable for Default404 {
|
|
||||||
fn render<'a>(
|
|
||||||
&'a self,
|
|
||||||
_ctx: &'a RequestContext,
|
|
||||||
) -> Pin<Box<dyn Future<Output = Rendered> + 'a + Send + Sync>> {
|
|
||||||
Box::pin(async {
|
|
||||||
return Rendered {
|
|
||||||
code: StatusCode::NOT_FOUND,
|
|
||||||
body: RenderedBody::String("page not found".into()),
|
|
||||||
ttl: Some(TimeDelta::days(1)),
|
|
||||||
immutable: true,
|
|
||||||
headers: HeaderMap::new(),
|
|
||||||
mime: Some(MimeType::Html),
|
|
||||||
};
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// MARK: server
|
|
||||||
//
|
|
||||||
|
|
||||||
pub struct PageServer {
|
|
||||||
/// If true, expired pages will be rerendered before being sent to the user.
|
|
||||||
/// If false, requests never trigger rerenders. We rely on the rerender task.
|
|
||||||
///
|
|
||||||
/// If true, we deliver fresher pages but delay responses.
|
|
||||||
/// TODO: replace this with a smarter rendering strategy?
|
|
||||||
never_rerender_on_request: bool,
|
|
||||||
|
|
||||||
/// Map of `{ route: page }`
|
|
||||||
pages: Mutex<HashMap<String, Arc<dyn Servable>>>,
|
|
||||||
|
|
||||||
notfound: Mutex<Arc<dyn Servable>>,
|
|
||||||
|
|
||||||
/// Map of `{ route: (page data, expire time) }`
|
|
||||||
///
|
|
||||||
/// We use an LruCache for bounded memory usage.
|
|
||||||
page_cache: Mutex<LruCache<RequestContext, (Rendered, DateTime<Utc>)>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PageServer {
|
|
||||||
pub fn new() -> Arc<Self> {
|
|
||||||
#[expect(clippy::unwrap_used)]
|
|
||||||
let cache_size = NonZero::new(128).unwrap();
|
|
||||||
|
|
||||||
Arc::new(Self {
|
|
||||||
pages: Mutex::new(HashMap::new()),
|
|
||||||
page_cache: Mutex::new(LruCache::new(cache_size)),
|
|
||||||
never_rerender_on_request: true,
|
|
||||||
notfound: Mutex::new(Arc::new(Default404 {})),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set this server's "not found" page
|
|
||||||
pub fn with_404<S: Servable + 'static>(&self, page: S) -> &Self {
|
|
||||||
*self.notfound.lock() = Arc::new(page);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_page<S: Servable + 'static>(&self, route: impl Into<String>, page: S) -> &Self {
|
|
||||||
#[expect(clippy::expect_used)]
|
|
||||||
let route = route
|
|
||||||
.into()
|
|
||||||
.strip_prefix("/")
|
|
||||||
.expect("route must start with /")
|
|
||||||
.to_owned();
|
|
||||||
|
|
||||||
self.pages.lock().insert(route, Arc::new(page));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Re-render the page at `route`, regardless of cache state.
|
|
||||||
/// Does nothing if there is no page at `route`.
|
|
||||||
///
|
|
||||||
/// Returns the rendered page's content.
|
|
||||||
async fn render_page(
|
|
||||||
&self,
|
|
||||||
reason: &'static str,
|
|
||||||
route: &str,
|
|
||||||
ctx: RequestContext,
|
|
||||||
) -> (Rendered, Option<DateTime<Utc>>) {
|
|
||||||
let now = Utc::now();
|
|
||||||
let start = Instant::now();
|
|
||||||
|
|
||||||
let page = match self.pages.lock().get(route) {
|
|
||||||
Some(x) => x.clone(),
|
|
||||||
None => self.notfound.lock().clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
trace!(
|
|
||||||
message = "Rendering page",
|
|
||||||
route = route.to_owned(),
|
|
||||||
reason,
|
|
||||||
lock_time_ms = start.elapsed().as_millis()
|
|
||||||
);
|
|
||||||
|
|
||||||
let rendered = page.render(&ctx).await;
|
|
||||||
|
|
||||||
let mut expires = None;
|
|
||||||
if let Some(ttl) = rendered.ttl {
|
|
||||||
expires = Some(now + ttl);
|
|
||||||
self.page_cache
|
|
||||||
.lock()
|
|
||||||
.put(ctx, (rendered.clone(), now + ttl));
|
|
||||||
}
|
|
||||||
|
|
||||||
let elapsed = start.elapsed().as_millis();
|
|
||||||
trace!(
|
|
||||||
message = "Rendered page",
|
|
||||||
route = route.to_owned(),
|
|
||||||
reason,
|
|
||||||
time_ms = elapsed
|
|
||||||
);
|
|
||||||
return (rendered, expires);
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handler(
|
|
||||||
Path(route): Path<String>,
|
|
||||||
Query(query): Query<BTreeMap<String, String>>,
|
|
||||||
State(state): State<Arc<Self>>,
|
|
||||||
ConnectInfo(addr): ConnectInfo<ServiceConnectInfo>,
|
|
||||||
headers: HeaderMap,
|
|
||||||
) -> Response {
|
|
||||||
let start = Instant::now();
|
|
||||||
let client_info = ClientInfo::from_headers(&headers);
|
|
||||||
let ua = headers
|
|
||||||
.get("user-agent")
|
|
||||||
.and_then(|x| x.to_str().ok())
|
|
||||||
.unwrap_or("");
|
|
||||||
|
|
||||||
trace!(
|
|
||||||
message = "Serving route",
|
|
||||||
route,
|
|
||||||
addr = ?addr.addr,
|
|
||||||
user_agent = ua,
|
|
||||||
device_type = ?client_info.device_type
|
|
||||||
);
|
|
||||||
|
|
||||||
// Normalize url with redirect
|
|
||||||
if route.ends_with('/') || route.contains("//") || route.starts_with('/') {
|
|
||||||
let mut new_route = route.clone();
|
|
||||||
while new_route.contains("//") {
|
|
||||||
new_route = new_route.replace("//", "/");
|
|
||||||
}
|
|
||||||
let new_route = new_route.trim_matches('/');
|
|
||||||
|
|
||||||
trace!(
|
|
||||||
message = "Redirecting",
|
|
||||||
route,
|
|
||||||
new_route,
|
|
||||||
addr = ?addr.addr,
|
|
||||||
user_agent = ua,
|
|
||||||
device_type = ?client_info.device_type
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut headers = HeaderMap::with_capacity(2);
|
|
||||||
|
|
||||||
let new_route = match HeaderValue::from_str(&format!("/{new_route}")) {
|
|
||||||
Ok(x) => x,
|
|
||||||
Err(_) => {
|
|
||||||
// Be extra careful, this is user-provided data
|
|
||||||
return StatusCode::BAD_REQUEST.into_response();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
headers.append(header::LOCATION, new_route);
|
|
||||||
headers.append("Accept-CH", HeaderValue::from_static("Sec-CH-UA-Mobile"));
|
|
||||||
return (StatusCode::PERMANENT_REDIRECT, headers).into_response();
|
|
||||||
}
|
|
||||||
|
|
||||||
let ctx = RequestContext {
|
|
||||||
client_info,
|
|
||||||
route: format!("/{route}"),
|
|
||||||
query,
|
|
||||||
};
|
|
||||||
|
|
||||||
let now = Utc::now();
|
|
||||||
let mut html_expires = None;
|
|
||||||
let mut cached = true;
|
|
||||||
|
|
||||||
// Get from cache, if available
|
|
||||||
if let Some((html, expires)) = state.page_cache.lock().get(&ctx)
|
|
||||||
&& (*expires > now || state.never_rerender_on_request)
|
|
||||||
{
|
|
||||||
html_expires = Some((html.clone(), Some(*expires)));
|
|
||||||
};
|
|
||||||
|
|
||||||
if html_expires.is_none() {
|
|
||||||
cached = false;
|
|
||||||
html_expires = Some(state.render_page("request", &route, ctx).await);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[expect(clippy::unwrap_used)]
|
|
||||||
let (mut html, expires) = html_expires.unwrap();
|
|
||||||
|
|
||||||
if !html.headers.contains_key(header::CACHE_CONTROL) {
|
|
||||||
let max_age = match expires {
|
|
||||||
Some(expires) => (expires - now).num_seconds().max(1),
|
|
||||||
None => 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut value = String::new();
|
|
||||||
if html.immutable {
|
|
||||||
value.push_str("immutable, ");
|
|
||||||
}
|
|
||||||
|
|
||||||
value.push_str("public, ");
|
|
||||||
value.push_str(&format!("max-age={}, ", max_age));
|
|
||||||
|
|
||||||
#[expect(clippy::unwrap_used)]
|
|
||||||
html.headers.insert(
|
|
||||||
header::CACHE_CONTROL,
|
|
||||||
HeaderValue::from_str(value.trim().trim_end_matches(',')).unwrap(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if !html.headers.contains_key("Accept-CH") {
|
|
||||||
html.headers
|
|
||||||
.insert("Accept-CH", HeaderValue::from_static("Sec-CH-UA-Mobile"));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(mime) = &html.mime {
|
|
||||||
#[expect(clippy::unwrap_used)]
|
|
||||||
html.headers.insert(
|
|
||||||
header::CONTENT_TYPE,
|
|
||||||
HeaderValue::from_str(&mime.to_string()).unwrap(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
trace!(
|
|
||||||
message = "Served route",
|
|
||||||
route,
|
|
||||||
addr = ?addr.addr,
|
|
||||||
user_agent = ua,
|
|
||||||
device_type = ?client_info.device_type,
|
|
||||||
cached,
|
|
||||||
time_ns = start.elapsed().as_nanos()
|
|
||||||
);
|
|
||||||
|
|
||||||
return match html.body {
|
|
||||||
RenderedBody::Markup(markup) => (html.code, html.headers, markup.0).into_response(),
|
|
||||||
RenderedBody::Static(data) => (html.code, html.headers, data).into_response(),
|
|
||||||
RenderedBody::Bytes(data) => (html.code, html.headers, data).into_response(),
|
|
||||||
RenderedBody::String(s) => (html.code, html.headers, s).into_response(),
|
|
||||||
RenderedBody::Empty => (html.code, html.headers).into_response(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn into_router(self: Arc<Self>) -> Router<()> {
|
|
||||||
let compression: CompressionLayer = CompressionLayer::new()
|
|
||||||
.br(true)
|
|
||||||
.deflate(true)
|
|
||||||
.gzip(true)
|
|
||||||
.zstd(true)
|
|
||||||
.compress_when(DefaultPredicate::new());
|
|
||||||
|
|
||||||
Router::new()
|
|
||||||
.route(
|
|
||||||
"/",
|
|
||||||
get(|state, query, conn, headers| async {
|
|
||||||
Self::handler(Path(String::new()), query, state, conn, headers).await
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.route("/{*path}", get(Self::handler))
|
|
||||||
.layer(compression)
|
|
||||||
.with_state(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +1,66 @@
|
|||||||
|
use axum::http::{HeaderMap, StatusCode};
|
||||||
|
use chrono::TimeDelta;
|
||||||
|
use maud::Markup;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
use toolbox::mime::MimeType;
|
||||||
|
|
||||||
use axum::http::HeaderMap;
|
//
|
||||||
|
// MARK: rendered
|
||||||
|
//
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum RenderedBody {
|
||||||
|
Markup(Markup),
|
||||||
|
Static(&'static [u8]),
|
||||||
|
Bytes(Vec<u8>),
|
||||||
|
String(String),
|
||||||
|
Empty,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait RenderedBodyType {}
|
||||||
|
impl RenderedBodyType for () {}
|
||||||
|
impl RenderedBodyType for RenderedBody {}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Rendered<T: RenderedBodyType> {
|
||||||
|
pub code: StatusCode,
|
||||||
|
pub headers: HeaderMap,
|
||||||
|
pub body: T,
|
||||||
|
pub mime: Option<MimeType>,
|
||||||
|
|
||||||
|
/// How long to cache this response.
|
||||||
|
/// If none, don't cache.
|
||||||
|
pub ttl: Option<TimeDelta>,
|
||||||
|
pub immutable: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Rendered<()> {
|
||||||
|
/// Turn this [Rendered] into a [Rendered] with a body.
|
||||||
|
pub fn with_body(self, body: RenderedBody) -> Rendered<RenderedBody> {
|
||||||
|
Rendered {
|
||||||
|
code: self.code,
|
||||||
|
headers: self.headers,
|
||||||
|
body,
|
||||||
|
mime: self.mime,
|
||||||
|
ttl: self.ttl,
|
||||||
|
immutable: self.immutable,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// MARK: context
|
||||||
|
//
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
pub struct RequestContext {
|
pub struct RenderContext {
|
||||||
pub client_info: ClientInfo,
|
pub client_info: ClientInfo,
|
||||||
pub route: String,
|
pub route: String,
|
||||||
pub query: BTreeMap<String, String>,
|
pub query: BTreeMap<String, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
//
|
// MARK: clientinfo
|
||||||
//
|
//
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
@@ -25,10 +75,6 @@ impl Default for DeviceType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
|
||||||
// MARK: clientinfo
|
|
||||||
//
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub struct ClientInfo {
|
pub struct ClientInfo {
|
||||||
/// This is an estimate, but it's probably good enough.
|
/// This is an estimate, but it's probably good enough.
|
||||||
@@ -38,6 +38,20 @@ impl TransformerChain {
|
|||||||
return image;
|
return image;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn output_mime(&self, input_mime: &MimeType) -> Option<MimeType> {
|
||||||
|
let mime = self
|
||||||
|
.steps
|
||||||
|
.last()
|
||||||
|
.and_then(|x| match x {
|
||||||
|
TransformerEnum::Format { format } => Some(MimeType::from(format.to_mime_type())),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.unwrap_or(input_mime.clone());
|
||||||
|
|
||||||
|
let fmt = ImageFormat::from_mime_type(mime.to_string());
|
||||||
|
fmt.map(|_| mime)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn transform_bytes(
|
pub fn transform_bytes(
|
||||||
&self,
|
&self,
|
||||||
image_bytes: &[u8],
|
image_bytes: &[u8],
|
||||||
|
|||||||
@@ -251,6 +251,23 @@ impl From<&MimeType> for String {
|
|||||||
// MARK: fromstr
|
// MARK: fromstr
|
||||||
//
|
//
|
||||||
|
|
||||||
|
impl MimeType {
|
||||||
|
/// Parse a mimetype from a string that may contain
|
||||||
|
/// whitespace or ";" parameters.
|
||||||
|
///
|
||||||
|
/// Parameters are discarded, write your own parser if you need them.
|
||||||
|
pub fn from_header(s: &str) -> Result<Self, <Self as FromStr>::Err> {
|
||||||
|
let s = s.trim();
|
||||||
|
let semi = s.find(';').unwrap_or(s.len());
|
||||||
|
let space = s.find(' ').unwrap_or(s.len());
|
||||||
|
let limit = semi.min(space);
|
||||||
|
let s = &s[0..limit];
|
||||||
|
let s = s.trim();
|
||||||
|
|
||||||
|
return Self::from_str(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl FromStr for MimeType {
|
impl FromStr for MimeType {
|
||||||
type Err = std::convert::Infallible;
|
type Err = std::convert::Infallible;
|
||||||
|
|
||||||
@@ -687,4 +704,108 @@ impl MimeType {
|
|||||||
Self::Xul => Some("xul"),
|
Self::Xul => Some("xul"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// MARK: is_text
|
||||||
|
//
|
||||||
|
|
||||||
|
/// Returns true if this MIME type is always plain text.
|
||||||
|
pub fn is_text(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
// Text types
|
||||||
|
Self::Text => true,
|
||||||
|
Self::Css => true,
|
||||||
|
Self::Csv => true,
|
||||||
|
Self::Html => true,
|
||||||
|
Self::Javascript => true,
|
||||||
|
Self::Json => true,
|
||||||
|
Self::JsonLd => true,
|
||||||
|
Self::Xml => true,
|
||||||
|
Self::Svg => true,
|
||||||
|
Self::Ics => true,
|
||||||
|
Self::Xhtml => true,
|
||||||
|
|
||||||
|
// Script types
|
||||||
|
Self::Csh => true,
|
||||||
|
Self::Php => true,
|
||||||
|
Self::Sh => true,
|
||||||
|
|
||||||
|
// All other types are not plain text
|
||||||
|
Self::Other(_) => false,
|
||||||
|
Self::Blob => false,
|
||||||
|
|
||||||
|
// Audio
|
||||||
|
Self::Aac => false,
|
||||||
|
Self::Flac => false,
|
||||||
|
Self::Midi => false,
|
||||||
|
Self::Mp3 => false,
|
||||||
|
Self::Oga => false,
|
||||||
|
Self::Opus => false,
|
||||||
|
Self::Wav => false,
|
||||||
|
Self::Weba => false,
|
||||||
|
|
||||||
|
// Video
|
||||||
|
Self::Avi => false,
|
||||||
|
Self::Mp4 => false,
|
||||||
|
Self::Mpeg => false,
|
||||||
|
Self::Ogv => false,
|
||||||
|
Self::Ts => false,
|
||||||
|
Self::WebmVideo => false,
|
||||||
|
Self::ThreeGp => false,
|
||||||
|
Self::ThreeG2 => false,
|
||||||
|
|
||||||
|
// Images
|
||||||
|
Self::Apng => false,
|
||||||
|
Self::Avif => false,
|
||||||
|
Self::Bmp => false,
|
||||||
|
Self::Gif => false,
|
||||||
|
Self::Ico => false,
|
||||||
|
Self::Jpg => false,
|
||||||
|
Self::Png => false,
|
||||||
|
Self::Qoi => false,
|
||||||
|
Self::Tiff => false,
|
||||||
|
Self::Webp => false,
|
||||||
|
|
||||||
|
// Documents
|
||||||
|
Self::Pdf => false,
|
||||||
|
Self::Rtf => false,
|
||||||
|
|
||||||
|
// Archives
|
||||||
|
Self::Arc => false,
|
||||||
|
Self::Bz => false,
|
||||||
|
Self::Bz2 => false,
|
||||||
|
Self::Gz => false,
|
||||||
|
Self::Jar => false,
|
||||||
|
Self::Ogg => false,
|
||||||
|
Self::Rar => false,
|
||||||
|
Self::SevenZ => false,
|
||||||
|
Self::Tar => false,
|
||||||
|
Self::Zip => false,
|
||||||
|
|
||||||
|
// Fonts
|
||||||
|
Self::Eot => false,
|
||||||
|
Self::Otf => false,
|
||||||
|
Self::Ttf => false,
|
||||||
|
Self::Woff => false,
|
||||||
|
Self::Woff2 => false,
|
||||||
|
|
||||||
|
// Applications
|
||||||
|
Self::Abiword => false,
|
||||||
|
Self::Azw => false,
|
||||||
|
Self::Cda => false,
|
||||||
|
Self::Doc => false,
|
||||||
|
Self::Docx => false,
|
||||||
|
Self::Epub => false,
|
||||||
|
Self::Mpkg => false,
|
||||||
|
Self::Odp => false,
|
||||||
|
Self::Ods => false,
|
||||||
|
Self::Odt => false,
|
||||||
|
Self::Ppt => false,
|
||||||
|
Self::Pptx => false,
|
||||||
|
Self::Vsd => false,
|
||||||
|
Self::Xls => false,
|
||||||
|
Self::Xlsx => false,
|
||||||
|
Self::Xul => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,3 +28,4 @@ toml = { workspace = true }
|
|||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
reqwest = { workspace = true }
|
reqwest = { workspace = true }
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
|
tower-http = { workspace = true }
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use markdown_it::generics::inline::full_link;
|
use markdown_it::generics::inline::full_link;
|
||||||
use markdown_it::{MarkdownIt, Node};
|
use markdown_it::{MarkdownIt, Node};
|
||||||
use maud::{Markup, PreEscaped, Render, html};
|
use maud::{Markup, PreEscaped, Render};
|
||||||
use page::RequestContext;
|
use page::servable::PageMetadata;
|
||||||
use page::page::{Page, PageMetadata};
|
|
||||||
|
|
||||||
use crate::components::md::emote::InlineEmote;
|
use crate::components::md::emote::InlineEmote;
|
||||||
use crate::components::md::frontmatter::{TomlFrontMatter, YamlFrontMatter};
|
use crate::components::md::frontmatter::{TomlFrontMatter, YamlFrontMatter};
|
||||||
@@ -86,10 +85,6 @@ impl Markdown<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
|
||||||
// MARK: helpers
|
|
||||||
//
|
|
||||||
|
|
||||||
/// Try to read page metadata from a markdown file's frontmatter.
|
/// Try to read page metadata from a markdown file's frontmatter.
|
||||||
/// - returns `none` if there is no frontmatter
|
/// - returns `none` if there is no frontmatter
|
||||||
/// - returns an error if we fail to parse frontmatter
|
/// - returns an error if we fail to parse frontmatter
|
||||||
@@ -101,33 +96,3 @@ pub fn meta_from_markdown(root_node: &Node) -> Result<Option<PageMetadata>, toml
|
|||||||
.map(|x| toml::from_str::<PageMetadata>(&x.content))
|
.map(|x| toml::from_str::<PageMetadata>(&x.content))
|
||||||
.map_or(Ok(None), |v| v.map(Some))
|
.map_or(Ok(None), |v| v.map(Some))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn backlinks(page: &Page, ctx: &RequestContext) -> Option<Markup> {
|
|
||||||
let mut last = None;
|
|
||||||
let mut backlinks = vec![("/", "home")];
|
|
||||||
|
|
||||||
if page.meta.backlinks.unwrap_or(false) {
|
|
||||||
let mut segments = ctx.route.split("/").skip(1).collect::<Vec<_>>();
|
|
||||||
last = segments.pop();
|
|
||||||
|
|
||||||
let mut end = 0;
|
|
||||||
for s in segments {
|
|
||||||
end += s.len();
|
|
||||||
backlinks.push((&ctx.route[0..=end], s));
|
|
||||||
end += 1; // trailing slash
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
last.map(|last| {
|
|
||||||
html! {
|
|
||||||
div {
|
|
||||||
@for (url, text) in backlinks {
|
|
||||||
a href=(url) style="padding-left:5pt;padding-right:5pt;" { (text) }
|
|
||||||
"/"
|
|
||||||
}
|
|
||||||
|
|
||||||
span style="color:var(--metaColor);padding-left:5pt;padding-right:5pt;" { (last) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,18 +6,17 @@ use std::{
|
|||||||
|
|
||||||
use chrono::{DateTime, TimeDelta, Utc};
|
use chrono::{DateTime, TimeDelta, Utc};
|
||||||
use maud::{Markup, PreEscaped, html};
|
use maud::{Markup, PreEscaped, html};
|
||||||
use page::{DeviceType, RequestContext, page::Page};
|
use page::{DeviceType, RenderContext, servable::Page};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use reqwest::StatusCode;
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use tracing::{debug, warn};
|
use tracing::{debug, warn};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
components::{
|
components::{
|
||||||
md::{Markdown, backlinks, meta_from_markdown},
|
md::{Markdown, meta_from_markdown},
|
||||||
misc::FarLink,
|
misc::FarLink,
|
||||||
},
|
},
|
||||||
pages::page_wrapper,
|
pages::{MAIN_TEMPLATE, backlinks, footer},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
@@ -114,11 +113,7 @@ async fn get_index() -> Result<Vec<HandoutEntry>, reqwest::Error> {
|
|||||||
return Ok(res);
|
return Ok(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_list_for_group(
|
fn build_list_for_group(handouts: &[HandoutEntry], group: &str, req_ctx: &RenderContext) -> Markup {
|
||||||
handouts: &[HandoutEntry],
|
|
||||||
group: &str,
|
|
||||||
req_ctx: &RequestContext,
|
|
||||||
) -> Markup {
|
|
||||||
let mobile = req_ctx.client_info.device_type == DeviceType::Mobile;
|
let mobile = req_ctx.client_info.device_type == DeviceType::Mobile;
|
||||||
|
|
||||||
if mobile {
|
if mobile {
|
||||||
@@ -198,74 +193,79 @@ pub fn handouts() -> Page {
|
|||||||
|
|
||||||
let html = PreEscaped(md.render());
|
let html = PreEscaped(md.render());
|
||||||
|
|
||||||
Page {
|
MAIN_TEMPLATE
|
||||||
meta,
|
.derive(meta, move |page, ctx| {
|
||||||
html_ttl: Some(TimeDelta::seconds(300)),
|
let html = html.clone();
|
||||||
immutable: false,
|
|
||||||
response_code: StatusCode::OK,
|
|
||||||
|
|
||||||
generate_html: Box::new(move |page, ctx| {
|
|
||||||
let html = html.clone(); // TODO: find a way to not clone here
|
|
||||||
let index = index.clone();
|
let index = index.clone();
|
||||||
Box::pin(async move {
|
render(html, index, page, ctx)
|
||||||
let handouts = index.get().await;
|
})
|
||||||
|
.html_ttl(Some(TimeDelta::seconds(300)))
|
||||||
let fallback = html! {
|
}
|
||||||
span style="color:var(--yellow)" {
|
|
||||||
"Could not load handouts, something broke."
|
fn render<'a>(
|
||||||
}
|
html: Markup,
|
||||||
" "
|
index: Arc<CachedRequest<Result<Vec<HandoutEntry>, reqwest::Error>>>,
|
||||||
(
|
_page: &'a Page,
|
||||||
FarLink(
|
ctx: &'a RenderContext,
|
||||||
"https://git.betalupi.com/Mark/-/packages/generic/ormc-handouts/latest",
|
) -> Pin<Box<dyn Future<Output = Markup> + Send + Sync + 'a>> {
|
||||||
"Try this direct link."
|
Box::pin(async move {
|
||||||
)
|
let handouts = index.get().await;
|
||||||
)
|
|
||||||
};
|
let fallback = html! {
|
||||||
|
span style="color:var(--yellow)" {
|
||||||
let warmups = match &*handouts {
|
"Could not load handouts, something broke."
|
||||||
Ok(handouts) => build_list_for_group(handouts, "Warm-Ups", ctx),
|
}
|
||||||
Err(error) => {
|
" "
|
||||||
warn!("Could not load handout index: {error:?}");
|
(
|
||||||
fallback.clone()
|
FarLink(
|
||||||
}
|
"https://git.betalupi.com/Mark/-/packages/generic/ormc-handouts/latest",
|
||||||
};
|
"Try this direct link."
|
||||||
|
)
|
||||||
let advanced = match &*handouts {
|
)
|
||||||
Ok(handouts) => build_list_for_group(handouts, "Advanced", ctx),
|
};
|
||||||
Err(_) => fallback,
|
|
||||||
};
|
let warmups = match &*handouts {
|
||||||
|
Ok(handouts) => build_list_for_group(handouts, "Warm-Ups", ctx),
|
||||||
let inner = html! {
|
Err(error) => {
|
||||||
@if let Some(backlinks) = backlinks(page, ctx) {
|
warn!("Could not load handout index: {error:?}");
|
||||||
(backlinks)
|
fallback.clone()
|
||||||
}
|
}
|
||||||
|
};
|
||||||
(html)
|
|
||||||
|
let advanced = match &*handouts {
|
||||||
(Markdown(concat!(
|
Ok(handouts) => build_list_for_group(handouts, "Advanced", ctx),
|
||||||
"## Warm-Ups",
|
Err(_) => fallback,
|
||||||
"\n\n",
|
};
|
||||||
"Students never show up on time. Some come early, some come late. Warm-ups ",
|
|
||||||
"are my solution to this problem: we hand these out as students walk in, ",
|
html! {
|
||||||
"giving them something to do until we can start the lesson.",
|
div class="wrapper" style="margin-top:3ex;" {
|
||||||
)))
|
@if let Some(backlinks) = backlinks(ctx) {
|
||||||
(warmups)
|
(backlinks)
|
||||||
br {}
|
}
|
||||||
|
|
||||||
(Markdown(concat!(
|
(html)
|
||||||
"## Advanced",
|
|
||||||
"\n\n",
|
(Markdown(concat!(
|
||||||
"The highest level of the ORMC, and the group I spend most of my time with. ",
|
"## Warm-Ups",
|
||||||
"Students in ORMC Advanced are in high school, which means ",
|
"\n\n",
|
||||||
"they're ~14-18 years old.",
|
"Students never show up on time. Some come early, some come late. Warm-ups ",
|
||||||
)))
|
"are my solution to this problem: we hand these out as students walk in, ",
|
||||||
(advanced)
|
"giving them something to do until we can start the lesson.",
|
||||||
br {}
|
)))
|
||||||
};
|
(warmups)
|
||||||
|
br {}
|
||||||
page_wrapper(&page.meta, inner, true).await
|
|
||||||
})
|
(Markdown(concat!(
|
||||||
}),
|
"## Advanced",
|
||||||
}
|
"\n\n",
|
||||||
|
"The highest level of the ORMC, and the group I spend most of my time with. ",
|
||||||
|
"Students in ORMC Advanced are in high school, which means ",
|
||||||
|
"they're ~14-18 years old.",
|
||||||
|
)))
|
||||||
|
(advanced)
|
||||||
|
br {}
|
||||||
|
(footer())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
use maud::html;
|
use maud::{Markup, html};
|
||||||
use page::page::{Page, PageMetadata};
|
use page::{
|
||||||
|
RenderContext,
|
||||||
|
servable::{Page, PageMetadata},
|
||||||
|
};
|
||||||
|
use std::pin::Pin;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
components::{
|
components::{
|
||||||
@@ -8,76 +12,78 @@ use crate::{
|
|||||||
md::Markdown,
|
md::Markdown,
|
||||||
misc::FarLink,
|
misc::FarLink,
|
||||||
},
|
},
|
||||||
pages::page_wrapper,
|
pages::{MAIN_TEMPLATE, footer},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn index() -> Page {
|
pub fn index() -> Page {
|
||||||
Page {
|
MAIN_TEMPLATE.derive(
|
||||||
meta: PageMetadata {
|
PageMetadata {
|
||||||
title: "Betalupi: About".into(),
|
title: "Betalupi: About".into(),
|
||||||
author: Some("Mark".into()),
|
author: Some("Mark".into()),
|
||||||
description: None,
|
description: None,
|
||||||
image: Some("/assets/img/icon.png".to_owned()),
|
image: Some("/assets/img/icon.png".to_owned()),
|
||||||
backlinks: Some(false),
|
|
||||||
},
|
},
|
||||||
|
render,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
generate_html: Box::new(move |page, _ctx| {
|
fn render<'a>(
|
||||||
Box::pin(async {
|
_page: &'a Page,
|
||||||
let inner = html! {
|
_ctx: &'a RenderContext,
|
||||||
h2 id="about" { "About" }
|
) -> Pin<Box<dyn Future<Output = Markup> + Send + Sync + 'a>> {
|
||||||
|
Box::pin(async {
|
||||||
|
html! {
|
||||||
|
div class="wrapper" style="margin-top:3ex;" {
|
||||||
|
h2 id="about" { "About" }
|
||||||
|
div {
|
||||||
|
|
||||||
div {
|
img
|
||||||
|
class="img-placeholder"
|
||||||
|
src="/assets/img/cover-small.jpg?t=maxdim(20,20)"
|
||||||
|
data-large="/assets/img/cover-small.jpg"
|
||||||
|
style="image-rendering:pixelated;float:left;margin:10px;display:block;width:25%;"
|
||||||
|
{}
|
||||||
|
|
||||||
img
|
div style="margin:2ex 1ex 2ex 1ex;display:inline-block;overflow:hidden;width:60%;" {
|
||||||
class="img-placeholder"
|
"Welcome, you've reached Mark's main page. Here you'll find"
|
||||||
src="/assets/img/cover-small.jpg?t=maxdim(20,20)"
|
" links to various projects I've worked on."
|
||||||
data-large="/assets/img/cover-small.jpg"
|
|
||||||
style="image-rendering:pixelated;float:left;margin:10px;display:block;width:25%;"
|
|
||||||
{}
|
|
||||||
|
|
||||||
div style="margin:2ex 1ex 2ex 1ex;display:inline-block;overflow:hidden;width:60%;" {
|
ul {
|
||||||
"Welcome, you've reached Mark's main page. Here you'll find"
|
li { (MangledBetaEmail {}) }
|
||||||
" links to various projects I've worked on."
|
li { (MangledGoogleEmail {}) }
|
||||||
|
|
||||||
ul {
|
li {
|
||||||
li { (MangledBetaEmail {}) }
|
(
|
||||||
li { (MangledGoogleEmail {}) }
|
FarLink(
|
||||||
|
"https://github.com/rm-dr",
|
||||||
li {
|
html!(
|
||||||
(
|
(FAIcon::Github)
|
||||||
FarLink(
|
"rm-dr"
|
||||||
"https://github.com/rm-dr",
|
|
||||||
html!(
|
|
||||||
(FAIcon::Github)
|
|
||||||
"rm-dr"
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
)
|
||||||
|
}
|
||||||
|
|
||||||
li {
|
li {
|
||||||
(
|
(
|
||||||
FarLink(
|
FarLink(
|
||||||
"https://git.betalupi.com",
|
"https://git.betalupi.com",
|
||||||
html!(
|
html!(
|
||||||
(FAIcon::Git)
|
(FAIcon::Git)
|
||||||
"git.betalupi.com"
|
"git.betalupi.com"
|
||||||
)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
br style="clear:both;" {}
|
|
||||||
}
|
}
|
||||||
|
br style="clear:both;" {}
|
||||||
|
}
|
||||||
|
|
||||||
(Markdown(include_str!("index.md")))
|
(Markdown(include_str!("index.md")))
|
||||||
};
|
|
||||||
|
|
||||||
page_wrapper(&page.meta, inner, true).await
|
(footer())
|
||||||
})
|
}
|
||||||
}),
|
}
|
||||||
|
})
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
use chrono::TimeDelta;
|
use chrono::TimeDelta;
|
||||||
use maud::{DOCTYPE, Markup, PreEscaped, html};
|
use maud::{Markup, PreEscaped, html};
|
||||||
use page::page::{Page, PageMetadata};
|
use page::{
|
||||||
use reqwest::StatusCode;
|
RenderContext,
|
||||||
use std::pin::Pin;
|
servable::{Page, PageMetadata, PageTemplate},
|
||||||
|
};
|
||||||
|
|
||||||
use crate::components::{
|
use crate::components::{
|
||||||
fa::FAIcon,
|
fa::FAIcon,
|
||||||
md::{Markdown, backlinks, meta_from_markdown},
|
md::{Markdown, meta_from_markdown},
|
||||||
misc::FarLink,
|
misc::FarLink,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -67,120 +68,119 @@ fn page_from_markdown(md: impl Into<String>, default_image: Option<String>) -> P
|
|||||||
|
|
||||||
let html = PreEscaped(md.render());
|
let html = PreEscaped(md.render());
|
||||||
|
|
||||||
Page {
|
MAIN_TEMPLATE
|
||||||
meta,
|
.derive(meta, move |_page, ctx| {
|
||||||
immutable: true,
|
|
||||||
response_code: StatusCode::OK,
|
|
||||||
|
|
||||||
html_ttl: Some(TimeDelta::days(1)),
|
|
||||||
generate_html: Box::new(move |page, ctx| {
|
|
||||||
let html = html.clone();
|
let html = html.clone();
|
||||||
|
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let inner = html! {
|
html! {
|
||||||
@if let Some(backlinks) = backlinks(page, ctx) {
|
div class="wrapper" style="margin-top:3ex;" {
|
||||||
(backlinks)
|
@if let Some(backlinks) = backlinks(ctx) {
|
||||||
|
(backlinks)
|
||||||
|
}
|
||||||
|
|
||||||
|
(html)
|
||||||
|
|
||||||
|
(footer())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
(html)
|
|
||||||
};
|
|
||||||
|
|
||||||
page_wrapper(&page.meta, inner, true).await
|
|
||||||
})
|
})
|
||||||
}),
|
})
|
||||||
}
|
.html_ttl(Some(TimeDelta::days(1)))
|
||||||
|
.immutable(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// MARK: wrapper
|
// MARK: components
|
||||||
//
|
//
|
||||||
|
|
||||||
pub fn page_wrapper<'a>(
|
const MAIN_TEMPLATE: PageTemplate = PageTemplate {
|
||||||
meta: &'a PageMetadata,
|
// Order matters, base htmx goes first
|
||||||
inner: Markup,
|
scripts_linked: &["/assets/htmx.js", "/assets/htmx-json.js"],
|
||||||
footer: bool,
|
|
||||||
) -> Pin<Box<dyn Future<Output = Markup> + 'a + Send + Sync>> {
|
// TODO: use htmx for this
|
||||||
Box::pin(async move {
|
scripts_inline: &["
|
||||||
|
window.onload = function() {
|
||||||
|
var imgs = document.querySelectorAll('.img-placeholder');
|
||||||
|
|
||||||
|
imgs.forEach(img => {
|
||||||
|
img.style.border = 'none';
|
||||||
|
img.style.filter = 'blur(10px)';
|
||||||
|
img.style.transition = 'filter 0.3s';
|
||||||
|
|
||||||
|
var lg = new Image();
|
||||||
|
lg.src = img.dataset.large;
|
||||||
|
lg.onload = function () {
|
||||||
|
img.src = img.dataset.large;
|
||||||
|
img.style.filter = 'blur(0px)';
|
||||||
|
};
|
||||||
|
})
|
||||||
|
}
|
||||||
|
"],
|
||||||
|
|
||||||
|
styles_inline: &[],
|
||||||
|
styles_linked: &["/assets/css/main.css"],
|
||||||
|
|
||||||
|
extra_meta: &[(
|
||||||
|
"viewport",
|
||||||
|
"width=device-width,initial-scale=1,user-scalable=no",
|
||||||
|
)],
|
||||||
|
|
||||||
|
..PageTemplate::const_default()
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn backlinks(ctx: &RenderContext) -> Option<Markup> {
|
||||||
|
let mut backlinks = vec![("/", "home")];
|
||||||
|
|
||||||
|
let mut segments = ctx.route.split("/").skip(1).collect::<Vec<_>>();
|
||||||
|
let last = segments.pop();
|
||||||
|
|
||||||
|
let mut end = 0;
|
||||||
|
for s in segments {
|
||||||
|
end += s.len();
|
||||||
|
backlinks.push((&ctx.route[0..=end], s));
|
||||||
|
end += 1; // trailing slash
|
||||||
|
}
|
||||||
|
|
||||||
|
last.map(|last| {
|
||||||
html! {
|
html! {
|
||||||
(DOCTYPE)
|
div {
|
||||||
html {
|
@for (url, text) in backlinks {
|
||||||
head {
|
a href=(url) style="padding-left:5pt;padding-right:5pt;" { (text) }
|
||||||
meta charset="UTF8" {}
|
"/"
|
||||||
meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" {}
|
|
||||||
meta content="text/html; charset=UTF-8" http-equiv="content-type" {}
|
|
||||||
meta property="og:type" content="website" {}
|
|
||||||
|
|
||||||
link rel="stylesheet" href=("/assets/css/main.css") {}
|
|
||||||
|
|
||||||
(&meta)
|
|
||||||
title { (PreEscaped(meta.title.clone())) }
|
|
||||||
|
|
||||||
|
|
||||||
// Use a small blurred placeholder while full-size images load.
|
|
||||||
// Requires no other special scripts or css, just add some tags
|
|
||||||
// to your <img>!
|
|
||||||
script {
|
|
||||||
(PreEscaped("
|
|
||||||
window.onload = function() {
|
|
||||||
var imgs = document.querySelectorAll('.img-placeholder');
|
|
||||||
|
|
||||||
imgs.forEach(img => {
|
|
||||||
img.style.border = 'none';
|
|
||||||
img.style.filter = 'blur(10px)';
|
|
||||||
img.style.transition = 'filter 0.3s';
|
|
||||||
|
|
||||||
var lg = new Image();
|
|
||||||
lg.src = img.dataset.large;
|
|
||||||
lg.onload = function () {
|
|
||||||
img.src = img.dataset.large;
|
|
||||||
img.style.filter = 'blur(0px)';
|
|
||||||
};
|
|
||||||
})
|
|
||||||
}
|
|
||||||
"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
span style="color:var(--metaColor);padding-left:5pt;padding-right:5pt;" { (last) }
|
||||||
main{
|
|
||||||
div class="wrapper" style=(
|
|
||||||
// for 404 page. Margin makes it scroll.
|
|
||||||
match footer {
|
|
||||||
true => "margin-top:3ex;",
|
|
||||||
false =>""
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
(inner)
|
|
||||||
|
|
||||||
@if footer {
|
|
||||||
footer style="margin-top:10rem;" {
|
|
||||||
hr class = "footline" {}
|
|
||||||
div class = "footContainer" {
|
|
||||||
p {
|
|
||||||
"This site was built by hand with "
|
|
||||||
(FarLink("https://rust-lang.org", "Rust"))
|
|
||||||
", "
|
|
||||||
(FarLink("https://maud.lambda.xyz", "Maud"))
|
|
||||||
", and "
|
|
||||||
(FarLink("https://docs.rs/axum/latest/axum", "Axum"))
|
|
||||||
". "
|
|
||||||
|
|
||||||
(
|
|
||||||
FarLink(
|
|
||||||
"https://git.betalupi.com/Mark/webpage",
|
|
||||||
html!(
|
|
||||||
(FAIcon::Git)
|
|
||||||
"Source here!"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn footer() -> Markup {
|
||||||
|
html!(
|
||||||
|
footer style="margin-top:10rem;" {
|
||||||
|
hr class = "footline";
|
||||||
|
div class = "footContainer" {
|
||||||
|
p {
|
||||||
|
"This site was built by hand with "
|
||||||
|
(FarLink("https://rust-lang.org", "Rust"))
|
||||||
|
", "
|
||||||
|
(FarLink("https://maud.lambda.xyz", "Maud"))
|
||||||
|
", and "
|
||||||
|
(FarLink("https://docs.rs/axum/latest/axum", "Axum"))
|
||||||
|
". "
|
||||||
|
|
||||||
|
(
|
||||||
|
FarLink(
|
||||||
|
"https://git.betalupi.com/Mark/webpage",
|
||||||
|
html!(
|
||||||
|
(FAIcon::Git)
|
||||||
|
"Source here!"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,34 +1,29 @@
|
|||||||
use maud::html;
|
use maud::html;
|
||||||
use page::page::{Page, PageMetadata};
|
use page::servable::{Page, PageMetadata};
|
||||||
use reqwest::StatusCode;
|
use reqwest::StatusCode;
|
||||||
|
|
||||||
use crate::pages::page_wrapper;
|
use crate::pages::MAIN_TEMPLATE;
|
||||||
|
|
||||||
pub fn notfound() -> Page {
|
pub fn notfound() -> Page {
|
||||||
Page {
|
MAIN_TEMPLATE.derive(
|
||||||
meta: PageMetadata {
|
PageMetadata {
|
||||||
title: "Betalupi: About".into(),
|
title: "Page not found".into(),
|
||||||
author: None,
|
author:None,
|
||||||
description: None,
|
description: None,
|
||||||
image: Some("/assets/img/icon.png".to_owned()),
|
image: Some("/assets/img/icon.png".to_owned()),
|
||||||
backlinks: Some(false),
|
|
||||||
},
|
},
|
||||||
|
move |_page, _ctx| {
|
||||||
response_code: StatusCode::NOT_FOUND,
|
|
||||||
generate_html: Box::new(move |page, _ctx| {
|
|
||||||
Box::pin(async {
|
Box::pin(async {
|
||||||
let inner = html! {
|
html! {
|
||||||
div style="display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100vh" {
|
div class="wrapper" {
|
||||||
p style="font-weight:bold;font-size:50pt;margin:0;" { "404" }
|
div style="display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100vh" {
|
||||||
p style="font-size:13pt;margin:0;color:var(--grey);" { "(page not found)" }
|
p style="font-weight:bold;font-size:50pt;margin:0;" { "404" }
|
||||||
a style="font-size:12pt;margin:10pt;padding:5px;" href="/" {"<- Back to site"}
|
p style="font-size:13pt;margin:0;color:var(--grey);" { "(page not found)" }
|
||||||
|
a style="font-size:12pt;margin:10pt;padding:5px;" href="/" {"<- Back to site"}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
page_wrapper(&page.meta, inner, false).await
|
|
||||||
})
|
})
|
||||||
}),
|
},
|
||||||
|
).response_code(StatusCode::NOT_FOUND)
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,39 @@
|
|||||||
use axum::Router;
|
use axum::Router;
|
||||||
use macro_sass::sass;
|
use macro_sass::sass;
|
||||||
use page::{PageServer, asset::StaticAsset, redirect::Redirect};
|
use page::{
|
||||||
use std::sync::Arc;
|
ServableRoute,
|
||||||
|
servable::{Redirect, StaticAsset},
|
||||||
|
};
|
||||||
use toolbox::mime::MimeType;
|
use toolbox::mime::MimeType;
|
||||||
|
use tower_http::compression::{CompressionLayer, DefaultPredicate};
|
||||||
|
|
||||||
use crate::pages;
|
use crate::pages;
|
||||||
|
|
||||||
pub(super) fn router() -> Router<()> {
|
pub(super) fn router() -> Router<()> {
|
||||||
build_server().into_router()
|
let compression: CompressionLayer = CompressionLayer::new()
|
||||||
|
.br(true)
|
||||||
|
.deflate(true)
|
||||||
|
.gzip(true)
|
||||||
|
.zstd(true)
|
||||||
|
.compress_when(DefaultPredicate::new());
|
||||||
|
|
||||||
|
build_server().into_router().layer(compression)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_server() -> Arc<PageServer> {
|
fn build_server() -> ServableRoute {
|
||||||
let server = PageServer::new();
|
ServableRoute::new()
|
||||||
|
|
||||||
#[expect(clippy::unwrap_used)]
|
|
||||||
server
|
|
||||||
.with_404(pages::notfound())
|
.with_404(pages::notfound())
|
||||||
.add_page("/", pages::index())
|
.add_page("/", pages::index())
|
||||||
.add_page("/links", pages::links())
|
.add_page("/links", pages::links())
|
||||||
.add_page("/whats-a-betalupi", pages::betalupi())
|
.add_page("/whats-a-betalupi", pages::betalupi())
|
||||||
.add_page("/handouts", pages::handouts())
|
.add_page("/handouts", pages::handouts())
|
||||||
.add_page("/htwah", Redirect::new("/handouts").unwrap())
|
.add_page("/htwah", {
|
||||||
|
#[expect(clippy::unwrap_used)]
|
||||||
|
Redirect::new("/handouts").unwrap()
|
||||||
|
})
|
||||||
.add_page("/htwah/typesetting", pages::htwah_typesetting())
|
.add_page("/htwah/typesetting", pages::htwah_typesetting())
|
||||||
|
.add_page("/assets/htmx.js", page::HTMX_2_0_8)
|
||||||
|
.add_page("/assets/htmx-json.js", page::EXT_JSON_1_19_12)
|
||||||
//
|
//
|
||||||
.add_page(
|
.add_page(
|
||||||
"/assets/css/main.css",
|
"/assets/css/main.css",
|
||||||
@@ -185,9 +197,7 @@ fn build_server() -> Arc<PageServer> {
|
|||||||
bytes: include_bytes!("../../assets/htwah/spacing-b.pdf"),
|
bytes: include_bytes!("../../assets/htwah/spacing-b.pdf"),
|
||||||
mime: MimeType::Pdf,
|
mime: MimeType::Pdf,
|
||||||
},
|
},
|
||||||
);
|
)
|
||||||
|
|
||||||
server
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -197,7 +207,6 @@ fn server_builds_without_panic() {
|
|||||||
.build()
|
.build()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.block_on(async {
|
.block_on(async {
|
||||||
// Needs tokio context
|
let _server = build_server();
|
||||||
let _server = build_server().into_router();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ extend-ignore-re = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[files]
|
[files]
|
||||||
extend-exclude = ["crates/service/service-webpage/css"]
|
extend-exclude = ["crates/service/service-webpage/css", "crates/lib/page/htmx"]
|
||||||
|
|||||||
Reference in New Issue
Block a user