Inital setup
Signed-off-by: prescientmoon <git@moonythm.dev>
This commit is contained in:
commit
9ffac14e2b
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/target
|
695
Cargo.lock
generated
Normal file
695
Cargo.lock
generated
Normal file
|
@ -0,0 +1,695 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"version_check",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "allocator-api2"
|
||||
version = "0.2.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.89"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "cached"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4d73155ae6b28cf5de4cfc29aeb02b8a1c6dab883cb015d15cd514e42766846"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"cached_proc_macro",
|
||||
"cached_proc_macro_types",
|
||||
"directories",
|
||||
"hashbrown",
|
||||
"once_cell",
|
||||
"rmp-serde",
|
||||
"serde",
|
||||
"sled",
|
||||
"thiserror",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cached_proc_macro"
|
||||
version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f42a145ed2d10dce2191e1dcf30cfccfea9026660e143662ba5eec4017d5daa"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cached_proc_macro_types"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-epoch"
|
||||
version = "0.9.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.20.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"darling_macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_core"
|
||||
version = "0.20.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.20.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "directories"
|
||||
version = "5.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35"
|
||||
dependencies = [
|
||||
"dirs-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-sys"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "fs2"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fxhash"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"allocator-api2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ident_case"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||
|
||||
[[package]]
|
||||
name = "instant"
|
||||
version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jotdown"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19fa1db3b285830176c8a940d4e3376679772bee9f58a83dd9285a4a54e30f6e"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.72"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9"
|
||||
dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.159"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5"
|
||||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
|
||||
|
||||
[[package]]
|
||||
name = "moonythm"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cached",
|
||||
"jotdown",
|
||||
"pulldown-latex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
|
||||
|
||||
[[package]]
|
||||
name = "option-ext"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
|
||||
dependencies = [
|
||||
"instant",
|
||||
"lock_api",
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot_core"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"instant",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"smallvec",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "paste"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.87"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pulldown-latex"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc65d4b47aef8d5f1903cbf2dc575ca4356c4b94da418f5428f8126038c5ad6d"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_users"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"libredox",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rmp"
|
||||
version = "0.8.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"num-traits",
|
||||
"paste",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rmp-serde"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"rmp",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.210"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.210"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sled"
|
||||
version = "0.34.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f96b4737c2ce5987354855aed3797279def4ebf734436c6aa4552cf8e169935"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-utils",
|
||||
"fs2",
|
||||
"fxhash",
|
||||
"libc",
|
||||
"log",
|
||||
"parking_lot",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.79"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.64"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.64"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.95"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"wasm-bindgen-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-backend"
|
||||
version = "0.2.95"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"log",
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.95"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.95"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.95"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d"
|
||||
|
||||
[[package]]
|
||||
name = "web-time"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||
dependencies = [
|
||||
"winapi-i686-pc-windows-gnu",
|
||||
"winapi-x86_64-pc-windows-gnu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.7.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.7.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
10
Cargo.toml
Normal file
10
Cargo.toml
Normal file
|
@ -0,0 +1,10 @@
|
|||
[package]
|
||||
name = "moonythm"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.89"
|
||||
cached = { version = "0.53.1", features = ["disk_store"] }
|
||||
jotdown = "0.6.0"
|
||||
pulldown-latex = "0.7.0"
|
16
build.py
Executable file
16
build.py
Executable file
|
@ -0,0 +1,16 @@
|
|||
#!/usr/bin/env nix-shell
|
||||
#!nix-shell -p python3 -i python3
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
shutil.rmtree("dist", ignore_errors=True)
|
||||
shutil.copytree("public", "dist")
|
||||
|
||||
with open("dist/index.html", "r") as file:
|
||||
template = file.read()
|
||||
|
||||
output = subprocess.check_output("cargo run", shell=True).decode("utf-8")
|
||||
result = template.replace("$CONTENT", output)
|
||||
|
||||
with open("dist/index.html", "w") as file:
|
||||
file.write(result)
|
190
content/arcaea.dj
Normal file
190
content/arcaea.dj
Normal file
|
@ -0,0 +1,190 @@
|
|||
# Why I love arcaea
|
||||
|
||||
## What is arcaea
|
||||
- explain the base mechanics
|
||||
|
||||
## What makes a good rhythm game
|
||||
- I don't need to reinvent the wheel here, I can link that one `mental checkpoint` video
|
||||
|
||||
## Gameplay
|
||||
### Scoring
|
||||
|
||||
#### How does scoring work?
|
||||
{% [[[ %}
|
||||
The game judges the way the player hits (or fails to do so) every note by quantizing the inputs into [a few judgements](https://arcaea.fandom.com/wiki/Scoring): a PURE judgement is awarded within a 50ms timing window, the more lenient FAR judgement has twice as much (100ms), with all the other notes being marked as LOST. Moreover, an optional tighter (25ms) timing window awards the MAX PURE (usually referred to as "shiny pure" by the community) judgement.
|
||||
|
||||
::: in-depth
|
||||
The game only awards judgements for notes in the chart. That is, tapping while no notes are nearby will not influence the score in any way! This turns out to be quite useful, leading to what is referred to as "ghost tapping", which can be a useful way to keep the rhythm or have fun with the chart. An example of this in action can be seen in [this clip](https://youtube.com/clip/UgkxY9g3nlFz8ZnPDTcn4yIofGdrFIxP3mYz?si=yI6Vs-_qpztONwtu) of [@gba553](https://www.youtube.com/@Gba553) timing the final pair of tricky notes in [ℵ~0~](https://arcaea.fandom.com/wiki/Aleph-0).
|
||||
:::
|
||||
|
||||
The score is then calculated using a 2:1 PURE to FAR ratio (that is, every PURE is awarded 2 points and every FAR is awarded 1), the result of which is scaled up so $`10,000,000` is the maximum possible score. Finally, the game awards one additional point for each MAX PURE, which is why perfect scores are usually a bit over $`10,000,000`.
|
||||
|
||||
::: in-depth
|
||||
This exact form of scoring is present in many other rhythm games. For instance, [sound voltex](https://en.wikipedia.org/wiki/Sound_Voltex) also uses the exact same judgement ratio!
|
||||
:::
|
||||
|
||||
A play with no LOST judgements is called a "full recall" (also known as a "full combo" in other games). Furthermore, a play with no FAR judgements is called a "pure memory" (also known as an "all perfect" in other games, and commonly abbreviated as a "PM"). Finally, a play where every note was hit perfectly (that is, one where everything is a MAX PURE) is called a "max pure memory" (commonly abbreviated as "MPM" by the community).
|
||||
|
||||
While full recalls and max-PMs are certainly celebrated by players and the larger community alike, PMs are usually the sweet spot between accurate play and fun which many Arcaea players strive for. This is usually the case because the game hardly rewards (if acknowledge at all) plays that are better than a PM (this will become obvious when we discuss the game's rating system).
|
||||
|
||||
::: in-depth
|
||||
Let's write the score formula in a nice, closed form!
|
||||
|
||||
Let $`m`, $`p`, $`f` and $`l` denote the amount of MAX PURE, PURE, FAR and LOST notes respectively. The final score can then be computed as
|
||||
|
||||
$$`\left\lfloor (2(m + p) + f) \frac{10'000'000}{2(m + p + f)} \right\rfloor`
|
||||
:::
|
||||
|
||||
{.label="Lagrange's extras: ζ-scoring"}
|
||||
:::: in-depth
|
||||
::: in-depth-header
|
||||
{.in-depth-icon}
|
||||
|
||||
{.in-depth-heading}
|
||||
### EX scoring
|
||||
:::
|
||||
|
||||
But what if we wanted MAX PURE notes to have a more major contribution to the score? For one, we could start by giving them their own place in the scoring ratio. What would a good ratio look like? A naive approach idea would be to keep the same rate of growth and go with a ratio of 4\:2\:1 for MAX PURE to PURE to FAR. Sadly, issues arise because this can lead to PMs possibly producing terrible scores — it's too big of a departure from the original formula. It turns out the aforementioned [sound voltex](https://en.wikipedia.org/wiki/Sound_Voltex) has already figured out a solution with their optional "EX-scoring" system, which uses a ratio of 5\:4\:2, thus awarding 1.25x the normal points for a MAX PURE.
|
||||
|
||||
Calling this "EX-scoring" in the context of Arcaea would be confusing (EX being an in-game grade and all), hence I decided to call the Arcaea equivalent of the system "ζ-scoring". In particular, we can compute the ζ-score by only knowing the base score and the number of notes in the chart (call it $`n`) as follows:
|
||||
|
||||
1. Start by finding the difference a single FAR has on the score, i.e. the ratio $`\frac{10,000,000}{2n}`.
|
||||
2. Perform euclidean division on the score by the amount calculated in step 1, producing the number of points $`2(m + p) + f` as the quotient and the number of shinies (i.e. $`m`) as the remainder (we make some assumptions about the maximum number of notes in a chart, but said assumptions haven't been broken by any chart except Testify BYD; Fortunately, Testify BYD does not break the algorithm because arcs and hold notes can only be awarded a MAX PURE or a LOST judgement)
|
||||
3. Double the quotient from step 2 and add the remainder to form the final expression of $`5m + 4p + 2f`, which can then be scaled up so $`10,000,000` is the maximum score again.
|
||||
|
||||
With a bit of care put into working around the floor function in the actual scoring formula, and performing the computations in a manner that avoids any risks of floating point arithmetic errors, one can implement a very reliable score converter. For instance, the score tracking Arcaea discord bot I'm developing has ζ-scoring well-integrated into everything!
|
||||
:::::
|
||||
{% ]]] %}
|
||||
|
||||
#### Why arcaea's scoring rocks
|
||||
{% [[[ %}
|
||||
|
||||
From the description I've written up above, it's pretty clear that Arcaea's scoring system only cares about one thing — accuracy. In contrast, certain games (notably [OSU!](https://osu.ppy.sh/), [Phigros](https://phigros.fandom.com/wiki/Phigros_Wiki), and more) take a different approach, rewarding additional points when players hit a series of multiple notes in a row, with said bonus usually scaling with the length of the so-called combo. This bonus could for example consist of a mulitplier (ala OSU!), or a linear score increase (ala Phigros). This approach does have certain advantages:
|
||||
|
||||
1. Live competitions for games like OSU! are extremely tense to watch, due to every drop in combo having a huge impact on the standings.
|
||||
2. Combo-based scoring can help certain styles of charting — for instance, more suspense can be built towards the end of a song, added emphasis can be put on certain short-but-difficult sections, etc.
|
||||
3. There's an intuitive feeling that "not messing up"=good.
|
||||
|
||||
All that being said, I think combo-based scoring is FUCKING IDIOTIC. It can BURN IN HELL. Actually, scrap that, Satan doesn't deserve the torture of dealing with combo-based scoring. Just... erase it from history... please????? God is dead, and game developers killed him when they thought combo=score was an idea worth leaving into their games.
|
||||
|
||||
Ok, sorry, I will calm down now. Back to the analytic style I go. I think combo-based scoring breaks some very nice properties I like having in my scoring system:
|
||||
|
||||
1. For combo-based scoring, a tiny change in inputs (i.e., a single dropped note in the middle of a chart) can have a dramatic impact on the final score. This feels wrong onto itself, but leads us to the next point:
|
||||
|
||||
2. For combo-based scoring, the final score can often be unexpected if the player hasn't paid attention to it throughout the chart. I'm not sure what the best way to put this is, but while playing Phigros, I very often found myself thinking "hmm, I did decently well this time, but there's a ~3.000.000 range my score could land anywhere in". In contrast, I often found myself being shockingly good at predicting my score after finishing an Arcaea play. Combos introduce more complicated arithmetic to the process, which indirectly makes the overall result less intuitive for our brains.
|
||||
|
||||
3. Combo based scoring makes playing the game annoying. I'm not kidding, every time I mess up in the exact middle of a chart in Phigros, I feel my soul leaving my body, and lose all motivation to keep playing the game. At that point, I might as well restart the chart — even if I've played perfectly thus far, there's mathematically no way for me to beat my PB from that game state anymore.
|
||||
|
||||
4. Combo based scoring encourages bad practices, like learning to cheese certain patterns by slightly sacrificing accuracy in favour of keeping the combo alive. In contrast, accuracy-based scoring encourages players to find unique ways to handle certain patterns without compromising on accuracy (for example, funky multi-finger playstyles in Arcaea).
|
||||
|
||||
5. Combo-based scoring reduces the number of intermediate goals the player can set for themselves. When I was first getting into Arcaea, I set a goal of full-recalling a FTR 7 chart. Not long after, I managed to do that on ["Brand new world"](https://www.youtube.com/watch?v=as0dIU6CCNk). While I was happy with my achievement, my score was barely an AA (that is, my accuracy was slightly higher than 95%)!
|
||||
|
||||
In a game like OSU!, I could either eye the harder goal of PMing the chart (which, at the time, I was far from skilled enough to do), or start ignoring the score and looking at accuracy instead — greatly worsening the play experience, with things like the game's UI, grading system, and rating computations all revolving around scores.
|
||||
|
||||
On the other hand, as an Arcaea player, I was able to simply set my next goal of EX-ing the chart (and so could you in any other accuracy-based scored game). Awesome!
|
||||
|
||||
Overall, I think Arcaea's (and by extension, many other similar games') scoring system is awesome, and activey contributes to the experience in a positive manner.
|
||||
|
||||
{% ]]] %}
|
||||
|
||||
### Expressivity
|
||||
{% [[[ %}
|
||||
- go through a few charts showing how unique the charting is between them
|
||||
- I feel like arcaea charts are a lot more memorable to me than other rhythm game charts, because of the added third dimenssion
|
||||
- sky note placement alone adds a lot of depth to existing patterns like streams or trills
|
||||
- wait, there's more! Talk about BYD charts
|
||||
{% ]]] %}
|
||||
|
||||
### Course mode
|
||||
- it's cool, wish there were more of them
|
||||
|
||||
### Unlocks and progression
|
||||
- Why world mode grindy:(
|
||||
- PTT gating charts is actually cool
|
||||
- Anomalies cool but I'm so bad at fray lmfao
|
||||
- mention LMW's video, and comment my quick thoughts on it
|
||||
|
||||
## Improvement
|
||||
{% [[[ %}
|
||||
### Potential
|
||||
#### How does potential work?
|
||||
|
||||
#### Potential — a potentially flawed system
|
||||
- f2p players are disadvantaged
|
||||
- r10 makes ceratin playstyles bad
|
||||
|
||||
#### PTT pushing
|
||||
- it can be fun!
|
||||
- it's harder to do without tracking
|
||||
- it can negatively effect your enjoyment of the game & your mental health
|
||||
|
||||
#### The Arcaea Online controversy
|
||||
- why b30 tracking is important
|
||||
- can help set mini challenges (raise my floor up to 12.10ptt)
|
||||
- cool for sharing around
|
||||
-
|
||||
- do you need to track your b30?
|
||||
- there used to be discord bots for b30 tracking
|
||||
- arcaea online offered an official way to do this
|
||||
- some people protested in very uncivilised manners, got banned, story ended
|
||||
- some people use spreadsheets
|
||||
- the legend says there's still working bots out there, but who knows :)
|
||||
|
||||
#### Arcaea Online — an annoying business practice
|
||||
- value is better than at launch
|
||||
- a lot of the provided value is a cure for a self-inflicted poison
|
||||
- I hate subscriptions
|
||||
- The website is horrible outside of mobile
|
||||
|
||||
#### The shimmers of the moon
|
||||
- my own way of tracking b30
|
||||
|
||||
### Collecting PMs
|
||||
- it's a super rewarding way to improve!
|
||||
- orthogonal to ptt pushing, thus can be used to take breaks from that
|
||||
|
||||
### Improvement for us mortals
|
||||
- setting your own goals (i.e. trying to EX+ every 9 when I was getting better at it)
|
||||
{% ]]] %}
|
||||
|
||||
## The story
|
||||
- gay
|
||||
- uhhh, read more of the story
|
||||
- it's not that great character-wise
|
||||
- the world building is awesome
|
||||
- I'm not a pjsekai player, but watching [thing] on youtube made me cry way more than the arcaea story ever could
|
||||
|
||||
## UI and QOL
|
||||
- They finally allow making the pause button harder to hit!
|
||||
- Pretty good offset setup
|
||||
- Some light mode charts are painful
|
||||
|
||||
## Pricing
|
||||
- omg, no gacha, so cool!
|
||||
- they do use FOMO and other toxic tactics
|
||||
- eh, still a bit expensive
|
||||
|
||||
## The community
|
||||
- one of the discord rules is a bit extreme but whatever
|
||||
- queer friendly
|
||||
|
||||
## Hardware
|
||||
### Thumb vs index play
|
||||
- thumbs = skill issue for me
|
||||
- both are viable, even thumbs on tablet is!
|
||||
- [guest paragraph by dyuan]
|
||||
|
||||
### The slipping issue
|
||||
- anti slip mats
|
||||
- Fatal's toilet paper solution
|
||||
- some people are using finger gloves (you can get them in bulk for cheap I think)
|
||||
|
||||
### Sliding across large screens
|
||||
- finger gloves fix this I guess but ion use them
|
||||
- greasy devices aleviate this a bit, but lead my fingertip skin to tear a lot
|
||||
- certain kinds of matte screen protectors fix the issue!
|
||||
|
||||
|
||||
::: in-depth
|
||||
- Not sure what the proper posture is, but I've found myself doing much better when putting some pillows on my chair so I sit slightly higher
|
||||
:::
|
58
flake.lock
Normal file
58
flake.lock
Normal file
|
@ -0,0 +1,58 @@
|
|||
{
|
||||
"nodes": {
|
||||
"flake-parts": {
|
||||
"inputs": {
|
||||
"nixpkgs-lib": "nixpkgs-lib"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1727826117,
|
||||
"narHash": "sha256-K5ZLCyfO/Zj9mPFldf3iwS6oZStJcU4tSpiXTMYaaL0=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"rev": "3d04084d54bedc3d6b8b736c70ef449225c361b1",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1728492678,
|
||||
"narHash": "sha256-9UTxR8eukdg+XZeHgxW5hQA9fIKHsKCdOIUycTryeVw=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "5633bcff0c6162b9e4b5f1264264611e950c8ec7",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-lib": {
|
||||
"locked": {
|
||||
"lastModified": 1727825735,
|
||||
"narHash": "sha256-0xHYkMkeLVQAMa7gvkddbPqpxph+hDzdu1XdGPJR+Os=",
|
||||
"type": "tarball",
|
||||
"url": "https://github.com/NixOS/nixpkgs/archive/fb192fec7cc7a4c26d51779e9bab07ce6fa5597a.tar.gz"
|
||||
},
|
||||
"original": {
|
||||
"type": "tarball",
|
||||
"url": "https://github.com/NixOS/nixpkgs/archive/fb192fec7cc7a4c26d51779e9bab07ce6fa5597a.tar.gz"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-parts": "flake-parts",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
29
flake.nix
Normal file
29
flake.nix
Normal file
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||
inputs.flake-parts.url = "github:hercules-ci/flake-parts";
|
||||
|
||||
outputs =
|
||||
inputs:
|
||||
inputs.flake-parts.lib.mkFlake { inherit inputs; } {
|
||||
systems = [ "x86_64-linux" ];
|
||||
perSystem =
|
||||
{ pkgs, lib, ... }:
|
||||
{
|
||||
devShells.default = pkgs.mkShell rec {
|
||||
nativeBuildInputs = with pkgs; [
|
||||
pkgs.cargo
|
||||
pkgs.rustc
|
||||
pkgs.clippy
|
||||
pkgs.rust-analyzer
|
||||
pkgs.rustfmt
|
||||
pkgs.ruff
|
||||
pkgs.perl538Packages.LaTeXML
|
||||
];
|
||||
|
||||
buildInputs = with pkgs; [ ];
|
||||
|
||||
LD_LIBRARY_PATH = lib.makeLibraryPath buildInputs;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
30
public/index.html
Normal file
30
public/index.html
Normal file
|
@ -0,0 +1,30 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1, shrink-to-fit=no"
|
||||
/>
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<title>Moonythm</title>
|
||||
|
||||
<link rel="stylesheet" href="styles.css" />
|
||||
|
||||
<!-- MathML -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/gh/carloskiki/pulldown-latex@0.7.0/styles.min.css"
|
||||
/>
|
||||
|
||||
<link
|
||||
rel="preload"
|
||||
href="https://cdn.jsdelivr.net/gh/carloskiki/pulldown-latex@0.7.0/font/"
|
||||
as="font"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
$CONTENT
|
||||
</body>
|
||||
</html>
|
68
public/styles.css
Normal file
68
public/styles.css
Normal file
|
@ -0,0 +1,68 @@
|
|||
html {
|
||||
font: 100%/1.5 sans-serif;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
padding-left: 1.25rem;
|
||||
border-left: 3px solid;
|
||||
}
|
||||
|
||||
.in-depth {
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
.in-depth-header {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.in-depth-header > .in-depth-heading {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.in-depth-header > * {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
img.in-depth-icon {
|
||||
height: 1.75rem;
|
||||
margin-right: 0.5rem;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
details {
|
||||
background: #ead3ed;
|
||||
border-radius: 3px;
|
||||
padding: 0.5rem 0.5rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
summary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
summary::marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
summary:before {
|
||||
content: "▶";
|
||||
font-size: 0.75rem;
|
||||
padding: 0 0.75rem 0 0.25rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
details[open] summary:before {
|
||||
content: "▼";
|
||||
}
|
||||
|
||||
details > .in-depth {
|
||||
padding-left: 1rem;
|
||||
}
|
1
rustfmt.toml
Normal file
1
rustfmt.toml
Normal file
|
@ -0,0 +1 @@
|
|||
hard_tabs=true
|
657
src/html.rs
Normal file
657
src/html.rs
Normal file
|
@ -0,0 +1,657 @@
|
|||
//! An HTML renderer that takes an iterator of [`Event`]s and emits HTML.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use jotdown::Alignment;
|
||||
use jotdown::Container;
|
||||
use jotdown::Event;
|
||||
use jotdown::LinkType;
|
||||
use jotdown::ListKind;
|
||||
use jotdown::OrderedListNumbering::*;
|
||||
use jotdown::Render;
|
||||
use jotdown::RenderRef;
|
||||
use jotdown::SpanLinkType;
|
||||
|
||||
// {{{ Renderer
|
||||
/// Render events into a string.
|
||||
pub fn render_to_string<'s, I>(events: I) -> String
|
||||
where
|
||||
I: Iterator<Item = Event<'s>>,
|
||||
{
|
||||
let mut s = String::new();
|
||||
Renderer::default().push(events, &mut s).unwrap();
|
||||
s
|
||||
}
|
||||
|
||||
/// [`Render`] implementor that writes HTML output.
|
||||
#[derive(Clone, Default)]
|
||||
pub struct Renderer {}
|
||||
|
||||
impl Render for Renderer {
|
||||
fn push<'s, I, W>(&self, mut events: I, mut out: W) -> std::fmt::Result
|
||||
where
|
||||
I: Iterator<Item = Event<'s>>,
|
||||
W: std::fmt::Write,
|
||||
{
|
||||
let mut w = Writer::new();
|
||||
events.try_for_each(|e| w.render_event(&e, &mut out))?;
|
||||
w.render_epilogue(&mut out)
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderRef for Renderer {
|
||||
fn push_ref<'s, E, I, W>(&self, mut events: I, mut out: W) -> std::fmt::Result
|
||||
where
|
||||
E: AsRef<Event<'s>>,
|
||||
I: Iterator<Item = E>,
|
||||
W: std::fmt::Write,
|
||||
{
|
||||
let mut w = Writer::new();
|
||||
events.try_for_each(|e| w.render_event(e.as_ref(), &mut out))?;
|
||||
w.render_epilogue(&mut out)
|
||||
}
|
||||
}
|
||||
// }}}
|
||||
|
||||
struct Writer<'s> {
|
||||
list_tightness: Vec<bool>,
|
||||
states: Vec<State>,
|
||||
footnotes: Footnotes<'s>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, Clone, Copy)]
|
||||
enum State {
|
||||
TextOnly,
|
||||
Ignore,
|
||||
Raw,
|
||||
Math(bool),
|
||||
}
|
||||
|
||||
impl<'s> Writer<'s> {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
list_tightness: Vec::new(),
|
||||
states: Vec::new(),
|
||||
footnotes: Footnotes::default(),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::single_match)]
|
||||
fn render_event<W>(&mut self, e: &Event<'s>, mut out: W) -> std::fmt::Result
|
||||
where
|
||||
W: std::fmt::Write,
|
||||
{
|
||||
// {{{ Handle footnotes
|
||||
if let Event::Start(Container::Footnote { label }, ..) = e {
|
||||
self.footnotes.start(label, Vec::new());
|
||||
return Ok(());
|
||||
} else if let Some(events) = self.footnotes.current() {
|
||||
if matches!(e, Event::End(Container::Footnote { .. })) {
|
||||
self.footnotes.end();
|
||||
} else {
|
||||
events.push(e.clone());
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
// }}}
|
||||
// {{{ Handle blocks which trigger the `Ignore` state.
|
||||
match e {
|
||||
Event::Start(Container::LinkDefinition { .. }, ..) => {
|
||||
self.states.push(State::Ignore);
|
||||
return Ok(());
|
||||
}
|
||||
Event::End(Container::LinkDefinition { .. }) => {
|
||||
assert_eq!(self.states.last(), Some(&State::Ignore));
|
||||
self.states.pop();
|
||||
}
|
||||
|
||||
Event::Start(Container::RawBlock { format } | Container::RawInline { format }, ..) => {
|
||||
if format == &"html" {
|
||||
self.states.push(State::Raw);
|
||||
} else {
|
||||
self.states.push(State::Ignore);
|
||||
};
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
Event::End(Container::RawBlock { format } | Container::RawInline { format }) => {
|
||||
if format == &"html" {
|
||||
assert_eq!(self.states.last(), Some(&State::Raw));
|
||||
} else {
|
||||
assert_eq!(self.states.last(), Some(&State::Ignore));
|
||||
};
|
||||
|
||||
self.states.pop();
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
// }}}
|
||||
|
||||
if self.states.last() == Some(&State::Ignore) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
match e {
|
||||
// {{{ Container start
|
||||
Event::Start(c, attrs) => {
|
||||
if self.states.last() == Some(&State::TextOnly) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
match &c {
|
||||
Container::RawBlock { .. } => unreachable!(),
|
||||
Container::RawInline { .. } => unreachable!(),
|
||||
Container::Footnote { .. } => unreachable!(),
|
||||
// {{{ List
|
||||
Container::List { kind, tight } => {
|
||||
self.list_tightness.push(*tight);
|
||||
match kind {
|
||||
ListKind::Unordered(..) | ListKind::Task(..) => out.write_str("<ul")?,
|
||||
ListKind::Ordered {
|
||||
numbering, start, ..
|
||||
} => {
|
||||
out.write_str("<ol")?;
|
||||
if *start > 1 {
|
||||
write!(out, r#" start="{}""#, start)?;
|
||||
}
|
||||
|
||||
if let Some(ty) = match numbering {
|
||||
Decimal => None,
|
||||
AlphaLower => Some('a'),
|
||||
AlphaUpper => Some('A'),
|
||||
RomanLower => Some('i'),
|
||||
RomanUpper => Some('I'),
|
||||
} {
|
||||
write!(out, r#" type="{}""#, ty)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// }}}
|
||||
// {{{ Link
|
||||
Container::Link(dst, ty) => {
|
||||
if matches!(ty, LinkType::Span(SpanLinkType::Unresolved)) {
|
||||
out.write_str("<a")?;
|
||||
} else {
|
||||
out.write_str(r#"<a href=""#)?;
|
||||
if matches!(ty, LinkType::Email) {
|
||||
out.write_str("mailto:")?;
|
||||
}
|
||||
write_attr(dst, &mut out)?;
|
||||
out.write_char('"')?;
|
||||
}
|
||||
}
|
||||
// }}}
|
||||
// {{{ Paragraph
|
||||
Container::Paragraph => {
|
||||
if self.list_tightness.last() == Some(&true) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
out.write_str("<p")?;
|
||||
}
|
||||
// }}}
|
||||
Container::Blockquote => out.write_str("<blockquote")?,
|
||||
Container::ListItem { .. } => out.write_str("<li")?,
|
||||
Container::TaskListItem { .. } => out.write_str("<li")?,
|
||||
Container::DescriptionList => out.write_str("<dl")?,
|
||||
Container::DescriptionDetails => out.write_str("<dd")?,
|
||||
Container::Table => out.write_str("<table")?,
|
||||
Container::TableRow { .. } => out.write_str("<tr")?,
|
||||
Container::Section { .. } => out.write_str("<section")?,
|
||||
Container::Div { .. } => out.write_str("<div")?,
|
||||
Container::Heading { level, .. } => write!(out, "<h{}", level)?,
|
||||
Container::TableCell { head: false, .. } => out.write_str("<td")?,
|
||||
Container::TableCell { head: true, .. } => out.write_str("<th")?,
|
||||
Container::Caption => out.write_str("<caption")?,
|
||||
Container::Image(..) => out.write_str("<img")?,
|
||||
Container::DescriptionTerm => out.write_str("<dt")?,
|
||||
Container::CodeBlock { .. } => out.write_str("<pre")?,
|
||||
Container::Span | Container::Math { .. } => out.write_str("<span")?,
|
||||
Container::Verbatim => out.write_str("<code")?,
|
||||
Container::Subscript => out.write_str("<sub")?,
|
||||
Container::Superscript => out.write_str("<sup")?,
|
||||
Container::Insert => out.write_str("<ins")?,
|
||||
Container::Delete => out.write_str("<del")?,
|
||||
Container::Strong => out.write_str("<strong")?,
|
||||
Container::Emphasis => out.write_str("<em")?,
|
||||
Container::Mark => out.write_str("<mark")?,
|
||||
Container::LinkDefinition { .. } => return Ok(()),
|
||||
}
|
||||
|
||||
// {{{ Write attributes
|
||||
let mut id_written = false;
|
||||
let mut class_written = false;
|
||||
|
||||
for (a, v) in attrs.unique_pairs() {
|
||||
write!(out, r#" {}=""#, a)?;
|
||||
v.parts().try_for_each(|part| write_attr(part, &mut out))?;
|
||||
match a {
|
||||
"class" => {
|
||||
class_written = true;
|
||||
write_class(c, true, &mut out)?;
|
||||
}
|
||||
"id" => id_written = true,
|
||||
_ => {}
|
||||
}
|
||||
out.write_char('"')?;
|
||||
}
|
||||
// }}}
|
||||
// {{{ Write default ids/classes
|
||||
if let Container::Heading {
|
||||
id,
|
||||
has_section: false,
|
||||
..
|
||||
}
|
||||
| Container::Section { id } = &c
|
||||
{
|
||||
if !id_written {
|
||||
out.write_str(r#" id=""#)?;
|
||||
write_attr(id, &mut out)?;
|
||||
out.write_char('"')?;
|
||||
}
|
||||
// TODO: do I not want this to add onto the provided class?
|
||||
} else if (matches!(c, Container::Div { class } if !class.is_empty())
|
||||
|| matches!(
|
||||
c,
|
||||
Container::Math { .. }
|
||||
| Container::List {
|
||||
kind: ListKind::Task(..),
|
||||
..
|
||||
} | Container::TaskListItem { .. }
|
||||
)) && !class_written
|
||||
{
|
||||
out.write_str(r#" class=""#)?;
|
||||
write_class(c, false, &mut out)?;
|
||||
out.write_char('"')?;
|
||||
}
|
||||
// }}}
|
||||
|
||||
match c {
|
||||
// {{{ Write css for aligning table cell text
|
||||
Container::TableCell { alignment, .. }
|
||||
if !matches!(alignment, Alignment::Unspecified) =>
|
||||
{
|
||||
let a = match alignment {
|
||||
Alignment::Unspecified => unreachable!(),
|
||||
Alignment::Left => "left",
|
||||
Alignment::Center => "center",
|
||||
Alignment::Right => "right",
|
||||
};
|
||||
write!(out, r#" style="text-align: {};">"#, a)?;
|
||||
}
|
||||
// }}}
|
||||
// {{{ Write language for codeblock
|
||||
Container::CodeBlock { language } => {
|
||||
if language.is_empty() {
|
||||
out.write_str("><code>")?;
|
||||
} else {
|
||||
out.write_str(r#"><code class="language-"#)?;
|
||||
write_attr(language, &mut out)?;
|
||||
out.write_str(r#"">"#)?;
|
||||
}
|
||||
}
|
||||
// }}}
|
||||
Container::Image(..) => out.write_str(r#" alt=""#)?,
|
||||
Container::Math { display } => {
|
||||
out.write_str(r#">"#)?;
|
||||
self.states.push(State::Math(*display));
|
||||
}
|
||||
_ => out.write_char('>')?,
|
||||
}
|
||||
|
||||
match &c {
|
||||
Container::Image(..) => {
|
||||
self.states.push(State::TextOnly);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
// }}}
|
||||
// {{{ Container end
|
||||
Event::End(c) => {
|
||||
match &c {
|
||||
Container::Image(..) => {
|
||||
assert_eq!(self.states.last(), Some(&State::TextOnly));
|
||||
self.states.pop();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if self.states.last() == Some(&State::TextOnly) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
match c {
|
||||
Container::RawBlock { .. } => unreachable!(),
|
||||
Container::RawInline { .. } => unreachable!(),
|
||||
Container::Footnote { .. } => unreachable!(),
|
||||
// {{{ List
|
||||
Container::List { kind, .. } => {
|
||||
self.list_tightness.pop();
|
||||
match kind {
|
||||
ListKind::Unordered(..) | ListKind::Task(..) => {
|
||||
out.write_str("</ul>")?
|
||||
}
|
||||
ListKind::Ordered { .. } => out.write_str("</ol>")?,
|
||||
}
|
||||
}
|
||||
// }}}
|
||||
// {{{ Paragraph
|
||||
Container::Paragraph => {
|
||||
if matches!(self.list_tightness.last(), Some(true)) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if !self.footnotes.in_epilogue() {
|
||||
out.write_str("</p>")?;
|
||||
}
|
||||
}
|
||||
// }}}
|
||||
// {{{ Image
|
||||
Container::Image(src, ..) => {
|
||||
if !src.is_empty() {
|
||||
out.write_str(r#"" src=""#)?;
|
||||
write_attr(src, &mut out)?;
|
||||
}
|
||||
|
||||
out.write_str(r#"">"#)?;
|
||||
}
|
||||
// }}}
|
||||
// {{{ Math
|
||||
Container::Math { .. } => {
|
||||
assert!(matches!(self.states.last(), Some(State::Math(_))));
|
||||
self.states.pop();
|
||||
out.write_str(r#"</span>"#)?;
|
||||
}
|
||||
// }}}
|
||||
Container::Blockquote => out.write_str("</blockquote>")?,
|
||||
Container::ListItem { .. } => out.write_str("</li>")?,
|
||||
Container::TaskListItem { .. } => out.write_str("</li>")?,
|
||||
Container::DescriptionList => out.write_str("</dl>")?,
|
||||
Container::DescriptionDetails => out.write_str("</dd>")?,
|
||||
Container::Table => out.write_str("</table>")?,
|
||||
Container::TableRow { .. } => out.write_str("</tr>")?,
|
||||
Container::Section { .. } => out.write_str("</section>")?,
|
||||
Container::Div { .. } => out.write_str("</div>")?,
|
||||
Container::Heading { level, .. } => write!(out, "</h{}>", level)?,
|
||||
Container::TableCell { head: false, .. } => out.write_str("</td>")?,
|
||||
Container::TableCell { head: true, .. } => out.write_str("</th>")?,
|
||||
Container::Caption => out.write_str("</caption>")?,
|
||||
Container::DescriptionTerm => out.write_str("</dt>")?,
|
||||
Container::CodeBlock { .. } => out.write_str("</code></pre>")?,
|
||||
Container::Span => out.write_str("</span>")?,
|
||||
Container::Link(..) => out.write_str("</a>")?,
|
||||
Container::Verbatim => out.write_str("</code>")?,
|
||||
Container::Subscript => out.write_str("</sub>")?,
|
||||
Container::Superscript => out.write_str("</sup>")?,
|
||||
Container::Insert => out.write_str("</ins>")?,
|
||||
Container::Delete => out.write_str("</del>")?,
|
||||
Container::Strong => out.write_str("</strong>")?,
|
||||
Container::Emphasis => out.write_str("</em>")?,
|
||||
Container::Mark => out.write_str("</mark>")?,
|
||||
Container::LinkDefinition { .. } => unreachable!(),
|
||||
}
|
||||
}
|
||||
// }}}
|
||||
// {{{ Raw string
|
||||
Event::Str(s) => match self.states.last() {
|
||||
Some(State::TextOnly) => write_attr(s, &mut out)?,
|
||||
Some(State::Raw) => out.write_str(s)?,
|
||||
Some(State::Math(display)) => {
|
||||
// let string: String = format!("{}{s}{}", delim, delim);
|
||||
let config = pulldown_latex::RenderConfig {
|
||||
display_mode: {
|
||||
use pulldown_latex::config::DisplayMode::*;
|
||||
if *display {
|
||||
Block
|
||||
} else {
|
||||
Inline
|
||||
}
|
||||
},
|
||||
annotation: None,
|
||||
error_color: (178, 34, 34),
|
||||
xml: true,
|
||||
math_style: pulldown_latex::config::MathStyle::TeX,
|
||||
};
|
||||
|
||||
let mut mathml = String::new();
|
||||
let storage = pulldown_latex::Storage::new();
|
||||
let parser = pulldown_latex::Parser::new(s, &storage);
|
||||
pulldown_latex::push_mathml(&mut mathml, parser, config).unwrap();
|
||||
out.write_str(&mathml)?;
|
||||
}
|
||||
_ => write_text(s, &mut out)?,
|
||||
},
|
||||
// }}}
|
||||
// {{{ Footnote reference
|
||||
Event::FootnoteReference(label) => {
|
||||
let number = self.footnotes.reference(label);
|
||||
if self.states.last() != Some(&State::TextOnly) {
|
||||
write!(
|
||||
out,
|
||||
r##"<a id="fnref{}" href="#fn{}" role="doc-noteref"><sup>{}</sup></a>"##,
|
||||
number, number, number
|
||||
)?;
|
||||
}
|
||||
}
|
||||
// }}}
|
||||
// {{{ Symbol
|
||||
Event::Symbol(sym) => write!(out, ":{}:", sym)?,
|
||||
Event::LeftSingleQuote => out.write_str("‘")?,
|
||||
Event::RightSingleQuote => out.write_str("’")?,
|
||||
Event::LeftDoubleQuote => out.write_str("“")?,
|
||||
Event::RightDoubleQuote => out.write_str("”")?,
|
||||
Event::Ellipsis => out.write_str("…")?,
|
||||
Event::EnDash => out.write_str("–")?,
|
||||
Event::EmDash => out.write_str("—")?,
|
||||
Event::NonBreakingSpace => out.write_str(" ")?,
|
||||
Event::Hardbreak => out.write_str("<br>")?,
|
||||
Event::Softbreak => out.write_char('\n')?,
|
||||
// }}}
|
||||
// {{{ Thematic break
|
||||
Event::ThematicBreak(attrs) => {
|
||||
out.write_str("<hr")?;
|
||||
for (a, v) in attrs.unique_pairs() {
|
||||
write!(out, r#" {}=""#, a)?;
|
||||
v.parts().try_for_each(|part| write_attr(part, &mut out))?;
|
||||
out.write_char('"')?;
|
||||
}
|
||||
out.write_str(">")?;
|
||||
}
|
||||
// }}}
|
||||
Event::Escape | Event::Blankline | Event::Attributes(..) => {}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// {{{ Render epilogue
|
||||
fn render_epilogue<W>(&mut self, mut out: W) -> std::fmt::Result
|
||||
where
|
||||
W: std::fmt::Write,
|
||||
{
|
||||
if self.footnotes.reference_encountered() {
|
||||
out.write_str("<section role=\"doc-endnotes\">")?;
|
||||
out.write_str("<hr>")?;
|
||||
out.write_str("<ol>")?;
|
||||
|
||||
while let Some((number, events)) = self.footnotes.next() {
|
||||
write!(out, "<li id=\"fn{}\">", number)?;
|
||||
|
||||
let mut unclosed_para = false;
|
||||
for e in events.iter().flatten() {
|
||||
if matches!(&e, Event::Blankline | Event::Escape) {
|
||||
continue;
|
||||
}
|
||||
if unclosed_para {
|
||||
// not a footnote, so no need to add href before para close
|
||||
out.write_str("</p>")?;
|
||||
}
|
||||
self.render_event(e, &mut out)?;
|
||||
unclosed_para = matches!(e, Event::End(Container::Paragraph { .. }))
|
||||
&& !matches!(self.list_tightness.last(), Some(true));
|
||||
}
|
||||
if !unclosed_para {
|
||||
// create a new paragraph
|
||||
out.write_str("<p>")?;
|
||||
}
|
||||
write!(
|
||||
out,
|
||||
"<a href=\"#fnref{}\" role=\"doc-backlink\">\u{21A9}\u{FE0E}</a></p>",
|
||||
number,
|
||||
)?;
|
||||
|
||||
out.write_str("</li>")?;
|
||||
}
|
||||
|
||||
out.write_str("</ol>")?;
|
||||
out.write_str("</section>")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
// }}}
|
||||
}
|
||||
|
||||
// {{{ Writing helpers
|
||||
fn write_class<W>(c: &Container, mut first_written: bool, out: &mut W) -> std::fmt::Result
|
||||
where
|
||||
W: std::fmt::Write,
|
||||
{
|
||||
if let Some(cls) = match c {
|
||||
Container::List {
|
||||
kind: ListKind::Task(..),
|
||||
..
|
||||
} => Some("task-list"),
|
||||
Container::TaskListItem { checked: false } => Some("unchecked"),
|
||||
Container::TaskListItem { checked: true } => Some("checked"),
|
||||
Container::Math { display: false } => Some("math inline"),
|
||||
Container::Math { display: true } => Some("math display"),
|
||||
_ => None,
|
||||
} {
|
||||
first_written = true;
|
||||
out.write_str(cls)?;
|
||||
}
|
||||
if let Container::Div { class } = c {
|
||||
if !class.is_empty() {
|
||||
if first_written {
|
||||
out.write_char(' ')?;
|
||||
}
|
||||
out.write_str(class)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_text<W>(s: &str, out: W) -> std::fmt::Result
|
||||
where
|
||||
W: std::fmt::Write,
|
||||
{
|
||||
write_escape(s, false, out)
|
||||
}
|
||||
|
||||
fn write_attr<W>(s: &str, out: W) -> std::fmt::Result
|
||||
where
|
||||
W: std::fmt::Write,
|
||||
{
|
||||
write_escape(s, true, out)
|
||||
}
|
||||
|
||||
fn write_escape<W>(mut s: &str, escape_quotes: bool, mut out: W) -> std::fmt::Result
|
||||
where
|
||||
W: std::fmt::Write,
|
||||
{
|
||||
let mut ent = "";
|
||||
while let Some(i) = s.find(|c| {
|
||||
match c {
|
||||
'<' => Some("<"),
|
||||
'>' => Some(">"),
|
||||
'&' => Some("&"),
|
||||
'"' if escape_quotes => Some("""),
|
||||
_ => None,
|
||||
}
|
||||
.map_or(false, |s| {
|
||||
ent = s;
|
||||
true
|
||||
})
|
||||
}) {
|
||||
out.write_str(&s[..i])?;
|
||||
out.write_str(ent)?;
|
||||
s = &s[i + 1..];
|
||||
}
|
||||
out.write_str(s)
|
||||
}
|
||||
// }}}
|
||||
// {{{ Footnotes
|
||||
/// Helper to aggregate footnotes for rendering at the end of the document. It will cache footnote
|
||||
/// events until they should be emitted at the end.
|
||||
///
|
||||
/// When footnotes should be rendered, they can be pulled with the [`Footnotes::next`] function in
|
||||
/// the order they were first referenced.
|
||||
#[derive(Default)]
|
||||
struct Footnotes<'s> {
|
||||
/// Stack of current open footnotes, with label and staging buffer.
|
||||
open: Vec<(&'s str, Vec<Event<'s>>)>,
|
||||
/// Footnote references in the order they were first encountered.
|
||||
references: Vec<&'s str>,
|
||||
/// Events for each footnote.
|
||||
events: HashMap<&'s str, Vec<Event<'s>>>,
|
||||
/// Number of last footnote that was emitted.
|
||||
number: usize,
|
||||
}
|
||||
|
||||
impl<'s> Footnotes<'s> {
|
||||
/// Returns `true` if any reference has been encountered.
|
||||
fn reference_encountered(&self) -> bool {
|
||||
!self.references.is_empty()
|
||||
}
|
||||
|
||||
/// Returns `true` if within the epilogue, i.e. if any footnotes have been pulled.
|
||||
fn in_epilogue(&self) -> bool {
|
||||
self.number > 0
|
||||
}
|
||||
|
||||
/// Add a footnote reference.
|
||||
fn reference(&mut self, label: &'s str) -> usize {
|
||||
self.references
|
||||
.iter()
|
||||
.position(|t| *t == label)
|
||||
.map_or_else(
|
||||
|| {
|
||||
self.references.push(label);
|
||||
self.references.len()
|
||||
},
|
||||
|i| i + 1,
|
||||
)
|
||||
}
|
||||
|
||||
/// Start aggregating a footnote.
|
||||
fn start(&mut self, label: &'s str, events: Vec<Event<'s>>) {
|
||||
self.open.push((label, events));
|
||||
}
|
||||
|
||||
/// Obtain the current (most recently started) footnote.
|
||||
fn current(&mut self) -> Option<&mut Vec<Event<'s>>> {
|
||||
self.open.last_mut().map(|(_, e)| e)
|
||||
}
|
||||
|
||||
/// End the current (most recently started) footnote.
|
||||
fn end(&mut self) {
|
||||
let (label, stage) = self.open.pop().unwrap();
|
||||
self.events.insert(label, stage);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'s> Iterator for Footnotes<'s> {
|
||||
type Item = (usize, Option<Vec<Event<'s>>>);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.references.get(self.number).map(|label| {
|
||||
self.number += 1;
|
||||
(self.number, self.events.remove(label))
|
||||
})
|
||||
}
|
||||
}
|
||||
// }}}
|
8
src/main.rs
Normal file
8
src/main.rs
Normal file
|
@ -0,0 +1,8 @@
|
|||
mod html;
|
||||
mod tex;
|
||||
fn main() {
|
||||
let djot_input = std::fs::read_to_string("content/arcaea.dj").unwrap();
|
||||
let events = jotdown::Parser::new(&djot_input);
|
||||
let html = crate::html::render_to_string(events);
|
||||
println!("{html}");
|
||||
}
|
32
src/tex.rs
Normal file
32
src/tex.rs
Normal file
|
@ -0,0 +1,32 @@
|
|||
use anyhow::anyhow;
|
||||
use cached::{proc_macro::io_cached, DiskCache};
|
||||
|
||||
fn make_cache() -> DiskCache<&'static str, String> {
|
||||
DiskCache::new("tex-cache")
|
||||
.set_disk_directory("/home/moon/.cache/moonythm")
|
||||
.build()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[io_cached(
|
||||
disk = true,
|
||||
map_error = r##"|err| anyhow::Error::from(err)"##,
|
||||
create = r"{ make_cache() }"
|
||||
)]
|
||||
pub fn compile_tex(string: &str) -> Result<String, anyhow::Error> {
|
||||
let res = std::process::Command::new("latexmlmath")
|
||||
.arg("--preload=amsfonts")
|
||||
.arg("--strict")
|
||||
.arg("--quiet")
|
||||
.arg(string)
|
||||
.output()
|
||||
.expect("failed to execute latexmathml process");
|
||||
|
||||
if res.status.success() {
|
||||
let output = String::from_utf8_lossy(&res.stdout);
|
||||
Ok(output.trim().to_owned())
|
||||
} else {
|
||||
let output = String::from_utf8_lossy(&res.stderr);
|
||||
Err(anyhow!("{}", output.trim()))
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue