Render handout page on server
All checks were successful
CI / Check typos (push) Successful in 22s
CI / Check links (push) Successful in 23s
CI / Clippy (push) Successful in 1m5s
CI / Build and test (push) Successful in 1m12s
CI / Build container (push) Successful in 1m37s
CI / Deploy on waypoint (push) Successful in 45s
All checks were successful
CI / Check typos (push) Successful in 22s
CI / Check links (push) Successful in 23s
CI / Clippy (push) Successful in 1m5s
CI / Build and test (push) Successful in 1m12s
CI / Build container (push) Successful in 1m37s
CI / Deploy on waypoint (push) Successful in 45s
This commit is contained in:
198
Cargo.lock
generated
198
Cargo.lock
generated
@@ -128,6 +128,19 @@ checksum = "3f8ebf5827e4ac4fd5946560e6a99776ea73b596d80898f357007317a7141e47"
|
|||||||
name = "assetserver"
|
name = "assetserver"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-compression"
|
||||||
|
version = "0.4.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a89bce6054c720275ac2432fbba080a66a2106a44a1b804553930ca6909f4e0"
|
||||||
|
dependencies = [
|
||||||
|
"compression-codecs",
|
||||||
|
"compression-core",
|
||||||
|
"futures-core",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "atomic-waker"
|
name = "atomic-waker"
|
||||||
version = "1.1.2"
|
version = "1.1.2"
|
||||||
@@ -349,6 +362,23 @@ version = "1.0.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "compression-codecs"
|
||||||
|
version = "0.4.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ef8a506ec4b81c460798f572caead636d57d3d7e940f998160f52bd254bf2d23"
|
||||||
|
dependencies = [
|
||||||
|
"compression-core",
|
||||||
|
"flate2",
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "compression-core"
|
||||||
|
version = "0.4.29"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e47641d3deaf41fb1538ac1f54735925e275eaf3bf4d55c81b137fba797e5cbb"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "const_format"
|
name = "const_format"
|
||||||
version = "0.2.35"
|
version = "0.2.35"
|
||||||
@@ -375,6 +405,35 @@ version = "0.4.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
|
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cookie"
|
||||||
|
version = "0.18.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
|
||||||
|
dependencies = [
|
||||||
|
"percent-encoding",
|
||||||
|
"time",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cookie_store"
|
||||||
|
version = "0.21.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2eac901828f88a5241ee0600950ab981148a18f2f756900ffba1b125ca6a3ef9"
|
||||||
|
dependencies = [
|
||||||
|
"cookie",
|
||||||
|
"document-features",
|
||||||
|
"idna 1.1.0",
|
||||||
|
"log",
|
||||||
|
"publicsuffix",
|
||||||
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
|
"serde_json",
|
||||||
|
"time",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-foundation-sys"
|
name = "core-foundation-sys"
|
||||||
version = "0.8.7"
|
version = "0.8.7"
|
||||||
@@ -474,6 +533,15 @@ dependencies = [
|
|||||||
"syn 2.0.108",
|
"syn 2.0.108",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "document-features"
|
||||||
|
version = "0.2.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61"
|
||||||
|
dependencies = [
|
||||||
|
"litrs",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "downcast-rs"
|
name = "downcast-rs"
|
||||||
version = "1.2.1"
|
version = "1.2.1"
|
||||||
@@ -550,12 +618,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"
|
||||||
@@ -572,6 +634,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
|
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
"futures-sink",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -580,6 +643,29 @@ 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-io"
|
||||||
|
version = "0.3.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-macro"
|
||||||
|
version = "0.3.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.108",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-sink"
|
||||||
|
version = "0.3.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-task"
|
name = "futures-task"
|
||||||
version = "0.3.31"
|
version = "0.3.31"
|
||||||
@@ -593,9 +679,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
"futures-io",
|
||||||
|
"futures-macro",
|
||||||
|
"futures-sink",
|
||||||
"futures-task",
|
"futures-task",
|
||||||
|
"memchr",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"pin-utils",
|
"pin-utils",
|
||||||
|
"slab",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -660,6 +751,25 @@ dependencies = [
|
|||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "h2"
|
||||||
|
version = "0.4.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386"
|
||||||
|
dependencies = [
|
||||||
|
"atomic-waker",
|
||||||
|
"bytes",
|
||||||
|
"fnv",
|
||||||
|
"futures-core",
|
||||||
|
"futures-sink",
|
||||||
|
"http",
|
||||||
|
"indexmap",
|
||||||
|
"slab",
|
||||||
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.14.5"
|
version = "0.14.5"
|
||||||
@@ -675,11 +785,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"
|
||||||
@@ -752,6 +857,7 @@ dependencies = [
|
|||||||
"bytes",
|
"bytes",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
"h2",
|
||||||
"http",
|
"http",
|
||||||
"http-body",
|
"http-body",
|
||||||
"httparse",
|
"httparse",
|
||||||
@@ -1063,6 +1169,12 @@ version = "0.8.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
|
checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "litrs"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lock_api"
|
name = "lock_api"
|
||||||
version = "0.4.14"
|
version = "0.4.14"
|
||||||
@@ -1088,15 +1200,6 @@ dependencies = [
|
|||||||
"prost-types",
|
"prost-types",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[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"
|
||||||
@@ -1553,6 +1656,12 @@ dependencies = [
|
|||||||
"prost",
|
"prost",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "psl-types"
|
||||||
|
version = "2.0.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "psm"
|
name = "psm"
|
||||||
version = "0.1.28"
|
version = "0.1.28"
|
||||||
@@ -1563,6 +1672,16 @@ dependencies = [
|
|||||||
"cc",
|
"cc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "publicsuffix"
|
||||||
|
version = "2.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6f42ea446cab60335f76979ec15e12619a2165b5ae2c12166bef27d283a9fadf"
|
||||||
|
dependencies = [
|
||||||
|
"idna 1.1.0",
|
||||||
|
"psl-types",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quick-xml"
|
name = "quick-xml"
|
||||||
version = "0.38.3"
|
version = "0.38.3"
|
||||||
@@ -1756,9 +1875,16 @@ version = "0.12.24"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f"
|
checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"async-compression",
|
||||||
"base64",
|
"base64",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
"cookie",
|
||||||
|
"cookie_store",
|
||||||
|
"encoding_rs",
|
||||||
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
"futures-util",
|
||||||
|
"h2",
|
||||||
"http",
|
"http",
|
||||||
"http-body",
|
"http-body",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
@@ -1767,6 +1893,7 @@ dependencies = [
|
|||||||
"hyper-util",
|
"hyper-util",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"log",
|
"log",
|
||||||
|
"mime",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"quinn",
|
"quinn",
|
||||||
@@ -1778,12 +1905,14 @@ dependencies = [
|
|||||||
"sync_wrapper",
|
"sync_wrapper",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-rustls",
|
"tokio-rustls",
|
||||||
|
"tokio-util",
|
||||||
"tower",
|
"tower",
|
||||||
"tower-http",
|
"tower-http",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"url",
|
"url",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
|
"wasm-streams",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
"webpki-roots",
|
"webpki-roots",
|
||||||
]
|
]
|
||||||
@@ -2008,15 +2137,16 @@ dependencies = [
|
|||||||
"emojis",
|
"emojis",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"libservice",
|
"libservice",
|
||||||
"lru",
|
|
||||||
"macro-assets",
|
"macro-assets",
|
||||||
"macro-sass",
|
"macro-sass",
|
||||||
"markdown-it",
|
"markdown-it",
|
||||||
"maud",
|
"maud",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_yaml",
|
"serde_yaml",
|
||||||
"strum",
|
"strum",
|
||||||
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2350,6 +2480,19 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-util"
|
||||||
|
version = "0.7.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"futures-core",
|
||||||
|
"futures-sink",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toolbox"
|
name = "toolbox"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
@@ -2755,6 +2898,19 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-streams"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"
|
||||||
|
dependencies = [
|
||||||
|
"futures-util",
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "web-sys"
|
name = "web-sys"
|
||||||
version = "0.3.82"
|
version = "0.3.82"
|
||||||
|
|||||||
13
Cargo.toml
13
Cargo.toml
@@ -74,7 +74,7 @@ service-webpage = { path = "crates/service/service-webpage" }
|
|||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# MARK: Servers
|
# MARK: Server
|
||||||
#
|
#
|
||||||
axum = { version = "0.8.6", features = ["macros", "multipart"] }
|
axum = { version = "0.8.6", features = ["macros", "multipart"] }
|
||||||
tower-http = { version = "0.6.6", features = ["trace"] }
|
tower-http = { version = "0.6.6", features = ["trace"] }
|
||||||
@@ -88,6 +88,17 @@ maud = { version = "0.27.0", features = ["axum"] }
|
|||||||
grass = "0.13.4"
|
grass = "0.13.4"
|
||||||
markdown-it = "0.6.1"
|
markdown-it = "0.6.1"
|
||||||
emojis = "0.8.0"
|
emojis = "0.8.0"
|
||||||
|
reqwest = { version = "0.12.24", default-features = false, features = [
|
||||||
|
"http2",
|
||||||
|
"rustls-tls",
|
||||||
|
"cookies",
|
||||||
|
"gzip",
|
||||||
|
"stream",
|
||||||
|
"json",
|
||||||
|
"charset",
|
||||||
|
"blocking",
|
||||||
|
] }
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# MARK: Async & Parallelism
|
# MARK: Async & Parallelism
|
||||||
|
|||||||
@@ -68,6 +68,8 @@ impl From<LoggingConfig> for EnvFilter {
|
|||||||
format!("h2={}", conf.silence),
|
format!("h2={}", conf.silence),
|
||||||
format!("rustls={}", conf.silence),
|
format!("rustls={}", conf.silence),
|
||||||
format!("tower={}", conf.silence),
|
format!("tower={}", conf.silence),
|
||||||
|
format!("reqwest={}", conf.silence),
|
||||||
|
format!("axum={}", conf.silence),
|
||||||
//
|
//
|
||||||
// Libs
|
// Libs
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ emojis = { workspace = true }
|
|||||||
strum = { workspace = true }
|
strum = { workspace = true }
|
||||||
chrono = { workspace = true }
|
chrono = { workspace = true }
|
||||||
parking_lot = { workspace = true }
|
parking_lot = { workspace = true }
|
||||||
lru = { workspace = true }
|
|
||||||
lazy_static = { workspace = true }
|
lazy_static = { workspace = true }
|
||||||
serde_yaml = { workspace = true }
|
serde_yaml = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
|
reqwest = { workspace = true }
|
||||||
|
tokio = { workspace = true }
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
// Handout list pages
|
|
||||||
// works with "{{ handout() }}" shortcode.
|
|
||||||
|
|
||||||
.handout-li-links {
|
.handout-li-links {
|
||||||
color: var(--grey);
|
color: var(--grey);
|
||||||
}
|
}
|
||||||
@@ -39,10 +36,6 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.handout-star {
|
|
||||||
color: var(--yellow);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Email obfuscation
|
// Email obfuscation
|
||||||
// Works with "{{ email_*() }}" shortcodes.
|
// Works with "{{ email_*() }}" shortcodes.
|
||||||
.eobf {
|
.eobf {
|
||||||
|
|||||||
@@ -9,14 +9,19 @@ use axum::{
|
|||||||
response::{IntoResponse, Response},
|
response::{IntoResponse, Response},
|
||||||
routing::get,
|
routing::get,
|
||||||
};
|
};
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, TimeDelta, Utc};
|
||||||
use libservice::ServiceConnectInfo;
|
use libservice::ServiceConnectInfo;
|
||||||
use lru::LruCache;
|
use markdown_it::Node;
|
||||||
use maud::{Markup, PreEscaped, Render, html};
|
use maud::{Markup, PreEscaped, Render, html};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::{Mutex, RwLock};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::{collections::HashMap, num::NonZero, sync::Arc, time::Duration};
|
use std::{
|
||||||
use tracing::{debug, trace};
|
collections::HashMap,
|
||||||
|
pin::Pin,
|
||||||
|
sync::Arc,
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
|
use tracing::{trace, warn};
|
||||||
|
|
||||||
use crate::components::{
|
use crate::components::{
|
||||||
md::{FrontMatter, Markdown},
|
md::{FrontMatter, Markdown},
|
||||||
@@ -70,6 +75,21 @@ impl Render for PageMetadata {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl PageMetadata {
|
||||||
|
/// Try to read page metadata from a markdown file's frontmatter.
|
||||||
|
/// - returns `none` if there is no frontmatter
|
||||||
|
/// - returns an error if we fail to parse frontmatter
|
||||||
|
pub fn from_markdown_frontmatter(
|
||||||
|
root_node: &Node,
|
||||||
|
) -> Result<Option<PageMetadata>, serde_yaml::Error> {
|
||||||
|
root_node
|
||||||
|
.children.first()
|
||||||
|
.and_then(|x| x.cast::<FrontMatter>())
|
||||||
|
.map(|x| serde_yaml::from_str::<PageMetadata>(&x.content))
|
||||||
|
.map_or(Ok(None), |v| v.map(Some))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// MARK: page
|
// MARK: page
|
||||||
//
|
//
|
||||||
@@ -79,9 +99,10 @@ pub struct Page {
|
|||||||
pub meta: PageMetadata,
|
pub meta: PageMetadata,
|
||||||
|
|
||||||
/// How long this page's html may be cached.
|
/// How long this page's html may be cached.
|
||||||
|
/// This controls the maximum age of a page shown to the user.
|
||||||
///
|
///
|
||||||
/// If `None`, this page is always rendered from scratch.
|
/// If `None`, this page is always rendered from scratch.
|
||||||
pub html_ttl: Option<Duration>,
|
pub html_ttl: Option<TimeDelta>,
|
||||||
|
|
||||||
/// A function that generates this page's html.
|
/// A function that generates this page's html.
|
||||||
///
|
///
|
||||||
@@ -89,41 +110,40 @@ 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<dyn Send + Sync + Fn(&Self) -> Markup>,
|
pub generate_html: Box<
|
||||||
|
dyn Send
|
||||||
|
+ Sync
|
||||||
|
+ for<'a> Fn(&'a Page) -> Pin<Box<dyn Future<Output = Markup> + 'a + Send + Sync>>,
|
||||||
|
>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Page {
|
impl Default for Page {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Page {
|
Page {
|
||||||
meta: Default::default(),
|
meta: Default::default(),
|
||||||
html_ttl: Some(Duration::from_secs(60 * 24 * 30)),
|
html_ttl: Some(TimeDelta::seconds(60 * 24 * 30)),
|
||||||
//css_ttl: Duration::from_secs(60 * 24 * 30),
|
//css_ttl: Duration::from_secs(60 * 24 * 30),
|
||||||
//generate_css: None,
|
//generate_css: None,
|
||||||
generate_html: Box::new(|_| html!()),
|
generate_html: Box::new(|_| Box::pin(async { html!() })),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Page {
|
impl Page {
|
||||||
pub fn generate_html(&self) -> Markup {
|
pub async fn generate_html(&self) -> Markup {
|
||||||
(self.generate_html)(self)
|
(self.generate_html)(self).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_markdown(md: impl Into<String>, default_image: Option<String>) -> Self {
|
pub fn from_markdown(md: impl Into<String>, default_image: Option<String>) -> Self {
|
||||||
let md: String = md.into();
|
let md: String = md.into();
|
||||||
let md = Markdown::parse(&md);
|
let md = Markdown::parse(&md);
|
||||||
|
|
||||||
let mut meta = md
|
let mut meta = PageMetadata::from_markdown_frontmatter(&md)
|
||||||
.children
|
.unwrap_or(Some(PageMetadata {
|
||||||
.get(0)
|
|
||||||
.map(|x| x.cast::<FrontMatter>())
|
|
||||||
.flatten()
|
|
||||||
.map(|x| serde_yaml::from_str::<PageMetadata>(&x.content))
|
|
||||||
.unwrap_or(Ok(Default::default()))
|
|
||||||
.unwrap_or(PageMetadata {
|
|
||||||
title: "Invalid frontmatter!".into(),
|
title: "Invalid frontmatter!".into(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
}))
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
if meta.image.is_none() {
|
if meta.image.is_none() {
|
||||||
meta.image = default_image
|
meta.image = default_image
|
||||||
@@ -134,6 +154,8 @@ impl Page {
|
|||||||
Page {
|
Page {
|
||||||
meta,
|
meta,
|
||||||
generate_html: Box::new(move |page| {
|
generate_html: Box::new(move |page| {
|
||||||
|
let html = html.clone();
|
||||||
|
Box::pin(async move {
|
||||||
html! {
|
html! {
|
||||||
@if let Some(slug) = &page.meta.slug {
|
@if let Some(slug) = &page.meta.slug {
|
||||||
(Backlinks(&[("/", "home")], slug))
|
(Backlinks(&[("/", "home")], slug))
|
||||||
@@ -141,6 +163,7 @@ impl Page {
|
|||||||
|
|
||||||
(html)
|
(html)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}),
|
}),
|
||||||
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
@@ -153,35 +176,50 @@ impl Page {
|
|||||||
//
|
//
|
||||||
|
|
||||||
pub struct PageServer {
|
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 }`
|
/// Map of `{ route: page }`
|
||||||
pages: HashMap<String, Page>,
|
pages: Arc<Mutex<HashMap<String, Arc<Page>>>>,
|
||||||
|
|
||||||
/// Map of `{ route: (page data, expire time) }`
|
/// Map of `{ route: (page data, expire time) }`
|
||||||
///
|
///
|
||||||
/// We use an LruCache for bounded memory usage.
|
/// We use an LruCache for bounded memory usage.
|
||||||
html_cache: Mutex<LruCache<String, (String, DateTime<Utc>)>>,
|
html_cache: RwLock<HashMap<String, (String, DateTime<Utc>)>>,
|
||||||
|
|
||||||
/// Called whenever we need to render a page.
|
/// Called whenever we need to render a page.
|
||||||
/// - this method should call `page.generate_html()`,
|
/// - this method should call `page.generate_html()`,
|
||||||
/// - wrap the result in `<html><body>`,
|
/// - wrap the result in `<html><body>`,
|
||||||
/// - and add `<head>`
|
/// - and add `<head>`
|
||||||
/// ```
|
/// ```
|
||||||
render_page: Box<dyn Send + Sync + Fn(&Page) -> Markup>,
|
render_page: Box<
|
||||||
|
dyn Send
|
||||||
|
+ Sync
|
||||||
|
+ for<'a> Fn(&'a Page) -> Pin<Box<dyn Future<Output = Markup> + 'a + Send + Sync>>,
|
||||||
|
>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PageServer {
|
impl PageServer {
|
||||||
pub fn new(page_wrapper: Box<dyn Send + Sync + Fn(&Page) -> Markup>) -> Self {
|
pub fn new(
|
||||||
#[expect(clippy::unwrap_used)]
|
render_page: Box<
|
||||||
let cache_size = LruCache::new(NonZero::new(128).unwrap());
|
dyn Send
|
||||||
|
+ Sync
|
||||||
Self {
|
+ for<'a> Fn(&'a Page) -> Pin<Box<dyn Future<Output = Markup> + 'a + Send + Sync>>,
|
||||||
pages: HashMap::new(),
|
>,
|
||||||
html_cache: Mutex::new(cache_size),
|
) -> Arc<Self> {
|
||||||
render_page: Box::new(page_wrapper),
|
Arc::new(Self {
|
||||||
}
|
pages: Arc::new(Mutex::new(HashMap::new())),
|
||||||
|
html_cache: RwLock::new(HashMap::new()),
|
||||||
|
render_page,
|
||||||
|
never_rerender_on_request: true,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_page(mut self, route: impl Into<String>, page: Page) -> Self {
|
pub fn add_page(&self, route: impl Into<String>, page: Page) -> &Self {
|
||||||
#[expect(clippy::expect_used)]
|
#[expect(clippy::expect_used)]
|
||||||
let route = route
|
let route = route
|
||||||
.into()
|
.into()
|
||||||
@@ -189,24 +227,79 @@ impl PageServer {
|
|||||||
.expect("page route must start with /")
|
.expect("page route must start with /")
|
||||||
.to_owned();
|
.to_owned();
|
||||||
|
|
||||||
self.pages.insert(route, page);
|
self.pages.lock().insert(route, Arc::new(page));
|
||||||
|
|
||||||
self
|
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) -> Option<String> {
|
||||||
|
let now = Utc::now();
|
||||||
|
let start = Instant::now();
|
||||||
|
trace!(message = "Rendering page", route, reason);
|
||||||
|
|
||||||
|
let page = match self.pages.lock().get(route) {
|
||||||
|
Some(x) => x.clone(),
|
||||||
|
None => {
|
||||||
|
warn!(message = "Not rerendering, no such route", route, reason);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let html = (self.render_page)(&page).await.0;
|
||||||
|
|
||||||
|
if let Some(ttl) = page.html_ttl {
|
||||||
|
self.html_cache
|
||||||
|
.write()
|
||||||
|
.insert(route.to_owned(), (html.clone(), now + ttl));
|
||||||
|
}
|
||||||
|
|
||||||
|
let elapsed = start.elapsed().as_millis();
|
||||||
|
trace!(message = "Rendered page", route, reason, time_ms = elapsed);
|
||||||
|
return Some(html);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rerender considerations:
|
||||||
|
// - rerendering often in the background is wasteful. Maybe we should fall asleep?
|
||||||
|
// - rerendering on request is slow
|
||||||
|
// - rerendering in the background after a request could be a good idea. Maybe implement?
|
||||||
|
//
|
||||||
|
// - cached pages only make sense for static assets.
|
||||||
|
// - user pages can't be pre-rendered!
|
||||||
|
pub async fn start_rerender_task(self: Arc<Self>, interval: Duration) {
|
||||||
|
loop {
|
||||||
|
tokio::time::sleep(interval).await;
|
||||||
|
|
||||||
|
let now = Utc::now();
|
||||||
|
let pages = self
|
||||||
|
.pages
|
||||||
|
.lock()
|
||||||
|
.iter()
|
||||||
|
.filter(|(_, v)| v.html_ttl.is_some())
|
||||||
|
.map(|(k, _)| k.clone())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
for route in pages {
|
||||||
|
let needs_render = match self.html_cache.read().get(&route) {
|
||||||
|
Some(x) => x.1 < now, // Expired
|
||||||
|
None => true, // Never rendered
|
||||||
|
};
|
||||||
|
|
||||||
|
if needs_render {
|
||||||
|
self.render_page("rerender_task", &route).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn handler(
|
async fn handler(
|
||||||
Path(path): Path<String>,
|
Path(route): Path<String>,
|
||||||
State(state): State<Arc<Self>>,
|
State(state): State<Arc<Self>>,
|
||||||
ConnectInfo(addr): ConnectInfo<ServiceConnectInfo>,
|
ConnectInfo(addr): ConnectInfo<ServiceConnectInfo>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
trace!("Serving {path} to {}", addr.addr);
|
trace!("Serving {route} to {}", addr.addr);
|
||||||
|
|
||||||
let page = match state.pages.get(&path) {
|
|
||||||
Some(x) => x,
|
|
||||||
|
|
||||||
// TODO: 404 page
|
|
||||||
None => return (StatusCode::NOT_FOUND, "page doesn't exist").into_response(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let now = Utc::now();
|
let now = Utc::now();
|
||||||
let headers = [(
|
let headers = [(
|
||||||
@@ -214,30 +307,28 @@ impl PageServer {
|
|||||||
HeaderValue::from_static("text/html; charset=utf-8"),
|
HeaderValue::from_static("text/html; charset=utf-8"),
|
||||||
)];
|
)];
|
||||||
|
|
||||||
if let Some((html, expires)) = state.html_cache.lock().get(&path)
|
if let Some((html, expires)) = state.html_cache.read().get(&route)
|
||||||
&& *expires > now
|
&& (*expires > now || state.never_rerender_on_request)
|
||||||
{
|
{
|
||||||
// TODO: no clone?
|
// TODO: no clone?
|
||||||
return (headers, html.clone()).into_response();
|
return (headers, html.clone()).into_response();
|
||||||
};
|
};
|
||||||
|
|
||||||
debug!("Rendering {path}");
|
let html = match state.render_page("request", &route).await {
|
||||||
let html = (state.render_page)(page).0;
|
Some(x) => x.clone(),
|
||||||
|
None => return (StatusCode::NOT_FOUND, "page doesn't exist").into_response(),
|
||||||
|
};
|
||||||
|
|
||||||
if let Some(ttl) = page.html_ttl {
|
return (headers, html).into_response();
|
||||||
state.html_cache.lock().put(path, (html.clone(), now + ttl));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (headers, html.clone()).into_response();
|
pub fn into_router(self: Arc<Self>) -> Router<()> {
|
||||||
}
|
|
||||||
|
|
||||||
pub fn into_router(self) -> Router<()> {
|
|
||||||
Router::new()
|
Router::new()
|
||||||
.route(
|
.route(
|
||||||
"/",
|
"/",
|
||||||
get(|state, conn| async { Self::handler(Path(String::new()), state, conn).await }),
|
get(|state, conn| async { Self::handler(Path(String::new()), state, conn).await }),
|
||||||
)
|
)
|
||||||
.route("/{*path}", get(Self::handler))
|
.route("/{*path}", get(Self::handler))
|
||||||
.with_state(Arc::new(self))
|
.with_state(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,200 +33,10 @@ are written with this in mind.\
|
|||||||
I do not expect the average student to finish all problems during this two-hour session.
|
I do not expect the average student to finish all problems during this two-hour session.
|
||||||
If the class finishes early, the lesson is either too short or too easy.
|
If the class finishes early, the lesson is either too short or too easy.
|
||||||
|
|
||||||
|
The sources for all these handouts are available [here](https://git.betalupi.com/mark/handouts).\
|
||||||
|
Some are written in LaTeX, some are in [Typst](https://typst.app). \
|
||||||
|
The latter is vastly superior.
|
||||||
|
|
||||||
<br></br>
|
<br></br>
|
||||||
<hr></hr>
|
<hr></hr>
|
||||||
<br></br>
|
<br></br>
|
||||||
|
|
||||||
## Warm-Ups
|
|
||||||
|
|
||||||
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,
|
|
||||||
giving them something to do until we can start the lesson.
|
|
||||||
|
|
||||||
|
|
||||||
<ul id="handout-ul-Warm-Ups" class="handout-ul"></ul>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
fetch("https://git.betalupi.com/api/packages/Mark/generic/ormc-handouts/latest/index.json")
|
|
||||||
.then(res => res.json())
|
|
||||||
.then(out => {
|
|
||||||
out = out.sort((a, b) => (
|
|
||||||
a["title"].toLowerCase() < b["title"].toLowerCase()
|
|
||||||
));
|
|
||||||
|
|
||||||
out.forEach(element => {
|
|
||||||
if (element["group"] != "Warm-Ups") { return }
|
|
||||||
|
|
||||||
// Handout title
|
|
||||||
const title = document.createElement("span");
|
|
||||||
const title_a = document.createElement("strong");
|
|
||||||
title_a.appendChild(document.createTextNode(element["title"] + " "));
|
|
||||||
title.appendChild(title_a)
|
|
||||||
title.classList.add("handout-li-title");
|
|
||||||
|
|
||||||
// Handout title
|
|
||||||
const desc = document.createElement("span");
|
|
||||||
desc.appendChild(document.createTextNode(element["description"]));
|
|
||||||
desc.classList.add("handout-li-desc");
|
|
||||||
|
|
||||||
const handout_link = element["handout"];
|
|
||||||
const solutions_link = element["solutions"];
|
|
||||||
|
|
||||||
const links = document.createElement("span");
|
|
||||||
links.classList.add("handout-li-links");
|
|
||||||
const h = document.createElement("a");
|
|
||||||
h.appendChild(document.createTextNode("handout"))
|
|
||||||
h.href = handout_link;
|
|
||||||
if (solutions_link === null) {
|
|
||||||
links.appendChild(document.createTextNode("[ "));
|
|
||||||
links.appendChild(h);
|
|
||||||
links.appendChild(document.createTextNode(" ]"));
|
|
||||||
} else {
|
|
||||||
var s = document.createElement("a");
|
|
||||||
s.appendChild(document.createTextNode("solutions"))
|
|
||||||
s.href = solutions_link;
|
|
||||||
links.appendChild(document.createTextNode("[ "));
|
|
||||||
links.appendChild(h);
|
|
||||||
links.appendChild(document.createTextNode(" | "));
|
|
||||||
links.appendChild(s);
|
|
||||||
links.appendChild(document.createTextNode(" ]"));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Add to main list
|
|
||||||
const item = document.createElement("li");
|
|
||||||
item.appendChild(title)
|
|
||||||
item.appendChild(links);
|
|
||||||
//item.appendChild(desc)
|
|
||||||
const list = document.getElementById("handout-ul-Warm-Ups");
|
|
||||||
list.insertBefore(item, list.children[0]);
|
|
||||||
})}
|
|
||||||
)
|
|
||||||
.catch(err => {
|
|
||||||
// Print fallback link if we failed to load json index
|
|
||||||
console.log(err)
|
|
||||||
|
|
||||||
const title = document.createElement("span");
|
|
||||||
const title_a = document.createElement("strong");
|
|
||||||
title_a.appendChild(document.createTextNode("Error: "));
|
|
||||||
title.appendChild(title_a)
|
|
||||||
title.appendChild(document.createTextNode("failed to load handouts, something broke."))
|
|
||||||
title.classList.add("handout-li-title");
|
|
||||||
|
|
||||||
const fallback = "https://git.betalupi.com/Mark/-/packages/generic/ormc-handouts/latest";
|
|
||||||
const link = document.createElement("a");
|
|
||||||
link.href = fallback
|
|
||||||
link.appendChild(document.createTextNode("ormc-handouts"));
|
|
||||||
|
|
||||||
const item_a = document.createElement("li");
|
|
||||||
item_a.appendChild(title)
|
|
||||||
const item_b = document.createElement("li");
|
|
||||||
item_b.appendChild(document.createTextNode("Fallback link: "))
|
|
||||||
item_b.appendChild(link)
|
|
||||||
|
|
||||||
const list = document.getElementById("handout-ul-Warm-Ups");
|
|
||||||
list.insertBefore(item_b, list.children[0]);
|
|
||||||
list.insertBefore(item_a, list.children[0]);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|
||||||
<br></br>
|
|
||||||
|
|
||||||
|
|
||||||
## Advanced
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
|
|
||||||
<ul id="handout-ul-Advanced" class="handout-ul"></ul>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
fetch("https://git.betalupi.com/api/packages/Mark/generic/ormc-handouts/latest/index.json")
|
|
||||||
.then(res => res.json())
|
|
||||||
.then(out => {
|
|
||||||
out = out.sort((a, b) => (
|
|
||||||
a["title"].toLowerCase() < b["title"].toLowerCase()
|
|
||||||
));
|
|
||||||
|
|
||||||
out.forEach(element => {
|
|
||||||
if (element["group"] != "Advanced") { return }
|
|
||||||
|
|
||||||
// Handout title
|
|
||||||
const title = document.createElement("span");
|
|
||||||
const title_a = document.createElement("strong");
|
|
||||||
title_a.appendChild(document.createTextNode(element["title"] + " "));
|
|
||||||
title.appendChild(title_a)
|
|
||||||
title.classList.add("handout-li-title");
|
|
||||||
|
|
||||||
// Handout title
|
|
||||||
const desc = document.createElement("span");
|
|
||||||
desc.appendChild(document.createTextNode(element["description"]));
|
|
||||||
desc.classList.add("handout-li-desc");
|
|
||||||
|
|
||||||
const handout_link = element["handout"];
|
|
||||||
const solutions_link = element["solutions"];
|
|
||||||
|
|
||||||
const links = document.createElement("span");
|
|
||||||
links.classList.add("handout-li-links");
|
|
||||||
const h = document.createElement("a");
|
|
||||||
h.appendChild(document.createTextNode("handout"))
|
|
||||||
h.href = handout_link;
|
|
||||||
if (solutions_link === null) {
|
|
||||||
links.appendChild(document.createTextNode("[ "));
|
|
||||||
links.appendChild(h);
|
|
||||||
links.appendChild(document.createTextNode(" ]"));
|
|
||||||
} else {
|
|
||||||
var s = document.createElement("a");
|
|
||||||
s.appendChild(document.createTextNode("solutions"))
|
|
||||||
s.href = solutions_link;
|
|
||||||
links.appendChild(document.createTextNode("[ "));
|
|
||||||
links.appendChild(h);
|
|
||||||
links.appendChild(document.createTextNode(" | "));
|
|
||||||
links.appendChild(s);
|
|
||||||
links.appendChild(document.createTextNode(" ]"));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Add to main list
|
|
||||||
const item = document.createElement("li");
|
|
||||||
item.appendChild(title)
|
|
||||||
item.appendChild(links);
|
|
||||||
//item.appendChild(desc)
|
|
||||||
const list = document.getElementById("handout-ul-Advanced");
|
|
||||||
list.insertBefore(item, list.children[0]);
|
|
||||||
})}
|
|
||||||
)
|
|
||||||
.catch(err => {
|
|
||||||
// Print fallback link if we failed to load json index
|
|
||||||
console.log(err)
|
|
||||||
|
|
||||||
const title = document.createElement("span");
|
|
||||||
const title_a = document.createElement("strong");
|
|
||||||
title_a.appendChild(document.createTextNode("Error: "));
|
|
||||||
title.appendChild(title_a)
|
|
||||||
title.appendChild(document.createTextNode("failed to load handouts, something broke."))
|
|
||||||
title.classList.add("handout-li-title");
|
|
||||||
|
|
||||||
const fallback = "https://git.betalupi.com/Mark/-/packages/generic/ormc-handouts/latest";
|
|
||||||
const link = document.createElement("a");
|
|
||||||
link.href = fallback
|
|
||||||
link.appendChild(document.createTextNode("ormc-handouts"));
|
|
||||||
|
|
||||||
const item_a = document.createElement("li");
|
|
||||||
item_a.appendChild(title)
|
|
||||||
const item_b = document.createElement("li");
|
|
||||||
item_b.appendChild(document.createTextNode("Fallback link: "))
|
|
||||||
item_b.appendChild(link)
|
|
||||||
|
|
||||||
const list = document.getElementById("handout-ul-Advanced");
|
|
||||||
list.insertBefore(item_b, list.children[0]);
|
|
||||||
list.insertBefore(item_a, list.children[0]);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<br></br>
|
|
||||||
|
|||||||
173
crates/service/service-webpage/src/pages/handouts.rs
Normal file
173
crates/service/service-webpage/src/pages/handouts.rs
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
use assetserver::Asset;
|
||||||
|
use chrono::TimeDelta;
|
||||||
|
use maud::{Markup, PreEscaped, html};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use tracing::{debug, warn};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
components::{
|
||||||
|
md::Markdown,
|
||||||
|
misc::{Backlinks, FarLink},
|
||||||
|
},
|
||||||
|
page::{Page, PageMetadata},
|
||||||
|
routes::assets::Image_Icon,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct HandoutEntry {
|
||||||
|
title: String,
|
||||||
|
group: String,
|
||||||
|
handout: String,
|
||||||
|
solutions: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_index() -> Result<Vec<HandoutEntry>, reqwest::Error> {
|
||||||
|
let start = Instant::now();
|
||||||
|
let res = reqwest::get(
|
||||||
|
"https://git.betalupi.com/api/packages/Mark/generic/ormc-handouts/latest/index.json",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let res = match res {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(err) => {
|
||||||
|
warn!("Error while getting index: {err:?}");
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut res: Vec<HandoutEntry> = res.json().await?;
|
||||||
|
res.sort_by_key(|x| x.title.clone());
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
message = "Fetched handout index",
|
||||||
|
n_handouts = res.len(),
|
||||||
|
time_ms = start.elapsed().as_millis()
|
||||||
|
);
|
||||||
|
return Ok(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_list_for_group(handouts: &[HandoutEntry], group: &str) -> Markup {
|
||||||
|
html! {
|
||||||
|
ul class="handout-ul" {
|
||||||
|
|
||||||
|
@for h in handouts {
|
||||||
|
@if h.group ==group {
|
||||||
|
li {
|
||||||
|
span class="handdout-li-title" {
|
||||||
|
strong { (h.title) }
|
||||||
|
}
|
||||||
|
span class="handout-li-links" {
|
||||||
|
"[ "
|
||||||
|
|
||||||
|
@if let Some(solutions) = &h.solutions {
|
||||||
|
a href=(h.handout) {"handout"}
|
||||||
|
" | "
|
||||||
|
a href=(solutions) {"solutions"}
|
||||||
|
} @else {
|
||||||
|
a href=(h.handout) {"handout"}
|
||||||
|
}
|
||||||
|
"] "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// MARK: page
|
||||||
|
//
|
||||||
|
|
||||||
|
pub fn handouts() -> Page {
|
||||||
|
let md = Markdown::parse(include_str!("handouts.md"));
|
||||||
|
|
||||||
|
#[expect(clippy::unwrap_used)]
|
||||||
|
let mut meta = PageMetadata::from_markdown_frontmatter(&md)
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
if meta.image.is_none() {
|
||||||
|
meta.image = Some(Image_Icon::URL.to_owned());
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = PreEscaped(md.render());
|
||||||
|
|
||||||
|
Page {
|
||||||
|
meta,
|
||||||
|
html_ttl: Some(TimeDelta::seconds(300)),
|
||||||
|
|
||||||
|
generate_html: Box::new(move |page| {
|
||||||
|
let html = html.clone(); // TODO: find a way to not clone here
|
||||||
|
Box::pin(async move {
|
||||||
|
let handouts = get_index().await;
|
||||||
|
|
||||||
|
let warmups = match &handouts {
|
||||||
|
Ok(handouts) => build_list_for_group(handouts, "Warm-Ups"),
|
||||||
|
Err(error) => {
|
||||||
|
warn!("Could not load handout index: {error:?}");
|
||||||
|
html! {
|
||||||
|
span style="color:var(--yellow)" {
|
||||||
|
"Could not load handouts, something broke."
|
||||||
|
}
|
||||||
|
" "
|
||||||
|
(
|
||||||
|
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"),
|
||||||
|
Err(_) => html! {
|
||||||
|
span style="color:var(--yellow)" {
|
||||||
|
"Could not load handouts, something broke."
|
||||||
|
}
|
||||||
|
" "
|
||||||
|
(
|
||||||
|
FarLink(
|
||||||
|
"https://git.betalupi.com/Mark/-/packages/generic/ormc-handouts/latest",
|
||||||
|
"Try this direct link."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
html! {
|
||||||
|
@if let Some(slug) = &page.meta.slug {
|
||||||
|
(Backlinks(&[("/", "home")], slug))
|
||||||
|
}
|
||||||
|
|
||||||
|
(html)
|
||||||
|
|
||||||
|
(Markdown(concat!(
|
||||||
|
"## Warm-Ups",
|
||||||
|
"\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, ",
|
||||||
|
"giving them something to do until we can start the lesson.",
|
||||||
|
)))
|
||||||
|
(warmups)
|
||||||
|
br {}
|
||||||
|
|
||||||
|
(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 {}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@ pub fn index() -> Page {
|
|||||||
},
|
},
|
||||||
|
|
||||||
generate_html: Box::new(move |_page| {
|
generate_html: Box::new(move |_page| {
|
||||||
|
Box::pin(async {
|
||||||
html! {
|
html! {
|
||||||
h2 id="about" { "About" }
|
h2 id="about" { "About" }
|
||||||
|
|
||||||
@@ -70,6 +71,7 @@ pub fn index() -> Page {
|
|||||||
|
|
||||||
(Markdown(include_str!("index.md")))
|
(Markdown(include_str!("index.md")))
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}),
|
}),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
mod index;
|
|
||||||
use assetserver::Asset;
|
use assetserver::Asset;
|
||||||
pub use index::index;
|
|
||||||
|
|
||||||
use crate::{page::Page, routes::assets::Image_Icon};
|
use crate::{page::Page, routes::assets::Image_Icon};
|
||||||
|
|
||||||
|
mod handouts;
|
||||||
|
mod index;
|
||||||
|
|
||||||
|
pub use handouts::handouts;
|
||||||
|
pub use index::index;
|
||||||
|
|
||||||
pub fn links() -> Page {
|
pub fn links() -> Page {
|
||||||
/*
|
/*
|
||||||
Dead links:
|
Dead links:
|
||||||
@@ -12,19 +16,12 @@ pub fn links() -> Page {
|
|||||||
http://www.3dprintmath.com/
|
http://www.3dprintmath.com/
|
||||||
*/
|
*/
|
||||||
|
|
||||||
Page::from_markdown(include_str!("links.md"), Some(Image_Icon::URL.to_string()))
|
Page::from_markdown(include_str!("links.md"), Some(Image_Icon::URL.to_owned()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn betalupi() -> Page {
|
pub fn betalupi() -> Page {
|
||||||
Page::from_markdown(
|
Page::from_markdown(
|
||||||
include_str!("betalupi.md"),
|
include_str!("betalupi.md"),
|
||||||
Some(Image_Icon::URL.to_string()),
|
Some(Image_Icon::URL.to_owned()),
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn handouts() -> Page {
|
|
||||||
Page::from_markdown(
|
|
||||||
include_str!("handouts.md"),
|
|
||||||
Some(Image_Icon::URL.to_string()),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use std::{pin::Pin, sync::Arc, time::Duration};
|
||||||
|
|
||||||
use assetserver::Asset;
|
use assetserver::Asset;
|
||||||
use axum::Router;
|
use axum::Router;
|
||||||
use maud::{DOCTYPE, Markup, PreEscaped, html};
|
use maud::{DOCTYPE, Markup, PreEscaped, html};
|
||||||
@@ -16,20 +18,25 @@ pub(super) fn router() -> Router<()> {
|
|||||||
let (asset_prefix, asset_router) = assets::asset_router();
|
let (asset_prefix, asset_router) = assets::asset_router();
|
||||||
info!("Serving assets at {asset_prefix}");
|
info!("Serving assets at {asset_prefix}");
|
||||||
|
|
||||||
let server = build_server().into_router();
|
let server = build_server();
|
||||||
|
tokio::task::spawn(server.clone().start_rerender_task(Duration::from_secs(3)));
|
||||||
|
let router = server.into_router();
|
||||||
|
|
||||||
Router::new().merge(server).nest(asset_prefix, asset_router)
|
Router::new().merge(router).nest(asset_prefix, asset_router)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_server() -> PageServer {
|
fn build_server() -> Arc<PageServer> {
|
||||||
PageServer::new(Box::new(page_wrapper))
|
let server = PageServer::new(Box::new(page_wrapper));
|
||||||
|
server
|
||||||
.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());
|
||||||
|
server
|
||||||
}
|
}
|
||||||
|
|
||||||
fn page_wrapper(page: &Page) -> Markup {
|
fn page_wrapper<'a>(page: &'a Page) -> Pin<Box<dyn Future<Output = Markup> + 'a + Send + Sync>> {
|
||||||
|
Box::pin(async move {
|
||||||
html! {
|
html! {
|
||||||
(DOCTYPE)
|
(DOCTYPE)
|
||||||
html {
|
html {
|
||||||
@@ -47,7 +54,7 @@ fn page_wrapper(page: &Page) -> Markup {
|
|||||||
|
|
||||||
body {
|
body {
|
||||||
div class="wrapper" {
|
div class="wrapper" {
|
||||||
main { ( page.generate_html() ) }
|
main { ( page.generate_html().await ) }
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
hr class = "footline" {}
|
hr class = "footline" {}
|
||||||
@@ -69,6 +76,7 @@ fn page_wrapper(page: &Page) -> Markup {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user