diff --git a/.air.toml b/.air.toml deleted file mode 100644 index 2ab8fbe..0000000 --- a/.air.toml +++ /dev/null @@ -1,46 +0,0 @@ -root = "." -testdata_dir = "testdata" -tmp_dir = "tmp" - -[build] -args_bin = [] -bin = "./tmp/main" -cmd = "go build -o ./tmp/main ." -delay = 1000 -exclude_dir = ["assets", "tmp", "vendor", "testdata", "styles", ".git", ".github"] -exclude_file = ["flake.lock", ".envrc"] -exclude_regex = ["_test.go", "*.nix", ".git*"] -exclude_unchanged = false -follow_symlink = false -full_bin = "" -include_dir = [] -include_ext = ["go", "tpl", "tmpl", "html"] -include_file = [".env"] -kill_delay = "0s" -log = "build-errors.log" -poll = false -poll_interval = 0 -post_cmd = [] -pre_cmd = [] -rerun = false -rerun_delay = 500 -send_interrupt = false -stop_on_error = false - -[color] -app = "" -build = "yellow" -main = "magenta" -runner = "green" -watcher = "cyan" - -[log] -main_only = false -time = false - -[misc] -clean_on_exit = false - -[screen] -clear_on_rebuild = false -keep_scroll = true diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..d0ced91 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1531 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "adler32" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" + +[[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 = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45862d1c77f2228b9e10bc609d5bc203d86ebc9b87ad8d5d5167a6c9abf739d9" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "async-trait" +version = "0.1.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atom_syndication" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ee79fb83c725eae67b55218870813d2fc39fd85e4f1583848ef9f4f823cfe7c" +dependencies = [ + "chrono", + "derive_builder", + "diligent-date-parser", + "never", + "quick-xml", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "axum" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "504e3947307ac8326a5437504c517c4b56716c9d98fac0028c2acc7ca47d70ae" +dependencies = [ + "async-trait", + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper 1.0.1", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper 1.0.1", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets", +] + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bstr" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40723b8fb387abc38f4f4a37c09073622e41dd12327033091ef8950659e6dc0c" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "bytes" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" + +[[package]] +name = "cc" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" +dependencies = [ + "memchr", +] + +[[package]] +name = "cpufeatures" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca741a962e1b0bff6d724a1a0958b686406e853bb14061f218562e1896f95e6" +dependencies = [ + "libc", +] + +[[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-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[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 = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[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 = "dary_heap" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04d2cd9c18b9f454ed67da600630b021a8a80bf33f8c95896ab33aaf1c26b728" + +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "diligent-date-parser" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ede7d79366f419921e2e2f67889c12125726692a313bffb474bd5f37a581e9" +dependencies = [ + "chrono", +] + +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "pin-utils", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "globset" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15f1ce686646e7f1e19bf7d5533fe443a45dbfb990e00629110797578b42fb19" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "globwalk" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" +dependencies = [ + "bitflags", + "ignore", + "walkdir", +] + +[[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 = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "ignore" +version = "0.4.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata", + "same-file", + "walkdir", + "winapi-util", +] + +[[package]] +name = "include-flate" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df49c16750695486c1f34de05da5b7438096156466e7f76c38fcdf285cf0113e" +dependencies = [ + "include-flate-codegen", + "lazy_static", + "libflate", +] + +[[package]] +name = "include-flate-codegen" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c5b246c6261be723b85c61ecf87804e8ea4a35cb68be0ff282ed84b95ffe7d7" +dependencies = [ + "libflate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "isabelroses-website" +version = "0.2.0" +dependencies = [ + "atom_syndication", + "axum", + "chrono", + "dotenv", + "lazy_static", + "markdown", + "mime_guess", + "nom", + "rust-embed", + "serde", + "serde_json", + "tera", + "tokio", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[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 = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.162" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398" + +[[package]] +name = "libflate" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45d9dfdc14ea4ef0900c1cddbc8dcd553fbaacd8a4a282cf4018ae9dd04fb21e" +dependencies = [ + "adler32", + "core2", + "crc32fast", + "dary_heap", + "libflate_lz77", +] + +[[package]] +name = "libflate_lz77" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e0d73b369f386f1c44abd9c570d5318f55ccde816ff4b562fa452e5182863d" +dependencies = [ + "core2", + "hashbrown", + "rle-decode-fast", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "markdown" +version = "1.0.0-alpha.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6491e6c702bf7e3b24e769d800746d5f2c06a6c6a2db7992612e0f429029e81" +dependencies = [ + "unicode-id", +] + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi", + "libc", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "never" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96aba5aa877601bb3f6dd6a63a969e1f82e60646e81e71b14496995e9853c91" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.36.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pest" +version = "2.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879952a81a83930934cbf1786752d6dedc3b1f29e8f8fb2ad1d0a36f377cf442" +dependencies = [ + "memchr", + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d214365f632b123a47fd913301e14c946c61d1c183ee245fa76eb752e59a02dd" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb55586734301717aea2ac313f50b2eb8f60d2fc3dc01d190eefa2e625f60c4e" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75da2a70cf4d9cb76833c990ac9cd3923c9a8905a8929789ce347c84564d03d" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "proc-macro2" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffbfb3ddf5364c9cfcd65549a1e7b801d0e8d1b14c1a1590a6408aa93cfbfa84" +dependencies = [ + "encoding_rs", + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "rle-decode-fast" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3582f63211428f83597b51b2ddb88e2a91a9d52d12831f9d08f5e624e8977422" + +[[package]] +name = "rust-embed" +version = "8.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa66af4a4fdd5e7ebc276f115e895611a34739a9c1c01028383d612d550953c0" +dependencies = [ + "include-flate", + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6125dbc8867951125eec87294137f4e9c2c96566e61bf72c45095a7c77761478" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e5347777e9aacb56039b0e1f28785929a8a3b709e87482e7442c72e7c12529d" +dependencies = [ + "globset", + "sha2", + "walkdir", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustversion" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "serde" +version = "1.0.215" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.215" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.132" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" +dependencies = [ + "itoa", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[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.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "sync_wrapper" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" + +[[package]] +name = "tera" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab9d851b45e865f178319da0abdbfe6acbc4328759ff18dafc3a41c16b4cd2ee" +dependencies = [ + "globwalk", + "lazy_static", + "pest", + "pest_derive", + "regex", + "serde", + "serde_json", + "unic-segment", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio" +version = "1.41.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" +dependencies = [ + "backtrace", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tower" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2873938d487c3cfb9aed7546dc9f2711d867c9f90c46b889989a2cb84eba6b4f" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper 0.1.2", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "log", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-segment" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4ed5d26be57f84f176157270c112ef57b86debac9cd21daaabbe56db0f88f23" +dependencies = [ + "unic-ucd-segment", +] + +[[package]] +name = "unic-ucd-segment" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2079c122a62205b421f499da10f3ee0f7697f012f55b675e002483c73ea34700" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicase" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" + +[[package]] +name = "unicode-id" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10103c57044730945224467c09f71a4db0071c123a0648cc3e818913bde6b561" + +[[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 = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.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 = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[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", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..fdc52b7 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "isabelroses-website" +version = "0.2.0" +license = "MIT" +description = "my lovely website" +homepage = "https://github.com/isabelroses/website" +authors = ["Isabel Roses "] +edition = "2021" + +[dependencies] +axum = "0.7.7" +dotenv = "0.15.0" +lazy_static = "1.5.0" +markdown = "1.0.0-alpha.21" +mime_guess = "2.0.5" +nom = "7.1.3" +atom_syndication = "0.12.5" +rust-embed = { version = "8.5.0", features = ["include-exclude", "compression"] } +serde = { version = "1.0.132", features = ["derive"] } +serde_json = "1.0.132" +tera = { version = "1.20.0", default-features = false } +tokio = { version = "1.41.1", features = ["rt-multi-thread"] } +chrono = "0.4.38" + +[lints.clippy] +all = "warn" +pedantic = "warn" +similar_names = { level = "allow", priority = 1 } diff --git a/api/github.go b/api/github.go deleted file mode 100644 index dc3a6c2..0000000 --- a/api/github.go +++ /dev/null @@ -1,53 +0,0 @@ -package api - -import ( - "net/http" - - "github.com/labstack/echo/v4" - "isabelroses.com/lib" -) - -func Github(c echo.Context) error { - g := new(lib.GitHubDono) - - if err := c.Bind(g); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) - } - - if g.Sponsorship.PrivaryLevel == "SECRET" { - return c.String(http.StatusOK, "Accepted, and hidden") - } - - if g.Action != "created" { - return c.String(http.StatusOK, "Not a new sponsorship") - } - - var newData map[string]interface{} - switch { - case g.Sponsorship.Tier.IsOneTime: - newData = map[string]interface{}{ - "tier": "OneTime", - "name": getName(g.Sponsorship.Sponsor), - "url": g.Sponsorship.Sponsor.HTMLURL, - "avatar": g.Sponsorship.Sponsor.AvatarURL, - } - case !g.Sponsorship.Tier.IsOneTime: - newData = map[string]interface{}{ - "tier": g.Sponsorship.Tier.Name, - "name": getName(g.Sponsorship.Sponsor), - "url": g.Sponsorship.Sponsor.HTMLURL, - "avatar": g.Sponsorship.Sponsor.AvatarURL, - } - } - - lib.AppendToDonos(newData) - - return c.JSON(http.StatusOK, g) -} - -func getName(sponsor lib.GitHubSponsor) string { - if sponsor.Name != "" { - return sponsor.Name - } - return sponsor.Login -} diff --git a/api/kofi.go b/api/kofi.go deleted file mode 100644 index c8c17ac..0000000 --- a/api/kofi.go +++ /dev/null @@ -1,54 +0,0 @@ -package api - -import ( - "crypto/sha256" - "encoding/hex" - "net/http" - "strings" - - "isabelroses.com/lib" - - "github.com/labstack/echo/v4" -) - -func Kofi(c echo.Context) error { - k := new(lib.KofiDono) - - if err := c.Bind(k); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) - } - - if !k.IsPublic { - return c.String(http.StatusOK, "Accepted, and hidden") - } - - var newData map[string]interface{} - switch k.Type { - case "Donation": - newData = map[string]interface{}{ - "tier": "OneTime", - "name": k.FromName, - "url": k.URL, - "avatar": hashString(k.Email), - } - case "Subscription": - if k.IsFirstSubscription { - newData = map[string]interface{}{ - "tier": k.TierName, - "name": k.FromName, - "url": k.URL, - "avatar": hashString(k.Email), - } - } - } - - lib.AppendToDonos(newData) - - return c.JSON(http.StatusOK, k) -} - -func hashString(s string) string { - ls := strings.ToLower(s) - bs := sha256.Sum256([]byte(ls)) - return hex.EncodeToString(bs[:]) -} diff --git a/lib/content/2023-wrapped.md b/content/2023-wrapped.md similarity index 100% rename from lib/content/2023-wrapped.md rename to content/2023-wrapped.md diff --git a/lib/content/LICENCE b/content/LICENCE similarity index 100% rename from lib/content/LICENCE rename to content/LICENCE diff --git a/lib/content/experimenting-with-nix.md b/content/experimenting-with-nix.md similarity index 100% rename from lib/content/experimenting-with-nix.md rename to content/experimenting-with-nix.md diff --git a/lib/content/my-journey-so-far.md b/content/my-journey-so-far.md similarity index 100% rename from lib/content/my-journey-so-far.md rename to content/my-journey-so-far.md diff --git a/lib/content/my-workflow.md b/content/my-workflow.md similarity index 100% rename from lib/content/my-workflow.md rename to content/my-workflow.md diff --git a/lib/content/nix-shells.md b/content/nix-shells.md similarity index 100% rename from lib/content/nix-shells.md rename to content/nix-shells.md diff --git a/lib/content/nixos-and-postgresql.md b/content/nixos-and-postgresql.md similarity index 100% rename from lib/content/nixos-and-postgresql.md rename to content/nixos-and-postgresql.md diff --git a/lib/content/self-healing-urls.md b/content/self-healing-urls.md similarity index 100% rename from lib/content/self-healing-urls.md rename to content/self-healing-urls.md diff --git a/lib/content/writing-a-neovim-plugin.md b/content/writing-a-neovim-plugin.md similarity index 100% rename from lib/content/writing-a-neovim-plugin.md rename to content/writing-a-neovim-plugin.md diff --git a/go.mod b/go.mod deleted file mode 100644 index 3d1d7d3..0000000 --- a/go.mod +++ /dev/null @@ -1,28 +0,0 @@ -module isabelroses.com - -go 1.21.5 - -require ( - github.com/gorilla/feeds v1.2.0 - github.com/joho/godotenv v1.5.1 - github.com/labstack/echo/v4 v4.12.0 - github.com/yuin/goldmark-meta v1.1.0 -) - -require gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect - -require ( - github.com/golang-jwt/jwt v3.2.2+incompatible // indirect - github.com/labstack/gommon v0.4.2 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasttemplate v1.2.2 // indirect - github.com/yuin/goldmark v1.7.4 - golang.org/x/crypto v0.28.0 // indirect - golang.org/x/net v0.30.0 // indirect - golang.org/x/sys v0.26.0 // indirect - golang.org/x/text v0.19.0 // indirect - golang.org/x/time v0.7.0 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect -) diff --git a/go.sum b/go.sum deleted file mode 100644 index bb861cd..0000000 --- a/go.sum +++ /dev/null @@ -1,54 +0,0 @@ -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= -github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= -github.com/gorilla/feeds v1.2.0 h1:O6pBiXJ5JHhPvqy53NsjKOThq+dNFm8+DFrxBEdzSCc= -github.com/gorilla/feeds v1.2.0/go.mod h1:WMib8uJP3BbY+X8Szd1rA5Pzhdfh+HCCAYT2z7Fza6Y= -github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= -github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0= -github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM= -github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= -github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= -github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= -github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= -github.com/yuin/goldmark v1.7.4 h1:BDXOHExt+A7gwPCJgPIIq7ENvceR7we7rOS9TNoLZeg= -github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= -github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUeiOUc= -github.com/yuin/goldmark-meta v1.1.0/go.mod h1:U4spWENafuA7Zyg+Lj5RqK/MF+ovMYtBvXi1lBb2VP0= -golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= -golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= -golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= -golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= -golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= -golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= -golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/lib/blog.go b/lib/blog.go deleted file mode 100644 index 8ef0e31..0000000 --- a/lib/blog.go +++ /dev/null @@ -1,158 +0,0 @@ -package lib - -import ( - "bytes" - "embed" - "fmt" - "html/template" - "io/fs" - "log" - "sort" - "strings" - "time" - - "github.com/yuin/goldmark" - meta "github.com/yuin/goldmark-meta" - "github.com/yuin/goldmark/extension" - "github.com/yuin/goldmark/parser" - "github.com/yuin/goldmark/renderer/html" -) - -//go:embed content/* -var content embed.FS - -func GetBlogPosts() Posts { - var posts Posts - - files, err := fs.ReadDir(content, "content") - if err != nil { - log.Fatalf("failed to open file: %v", err) - } - - for _, file := range files { - if strings.HasSuffix(file.Name(), ".md") { - content, err := fs.ReadFile(content, "content/"+file.Name()) - if err != nil { - log.Fatal(err) - } - - post := createPost(content, file.Name()) - posts = append(posts, post) - } - } - - sortPosts(posts) - - return posts -} - -func createPost(content []byte, fileName string) Post { - markdown := goldmark.New( - goldmark.WithExtensions( - extension.GFM, - meta.Meta, - NewCodewrap(), - ), - goldmark.WithParserOptions( - parser.WithAutoHeadingID(), - ), - goldmark.WithRendererOptions( - html.WithXHTML(), - ), - ) - - var buf bytes.Buffer - context := parser.NewContext() - if err := markdown.Convert(content, &buf, parser.WithContext(context)); err != nil { - log.Fatal(err) - } - metaData := meta.Get(context) - - post := Post{} - post.Content = template.HTML(buf.String()) - post.Title = metaData["title"].(string) - post.Date = metaData["date"].(string) - post.Description = metaData["description"].(string) - post.Slug = fmt.Sprintf("%v", strings.TrimSuffix(fileName, ".md")) - - tagsInterface, ok := metaData["tags"].([]interface{}) - if !ok { - log.Fatal(ok) - } - - // Convert each element from interface{} to string - var tags []string - for _, tag := range tagsInterface { - if str, ok := tag.(string); ok { - tags = append(tags, str) - } else { - log.Fatal(ok) - } - } - post.Tags = tags - - return post -} - -func sortPosts(posts Posts) { - const layout = "02/01/2006" - - sort.Slice(posts, func(i, j int) bool { - iDate, err := time.Parse(layout, posts[i].Date) - if err != nil { - log.Fatal(err) - } - - jDate, err := time.Parse(layout, posts[j].Date) - if err != nil { - log.Fatal(err) - } - - return iDate.Before(jDate) - }) - - for i, post := range posts { - post.ID = i + 1 - post.Slug = fmt.Sprintf("%v-%v", post.Slug, post.ID) - posts[i] = post - } - - for i := 0; i < len(posts)/2; i++ { - posts[i], posts[len(posts)-i-1] = posts[len(posts)-i-1], posts[i] - } -} - -func (posts Posts) FilterByTag(tag string) Posts { - var filteredPosts Posts - - for _, post := range posts { - if contains(post.Tags, tag) { - filteredPosts = append(filteredPosts, post) - } - } - - return filteredPosts -} - -func GetAllBlogTags() []string { - var tags []string - - for _, post := range GetBlogPosts() { - for _, tag := range post.Tags { - if !contains(tags, tag) { - tags = append(tags, tag) - } - } - } - - return tags -} - -func contains(tags []string, tag string) bool { - for _, t := range tags { - if t == tag { - return true - } - } - return false -} diff --git a/lib/codeparser.go b/lib/codeparser.go deleted file mode 100644 index 4f3a172..0000000 --- a/lib/codeparser.go +++ /dev/null @@ -1,208 +0,0 @@ -package lib - -import ( - "github.com/yuin/goldmark" - "github.com/yuin/goldmark/ast" - "github.com/yuin/goldmark/parser" - "github.com/yuin/goldmark/renderer" - "github.com/yuin/goldmark/renderer/html" - "github.com/yuin/goldmark/text" - "github.com/yuin/goldmark/util" -) - -type codeBlockContext struct { - attributes ImmutableAttributes - language []byte -} - -func newCodeBlockContext(language []byte, attrs ImmutableAttributes) CodeBlockContext { - return &codeBlockContext{ - language: language, - attributes: attrs, - } -} - -// CodeBlockContext holds contextual information of code highlighting. -type CodeBlockContext interface { - // Language returns (language, true) if specified, otherwise (nil, false). - Language() ([]byte, bool) - - // Attributes return attributes of the code block. - Attributes() ImmutableAttributes -} - -func (c *codeBlockContext) Attributes() ImmutableAttributes { - return c.attributes -} - -func (c *codeBlockContext) Language() ([]byte, bool) { - if c.language != nil { - return c.language, true - } - return nil, false -} - -// ImmutableAttributes is a read-only interface for ast.Attributes. -type ImmutableAttributes interface { - // Get returns (value, true) if an attribute associated with given - // name exists, otherwise (nil, false) - Get(name []byte) (interface{}, bool) - - // GetString returns (value, true) if an attribute associated with given - // name exists, otherwise (nil, false) - GetString(name string) (interface{}, bool) - - // All returns all attributes. - All() []ast.Attribute -} - -type immutableAttributes struct { - n ast.Node -} - -func (a *immutableAttributes) Get(name []byte) (interface{}, bool) { - return a.n.Attribute(name) -} - -func (a *immutableAttributes) GetString(name string) (interface{}, bool) { - return a.n.AttributeString(name) -} - -func (a *immutableAttributes) All() []ast.Attribute { - if a.n.Attributes() == nil { - return []ast.Attribute{} - } - return a.n.Attributes() -} - -// Config struct holds options for the extension. -type Config struct { - html.Config -} - -// NewConfig returns a new Config with defaults. -func NewConfig() Config { - return Config{ - Config: html.NewConfig(), - } -} - -// SetOption implements renderer.SetOptioner. -func (c *Config) SetOption(name renderer.OptionName, value interface{}) { - c.Config.SetOption(name, value) -} - -// Option interface is a functional option interface for the extension. -type Option interface { - renderer.Option -} - -type withHTMLOptions struct { - value []html.Option -} - -func (o *withHTMLOptions) SetConfig(c *renderer.Config) { - if o.value != nil { - for _, v := range o.value { - v.(renderer.Option).SetConfig(c) - } - } -} - -// WithHTMLOptions is functional option that wraps goldmark HTMLRenderer options. -func WithHTMLOptions(opts ...html.Option) Option { - return &withHTMLOptions{opts} -} - -// HTMLRenderer struct is a renderer.NodeRenderer implementation for the extension. -type HTMLRenderer struct { - Config -} - -// NewHTMLRenderer builds a new HTMLRenderer with given options and returns it. -func NewHTMLRenderer() renderer.NodeRenderer { - r := &HTMLRenderer{ - Config: NewConfig(), - } - return r -} - -// RegisterFuncs implements NodeRenderer.RegisterFuncs. -func (r *HTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { - reg.Register(ast.KindFencedCodeBlock, r.renderFencedCodeBlock) -} - -func getAttributes(node *ast.FencedCodeBlock, infostr []byte) ImmutableAttributes { - if node.Attributes() != nil { - return &immutableAttributes{node} - } - if infostr != nil { - attrStartIdx := -1 - - for idx, char := range infostr { - if char == '{' { - attrStartIdx = idx - break - } - } - if attrStartIdx > 0 { - n := ast.NewTextBlock() // dummy node for storing attributes - attrStr := infostr[attrStartIdx:] - if attrs, hasAttr := parser.ParseAttributes(text.NewReader(attrStr)); hasAttr { - for _, attr := range attrs { - n.SetAttribute(attr.Name, attr.Value) - } - return &immutableAttributes{n} - } - } - } - return nil -} - -func (r *HTMLRenderer) renderFencedCodeBlock(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { - n := node.(*ast.FencedCodeBlock) - if !entering { - return ast.WalkContinue, nil - } - - _, _ = w.WriteString("
')
-
-	l := n.Lines().Len()
-	for i := 0; i < l; i++ {
-		line := n.Lines().At(i)
-		r.Writer.RawWrite(w, line.Value(source))
-	}
-
-	_, _ = w.WriteString("
\n") - - return ast.WalkContinue, nil -} - -type codewrap struct { - options []Option -} - -var Codewrap = &codewrap{ - options: []Option{}, -} - -// NewCodewrap returns a new extension with given options. -func NewCodewrap(opts ...Option) goldmark.Extender { - return &codewrap{ - options: opts, - } -} - -// Extend implements goldmark.Extender. -func (e *codewrap) Extend(m goldmark.Markdown) { - m.Renderer().AddOptions(renderer.WithNodeRenderers( - util.Prioritized(NewHTMLRenderer(), 200), - )) -} diff --git a/lib/donos.go b/lib/donos.go deleted file mode 100644 index 8d5abcd..0000000 --- a/lib/donos.go +++ /dev/null @@ -1,80 +0,0 @@ -package lib - -import ( - "bufio" - "encoding/json" - "io" - "log" - "os" -) - -var donosList = GetServePath("/donos.json") - -func readDonoFile() []string { - file, err := os.Open(donosList) - if err != nil { - log.Fatal(err) - return nil - } - defer file.Close() - - scanner := bufio.NewScanner(file) - - var all []string - for scanner.Scan() { - line := scanner.Text() - all = append(all, line) - } - - return all -} - -func GetDonors() Donors { - file, err := os.Open(donosList) - if err != nil { - log.Println("Error opening file:", err) - return nil - } - defer file.Close() - - content, err := io.ReadAll(file) - if err != nil { - log.Println("Error reading file:", err) - return nil - } - - var donors Donors - err = json.Unmarshal(content, &donors) - if err != nil { - log.Println("Error parsing JSON:", err) - return nil - } - - return donors -} - -func AppendToDonos(newData map[string]interface{}) { - file, err := os.OpenFile(donosList, os.O_RDWR|os.O_CREATE, 0644) - if err != nil { - log.Fatal(err) - } - defer file.Close() - - var existingData []map[string]interface{} - if err = json.NewDecoder(file).Decode(&existingData); err != nil && err.Error() != "EOF" { - log.Println("Error decoding JSON:", err) - } - existingData = append(existingData, newData) - - // Move the file pointer to the beginning - _, err = file.Seek(0, 0) - if err != nil { - log.Println("Error seeking file:", err) - } - - encoder := json.NewEncoder(file) - encoder.SetIndent("", " ") - if err := encoder.Encode(existingData); err != nil { - log.Println("Error encoding JSON:", err) - } -} diff --git a/lib/feeds.go b/lib/feeds.go deleted file mode 100644 index 0519c37..0000000 --- a/lib/feeds.go +++ /dev/null @@ -1,47 +0,0 @@ -package lib - -import ( - "time" - - "github.com/gorilla/feeds" -) - -func genFeed() *feeds.Feed { - feed := &feeds.Feed{ - Title: "Isabel Roses", - Link: &feeds.Link{Href: "https://isabelroses.com", Rel: "self"}, - Description: "Isabel Roses' blog", - Author: &feeds.Author{Name: "Isabel Roses", Email: "isabel@isabelroses.com"}, - Created: time.Now(), - } - - posts := GetBlogPosts() - - var feedItems []*feeds.Item - for _, post := range posts { - created, _ := time.Parse("02/01/2006", post.Date) - _ = created.Format(time.RFC3339) - href := "https://isabelroses.com/blog/" + post.Slug - feedItems = append(feedItems, &feeds.Item{ - Id: href, - Title: post.Title, - Link: &feeds.Link{Href: href, Rel: "self"}, - Content: string(post.Content), - Created: created, - }) - } - feed.Items = feedItems - - return feed -} - -func AtomFeed() *feeds.AtomFeed { - feed := (&feeds.Atom{Feed: genFeed()}).AtomFeed() - atomFeed := feed.FeedXml() - return atomFeed.(*feeds.AtomFeed) -} - -func JSONFeed() *feeds.JSONFeed { - feed := (&feeds.JSON{Feed: genFeed()}).JSONFeed() - return feed -} diff --git a/lib/pfp.go b/lib/pfp.go deleted file mode 100644 index 3d5e76a..0000000 --- a/lib/pfp.go +++ /dev/null @@ -1,6 +0,0 @@ -package lib - -import _ "embed" - -//go:embed pfp.txt -var Pfp []byte diff --git a/lib/pfp.txt b/lib/pfp.txt deleted file mode 100644 index b576ca9..0000000 --- a/lib/pfp.txt +++ /dev/null @@ -1,16 +0,0 @@ -[?25l▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ -▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ -▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ -▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ -▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ -▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ -▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ -▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ -▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ -▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ -▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ -▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ -▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ -▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ -▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ -[?25h diff --git a/lib/settings.go b/lib/settings.go deleted file mode 100644 index 87f7f2b..0000000 --- a/lib/settings.go +++ /dev/null @@ -1,11 +0,0 @@ -package lib - -import ( - "os" - - _ "github.com/joho/godotenv/autoload" -) - -func GetServePath(path string) string { - return os.Getenv("SERVE_DIR") + path -} diff --git a/lib/types.go b/lib/types.go deleted file mode 100644 index ac7ffc9..0000000 --- a/lib/types.go +++ /dev/null @@ -1,90 +0,0 @@ -package lib - -import "html/template" - -type IconLink struct { - Name string - Href string - Icon template.HTML -} - -type Project struct { - Name string - Description string - Href string - Repo string - Icon string - Banner string -} - -type Post struct { - Title string - Description string - Content template.HTML - Date string - Slug string - Tags []string - ID int -} - -type Posts []Post - -type Subscriber struct { - Tier string - Name string - Avatar string -} - -type Subscribers []Subscriber - -type KofiDono struct { - Message string `json:"message"` - ShopItems string `json:"shop_items"` - Timestamp string `json:"timestamp"` - Type string `json:"type"` - VerificationToken string `json:"verification_token"` - FromName string `json:"from_name"` - MessageID string `json:"message_id"` - Amount string `json:"amount"` - Currency string `json:"currency"` - Email string `json:"email"` - URL string `json:"url"` - Shipping string `json:"shipping"` - TierName string `json:"tier_name"` - TransactionID string `json:"kofi_transaction_id"` - IsPublic bool `json:"is_public"` - IsFirstSubscription bool `json:"is_first_subscription_payment"` - IsSubscription bool `json:"is_subscription_payment"` -} - -type GitHubDono struct { - Action string `json:"action"` - - Sponsorship struct { - CreatedAt string `json:"created_at"` - PrivaryLevel string `json:"privacy_level"` - - Sponsor GitHubSponsor `json:"sponsor"` - - Tier struct { - Name string `json:"name"` - IsOneTime bool `json:"is_one_time"` - } `json:"tier"` - } `json:"sponsorship"` -} - -type GitHubSponsor struct { - AvatarURL string `json:"avatar_url"` - Login string `json:"login"` - HTMLURL string `json:"html_url"` - Name string `json:"name"` -} - -type Donor struct { - Tier string - Name string - URL string - Avatar string -} - -type Donors []Donor diff --git a/main.go b/main.go deleted file mode 100644 index d2da897..0000000 --- a/main.go +++ /dev/null @@ -1,93 +0,0 @@ -package main - -import ( - "embed" - "fmt" - "io/fs" - "net/http" - "strings" - - "isabelroses.com/api" - "isabelroses.com/lib" - "isabelroses.com/pages" - - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" -) - -func customHTTPErrorHandler(err error, c echo.Context) { - // Ignore the error for requests to /api URLs - if strings.Contains(c.Path(), "/api") { - c.Logger().Error(err) - return - } - - code := http.StatusInternalServerError - if he, ok := err.(*echo.HTTPError); ok { - code = he.Code - } - c.Logger().Error(err) - if err := c.Render(code, fmt.Sprintf("%v", pages.ErrorPage(c, code)), nil); err != nil { - c.Logger().Error(err) - } -} - -//go:embed public/* -var PubFS embed.FS - -func main() { - e := echo.New() - - e.Use(middleware.Logger()) - e.Use(middleware.Recover()) - e.Use(middleware.RequestID()) - e.Use(middleware.Secure()) - e.Pre(middleware.RemoveTrailingSlash()) - e.Use(middleware.GzipWithConfig(middleware.GzipConfig{ - Level: 5, - })) - e.Use(middleware.RateLimiter(middleware.NewRateLimiterMemoryStore(50))) - - e.HTTPErrorHandler = customHTTPErrorHandler - - e.GET("/", pages.Home) - - e.GET("/projects", pages.Projects) - - e.GET("/donations", pages.Donos) - - e.GET("/2004", pages.A2004) - - blogGroup := e.Group("/blog") - blogGroup.GET("", pages.Blog) - blogGroup.GET("/:slug", pages.Post) - blogGroup.GET("/tag/:tag", pages.Blog) - - apiGroup := e.Group("/api") - apiGroup.POST("/kofi", api.Kofi) - apiGroup.POST("/github", api.Github) - - e.GET("/rss.xml", func(c echo.Context) error { - c.Redirect(http.StatusMovedPermanently, "/feed.xml") - return nil - }) - - e.GET("/feed.xml", func(c echo.Context) error { - atom := lib.AtomFeed() - return c.XML(http.StatusOK, atom) - }) - - e.GET("/feed.json", func(c echo.Context) error { - json := lib.JSONFeed() - return c.JSON(http.StatusOK, json) - }) - - strippedFS, err := fs.Sub(PubFS, "public") - if err != nil { - panic(err) - } - fs := http.FS(strippedFS) - e.GET("/*", echo.WrapHandler(http.FileServer(fs))) - - e.Logger.Fatal(e.Start(":3000")) -} diff --git a/nix/default.nix b/nix/default.nix index 643ea99..deda045 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -1,30 +1,26 @@ -{ lib, buildGoModule }: -buildGoModule { +{ lib, rustPlatform }: +let + toml = (lib.importTOML ../Cargo.toml).package; +in +rustPlatform.buildRustPackage { pname = "isabelroses-website"; - version = "0.1.1"; + inherit (toml) version; src = lib.fileset.toSource { root = ../.; fileset = lib.fileset.intersection (lib.fileset.fromSource (lib.sources.cleanSource ../.)) ( lib.fileset.unions [ - ../go.mod - ../go.sum - ../main.go - ../api - ../lib - ../pages - ../public + ../Cargo.toml + ../Cargo.lock + ../src ../templates + ../content + ../static ] ); }; - vendorHash = "sha256-s+lePVX0JNVkLmDYK1WGMsZd8vY7gKt4GP0CPkkgyfo="; - - ldflags = [ - "-s" - "-w" - ]; + cargoLock.lockFile = ../Cargo.lock; meta = { description = "isabelroses.com"; diff --git a/nix/module.nix b/nix/module.nix index 2d6bbd0..9101691 100644 --- a/nix/module.nix +++ b/nix/module.nix @@ -22,7 +22,7 @@ in Type = "simple"; ReadWritePaths = [ "/srv/storage/isabelroses.com" ]; DynamicUser = true; - ExecStart = "${getExe self.packages.${pkgs.stdenv.hostPlatform.system}.default}"; + ExecStart = getExe self.packages.${pkgs.stdenv.hostPlatform.system}.default; Restart = "always"; }; }; diff --git a/nix/shell.nix b/nix/shell.nix index 863a928..37002ee 100644 --- a/nix/shell.nix +++ b/nix/shell.nix @@ -1,20 +1,18 @@ { - go, bun, - air, - gopls, - typos, + clippy, + rustfmt, callPackage, + rust-analyzer, }: let mainPkg = callPackage ./default.nix { }; in mainPkg.overrideAttrs (oa: { nativeBuildInputs = [ - go bun - air - gopls - typos + clippy + rustfmt + rust-analyzer ] ++ (oa.nativeBuildInputs or [ ]); }) diff --git a/pages/2004.go b/pages/2004.go deleted file mode 100644 index c09138d..0000000 --- a/pages/2004.go +++ /dev/null @@ -1,11 +0,0 @@ -package pages - -import ( - "github.com/labstack/echo/v4" - - "isabelroses.com/templates" -) - -func A2004(c echo.Context) error { - return templates.RenderTemplate(c.Response().Writer, "stripped", nil, nil) -} diff --git a/pages/blog.go b/pages/blog.go deleted file mode 100644 index b1af6eb..0000000 --- a/pages/blog.go +++ /dev/null @@ -1,34 +0,0 @@ -package pages - -import ( - "github.com/labstack/echo/v4" - - "isabelroses.com/lib" - "isabelroses.com/templates" -) - -type BlogProps struct { - Tag string - Posts lib.Posts - Tags []string -} - -func Blog(c echo.Context) error { - tag := c.Param("tag") - var posts lib.Posts - - if tag == "" { - posts = lib.GetBlogPosts() - } else { - posts = lib.GetBlogPosts().FilterByTag(tag) - } - - props := BlogProps{ - Posts: posts, - Tags: lib.GetAllBlogTags(), - Tag: tag, - } - - components := []string{"header", "blogpreview"} - return templates.RenderTemplate(c.Response().Writer, "base", components, props) -} diff --git a/pages/donos.go b/pages/donos.go deleted file mode 100644 index 6c2810f..0000000 --- a/pages/donos.go +++ /dev/null @@ -1,43 +0,0 @@ -package pages - -import ( - "strings" - - "github.com/labstack/echo/v4" - - "isabelroses.com/lib" - "isabelroses.com/templates" -) - -type DonoProps struct { - Subscribers lib.Donors - OneTimeDonors lib.Donors -} - -func Donos(c echo.Context) error { - donors := lib.GetDonors() - - props := DonoProps{} - - for _, dono := range donors { - if dono.Tier == "OneTime" { - dono = handleAvatar(dono) - props.OneTimeDonors = append(props.OneTimeDonors, dono) - } else { - dono = handleAvatar(dono) - props.Subscribers = append(props.Subscribers, dono) - } - } - - components := []string{"header", "usercard"} - return templates.RenderTemplate(c.Response().Writer, "base", components, props) -} - -func handleAvatar(dono lib.Donor) lib.Donor { - if dono.Avatar == "" { - dono.Avatar = "/you.webp" - } else if !strings.Contains(dono.Avatar, "avatars.githubusercontent.com") { - dono.Avatar = "https://gravatar.com/avatar/" + dono.Avatar - } - return dono -} diff --git a/pages/error.go b/pages/error.go deleted file mode 100644 index 69e9de9..0000000 --- a/pages/error.go +++ /dev/null @@ -1,23 +0,0 @@ -package pages - -import ( - "strconv" - - "github.com/labstack/echo/v4" - - "isabelroses.com/templates" -) - -type ErrorProps struct { - Code string -} - -func ErrorPage(c echo.Context, code int) error { - props := ErrorProps{ - Code: strconv.Itoa(code), - } - - c.Response().WriteHeader(code) - components := []string{"header"} - return templates.RenderTemplate(c.Response().Writer, "base", components, props) -} diff --git a/pages/home.go b/pages/home.go deleted file mode 100644 index 6e5eabc..0000000 --- a/pages/home.go +++ /dev/null @@ -1,94 +0,0 @@ -package pages - -import ( - "fmt" - "image/color" - "regexp" - "strings" - - "github.com/labstack/echo/v4" - - "isabelroses.com/lib" - "isabelroses.com/templates" -) - -var ( - fields = [8]string{"isabel roses", "", "Pronouns", "GitHub", "Contact", "Discord", "Fedi", "Email"} - data = [8]string{"", "", "she/her", "https://github.com/isabelroses", "https://isabel.contact", "https://discord.gg/8RVhHeJH3x", "@isabel@akko.isabelroses.com", "me@isabelroses.com"} - - groupSize = 10 - transFlagColors = []color.RGBA{ - {91, 206, 250, 255}, // Light Blue - {245, 169, 184, 255}, // Pink - {255, 255, 255, 255}, // White - {245, 169, 184, 255}, // Pink - {91, 206, 250, 255}, // Light Blue - } -) - -func Home(c echo.Context) error { - match, err := regexp.MatchString(".*(curl|wget).*", c.Request().Header.Get("User-Agent")) - if err != nil { - return err - } - - if match { - pfp_arr := strings.Split(string(lib.Pfp), "\n") - - larger := max(len(pfp_arr), len(fields)) - - max_field_len := 0 - for _, v := range fields { - length := len(v) - if length > max_field_len { - max_field_len = length - } - } - - // Define the fixed length of the right side (field + data) - max_data_len := 50 // Adjust as needed based on your layout - if len(fields) > larger { - larger = len(fields) - } - - var out strings.Builder - for i := 0; i < larger; i++ { - left := "" - right := "" - - if i < len(pfp_arr) { - left = pfp_arr[i] - } - - if i < len(fields) { - field := fields[i] - field_with_padding := fmt.Sprint(field, strings.Repeat(" ", max_field_len-len(field))) - right = fmt.Sprint(field_with_padding, data[i]) - right += strings.Repeat(" ", max_data_len-len(right)) - } - - remainder := right - right = "" - - for idx := 0; idx < len(remainder); { - colorIndex := idx / groupSize % len(transFlagColors) - rgba := transFlagColors[colorIndex] - - chunkSize := groupSize - if idx+chunkSize > len(remainder) { - chunkSize = len(remainder) - idx - } - - right += fmt.Sprintf("\x1b[48;2;%d;%d;%dm\x1b[38;2;0;0;0m%s", rgba.R, rgba.G, rgba.B, remainder[idx:idx+chunkSize]) - idx += chunkSize - } - - out.WriteString(fmt.Sprintln(left, " ", right)) - } - - return c.String(200, out.String()) - } - - components := []string{"header"} - return templates.RenderTemplate(c.Response().Writer, "base", components, nil) -} diff --git a/pages/post.go b/pages/post.go deleted file mode 100644 index 224d8f6..0000000 --- a/pages/post.go +++ /dev/null @@ -1,50 +0,0 @@ -package pages - -import ( - "log" - "net/http" - "strconv" - "strings" - - "github.com/labstack/echo/v4" - "isabelroses.com/lib" - "isabelroses.com/templates" -) - -func Post(c echo.Context) error { - var ( - finalPost lib.Post - posts = lib.GetBlogPosts() - slug = c.Param("slug") - ) - - parts := strings.Split(slug, "-") - id, err := strconv.Atoi(parts[len(parts)-1]) - if err != nil { - log.Print(err) - } - - for _, post := range posts { - if id == post.ID { - // if the slug is not the same as the post's slug - if post.Slug != slug { - c.Redirect(http.StatusSeeOther, "/blog/"+post.Slug) - } - - finalPost = post - } - } - - props := lib.Post{ - ID: finalPost.ID, - Title: finalPost.Title, - Description: finalPost.Description, - Content: finalPost.Content, - Date: finalPost.Date, - Tags: finalPost.Tags, - Slug: finalPost.Slug, - } - - components := []string{"header"} - return templates.RenderTemplate(c.Response().Writer, "base", components, props) -} diff --git a/pages/projects.go b/pages/projects.go deleted file mode 100644 index ff001a3..0000000 --- a/pages/projects.go +++ /dev/null @@ -1,59 +0,0 @@ -package pages - -import ( - "github.com/labstack/echo/v4" - "isabelroses.com/lib" - "isabelroses.com/templates" -) - -type ProjectProps struct { - Projects []lib.Project -} - -func Projects(c echo.Context) error { - projects := []lib.Project{ - { - Name: "This site", - Description: "This site that your currently on.", - Href: "https://isabelroses.com", - Repo: "https://github.com/isabelroses/website", - }, - { - Name: "Userstyles", - Icon: "userstyles-icon.webp", - Banner: "userstyles-banner.webp", - Repo: "https://github.com/catppuccin/userstyles", - }, - { - Name: "freeze.nvim", - Icon: "freeze-icon.webp", - Banner: "freeze-banner.webp", - Repo: "https://github.com/charm-community/freeze.nvim", - }, - { - Name: "izrss", - Banner: "izrss-banner.webp", - Repo: "https://github.com/isabelroses/izrss", - }, - { - Name: "Dotfiles", - Icon: "dotfiles-icon.svg", - Banner: "dotfiles-banner.svg", - Repo: "https://github.com/isabelroses/dotfiles", - }, - { - Name: "Bellado", - Description: "A fast and once simple cli todo tool", - Icon: "bellado-icon.webp", - Banner: "bellado-banner.webp", - Repo: "https://github.com/isabelroses/bellado", - }, - } - - props := ProjectProps{ - Projects: projects, - } - - components := []string{"header", "project"} - return templates.RenderTemplate(c.Response().Writer, "base", components, props) -} diff --git a/public/styles.css b/public/styles.css deleted file mode 100644 index 2180a58..0000000 --- a/public/styles.css +++ /dev/null @@ -1 +0,0 @@ -/*! tailwindcss v3.4.4 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:Roboto,ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:Roboto Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}@font-face{font-family:Maple Mono;font-style:normal;font-weight:400;font-display:swap;src:url(/fonts/MapleMono-Regular.woff2) format("woff2")}@font-face{font-family:Maple Mono;font-style:bold;font-weight:400;font-display:swap;src:url(/fonts/MapleMono-Bold.woff2) format("woff2")}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }.prose{color:var(--tw-prose-body);max-width:65ch}.prose :where(p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em}.prose :where([class~=lead]):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-lead);font-size:1.25em;line-height:1.6;margin-top:1.2em;margin-bottom:1.2em}.prose :where(a):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-links);text-decoration:underline;font-weight:500}.prose :where(strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-bold);font-weight:600}.prose :where(a strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(blockquote strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(thead th strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(ol):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:decimal;margin-top:1.25em;margin-bottom:1.25em;padding-inline-start:1.625em}.prose :where(ol[type=A]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=A s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=I]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type=I s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type="1"]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:decimal}.prose :where(ul):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:disc;margin-top:1.25em;margin-bottom:1.25em;padding-inline-start:1.625em}.prose :where(ol>li):not(:where([class~=not-prose],[class~=not-prose] *))::marker{font-weight:400;color:var(--tw-prose-counters)}.prose :where(ul>li):not(:where([class~=not-prose],[class~=not-prose] *))::marker{color:var(--tw-prose-bullets)}.prose :where(dt):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;margin-top:1.25em}.prose :where(hr):not(:where([class~=not-prose],[class~=not-prose] *)){border-color:var(--tw-prose-hr);border-top-width:1px;margin-top:3em;margin-bottom:3em}.prose :where(blockquote):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:500;font-style:italic;color:var(--tw-prose-quotes);border-inline-start-width:.25rem;border-inline-start-color:var(--tw-prose-quote-borders);quotes:"\201C""\201D""\2018""\2019";margin-top:1.6em;margin-bottom:1.6em;padding-inline-start:1em}.prose :where(blockquote p:first-of-type):not(:where([class~=not-prose],[class~=not-prose] *)):before{content:open-quote}.prose :where(blockquote p:last-of-type):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:close-quote}.prose :where(h1):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:800;font-size:2.25em;margin-top:0;margin-bottom:.8888889em;line-height:1.1111111}.prose :where(h1 strong):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:900;color:inherit}.prose :where(h2):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:700;font-size:1.5em;margin-top:2em;margin-bottom:1em;line-height:1.3333333}.prose :where(h2 strong):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:800;color:inherit}.prose :where(h3):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;font-size:1.25em;margin-top:1.6em;margin-bottom:.6em;line-height:1.6}.prose :where(h3 strong):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:700;color:inherit}.prose :where(h4):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;margin-top:1.5em;margin-bottom:.5em;line-height:1.5}.prose :where(h4 strong):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:700;color:inherit}.prose :where(img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(picture):not(:where([class~=not-prose],[class~=not-prose] *)){display:block;margin-top:2em;margin-bottom:2em}.prose :where(video):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(kbd):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:500;font-family:inherit;color:var(--tw-prose-kbd);box-shadow:0 0 0 1px rgb(var(--tw-prose-kbd-shadows)/10%),0 3px 0 rgb(var(--tw-prose-kbd-shadows)/10%);font-size:.875em;border-radius:.3125rem;padding-top:.1875em;padding-inline-end:.375em;padding-bottom:.1875em;padding-inline-start:.375em}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-code);font-weight:600;font-size:.875em}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)):before{content:"`"}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:"`"}.prose :where(a code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(h1 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(h2 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-size:.875em}.prose :where(h3 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-size:.9em}.prose :where(h4 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(blockquote code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(thead th code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(pre):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-pre-code);background-color:var(--tw-prose-pre-bg);overflow-x:auto;font-weight:400;font-size:.875em;line-height:1.7142857;margin-top:1.7142857em;margin-bottom:1.7142857em;border-radius:.375rem;padding-top:.8571429em;padding-inline-end:1.1428571em;padding-bottom:.8571429em;padding-inline-start:1.1428571em}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)){background-color:initial;border-width:0;border-radius:0;padding:0;font-weight:inherit;color:inherit;font-size:inherit;font-family:inherit;line-height:inherit}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)):before{content:none}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:none}.prose :where(table):not(:where([class~=not-prose],[class~=not-prose] *)){width:100%;table-layout:auto;text-align:start;margin-top:2em;margin-bottom:2em;font-size:.875em;line-height:1.7142857}.prose :where(thead):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:1px;border-bottom-color:var(--tw-prose-th-borders)}.prose :where(thead th):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;vertical-align:bottom;padding-inline-end:.5714286em;padding-bottom:.5714286em;padding-inline-start:.5714286em}.prose :where(tbody tr):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:1px;border-bottom-color:var(--tw-prose-td-borders)}.prose :where(tbody tr:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:0}.prose :where(tbody td):not(:where([class~=not-prose],[class~=not-prose] *)){vertical-align:initial}.prose :where(tfoot):not(:where([class~=not-prose],[class~=not-prose] *)){border-top-width:1px;border-top-color:var(--tw-prose-th-borders)}.prose :where(tfoot td):not(:where([class~=not-prose],[class~=not-prose] *)){vertical-align:top}.prose :where(figure>*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0;margin-bottom:0}.prose :where(figcaption):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-captions);font-size:.875em;line-height:1.4285714;margin-top:.8571429em}.prose{--tw-prose-body:#374151;--tw-prose-headings:#111827;--tw-prose-lead:#4b5563;--tw-prose-links:#111827;--tw-prose-bold:#111827;--tw-prose-counters:#6b7280;--tw-prose-bullets:#d1d5db;--tw-prose-hr:#e5e7eb;--tw-prose-quotes:#111827;--tw-prose-quote-borders:#e5e7eb;--tw-prose-captions:#6b7280;--tw-prose-kbd:#111827;--tw-prose-kbd-shadows:17 24 39;--tw-prose-code:#111827;--tw-prose-pre-code:#e5e7eb;--tw-prose-pre-bg:#1f2937;--tw-prose-th-borders:#d1d5db;--tw-prose-td-borders:#e5e7eb;--tw-prose-invert-body:#d1d5db;--tw-prose-invert-headings:#fff;--tw-prose-invert-lead:#9ca3af;--tw-prose-invert-links:#fff;--tw-prose-invert-bold:#fff;--tw-prose-invert-counters:#9ca3af;--tw-prose-invert-bullets:#4b5563;--tw-prose-invert-hr:#374151;--tw-prose-invert-quotes:#f3f4f6;--tw-prose-invert-quote-borders:#374151;--tw-prose-invert-captions:#9ca3af;--tw-prose-invert-kbd:#fff;--tw-prose-invert-kbd-shadows:255 255 255;--tw-prose-invert-code:#fff;--tw-prose-invert-pre-code:#d1d5db;--tw-prose-invert-pre-bg:#00000080;--tw-prose-invert-th-borders:#4b5563;--tw-prose-invert-td-borders:#374151;font-size:1rem;line-height:1.75}.prose :where(picture>img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0;margin-bottom:0}.prose :where(li):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.5em;margin-bottom:.5em}.prose :where(ol>li):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:.375em}.prose :where(ul>li):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:.375em}.prose :where(.prose>ul>li p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.75em;margin-bottom:.75em}.prose :where(.prose>ul>li>p:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ul>li>p:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em}.prose :where(.prose>ol>li>p:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ol>li>p:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em}.prose :where(ul ul,ul ol,ol ul,ol ol):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.75em;margin-bottom:.75em}.prose :where(dl):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em}.prose :where(dd):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.5em;padding-inline-start:1.625em}.prose :where(hr+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(h2+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(h3+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(h4+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(thead th:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:0}.prose :where(thead th:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-end:0}.prose :where(tbody td,tfoot td):not(:where([class~=not-prose],[class~=not-prose] *)){padding-top:.5714286em;padding-inline-end:.5714286em;padding-bottom:.5714286em;padding-inline-start:.5714286em}.prose :where(tbody td:first-child,tfoot td:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:0}.prose :where(tbody td:last-child,tfoot td:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-end:0}.prose :where(figure):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(.prose>:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(.prose>:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:0}::-webkit-scrollbar{max-height:.5rem;width:.5rem}::-webkit-scrollbar-track{background:#0000}::-webkit-scrollbar-thumb{background:var(--special);border-radius:.25rem}.theme-ctp-mocha{--fg:#cdd6f4;--fg-lighter:#a6adc8;--bg:#181825;--bg-lighter:#1e1e2e;--bg-darker:#11111b;--card:#313244;--card-border:#45475a;--special:#74c7ec;--raw-special:116,199,236;code.hljs{color:#cdd6f4};code .hljs-keyword{color:#cba6f7};code .hljs-built_in{color:#f38ba8};code .hljs-type{color:#f9e2af};code .hljs-literal,code .hljs-number{color:#fab387};code .hljs-operator{color:#94e2d5};code .hljs-punctuation{color:#bac2de};code .hljs-property{color:#94e2d5};code .hljs-regexp{color:#f5c2e7};code .hljs-char.escape_,code .hljs-string{color:#a6e3a1};code .hljs-subst{color:#a6adc8};code .hljs-symbol{color:#f2cdcd};code .hljs-variable,code .hljs-variable.language_{color:#cba6f7};code .hljs-variable.constant_{color:#fab387};code .hljs-title{color:#89b4fa};code .hljs-title.class_{color:#f9e2af};code .hljs-title.function_{color:#89b4fa};code .hljs-params{color:#cdd6f4};code .hljs-comment{color:#585b70};code .hljs-doctag{color:#f38ba8};code .hljs-meta{color:#fab387};code .hljs-section{color:#89b4fa};code .hljs-tag{color:#a6adc8};code .hljs-name{color:#cba6f7};code .hljs-attr{color:#89b4fa};code .hljs-attribute{color:#a6e3a1};code .hljs-bullet{color:#94e2d5};code .hljs-code{color:#a6e3a1};code .hljs-emphasis{color:#f38ba8;font-style:italic};code .hljs-strong{color:#f38ba8;font-weight:700};code .hljs-formula{color:#94e2d5};code .hljs-link{color:#74c7ec;font-style:italic};code .hljs-quote{color:#a6e3a1;font-style:italic};code .hljs-selector-tag{color:#f9e2af};code .hljs-selector-id{color:#89b4fa};code .hljs-selector-class{color:#94e2d5};code .hljs-selector-attr{color:#cba6f7};code .hljs-selector-pseudo{color:#94e2d5};code .hljs-template-tag,code .hljs-template-variable{color:#f2cdcd};code .hljs-addition{color:#a6e3a1};code .hljs-deletion{color:#f38ba8};}.theme-ctp-macchiato{--fg:#cad3f5;--fg-lighter:#a5adcb;--bg:#1e2030;--bg-lighter:#24273a;--bg-darker:#181926;--card:#363a4f;--card-border:#494d64;--special:#7dc4e4;--raw-special:125,196,228;code.hljs{color:#cad3f5};code .hljs-keyword{color:#c6a0f6};code .hljs-built_in{color:#ed8796};code .hljs-type{color:#eed49f};code .hljs-literal,code .hljs-number{color:#f5a97f};code .hljs-operator{color:#8bd5ca};code .hljs-punctuation{color:#b8c0e0};code .hljs-property{color:#8bd5ca};code .hljs-regexp{color:#f5bde6};code .hljs-char.escape_,code .hljs-string{color:#a6da95};code .hljs-subst{color:#a5adcb};code .hljs-symbol{color:#f0c6c6};code .hljs-variable,code .hljs-variable.language_{color:#c6a0f6};code .hljs-variable.constant_{color:#f5a97f};code .hljs-title{color:#8aadf4};code .hljs-title.class_{color:#eed49f};code .hljs-title.function_{color:#8aadf4};code .hljs-params{color:#cad3f5};code .hljs-comment{color:#5b6078};code .hljs-doctag{color:#ed8796};code .hljs-meta{color:#f5a97f};code .hljs-section{color:#8aadf4};code .hljs-tag{color:#a5adcb};code .hljs-name{color:#c6a0f6};code .hljs-attr{color:#8aadf4};code .hljs-attribute{color:#a6da95};code .hljs-bullet{color:#8bd5ca};code .hljs-code{color:#a6da95};code .hljs-emphasis{color:#ed8796;font-style:italic};code .hljs-strong{color:#ed8796;font-weight:700};code .hljs-formula{color:#8bd5ca};code .hljs-link{color:#7dc4e4;font-style:italic};code .hljs-quote{color:#a6da95;font-style:italic};code .hljs-selector-tag{color:#eed49f};code .hljs-selector-id{color:#8aadf4};code .hljs-selector-class{color:#8bd5ca};code .hljs-selector-attr{color:#c6a0f6};code .hljs-selector-pseudo{color:#8bd5ca};code .hljs-template-tag,code .hljs-template-variable{color:#f0c6c6};code .hljs-addition{color:#a6da95};code .hljs-deletion{color:#ed8796};}.theme-ctp-frappe{--fg:#c6d0f5;--fg-lighter:#a5adce;--bg:#292c3c;--bg-lighter:#303446;--bg-darker:#232634;--card:#414559;--card-border:#51576d;--special:#85c1dc;--raw-special:133,193,220;code.hljs{color:#c6d0f5};code .hljs-keyword{color:#ca9ee6};code .hljs-built_in{color:#e78284};code .hljs-type{color:#e5c890};code .hljs-literal,code .hljs-number{color:#ef9f76};code .hljs-operator{color:#81c8be};code .hljs-punctuation{color:#b5bfe2};code .hljs-property{color:#81c8be};code .hljs-regexp{color:#f4b8e4};code .hljs-char.escape_,code .hljs-string{color:#a6d189};code .hljs-subst{color:#a5adce};code .hljs-symbol{color:#eebebe};code .hljs-variable,code .hljs-variable.language_{color:#ca9ee6};code .hljs-variable.constant_{color:#ef9f76};code .hljs-title{color:#8caaee};code .hljs-title.class_{color:#e5c890};code .hljs-title.function_{color:#8caaee};code .hljs-params{color:#c6d0f5};code .hljs-comment{color:#626880};code .hljs-doctag{color:#e78284};code .hljs-meta{color:#ef9f76};code .hljs-section{color:#8caaee};code .hljs-tag{color:#a5adce};code .hljs-name{color:#ca9ee6};code .hljs-attr{color:#8caaee};code .hljs-attribute{color:#a6d189};code .hljs-bullet{color:#81c8be};code .hljs-code{color:#a6d189};code .hljs-emphasis{color:#e78284;font-style:italic};code .hljs-strong{color:#e78284;font-weight:700};code .hljs-formula{color:#81c8be};code .hljs-link{color:#85c1dc;font-style:italic};code .hljs-quote{color:#a6d189;font-style:italic};code .hljs-selector-tag{color:#e5c890};code .hljs-selector-id{color:#8caaee};code .hljs-selector-class{color:#81c8be};code .hljs-selector-attr{color:#ca9ee6};code .hljs-selector-pseudo{color:#81c8be};code .hljs-template-tag,code .hljs-template-variable{color:#eebebe};code .hljs-addition{color:#a6d189};code .hljs-deletion{color:#e78284};}.theme-ctp-latte{--fg:#4c4f69;--fg-lighter:#6c6f85;--bg:#e6e9ef;--bg-lighter:#eff1f5;--bg-darker:#dce0e8;--card:#ccd0da;--card-border:#bcc0cc;--special:#209fb5;--raw-special:32,159,181;code.hljs{color:#4c4f69};code .hljs-keyword{color:#8839ef};code .hljs-built_in{color:#d20f39};code .hljs-type{color:#df8e1d};code .hljs-literal,code .hljs-number{color:#fe640b};code .hljs-operator{color:#179299};code .hljs-punctuation{color:#5c5f77};code .hljs-property{color:#179299};code .hljs-regexp{color:#ea76cb};code .hljs-char.escape_,code .hljs-string{color:#40a02b};code .hljs-subst{color:#6c6f85};code .hljs-symbol{color:#dd7878};code .hljs-variable,code .hljs-variable.language_{color:#8839ef};code .hljs-variable.constant_{color:#fe640b};code .hljs-title{color:#1e66f5};code .hljs-title.class_{color:#df8e1d};code .hljs-title.function_{color:#1e66f5};code .hljs-params{color:#4c4f69};code .hljs-comment{color:#acb0be};code .hljs-doctag{color:#d20f39};code .hljs-meta{color:#fe640b};code .hljs-section{color:#1e66f5};code .hljs-tag{color:#6c6f85};code .hljs-name{color:#8839ef};code .hljs-attr{color:#1e66f5};code .hljs-attribute{color:#40a02b};code .hljs-bullet{color:#179299};code .hljs-code{color:#40a02b};code .hljs-emphasis{color:#d20f39;font-style:italic};code .hljs-strong{color:#d20f39;font-weight:700};code .hljs-formula{color:#179299};code .hljs-link{color:#209fb5;font-style:italic};code .hljs-quote{color:#40a02b;font-style:italic};code .hljs-selector-tag{color:#df8e1d};code .hljs-selector-id{color:#1e66f5};code .hljs-selector-class{color:#179299};code .hljs-selector-attr{color:#8839ef};code .hljs-selector-pseudo{color:#179299};code .hljs-template-tag,code .hljs-template-variable{color:#dd7878};code .hljs-addition{color:#40a02b};code .hljs-deletion{color:#d20f39};}code{background-color:var(--card)}body{background-color:var(--bg);color:var(--fg);margin-left:auto;margin-right:auto;display:flex;min-height:100vh;max-width:80rem;flex-direction:column;row-gap:.5rem;padding-left:2rem;padding-right:2rem;padding-top:2rem}@media (min-width:768px){body{flex-direction:row;-moz-column-gap:2rem;column-gap:2rem;row-gap:0}}.prose{--tw-prose-body:var(--fg);--tw-prose-headings:var(--fg);--tw-prose-code:var(--fg);--tw-prose-pre-bg:var(--card);--tw-prose-bold:var(--fg);--tw-prose-quotes:var(--fg);--tw-prose-quote-borders:var(--card);--tw-prose-bullets:var(--fg);--tw-prose-hr:var(--fg);--tw-prose-captions:var(--card);--tw-prose-th-borders:var(--card);--tw-prose-td-borders:var(--fg);--tw-prose-links:var(--special);:where(code):not(:where([class~=not-prose],[class~=not-prose] *)):after,:where(code):not(:where([class~=not-prose],[class~=not-prose] *)):before{content:none}max-width:100%}.banner{height:calc(68px + max(min(100vw, 600px), 360px)*.1);background:var(--bg-lighter);position:absolute;top:0;left:0;z-index:10;width:100%;background-size:cover;background-position:50%}.tag{padding:0 10px 0 6px;border-radius:0 3px 3px 0;color:var(--bg-darker);background-color:var(--special);position:relative;margin-left:.5rem;display:inline-flex;height:1.25rem}.tag-after{transform:rotate(180deg);left:-8px;border-bottom:10px solid #0000;border-top:10px solid #0000;border-left:8px solid var(--special);position:absolute;top:0}.glow-button:hover{box-shadow:0 6px 15px rgba(var(--raw-special),.4)}.absolute{position:absolute}.relative{position:relative}.right-0{right:0}.right-\[15px\]{right:15px}.top-0{top:0}.top-\[5px\]{top:5px}.z-10{z-index:10}.z-20{z-index:20}.m-2{margin:.5rem}.mx-auto{margin-left:auto;margin-right:auto}.mb-12{margin-bottom:3rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.ml-1{margin-left:.25rem}.ml-auto{margin-left:auto}.box-border{box-sizing:border-box}.block{display:block}.flex{display:flex}.hidden{display:none}.size-5{width:1.25rem;height:1.25rem}.size-6{width:1.5rem;height:1.5rem}.h-20{height:5rem}.w-1\/2{width:50%}.w-10{width:2.5rem}.w-20{width:5rem}.w-full{width:100%}.min-w-28{min-width:7rem}.min-w-80{min-width:20rem}.max-w-40{max-width:10rem}.shrink-0{flex-shrink:0}.flex-grow{flex-grow:1}.cursor-pointer{cursor:pointer}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-center{justify-content:center}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-y-8{row-gap:2rem}.self-start{align-self:flex-start}.self-end{align-self:flex-end}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-hidden{overflow-x:hidden}.whitespace-nowrap{white-space:nowrap}.rounded-2xl{border-radius:1rem}.rounded-full{border-radius:9999px}.rounded-md{border-radius:.375rem}.rounded-sm{border-radius:.125rem}.rounded-xl{border-radius:.75rem}.bg-bg{background-color:var(--bg)}.bg-card{background-color:var(--card)}.bg-clip-text{-webkit-background-clip:text;background-clip:text}.fill-fg{fill:var(--fg)}.object-contain{-o-object-fit:contain;object-fit:contain}.p-1{padding:.25rem}.px-4{padding-left:1rem;padding-right:1rem}.px-\[15px\]{padding-left:15px;padding-right:15px}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-5{padding-top:1.25rem;padding-bottom:1.25rem}.pb-2{padding-bottom:.5rem}.pb-4{padding-bottom:1rem}.pb-8{padding-bottom:2rem}.pr-5{padding-right:1.25rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.text-2xl{font-size:1.5rem;line-height:2rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-extrabold{font-weight:800}.text-fg{color:var(--fg)}.text-fg-lighter{color:var(--fg-lighter)}.text-special{color:var(--special)}.underline{text-decoration-line:underline}.no-underline{text-decoration-line:none}.shadow-xl{--tw-shadow:0 20px 25px -5px #0000001a,0 8px 10px -6px #0000001a;--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color),0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.hover\:bg-bg-darker:hover{background-color:var(--bg-darker)}.hover\:fill-special:hover{fill:var(--special)}.hover\:text-special:hover{color:var(--special)}@media (min-width:640px){.sm\:w-full{width:100%}}@media (min-width:768px){.md\:block{display:block}.md\:flex{display:flex}.md\:inline-flex{display:inline-flex}.md\:w-72{width:18rem}.md\:min-w-\[620px\]{min-width:620px}.md\:max-w-\[620px\]{max-width:620px}.md\:pb-8{padding-bottom:2rem}}@media (min-width:1024px){.lg\:block{display:block}} \ No newline at end of file diff --git a/src/api/github.rs b/src/api/github.rs new file mode 100644 index 0000000..5727264 --- /dev/null +++ b/src/api/github.rs @@ -0,0 +1,73 @@ +use axum::{http::StatusCode, response::IntoResponse, Json}; +use serde::{Deserialize, Serialize}; + +use crate::data::donos; + +#[derive(Debug, Deserialize, Serialize)] +pub struct Dono { + action: String, + sponsorship: Sponsorship, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct Sponsorship { + created_at: String, + privacy_level: String, + sponsor: Sponsor, + tier: Tier, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct Sponsor { + avatar_url: Option, + login: String, + html_url: Option, + name: Option, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct Tier { + name: String, + is_one_time: bool, +} + +pub async fn handler(Json(payload): Json) -> impl IntoResponse { + if payload.sponsorship.privacy_level == "SECRET" { + return (StatusCode::OK, "Accepted, and hidden".to_string()).into_response(); + } + + // Only process newly created sponsorships + if payload.action != "created" { + return (StatusCode::OK, "Not a new sponsorship".to_string()).into_response(); + } + + let new_data = donos::Donor { + tier: if payload.sponsorship.tier.is_one_time { + "OneTime".to_string() + } else { + payload.sponsorship.tier.name.clone() + }, + name: get_name(&payload.sponsorship.sponsor), + url: payload + .sponsorship + .sponsor + .html_url + .unwrap_or(String::new()), + avatar: payload + .sponsorship + .sponsor + .avatar_url + .unwrap_or(String::new()), + }; + + donos::add(new_data.clone()); + + (StatusCode::OK, Json(new_data)).into_response() +} + +fn get_name(sponsor: &Sponsor) -> String { + sponsor + .name + .clone() + .unwrap_or_else(|| sponsor.login.clone()) +} diff --git a/src/api/kofi.rs b/src/api/kofi.rs new file mode 100644 index 0000000..64ed8de --- /dev/null +++ b/src/api/kofi.rs @@ -0,0 +1,54 @@ +use axum::{http::StatusCode, response::IntoResponse, Json}; +use serde::{Deserialize, Serialize}; + +use crate::data::donos; + +#[derive(Debug, Deserialize, Serialize)] +pub struct Dono { + message: Option, + shop_items: Option, + timestamp: String, + #[serde(rename = "type")] + type_: String, + verification_token: String, + from_name: String, + message_id: String, + amount: String, + currency: String, + email: String, + url: String, + shipping: Option, + tier_name: Option, + kofi_transaction_id: String, + is_public: bool, + is_first_subscription_payment: bool, + is_subscription_payment: bool, +} + +pub async fn handler(Json(payload): Json) -> impl IntoResponse { + if !payload.is_public { + return (StatusCode::OK, "Accepted, and hidden".to_string()).into_response(); + } + + if !payload.is_first_subscription_payment && payload.type_ != "Donation" { + return (StatusCode::OK, "Not a new donation".to_string()).into_response(); + } + + let new_data = donos::Donor { + tier: if payload.type_ == "Donation" { + "OneTime".to_string() + } else { + payload + .tier_name + .unwrap_or("Subscription".to_owned()) + .clone() + }, + name: payload.from_name, + url: payload.url, + avatar: "https://cdn.ko-fi.com/cdn/kofi2.png?v=2".to_string(), + }; + + donos::add(new_data.clone()); + + (StatusCode::OK, Json(new_data)).into_response() +} diff --git a/src/api/mod.rs b/src/api/mod.rs new file mode 100644 index 0000000..0d3e2c9 --- /dev/null +++ b/src/api/mod.rs @@ -0,0 +1,9 @@ +pub mod github; +pub mod kofi; + +pub async fn fallback() -> (axum::http::StatusCode, axum::Json) { + ( + axum::http::StatusCode::NOT_FOUND, + axum::Json(serde_json::json!({ "status": "Not Found" })), + ) +} diff --git a/src/data/blog.rs b/src/data/blog.rs new file mode 100644 index 0000000..03944b1 --- /dev/null +++ b/src/data/blog.rs @@ -0,0 +1,446 @@ +use atom_syndication::{ + ContentBuilder, EntryBuilder, FeedBuilder, LinkBuilder, PersonBuilder, TextBuilder, +}; +use chrono::TimeZone; +use nom::{branch::permutation, IResult}; +use rust_embed::{Embed, EmbeddedFile}; +use serde::{Deserialize, Serialize}; +use std::collections::HashSet; +use std::error::Error; + +const DATE_FORMAT: &str = "%d/%m/%Y"; + +#[derive(Embed)] +#[folder = "content/"] +#[include = "*.md"] +struct Content; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Post { + title: String, + date: String, + description: String, + slug: String, + tags: Vec, + content: String, + id: Option, +} + +struct PostMeta { + title: String, + date: String, + description: String, + tags: Vec, +} + +impl Post { + pub fn parse(input: &str) -> Result> { + let raw = input.split("---\n").collect::>(); + let raw_metadata = raw[1]; + let raw_content = raw[2]; + + let meta = permutation(( + Self::parse_title, + Self::parse_date, + Self::parse_description, + Self::parse_tags, + ))(raw_metadata) + .map(|(_, (title, date, description, tags))| PostMeta { + title, + date, + description, + tags, + }); + + let meta = match meta { + Ok(meta) => meta, + Err(e) => { + eprintln!("{e:?}"); + return Err(Box::new(std::io::Error::new( + std::io::ErrorKind::Other, + e.to_string(), + ))); + } + }; + + let content = Self::parse_content(raw_content); + + match content { + Ok(content) => { + let ret = Self { + title: meta.title, + date: meta.date, + description: meta.description, + tags: meta.tags, + slug: String::new(), + content, + id: None, + }; + + Ok(ret) + } + Err(e) => { + eprintln!("{e:?}"); + Err(Box::new(std::io::Error::new( + std::io::ErrorKind::Other, + e.to_string(), + ))) + } + } + } + + fn parse_field<'a>(input: &'a str, field: &'a str) -> IResult<&'a str, String> { + let (input, _) = nom::bytes::complete::tag(field)(input)?; + let (input, _) = nom::character::complete::multispace0(input)?; + let (input, _) = nom::bytes::complete::tag(":")(input)?; + let (input, _) = nom::character::complete::multispace0(input)?; + let (input, value) = nom::character::complete::not_line_ending(input)?; + let (input, _) = nom::character::complete::newline(input)?; + Ok((input, value.to_string())) + } + + fn parse_title(input: &str) -> IResult<&str, String> { + Self::parse_field(input, "title") + } + + fn parse_date(input: &str) -> IResult<&str, String> { + Self::parse_field(input, "date") + } + + fn parse_description(input: &str) -> IResult<&str, String> { + Self::parse_field(input, "description") + } + + fn parse_tags(input: &str) -> IResult<&str, Vec> { + let (input, _) = nom::bytes::complete::tag("tags")(input)?; + let (input, _) = nom::character::complete::multispace0(input)?; + let (input, _) = nom::bytes::complete::tag(":")(input)?; + let (input, _) = nom::character::complete::multispace0(input)?; + let (input, tags) = nom::multi::many0(nom::sequence::preceded( + nom::sequence::pair( + nom::character::complete::multispace0, + nom::bytes::complete::tag("- "), + ), + nom::character::complete::not_line_ending, + ))(input)?; + let (input, _) = nom::character::complete::newline(input)?; + Ok((input, tags.iter().map(|s| (*s).to_string()).collect())) + } + + fn parse_content(input: &str) -> Result { + markdown::to_html_with_options(input, &markdown::Options::gfm()) + } +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Posts(Vec); + +impl Posts { + pub fn filter_by_tag(&self, tag: &str) -> Posts { + let filtered_posts: Vec = self + .0 + .iter() + .filter(|p| p.tags.contains(&tag.to_string())) + .cloned() + .collect(); + Posts(filtered_posts) + } + + pub fn get_post_by_slug(&self, slug: &str) -> Option<&Post> { + let slugrev = slug.chars().rev(); + + let mut id = Vec::new(); + for char in slugrev { + if char == '-' { + break; + } + + id.push(char); + } + + let id = id + .iter() + .rev() + .collect::() + .parse::() + .unwrap(); + + self.0.iter().find(|p| p.id == Some(id)) + } + + pub fn to_rss(&self) -> String { + let channel = FeedBuilder::default() + .title("Isabel Roses") + .id("https://isabelroses.com") + .link( + LinkBuilder::default() + .href("https://isabelroses.com") + .rel("self") + .build(), + ) + .author( + PersonBuilder::default() + .name("Isabel Roses") + .email("isabel@isabelroses.com".to_owned()) + .uri("https://isabelroses.com".to_owned()) + .build(), + ) + .subtitle( + TextBuilder::default() + .value("Isabel Roses' blog".to_owned()) + .lang("en".to_owned()) + .build(), + ) + .updated({ + let latest_post = self.0.last().unwrap(); + parse_post_date(&latest_post.date) + }) + .entries( + self.0 + .iter() + .map(|post| { + let url = format!("https://isabelroses.com/blog/{}", post.slug); + EntryBuilder::default() + .title(post.title.clone()) + .link(LinkBuilder::default().href(&url).build()) + .content( + ContentBuilder::default() + .base("https://isabelroses.com".to_owned()) + .lang("en".to_owned()) + .value(post.content.clone()) + .content_type("html".to_owned()) + .build(), + ) + .id(&url) + .updated(parse_post_date(&post.date)) + .build() + }) + .collect::>(), + ) + .build(); + + channel.to_string() + } +} + +pub fn get_blog_posts() -> Result> { + let mut posts = Vec::new(); + + for file in Content::iter() { + let content = Content::get(&file).unwrap(); + let post = create_post(&content, file.to_string())?; + posts.push(post); + } + + sort_by_date(&mut posts); + + for (i, post) in posts.iter_mut().rev().enumerate() { + post.id = Some(i + 1); + let slug = post.slug.trim_end_matches(".md"); + post.slug = format!("{}-{}", slug, post.id.unwrap()); + } + + Ok(Posts(posts)) +} + +fn create_post(file: &EmbeddedFile, slug: String) -> Result> { + let content = std::str::from_utf8(file.data.as_ref())?; + let mut post = Post::parse(content).map_err(|e| format!("{e:?}"))?; + post.slug = slug; + Ok(post) +} + +pub fn get_all_blog_tags() -> Result, Box> { + let posts = get_blog_posts()?; + let mut tags = HashSet::new(); + + for post in posts.0 { + for tag in post.tags { + tags.insert(tag); + } + } + + let tags = tags.into_iter().collect(); + + Ok(tags) +} + +fn sort_by_date(posts: &mut [Post]) { + posts.sort_by(|a, b| { + chrono::NaiveDate::parse_from_str(&b.date, DATE_FORMAT) + .unwrap() + .cmp(&chrono::NaiveDate::parse_from_str(&a.date, DATE_FORMAT).unwrap()) + }); +} + +fn parse_post_date(date: &str) -> chrono::DateTime { + let time = chrono::NaiveDate::parse_from_str(date, DATE_FORMAT) + .unwrap() + .and_hms_opt(0, 0, 0) + .unwrap(); + + let offset = chrono::FixedOffset::east_opt(0).unwrap(); + offset.from_local_datetime(&time).single().unwrap() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_post() { + let input = include_str!("./test.md"); + + let post = Post::parse(input).unwrap(); + + assert_eq!(post.title, "Hello, World!"); + assert_eq!(post.date, "02/01/2024"); + assert_eq!(post.description, "This is a test post"); + assert_eq!(post.tags, vec!["test", "post"]); + assert_eq!( + post.content, + "

Hello, World!

\n

This is a test post.

\n" + ); + } + + #[test] + fn test_filter_by_tag() { + let posts = Posts(vec![ + Post { + title: "Hello, World!".to_string(), + date: "02/01/2024".to_string(), + description: "This is a test post".to_string(), + slug: "hello-world".to_string(), + tags: vec!["test".to_string(), "post".to_string()], + content: "

Hello, World!

\n

This is a test post.

\n".to_string(), + id: Some(1), + }, + Post { + title: "Hello, World!".to_string(), + date: "02/01/2024".to_string(), + description: "This is a test post".to_string(), + slug: "hello-world".to_string(), + tags: vec!["post".to_string()], + content: "

Hello, World!

\n

This is a test post.

\n".to_string(), + id: Some(2), + }, + ]); + + let filtered_posts = posts.filter_by_tag("test"); + + assert_eq!(filtered_posts.0.len(), 1); + } + + #[test] + fn test_get_post_by_full_slug() { + let posts = Posts(vec![ + Post { + title: "Hello, World!".to_string(), + date: "02/01/2024".to_string(), + description: "This is a test post".to_string(), + slug: "hello-world-1".to_string(), + tags: vec!["test".to_string(), "post".to_string()], + content: "

Hello, World!

\n

This is a test post.

\n".to_string(), + id: Some(1), + }, + Post { + title: "Hello, World!".to_string(), + date: "02/01/2024".to_string(), + description: "This is a test post".to_string(), + slug: "hello-world-2".to_string(), + tags: vec!["post".to_string()], + content: "

Hello, World!

\n

This is a test post.

\n".to_string(), + id: Some(2), + }, + ]); + + let post = posts.get_post_by_slug("hello-world-1").unwrap(); + + assert_eq!(post.id, Some(1)); + } + + #[test] + fn test_get_post_by_partial_slug() { + let posts = Posts(vec![ + Post { + title: "Hello, World!".to_string(), + date: "2021-08-01".to_string(), + description: "This is a test post".to_string(), + slug: "hello-world-1".to_string(), + tags: vec!["test".to_string(), "post".to_string()], + content: "

Hello, World!

\n

This is a test post.

\n".to_string(), + id: Some(1), + }, + Post { + title: "Hello, World!".to_string(), + date: "02/01/2024".to_string(), + description: "This is a test post".to_string(), + slug: "hello-world-2".to_string(), + tags: vec!["post".to_string()], + content: "

Hello, World!

\n

This is a test post.

\n".to_string(), + id: Some(2), + }, + ]); + + let post = posts.get_post_by_slug("-1").unwrap(); + + assert_eq!(post.id, Some(1)); + } + + #[test] + fn test_sort_posts_by_date() { + let mut posts = vec![ + Post { + title: "Hello, World!".to_string(), + date: "02/01/2023".to_string(), + description: "This is a test post".to_string(), + slug: "hello-world-1".to_string(), + tags: vec!["test".to_string(), "post".to_string()], + content: "

Hello, World!

\n

This is a test post.

\n".to_string(), + id: Some(1), + }, + Post { + title: "Hello, World!".to_string(), + date: "02/01/2024".to_string(), + description: "This is a test post".to_string(), + slug: "hello-world-2".to_string(), + tags: vec!["post".to_string()], + content: "

Hello, World!

\n

This is a test post.

\n".to_string(), + id: Some(2), + }, + Post { + title: "Hello, World!".to_string(), + date: "02/03/2024".to_string(), + description: "This is a test post".to_string(), + slug: "hello-world-2".to_string(), + tags: vec!["post".to_string()], + content: "

Hello, World!

\n

This is a test post.

\n".to_string(), + id: Some(3), + }, + Post { + title: "Hello, World!".to_string(), + date: "01/03/2024".to_string(), + description: "This is a test post".to_string(), + slug: "hello-world-2".to_string(), + tags: vec!["post".to_string()], + content: "

Hello, World!

\n

This is a test post.

\n".to_string(), + id: Some(4), + }, + ]; + + sort_by_date(&mut posts); + + assert_eq!(posts[0].date, "02/03/2024"); + assert_eq!(posts[1].date, "01/03/2024"); + assert_eq!(posts[2].date, "02/01/2024"); + assert_eq!(posts[3].date, "02/01/2023"); + } + + #[test] + fn test_parse_post_date() { + let date = "02/01/2024"; + let parsed = parse_post_date(date); + + assert_eq!(parsed.to_rfc2822(), "Tue, 2 Jan 2024 00:00:00 +0000"); + } +} diff --git a/src/data/donos.rs b/src/data/donos.rs new file mode 100644 index 0000000..533a6bd --- /dev/null +++ b/src/data/donos.rs @@ -0,0 +1,34 @@ +use serde::{Deserialize, Serialize}; +use serde_json; +use std::fs::File; +use std::io::{self, BufReader}; + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Donor { + pub name: String, + pub tier: String, + pub url: String, + pub avatar: String, +} + +pub fn get() -> io::Result> { + let file_str = std::env::var("DONOS_FILE").unwrap_or_else(|_| "donors.json".to_string()); + let file = File::open(file_str)?; + let reader = BufReader::new(file); + + let donos = serde_json::from_reader(reader)?; + + Ok(donos) +} + +pub fn add(donor: Donor) { + let file_str = std::env::var("DONOS_FILE").unwrap_or_else(|_| "donors.json".to_string()); + let file = File::open(&file_str).unwrap(); + let reader = BufReader::new(file); + + let mut donos: Vec = serde_json::from_reader(reader).unwrap(); + donos.push(donor); + + let file = File::create(&file_str).unwrap(); + serde_json::to_writer(file, &donos).unwrap(); +} diff --git a/src/data/mod.rs b/src/data/mod.rs new file mode 100644 index 0000000..87b23a1 --- /dev/null +++ b/src/data/mod.rs @@ -0,0 +1,3 @@ +pub mod blog; +pub mod donos; +pub mod projects; diff --git a/src/data/projects.rs b/src/data/projects.rs new file mode 100644 index 0000000..50150a6 --- /dev/null +++ b/src/data/projects.rs @@ -0,0 +1,62 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +pub struct Project { + name: &'static str, + description: Option<&'static str>, + icon: Option<&'static str>, + banner: Option<&'static str>, + href: Option<&'static str>, + repo: &'static str, +} + +pub const PROJECTS: &[Project] = &[ + Project { + name: "This site", + description: Some("This site that your currently on."), + icon: None, + banner: None, + href: Some("https://isabelroses.com"), + repo: "https://github.com/isabelroses/website", + }, + Project { + name: "Userstyles", + description: None, + icon: Some("userstyles-icon.webp"), + banner: Some("userstyles-banner.webp"), + href: None, + repo: "https://github.com/catppuccin/userstyles", + }, + Project { + name: "freeze.nvim", + description: None, + icon: Some("freeze-icon.webp"), + banner: Some("freeze-banner.webp"), + href: None, + repo: "https://github.com/charm-community/freeze.nvim", + }, + Project { + name: "izrss", + description: None, + icon: None, + banner: Some("izrss-banner.webp"), + href: None, + repo: "https://github.com/isabelroses/izrss", + }, + Project { + name: "Dotfiles", + description: None, + icon: Some("dotfiles-icon.svg"), + banner: Some("dotfiles-banner.svg"), + href: None, + repo: "https://github.com/isabelroses/dotfiles", + }, + Project { + name: "Bellado", + description: Some("A fast and once simple cli todo tool"), + icon: Some("bellado-icon.webp"), + banner: Some("bellado-banner.webp"), + href: None, + repo: "https://github.com/isabelroses/bellado", + }, +]; diff --git a/src/data/test.md b/src/data/test.md new file mode 100644 index 0000000..364745e --- /dev/null +++ b/src/data/test.md @@ -0,0 +1,11 @@ +--- +title: Hello, World! +date: 02/01/2024 +description: This is a test post +tags: + - test + - post +--- +# Hello, World! + +This is a test post. diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..479dc4b --- /dev/null +++ b/src/main.rs @@ -0,0 +1,146 @@ +use axum::{ + extract::Path, + http::header, + response::{Html, Redirect}, + routing::{get, post}, + Router, +}; +use dotenv::dotenv; +use std::{env, net::SocketAddr, sync::Arc}; +use tera::Context; + +mod api; +mod data; +mod static_files; +mod templates; + +use crate::templates::TEMPLATES; + +#[tokio::main] +async fn main() { + dotenv().ok(); + let port = env::var("PORT") + .unwrap_or_else(|_| "3000".to_string()) + .parse() + .unwrap(); + let addr = SocketAddr::from(([127, 0, 0, 1], port)); + println!("listening on {addr}"); + let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); + + let projects = { + let mut ctx = tera::Context::new(); + ctx.insert("projects", &data::projects::PROJECTS); + render("projects", &ctx) + }; + + let posts = Arc::new(if let Ok(posts) = data::blog::get_blog_posts() { + posts + } else { + eprintln!("Error: Could not load blog posts"); + return; + }); + + let tags = match data::blog::get_all_blog_tags() { + Ok(tags) => tags, + Err(e) => { + eprintln!("Error: {e:?}"); + return; + } + }; + + let api_routes = Router::new() + .route("/github", post(api::github::handler)) + .route("/kofi", post(api::kofi::handler)) + .fallback(get(api::fallback)); + + let app = Router::new() + .route("/", get(render("home", &tera::Context::new()))) + .route("/projects", get(projects)) + .route("/2004", get(render("2004", &tera::Context::new()))) + .route( + "/donations", + get(match data::donos::get() { + Ok(donos) => { + let mut ctx = tera::Context::new(); + ctx.insert("donors", &donos); + render("donos", &ctx) + } + Err(_) => Html("Error".to_string()), + }), + ) + .route( + "/blog", + get({ + let posts = Arc::clone(&posts); + let ctx = make_blog_ctx(&posts, &tags, "all"); + render("blog", &ctx) + }), + ) + .route( + "/blog/tag/:current_tag", + get({ + let posts = Arc::clone(&posts); + + |Path(current_tag): Path| async move { + let filtered_posts = posts.filter_by_tag(¤t_tag); + let ctx = make_blog_ctx(&filtered_posts, &tags, ¤t_tag); + render("blog", &ctx) + } + }), + ) + .route( + "/blog/:slug", + get({ + let posts = Arc::clone(&posts); + + |Path(slug): Path| async move { + let Some(post) = posts.get_post_by_slug(&slug) else { + return Html("404".to_string()); + }; + let mut ctx = tera::Context::new(); + ctx.insert("post", &post); + render("post", &ctx) + } + }), + ) + .route( + "/error", + get({ + let mut ctx = tera::Context::new(); + ctx.insert("code", "404"); + render("error", &ctx) + }), + ) + .route( + "/feed.xml", + get({ + let posts = Arc::clone(&posts); + + || async move { + let rss = posts.to_rss(); + ([(header::CONTENT_TYPE, "application/xml")], rss) + } + }), + ) + .nest("/api", api_routes) + .route("/static/*file", get(static_files::handler)) + .fallback(get(Redirect::temporary("/error"))); + + axum::serve(listener, app).await.unwrap(); +} + +fn render(page: &str, ctx: &Context) -> Html { + let render = format!("pages/{page}.tera"); + + let rendered = TEMPLATES.render(&render, ctx).unwrap(); + + Html(rendered) +} + +fn make_blog_ctx(posts: &data::blog::Posts, tags: &[String], current_tag: &str) -> Context { + let mut ctx = tera::Context::new(); + ctx.insert("posts", &posts); + ctx.insert("tags", &tags); + ctx.insert("current_tag", current_tag); + ctx +} diff --git a/src/static_files.rs b/src/static_files.rs new file mode 100644 index 0000000..1f7d081 --- /dev/null +++ b/src/static_files.rs @@ -0,0 +1,38 @@ +use axum::{ + http::{header, StatusCode, Uri}, + response::{IntoResponse, Response}, +}; +use rust_embed::Embed; + +pub async fn handler(uri: Uri) -> impl IntoResponse { + let mut path = uri.path().trim_start_matches('/').to_string(); + + if path.starts_with("static/") { + path = path.replace("static/", ""); + } + + StaticFile(path) +} + +#[derive(Embed)] +#[folder = "static/"] +struct Asset; + +pub struct StaticFile(pub T); + +impl IntoResponse for StaticFile +where + T: Into, +{ + fn into_response(self) -> Response { + let path = self.0.into(); + + match Asset::get(path.as_str()) { + Some(content) => { + let mime = mime_guess::from_path(path).first_or_octet_stream(); + ([(header::CONTENT_TYPE, mime.as_ref())], content.data).into_response() + } + None => (StatusCode::NOT_FOUND, "404 Not Found").into_response(), + } + } +} diff --git a/src/templates.rs b/src/templates.rs new file mode 100644 index 0000000..c08a9f8 --- /dev/null +++ b/src/templates.rs @@ -0,0 +1,35 @@ +use std::str::from_utf8; + +use lazy_static::lazy_static; +use rust_embed::Embed; +use tera::Tera; + +lazy_static! { + pub static ref TEMPLATES: Tera = { + let mut tera = Tera::default(); + let _res = tera.add_raw_templates(Template::iter().map(|file| { + let raw_data = Template::get(&file).unwrap(); + let content = from_utf8(raw_data.data.as_ref()).unwrap(); + (file.to_string(), content.to_string()) + })); + tera + }; +} + +#[derive(Embed)] +#[folder = "templates/"] +struct Template; + +#[cfg(test)] +mod tests { + use super::*; + use tera::Context; + + #[test] + fn test_templates() { + assert!(TEMPLATES + .render("pages/home.tera", &Context::new()) + .unwrap() + .contains("Isabel Roses"),); + } +} diff --git a/public/2004/arc.webp b/static/2004/arc.webp similarity index 100% rename from public/2004/arc.webp rename to static/2004/arc.webp diff --git a/public/2004/bitwarden.gif b/static/2004/bitwarden.gif similarity index 100% rename from public/2004/bitwarden.gif rename to static/2004/bitwarden.gif diff --git a/public/2004/blink.gif b/static/2004/blink.gif similarity index 100% rename from public/2004/blink.gif rename to static/2004/blink.gif diff --git a/public/2004/catppuccin.webp b/static/2004/catppuccin.webp similarity index 100% rename from public/2004/catppuccin.webp rename to static/2004/catppuccin.webp diff --git a/public/2004/cc-by-nc-sa.gif b/static/2004/cc-by-nc-sa.gif similarity index 100% rename from public/2004/cc-by-nc-sa.gif rename to static/2004/cc-by-nc-sa.gif diff --git a/public/2004/discordserver.gif b/static/2004/discordserver.gif similarity index 100% rename from public/2004/discordserver.gif rename to static/2004/discordserver.gif diff --git a/public/2004/fedi.gif b/static/2004/fedi.gif similarity index 100% rename from public/2004/fedi.gif rename to static/2004/fedi.gif diff --git a/public/2004/gaywebring.gif b/static/2004/gaywebring.gif similarity index 100% rename from public/2004/gaywebring.gif rename to static/2004/gaywebring.gif diff --git a/public/2004/gimp.gif b/static/2004/gimp.gif similarity index 100% rename from public/2004/gimp.gif rename to static/2004/gimp.gif diff --git a/public/2004/iesucks.gif b/static/2004/iesucks.gif similarity index 100% rename from public/2004/iesucks.gif rename to static/2004/iesucks.gif diff --git a/public/2004/love_blahaj.gif b/static/2004/love_blahaj.gif similarity index 100% rename from public/2004/love_blahaj.gif rename to static/2004/love_blahaj.gif diff --git a/public/2004/me.gif b/static/2004/me.gif similarity index 100% rename from public/2004/me.gif rename to static/2004/me.gif diff --git a/public/2004/nec.gif b/static/2004/nec.gif similarity index 100% rename from public/2004/nec.gif rename to static/2004/nec.gif diff --git a/public/2004/nix.gif b/static/2004/nix.gif similarity index 100% rename from public/2004/nix.gif rename to static/2004/nix.gif diff --git a/public/2004/oneko.gif b/static/2004/oneko.gif similarity index 100% rename from public/2004/oneko.gif rename to static/2004/oneko.gif diff --git a/public/2004/oneko.js b/static/2004/oneko.js similarity index 100% rename from public/2004/oneko.js rename to static/2004/oneko.js diff --git a/public/2004/programmingsocks.webp b/static/2004/programmingsocks.webp similarity index 100% rename from public/2004/programmingsocks.webp rename to static/2004/programmingsocks.webp diff --git a/public/2004/queercoded.webp b/static/2004/queercoded.webp similarity index 100% rename from public/2004/queercoded.webp rename to static/2004/queercoded.webp diff --git a/public/2004/scripts.gif b/static/2004/scripts.gif similarity index 100% rename from public/2004/scripts.gif rename to static/2004/scripts.gif diff --git a/public/2004/she-her.webp b/static/2004/she-her.webp similarity index 100% rename from public/2004/she-her.webp rename to static/2004/she-her.webp diff --git a/public/2004/tgirl.webp b/static/2004/tgirl.webp similarity index 100% rename from public/2004/tgirl.webp rename to static/2004/tgirl.webp diff --git a/public/2004/transnow.png b/static/2004/transnow.png similarity index 100% rename from public/2004/transnow.png rename to static/2004/transnow.png diff --git a/public/2004/www.gif b/static/2004/www.gif similarity index 100% rename from public/2004/www.gif rename to static/2004/www.gif diff --git a/public/humans.txt b/static/humans.txt similarity index 100% rename from public/humans.txt rename to static/humans.txt diff --git a/public/icons/apple-icon-180.png b/static/icons/apple-icon-180.png similarity index 100% rename from public/icons/apple-icon-180.png rename to static/icons/apple-icon-180.png diff --git a/public/icons/apple-splash-1125-2436.jpg b/static/icons/apple-splash-1125-2436.jpg similarity index 100% rename from public/icons/apple-splash-1125-2436.jpg rename to static/icons/apple-splash-1125-2436.jpg diff --git a/public/icons/apple-splash-1136-640.jpg b/static/icons/apple-splash-1136-640.jpg similarity index 100% rename from public/icons/apple-splash-1136-640.jpg rename to static/icons/apple-splash-1136-640.jpg diff --git a/public/icons/apple-splash-1170-2532.jpg b/static/icons/apple-splash-1170-2532.jpg similarity index 100% rename from public/icons/apple-splash-1170-2532.jpg rename to static/icons/apple-splash-1170-2532.jpg diff --git a/public/icons/apple-splash-1179-2556.jpg b/static/icons/apple-splash-1179-2556.jpg similarity index 100% rename from public/icons/apple-splash-1179-2556.jpg rename to static/icons/apple-splash-1179-2556.jpg diff --git a/public/icons/apple-splash-1206-2622.jpg b/static/icons/apple-splash-1206-2622.jpg similarity index 100% rename from public/icons/apple-splash-1206-2622.jpg rename to static/icons/apple-splash-1206-2622.jpg diff --git a/public/icons/apple-splash-1242-2208.jpg b/static/icons/apple-splash-1242-2208.jpg similarity index 100% rename from public/icons/apple-splash-1242-2208.jpg rename to static/icons/apple-splash-1242-2208.jpg diff --git a/public/icons/apple-splash-1242-2688.jpg b/static/icons/apple-splash-1242-2688.jpg similarity index 100% rename from public/icons/apple-splash-1242-2688.jpg rename to static/icons/apple-splash-1242-2688.jpg diff --git a/public/icons/apple-splash-1284-2778.jpg b/static/icons/apple-splash-1284-2778.jpg similarity index 100% rename from public/icons/apple-splash-1284-2778.jpg rename to static/icons/apple-splash-1284-2778.jpg diff --git a/public/icons/apple-splash-1290-2796.jpg b/static/icons/apple-splash-1290-2796.jpg similarity index 100% rename from public/icons/apple-splash-1290-2796.jpg rename to static/icons/apple-splash-1290-2796.jpg diff --git a/public/icons/apple-splash-1320-2868.jpg b/static/icons/apple-splash-1320-2868.jpg similarity index 100% rename from public/icons/apple-splash-1320-2868.jpg rename to static/icons/apple-splash-1320-2868.jpg diff --git a/public/icons/apple-splash-1334-750.jpg b/static/icons/apple-splash-1334-750.jpg similarity index 100% rename from public/icons/apple-splash-1334-750.jpg rename to static/icons/apple-splash-1334-750.jpg diff --git a/public/icons/apple-splash-1488-2266.jpg b/static/icons/apple-splash-1488-2266.jpg similarity index 100% rename from public/icons/apple-splash-1488-2266.jpg rename to static/icons/apple-splash-1488-2266.jpg diff --git a/public/icons/apple-splash-1536-2048.jpg b/static/icons/apple-splash-1536-2048.jpg similarity index 100% rename from public/icons/apple-splash-1536-2048.jpg rename to static/icons/apple-splash-1536-2048.jpg diff --git a/public/icons/apple-splash-1620-2160.jpg b/static/icons/apple-splash-1620-2160.jpg similarity index 100% rename from public/icons/apple-splash-1620-2160.jpg rename to static/icons/apple-splash-1620-2160.jpg diff --git a/public/icons/apple-splash-1640-2360.jpg b/static/icons/apple-splash-1640-2360.jpg similarity index 100% rename from public/icons/apple-splash-1640-2360.jpg rename to static/icons/apple-splash-1640-2360.jpg diff --git a/public/icons/apple-splash-1668-2224.jpg b/static/icons/apple-splash-1668-2224.jpg similarity index 100% rename from public/icons/apple-splash-1668-2224.jpg rename to static/icons/apple-splash-1668-2224.jpg diff --git a/public/icons/apple-splash-1668-2388.jpg b/static/icons/apple-splash-1668-2388.jpg similarity index 100% rename from public/icons/apple-splash-1668-2388.jpg rename to static/icons/apple-splash-1668-2388.jpg diff --git a/public/icons/apple-splash-1792-828.jpg b/static/icons/apple-splash-1792-828.jpg similarity index 100% rename from public/icons/apple-splash-1792-828.jpg rename to static/icons/apple-splash-1792-828.jpg diff --git a/public/icons/apple-splash-2048-1536.jpg b/static/icons/apple-splash-2048-1536.jpg similarity index 100% rename from public/icons/apple-splash-2048-1536.jpg rename to static/icons/apple-splash-2048-1536.jpg diff --git a/public/icons/apple-splash-2048-2732.jpg b/static/icons/apple-splash-2048-2732.jpg similarity index 100% rename from public/icons/apple-splash-2048-2732.jpg rename to static/icons/apple-splash-2048-2732.jpg diff --git a/public/icons/apple-splash-2160-1620.jpg b/static/icons/apple-splash-2160-1620.jpg similarity index 100% rename from public/icons/apple-splash-2160-1620.jpg rename to static/icons/apple-splash-2160-1620.jpg diff --git a/public/icons/apple-splash-2208-1242.jpg b/static/icons/apple-splash-2208-1242.jpg similarity index 100% rename from public/icons/apple-splash-2208-1242.jpg rename to static/icons/apple-splash-2208-1242.jpg diff --git a/public/icons/apple-splash-2224-1668.jpg b/static/icons/apple-splash-2224-1668.jpg similarity index 100% rename from public/icons/apple-splash-2224-1668.jpg rename to static/icons/apple-splash-2224-1668.jpg diff --git a/public/icons/apple-splash-2266-1488.jpg b/static/icons/apple-splash-2266-1488.jpg similarity index 100% rename from public/icons/apple-splash-2266-1488.jpg rename to static/icons/apple-splash-2266-1488.jpg diff --git a/public/icons/apple-splash-2360-1640.jpg b/static/icons/apple-splash-2360-1640.jpg similarity index 100% rename from public/icons/apple-splash-2360-1640.jpg rename to static/icons/apple-splash-2360-1640.jpg diff --git a/public/icons/apple-splash-2388-1668.jpg b/static/icons/apple-splash-2388-1668.jpg similarity index 100% rename from public/icons/apple-splash-2388-1668.jpg rename to static/icons/apple-splash-2388-1668.jpg diff --git a/public/icons/apple-splash-2436-1125.jpg b/static/icons/apple-splash-2436-1125.jpg similarity index 100% rename from public/icons/apple-splash-2436-1125.jpg rename to static/icons/apple-splash-2436-1125.jpg diff --git a/public/icons/apple-splash-2532-1170.jpg b/static/icons/apple-splash-2532-1170.jpg similarity index 100% rename from public/icons/apple-splash-2532-1170.jpg rename to static/icons/apple-splash-2532-1170.jpg diff --git a/public/icons/apple-splash-2556-1179.jpg b/static/icons/apple-splash-2556-1179.jpg similarity index 100% rename from public/icons/apple-splash-2556-1179.jpg rename to static/icons/apple-splash-2556-1179.jpg diff --git a/public/icons/apple-splash-2622-1206.jpg b/static/icons/apple-splash-2622-1206.jpg similarity index 100% rename from public/icons/apple-splash-2622-1206.jpg rename to static/icons/apple-splash-2622-1206.jpg diff --git a/public/icons/apple-splash-2688-1242.jpg b/static/icons/apple-splash-2688-1242.jpg similarity index 100% rename from public/icons/apple-splash-2688-1242.jpg rename to static/icons/apple-splash-2688-1242.jpg diff --git a/public/icons/apple-splash-2732-2048.jpg b/static/icons/apple-splash-2732-2048.jpg similarity index 100% rename from public/icons/apple-splash-2732-2048.jpg rename to static/icons/apple-splash-2732-2048.jpg diff --git a/public/icons/apple-splash-2778-1284.jpg b/static/icons/apple-splash-2778-1284.jpg similarity index 100% rename from public/icons/apple-splash-2778-1284.jpg rename to static/icons/apple-splash-2778-1284.jpg diff --git a/public/icons/apple-splash-2796-1290.jpg b/static/icons/apple-splash-2796-1290.jpg similarity index 100% rename from public/icons/apple-splash-2796-1290.jpg rename to static/icons/apple-splash-2796-1290.jpg diff --git a/public/icons/apple-splash-2868-1320.jpg b/static/icons/apple-splash-2868-1320.jpg similarity index 100% rename from public/icons/apple-splash-2868-1320.jpg rename to static/icons/apple-splash-2868-1320.jpg diff --git a/public/icons/apple-splash-640-1136.jpg b/static/icons/apple-splash-640-1136.jpg similarity index 100% rename from public/icons/apple-splash-640-1136.jpg rename to static/icons/apple-splash-640-1136.jpg diff --git a/public/icons/apple-splash-750-1334.jpg b/static/icons/apple-splash-750-1334.jpg similarity index 100% rename from public/icons/apple-splash-750-1334.jpg rename to static/icons/apple-splash-750-1334.jpg diff --git a/public/icons/apple-splash-828-1792.jpg b/static/icons/apple-splash-828-1792.jpg similarity index 100% rename from public/icons/apple-splash-828-1792.jpg rename to static/icons/apple-splash-828-1792.jpg diff --git a/public/icons/favicon.ico b/static/icons/favicon.ico similarity index 100% rename from public/icons/favicon.ico rename to static/icons/favicon.ico diff --git a/public/icons/manifest-icon-192.maskable.png b/static/icons/manifest-icon-192.maskable.png similarity index 100% rename from public/icons/manifest-icon-192.maskable.png rename to static/icons/manifest-icon-192.maskable.png diff --git a/public/icons/manifest-icon-512.maskable.png b/static/icons/manifest-icon-512.maskable.png similarity index 100% rename from public/icons/manifest-icon-512.maskable.png rename to static/icons/manifest-icon-512.maskable.png diff --git a/public/me.webp b/static/me.webp similarity index 100% rename from public/me.webp rename to static/me.webp diff --git a/public/posts/2024-03-29_freeze.webp b/static/posts/2024-03-29_freeze.webp similarity index 100% rename from public/posts/2024-03-29_freeze.webp rename to static/posts/2024-03-29_freeze.webp diff --git a/public/repos/bellado-banner.webp b/static/repos/bellado-banner.webp similarity index 100% rename from public/repos/bellado-banner.webp rename to static/repos/bellado-banner.webp diff --git a/public/repos/bellado-icon.webp b/static/repos/bellado-icon.webp similarity index 100% rename from public/repos/bellado-icon.webp rename to static/repos/bellado-icon.webp diff --git a/public/repos/dotfiles-banner.svg b/static/repos/dotfiles-banner.svg similarity index 100% rename from public/repos/dotfiles-banner.svg rename to static/repos/dotfiles-banner.svg diff --git a/public/repos/dotfiles-icon.svg b/static/repos/dotfiles-icon.svg similarity index 100% rename from public/repos/dotfiles-icon.svg rename to static/repos/dotfiles-icon.svg diff --git a/public/repos/freeze-banner.webp b/static/repos/freeze-banner.webp similarity index 100% rename from public/repos/freeze-banner.webp rename to static/repos/freeze-banner.webp diff --git a/public/repos/freeze-icon.webp b/static/repos/freeze-icon.webp similarity index 100% rename from public/repos/freeze-icon.webp rename to static/repos/freeze-icon.webp diff --git a/public/repos/izrss-banner.webp b/static/repos/izrss-banner.webp similarity index 100% rename from public/repos/izrss-banner.webp rename to static/repos/izrss-banner.webp diff --git a/public/repos/userstyles-banner.webp b/static/repos/userstyles-banner.webp similarity index 100% rename from public/repos/userstyles-banner.webp rename to static/repos/userstyles-banner.webp diff --git a/public/repos/userstyles-icon.webp b/static/repos/userstyles-icon.webp similarity index 100% rename from public/repos/userstyles-icon.webp rename to static/repos/userstyles-icon.webp diff --git a/public/robots.txt b/static/robots.txt similarity index 100% rename from public/robots.txt rename to static/robots.txt diff --git a/static/styles.css b/static/styles.css new file mode 100644 index 0000000..2b517c7 --- /dev/null +++ b/static/styles.css @@ -0,0 +1 @@ +*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*! tailwindcss v3.4.15 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:Roboto,ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:Roboto Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.prose{color:var(--tw-prose-body);max-width:65ch}.prose :where(p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em}.prose :where([class~=lead]):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-lead);font-size:1.25em;line-height:1.6;margin-top:1.2em;margin-bottom:1.2em}.prose :where(a):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-links);text-decoration:underline;font-weight:500}.prose :where(strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-bold);font-weight:600}.prose :where(a strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(blockquote strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(thead th strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(ol):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:decimal;margin-top:1.25em;margin-bottom:1.25em;padding-inline-start:1.625em}.prose :where(ol[type=A]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=A s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=I]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type=I s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type="1"]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:decimal}.prose :where(ul):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:disc;margin-top:1.25em;margin-bottom:1.25em;padding-inline-start:1.625em}.prose :where(ol>li):not(:where([class~=not-prose],[class~=not-prose] *))::marker{font-weight:400;color:var(--tw-prose-counters)}.prose :where(ul>li):not(:where([class~=not-prose],[class~=not-prose] *))::marker{color:var(--tw-prose-bullets)}.prose :where(dt):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;margin-top:1.25em}.prose :where(hr):not(:where([class~=not-prose],[class~=not-prose] *)){border-color:var(--tw-prose-hr);border-top-width:1px;margin-top:3em;margin-bottom:3em}.prose :where(blockquote):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:500;font-style:italic;color:var(--tw-prose-quotes);border-inline-start-width:.25rem;border-inline-start-color:var(--tw-prose-quote-borders);quotes:"\201C""\201D""\2018""\2019";margin-top:1.6em;margin-bottom:1.6em;padding-inline-start:1em}.prose :where(blockquote p:first-of-type):not(:where([class~=not-prose],[class~=not-prose] *)):before{content:open-quote}.prose :where(blockquote p:last-of-type):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:close-quote}.prose :where(h1):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:800;font-size:2.25em;margin-top:0;margin-bottom:.8888889em;line-height:1.1111111}.prose :where(h1 strong):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:900;color:inherit}.prose :where(h2):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:700;font-size:1.5em;margin-top:2em;margin-bottom:1em;line-height:1.3333333}.prose :where(h2 strong):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:800;color:inherit}.prose :where(h3):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;font-size:1.25em;margin-top:1.6em;margin-bottom:.6em;line-height:1.6}.prose :where(h3 strong):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:700;color:inherit}.prose :where(h4):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;margin-top:1.5em;margin-bottom:.5em;line-height:1.5}.prose :where(h4 strong):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:700;color:inherit}.prose :where(img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(picture):not(:where([class~=not-prose],[class~=not-prose] *)){display:block;margin-top:2em;margin-bottom:2em}.prose :where(video):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(kbd):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:500;font-family:inherit;color:var(--tw-prose-kbd);box-shadow:0 0 0 1px rgb(var(--tw-prose-kbd-shadows)/10%),0 3px 0 rgb(var(--tw-prose-kbd-shadows)/10%);font-size:.875em;border-radius:.3125rem;padding-top:.1875em;padding-inline-end:.375em;padding-bottom:.1875em;padding-inline-start:.375em}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-code);font-weight:600;font-size:.875em}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)):before{content:"`"}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:"`"}.prose :where(a code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(h1 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(h2 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-size:.875em}.prose :where(h3 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-size:.9em}.prose :where(h4 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(blockquote code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(thead th code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(pre):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-pre-code);background-color:var(--tw-prose-pre-bg);overflow-x:auto;font-weight:400;font-size:.875em;line-height:1.7142857;margin-top:1.7142857em;margin-bottom:1.7142857em;border-radius:.375rem;padding-top:.8571429em;padding-inline-end:1.1428571em;padding-bottom:.8571429em;padding-inline-start:1.1428571em}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)){background-color:transparent;border-width:0;border-radius:0;padding:0;font-weight:inherit;color:inherit;font-size:inherit;font-family:inherit;line-height:inherit}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)):before{content:none}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:none}.prose :where(table):not(:where([class~=not-prose],[class~=not-prose] *)){width:100%;table-layout:auto;margin-top:2em;margin-bottom:2em;font-size:.875em;line-height:1.7142857}.prose :where(thead):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:1px;border-bottom-color:var(--tw-prose-th-borders)}.prose :where(thead th):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;vertical-align:bottom;padding-inline-end:.5714286em;padding-bottom:.5714286em;padding-inline-start:.5714286em}.prose :where(tbody tr):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:1px;border-bottom-color:var(--tw-prose-td-borders)}.prose :where(tbody tr:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:0}.prose :where(tbody td):not(:where([class~=not-prose],[class~=not-prose] *)){vertical-align:baseline}.prose :where(tfoot):not(:where([class~=not-prose],[class~=not-prose] *)){border-top-width:1px;border-top-color:var(--tw-prose-th-borders)}.prose :where(tfoot td):not(:where([class~=not-prose],[class~=not-prose] *)){vertical-align:top}.prose :where(th,td):not(:where([class~=not-prose],[class~=not-prose] *)){text-align:start}.prose :where(figure>*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0;margin-bottom:0}.prose :where(figcaption):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-captions);font-size:.875em;line-height:1.4285714;margin-top:.8571429em}.prose{--tw-prose-body:#374151;--tw-prose-headings:#111827;--tw-prose-lead:#4b5563;--tw-prose-links:#111827;--tw-prose-bold:#111827;--tw-prose-counters:#6b7280;--tw-prose-bullets:#d1d5db;--tw-prose-hr:#e5e7eb;--tw-prose-quotes:#111827;--tw-prose-quote-borders:#e5e7eb;--tw-prose-captions:#6b7280;--tw-prose-kbd:#111827;--tw-prose-kbd-shadows:17 24 39;--tw-prose-code:#111827;--tw-prose-pre-code:#e5e7eb;--tw-prose-pre-bg:#1f2937;--tw-prose-th-borders:#d1d5db;--tw-prose-td-borders:#e5e7eb;--tw-prose-invert-body:#d1d5db;--tw-prose-invert-headings:#fff;--tw-prose-invert-lead:#9ca3af;--tw-prose-invert-links:#fff;--tw-prose-invert-bold:#fff;--tw-prose-invert-counters:#9ca3af;--tw-prose-invert-bullets:#4b5563;--tw-prose-invert-hr:#374151;--tw-prose-invert-quotes:#f3f4f6;--tw-prose-invert-quote-borders:#374151;--tw-prose-invert-captions:#9ca3af;--tw-prose-invert-kbd:#fff;--tw-prose-invert-kbd-shadows:255 255 255;--tw-prose-invert-code:#fff;--tw-prose-invert-pre-code:#d1d5db;--tw-prose-invert-pre-bg:rgba(0,0,0,.5);--tw-prose-invert-th-borders:#4b5563;--tw-prose-invert-td-borders:#374151;font-size:1rem;line-height:1.75}.prose :where(picture>img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0;margin-bottom:0}.prose :where(li):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.5em;margin-bottom:.5em}.prose :where(ol>li):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:.375em}.prose :where(ul>li):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:.375em}.prose :where(.prose>ul>li p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.75em;margin-bottom:.75em}.prose :where(.prose>ul>li>p:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ul>li>p:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em}.prose :where(.prose>ol>li>p:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ol>li>p:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em}.prose :where(ul ul,ul ol,ol ul,ol ol):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.75em;margin-bottom:.75em}.prose :where(dl):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em}.prose :where(dd):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.5em;padding-inline-start:1.625em}.prose :where(hr+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(h2+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(h3+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(h4+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(thead th:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:0}.prose :where(thead th:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-end:0}.prose :where(tbody td,tfoot td):not(:where([class~=not-prose],[class~=not-prose] *)){padding-top:.5714286em;padding-inline-end:.5714286em;padding-bottom:.5714286em;padding-inline-start:.5714286em}.prose :where(tbody td:first-child,tfoot td:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:0}.prose :where(tbody td:last-child,tfoot td:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-end:0}.prose :where(figure):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(.prose>:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(.prose>:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:0}::-webkit-scrollbar{max-height:.5rem;width:.5rem}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:var(--special);border-radius:.25rem}.theme-ctp-mocha{--fg:#cdd6f4;--fg-lighter:#a6adc8;--bg:#181825;--bg-lighter:#1e1e2e;--bg-darker:#11111b;--card:#313244;--card-border:#45475a;--special:#74c7ec;--raw-special:116,199,236;code.hljs{color:#cdd6f4};code .hljs-keyword{color:#cba6f7};code .hljs-built_in{color:#f38ba8};code .hljs-type{color:#f9e2af};code .hljs-literal,code .hljs-number{color:#fab387};code .hljs-operator{color:#94e2d5};code .hljs-punctuation{color:#bac2de};code .hljs-property{color:#94e2d5};code .hljs-regexp{color:#f5c2e7};code .hljs-char.escape_,code .hljs-string{color:#a6e3a1};code .hljs-subst{color:#a6adc8};code .hljs-symbol{color:#f2cdcd};code .hljs-variable,code .hljs-variable.language_{color:#cba6f7};code .hljs-variable.constant_{color:#fab387};code .hljs-title{color:#89b4fa};code .hljs-title.class_{color:#f9e2af};code .hljs-title.function_{color:#89b4fa};code .hljs-params{color:#cdd6f4};code .hljs-comment{color:#585b70};code .hljs-doctag{color:#f38ba8};code .hljs-meta{color:#fab387};code .hljs-section{color:#89b4fa};code .hljs-tag{color:#a6adc8};code .hljs-name{color:#cba6f7};code .hljs-attr{color:#89b4fa};code .hljs-attribute{color:#a6e3a1};code .hljs-bullet{color:#94e2d5};code .hljs-code{color:#a6e3a1};code .hljs-emphasis{color:#f38ba8;font-style:italic};code .hljs-strong{color:#f38ba8;font-weight:700};code .hljs-formula{color:#94e2d5};code .hljs-link{color:#74c7ec;font-style:italic};code .hljs-quote{color:#a6e3a1;font-style:italic};code .hljs-selector-tag{color:#f9e2af};code .hljs-selector-id{color:#89b4fa};code .hljs-selector-class{color:#94e2d5};code .hljs-selector-attr{color:#cba6f7};code .hljs-selector-pseudo{color:#94e2d5};code .hljs-template-tag,code .hljs-template-variable{color:#f2cdcd};code .hljs-addition{color:#a6e3a1};code .hljs-deletion{color:#f38ba8};}.theme-ctp-macchiato{--fg:#cad3f5;--fg-lighter:#a5adcb;--bg:#1e2030;--bg-lighter:#24273a;--bg-darker:#181926;--card:#363a4f;--card-border:#494d64;--special:#7dc4e4;--raw-special:125,196,228;code.hljs{color:#cad3f5};code .hljs-keyword{color:#c6a0f6};code .hljs-built_in{color:#ed8796};code .hljs-type{color:#eed49f};code .hljs-literal,code .hljs-number{color:#f5a97f};code .hljs-operator{color:#8bd5ca};code .hljs-punctuation{color:#b8c0e0};code .hljs-property{color:#8bd5ca};code .hljs-regexp{color:#f5bde6};code .hljs-char.escape_,code .hljs-string{color:#a6da95};code .hljs-subst{color:#a5adcb};code .hljs-symbol{color:#f0c6c6};code .hljs-variable,code .hljs-variable.language_{color:#c6a0f6};code .hljs-variable.constant_{color:#f5a97f};code .hljs-title{color:#8aadf4};code .hljs-title.class_{color:#eed49f};code .hljs-title.function_{color:#8aadf4};code .hljs-params{color:#cad3f5};code .hljs-comment{color:#5b6078};code .hljs-doctag{color:#ed8796};code .hljs-meta{color:#f5a97f};code .hljs-section{color:#8aadf4};code .hljs-tag{color:#a5adcb};code .hljs-name{color:#c6a0f6};code .hljs-attr{color:#8aadf4};code .hljs-attribute{color:#a6da95};code .hljs-bullet{color:#8bd5ca};code .hljs-code{color:#a6da95};code .hljs-emphasis{color:#ed8796;font-style:italic};code .hljs-strong{color:#ed8796;font-weight:700};code .hljs-formula{color:#8bd5ca};code .hljs-link{color:#7dc4e4;font-style:italic};code .hljs-quote{color:#a6da95;font-style:italic};code .hljs-selector-tag{color:#eed49f};code .hljs-selector-id{color:#8aadf4};code .hljs-selector-class{color:#8bd5ca};code .hljs-selector-attr{color:#c6a0f6};code .hljs-selector-pseudo{color:#8bd5ca};code .hljs-template-tag,code .hljs-template-variable{color:#f0c6c6};code .hljs-addition{color:#a6da95};code .hljs-deletion{color:#ed8796};}.theme-ctp-frappe{--fg:#c6d0f5;--fg-lighter:#a5adce;--bg:#292c3c;--bg-lighter:#303446;--bg-darker:#232634;--card:#414559;--card-border:#51576d;--special:#85c1dc;--raw-special:133,193,220;code.hljs{color:#c6d0f5};code .hljs-keyword{color:#ca9ee6};code .hljs-built_in{color:#e78284};code .hljs-type{color:#e5c890};code .hljs-literal,code .hljs-number{color:#ef9f76};code .hljs-operator{color:#81c8be};code .hljs-punctuation{color:#b5bfe2};code .hljs-property{color:#81c8be};code .hljs-regexp{color:#f4b8e4};code .hljs-char.escape_,code .hljs-string{color:#a6d189};code .hljs-subst{color:#a5adce};code .hljs-symbol{color:#eebebe};code .hljs-variable,code .hljs-variable.language_{color:#ca9ee6};code .hljs-variable.constant_{color:#ef9f76};code .hljs-title{color:#8caaee};code .hljs-title.class_{color:#e5c890};code .hljs-title.function_{color:#8caaee};code .hljs-params{color:#c6d0f5};code .hljs-comment{color:#626880};code .hljs-doctag{color:#e78284};code .hljs-meta{color:#ef9f76};code .hljs-section{color:#8caaee};code .hljs-tag{color:#a5adce};code .hljs-name{color:#ca9ee6};code .hljs-attr{color:#8caaee};code .hljs-attribute{color:#a6d189};code .hljs-bullet{color:#81c8be};code .hljs-code{color:#a6d189};code .hljs-emphasis{color:#e78284;font-style:italic};code .hljs-strong{color:#e78284;font-weight:700};code .hljs-formula{color:#81c8be};code .hljs-link{color:#85c1dc;font-style:italic};code .hljs-quote{color:#a6d189;font-style:italic};code .hljs-selector-tag{color:#e5c890};code .hljs-selector-id{color:#8caaee};code .hljs-selector-class{color:#81c8be};code .hljs-selector-attr{color:#ca9ee6};code .hljs-selector-pseudo{color:#81c8be};code .hljs-template-tag,code .hljs-template-variable{color:#eebebe};code .hljs-addition{color:#a6d189};code .hljs-deletion{color:#e78284};}.theme-ctp-latte{--fg:#4c4f69;--fg-lighter:#6c6f85;--bg:#e6e9ef;--bg-lighter:#eff1f5;--bg-darker:#dce0e8;--card:#ccd0da;--card-border:#bcc0cc;--special:#209fb5;--raw-special:32,159,181;code.hljs{color:#4c4f69};code .hljs-keyword{color:#8839ef};code .hljs-built_in{color:#d20f39};code .hljs-type{color:#df8e1d};code .hljs-literal,code .hljs-number{color:#fe640b};code .hljs-operator{color:#179299};code .hljs-punctuation{color:#5c5f77};code .hljs-property{color:#179299};code .hljs-regexp{color:#ea76cb};code .hljs-char.escape_,code .hljs-string{color:#40a02b};code .hljs-subst{color:#6c6f85};code .hljs-symbol{color:#dd7878};code .hljs-variable,code .hljs-variable.language_{color:#8839ef};code .hljs-variable.constant_{color:#fe640b};code .hljs-title{color:#1e66f5};code .hljs-title.class_{color:#df8e1d};code .hljs-title.function_{color:#1e66f5};code .hljs-params{color:#4c4f69};code .hljs-comment{color:#acb0be};code .hljs-doctag{color:#d20f39};code .hljs-meta{color:#fe640b};code .hljs-section{color:#1e66f5};code .hljs-tag{color:#6c6f85};code .hljs-name{color:#8839ef};code .hljs-attr{color:#1e66f5};code .hljs-attribute{color:#40a02b};code .hljs-bullet{color:#179299};code .hljs-code{color:#40a02b};code .hljs-emphasis{color:#d20f39;font-style:italic};code .hljs-strong{color:#d20f39;font-weight:700};code .hljs-formula{color:#179299};code .hljs-link{color:#209fb5;font-style:italic};code .hljs-quote{color:#40a02b;font-style:italic};code .hljs-selector-tag{color:#df8e1d};code .hljs-selector-id{color:#1e66f5};code .hljs-selector-class{color:#179299};code .hljs-selector-attr{color:#8839ef};code .hljs-selector-pseudo{color:#179299};code .hljs-template-tag,code .hljs-template-variable{color:#dd7878};code .hljs-addition{color:#40a02b};code .hljs-deletion{color:#d20f39};}code{background-color:var(--card)}body{background-color:var(--bg);color:var(--fg);margin-left:auto;margin-right:auto;display:flex;min-height:100vh;max-width:80rem;flex-direction:column;row-gap:.5rem;padding-left:2rem;padding-right:2rem;padding-top:2rem}@media (min-width:768px){body{flex-direction:row;-moz-column-gap:2rem;column-gap:2rem;row-gap:0}}.prose{--tw-prose-body:var(--fg);--tw-prose-headings:var(--fg);--tw-prose-code:var(--fg);--tw-prose-pre-bg:var(--card);--tw-prose-bold:var(--fg);--tw-prose-quotes:var(--fg);--tw-prose-quote-borders:var(--card);--tw-prose-bullets:var(--fg);--tw-prose-hr:var(--fg);--tw-prose-captions:var(--card);--tw-prose-th-borders:var(--card);--tw-prose-td-borders:var(--fg);--tw-prose-links:var(--special);:where(code):not(:where([class~=not-prose],[class~=not-prose] *)):after,:where(code):not(:where([class~=not-prose],[class~=not-prose] *)):before{content:none}max-width:100%}.banner{height:calc(68px + max(min(100vw, 600px), 360px)*.1);background:var(--bg-lighter);position:absolute;top:0;left:0;z-index:10;width:100%;background-size:cover;background-position:50%}.tag{padding:0 10px 0 6px;border-radius:0 3px 3px 0;color:var(--bg-darker);background-color:var(--special);position:relative;margin-left:.5rem;display:inline-flex;height:1.25rem}.tag-after{transform:rotate(180deg);left:-8px;border-bottom:10px solid transparent;border-top:10px solid transparent;border-left:8px solid var(--special);position:absolute;top:0}.glow-button:hover{box-shadow:0 6px 15px rgba(var(--raw-special),.4)}.absolute{position:absolute}.relative{position:relative}.right-\[15px\]{right:15px}.top-\[5px\]{top:5px}.z-10{z-index:10}.z-20{z-index:20}.mx-auto{margin-left:auto;margin-right:auto}.mb-12{margin-bottom:3rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.ml-1{margin-left:.25rem}.ml-auto{margin-left:auto}.box-border{box-sizing:border-box}.block{display:block}.flex{display:flex}.hidden{display:none}.size-5{width:1.25rem;height:1.25rem}.size-6{width:1.5rem;height:1.5rem}.h-20{height:5rem}.w-1\/2{width:50%}.w-10{width:2.5rem}.w-20{width:5rem}.w-full{width:100%}.min-w-28{min-width:7rem}.min-w-80{min-width:20rem}.max-w-40{max-width:10rem}.shrink-0{flex-shrink:0}.flex-grow{flex-grow:1}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-center{justify-content:center}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-y-8{row-gap:2rem}.self-start{align-self:flex-start}.self-end{align-self:flex-end}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-hidden{overflow-x:hidden}.whitespace-nowrap{white-space:nowrap}.rounded-2xl{border-radius:1rem}.rounded-full{border-radius:9999px}.rounded-sm{border-radius:.125rem}.rounded-xl{border-radius:.75rem}.bg-card{background-color:var(--card)}.bg-clip-text{-webkit-background-clip:text;background-clip:text}.fill-fg{fill:var(--fg)}.object-contain{-o-object-fit:contain;object-fit:contain}.p-1{padding:.25rem}.px-4{padding-left:1rem;padding-right:1rem}.px-\[15px\]{padding-left:15px;padding-right:15px}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-5{padding-top:1.25rem;padding-bottom:1.25rem}.pb-2{padding-bottom:.5rem}.pb-4{padding-bottom:1rem}.pb-8{padding-bottom:2rem}.pr-5{padding-right:1.25rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.text-2xl{font-size:1.5rem;line-height:2rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-extrabold{font-weight:800}.text-fg{color:var(--fg)}.text-fg-lighter{color:var(--fg-lighter)}.text-special{color:var(--special)}.underline{text-decoration-line:underline}.no-underline{text-decoration-line:none}.shadow-xl{--tw-shadow:0 20px 25px -5px rgba(0,0,0,.1),0 8px 10px -6px rgba(0,0,0,.1);--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color),0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.hover\:fill-special:hover{fill:var(--special)}.hover\:text-special:hover{color:var(--special)}@media (min-width:640px){.sm\:w-full{width:100%}}@media (min-width:768px){.md\:flex{display:flex}.md\:inline-flex{display:inline-flex}.md\:w-72{width:18rem}.md\:min-w-\[620px\]{min-width:620px}.md\:max-w-\[620px\]{max-width:620px}.md\:pb-8{padding-bottom:2rem}}@media (min-width:1024px){.lg\:block{display:block}} \ No newline at end of file diff --git a/public/you.webp b/static/you.webp similarity index 100% rename from public/you.webp rename to static/you.webp diff --git a/styles/base.css b/styles/base.css index 1e6e3db..c23083a 100644 --- a/styles/base.css +++ b/styles/base.css @@ -2,23 +2,6 @@ @tailwind components; @tailwind utilities; -@layer base { - @font-face { - font-family: 'Maple Mono'; - font-style: normal; - font-weight: 400; - font-display: swap; - src: url(/fonts/MapleMono-Regular.woff2) format('woff2'); - } - @font-face { - font-family: 'Maple Mono'; - font-style: bold; - font-weight: 400; - font-display: swap; - src: url(/fonts/MapleMono-Bold.woff2) format('woff2'); - } -} - @layer components { /* Scrollbar */ ::-webkit-scrollbar { diff --git a/styles/bun.lockb b/styles/bun.lockb index 81ccac2a27dcfea6a714f73b42762f30aa7a9590..42bd7d8c9e7158fb35d61b1698e2ad1dcafa3faf 100755 GIT binary patch delta 9610 zcmezRhN<-q(*!-ump}JaDr{3Pik!1*(Zj8;I@I56m1pC5mSVQS?y}S49~(>8GJrwZ z#Blj~xl%A2i4bRCVBlt8XecgCE=nv;Wmw0^z#ziF&`^+Blvt8l%#d3JQQXS}QCE_Y znwObY$xsUAH!v|U@G>wo%w=R?5M*FzNYBYn0_jL#fvC^T%*!lDEH2*7!oa{;&%n@N z3t})ZG!(KjFz_=lG#q1PVBlk5XfT0_$Fngo2rw`-1hGL3_GV{b;ACKEkYi_H;9_8C z_y(n4voSF6FfcTl*fOa_Qg?n3pS6osfe54EUJ3}S8pD1k6AFvN;M z($X$y8e3BjRk#Rhz*ljIfk{P)$*IK*3^@`I12#!OT+Sj1k%w8Fm06Mra!6*eE;yZi zmV~J9gIdrEbs)@qbaO*4=gJsC)dyqAF_ zGHY2#+-`)bPsu3*xr3n^-)tan+@1H7$;v~F_@gdBEb>4TKN@kYuTR3 z4J-Sha!)#YSL#k;GqB(}s&qYYdZ3bwv5$F5Ti@=f{VKfF7oB|%{-j=enNQPvAmy_KYX)-q$ewI_^{eDFf=ev z-l$~G5aZ-5aXThqrpc?s?U+27CchH5Wv{x{IBG!DezU zw>2jN2Lpo*149EV$RJJ|4h9AXup13U%~|JhFffEoe#>Re`Go`G6AqAWP9shTk7Y8* zU$vYNe?gpbl5_GaSvyWvE{Js;lQ(jhbJ}x3EMW)nSgS!iu;tsiAl9)?F61@mWaWk! z%Q^X@pgE^AHv@wU149D?$oEX`+>=+y+i@P{W?=9Dhi@UjIj1rYL<0-RSk6Q!j~yI9 zCwLebjKBc|^8GIakIS4>hZh!jps=XtWngdu`|=0&@F~s?k7vcqQd~q7{F)(<6 zo%2!Doaq?f-iyaV12A7`5748Cg<{6Gf4?d*5a|_>=0mJNCPL4 zk6h+V?*t~V;<01W5}f>s$Br{!kb%Jj;!G}crVWCVvv}<|KM680gfTEQuz`YwDNG0y zxvWcs7#K{z&b%Q62^ls}$Oy^^GccGlFf?#5Ffgz%FfbTSR^(8hoWQ}snIH^_7{F;L>L(S!14W&!<;EkWb!IbJJzKl-~i#U=6o&!(*tt3 zvgqVhTy{*sqLW{7*>U!YLgZM$3Fn$9#4d0m;FJ(!U~pw%Xy6A&Otlz9B_k;DFl`r` z{EF9(^SKxULjc%jLvC|cb8!X+JCNsCOT`%&TtF zroWPtv-s^eEu|nSiw%@_n98LlX9?LcU6h)#2uXR4K+oF#0> zxltOT9h|P2-b+t@C2Ys!C^K10#Ez*%W^$H@9n&hA$*V-{IDgAPasVSJE}6V!Cu@n? zaZZqBU;vdqOp`YXnRA|(Wnj>QBp)$zroXb2UkTbVS;|dbC2Gf1FE{y>s2$UGFpu-S z9K_RLk8!%lLsBIZ*yuj_$*;uhIFHFg3}Xd*?2G(lEp9tOQw4}hP$;u9Ffj0gLwbQE zC!@mTRjPKZx{3@80h8B?TQk)vPR>#@1Enseql%MXshUkT;9!}Yz`-(kg&I#iI|I0J z05S&DNPyGK3=9lP3=9m63=9m)P+A2n%fNt*2Dw3#fq{XEfq_9Ast%b3nWFN2Wm**h1CYLFGX-NWCLe+zBcU zqCw)W44{?-132M>rCh-zgb(tGJCqNiK^nZE26;opK{Uu>A1EIi4btZi4VgfwIyeor zp8+HQQWyeNfJ}od2!&c429*cVAO}W3`N%X#JPI0uu~2zzG$@nSCqfmbKo#PnK?bKm z)q!Y`0a;Lof!ZY?DSR|Ye=bN70|NtyW(3859@Kz*C|v+mfQ<%)1gLohQUanu{2~Sh z22kQFhX!#a)ErQM10)Y>T7lGqXb`^!s;&;Kt{&tBkTiq_xv&u|!NAZ25yne{3h*wd zIuH%Apc|^c2PzJtLFR(miXb6mnpK8@fnhS#MN>ht3=9l#dh$#)>Cp@d4i)5#2@(MD zL4gX&m>@og2IZg83<}Dapb!DkqZyRO8Prm*#9@82TwM9Vnab-lU6;kaTX;j=sNTp) z$dC2XtX%6EPv^FDgsG~8WlJ5hH!+`4Hs$SugC{5DcYe2fTvz?D=}Pvd>672;NlgCg zw{6UMxgkGgv^ z3QiXE2%lVG!aKRt!<$if@2w0kAC5kBdmER zhx&Uns!v|%A3mAGhIcY+fH$M&2s~N|W%(Hyn5;-!<`OG@q<#8a}zhk#};hskbkvT>;87Ak5-?YC_GAnLiEB{V&V5 z+PtM;l2_d{rYvrsAlKV-+@Cdie2C-Q{5K)aT4gW82S*!~OC6?ln_D(GWbZixZ!+nLblfQYNV!wMxMBRH|hiOF6 z{VVnxXPiDS6_B&dD_C&)spk)Pzlon*c&1TAIcc@Zj*q(%&N719VgLC5F)}cKnm8b5 zf-sAPs{fqk*_)fozWD_A8{H4u(>Ljx%j#G0pG=Nkim(yh-QxGOo%!`|$^LTtBQqDR zn)>6pZ|#+xX{leiCf20*?b$tNa<8*QePC&ime9F_L9^fIa|B)Cj~BlY{CZ8f+S{N_ z`#(Kw`JlqHZ-?J?>@1irBV85z~@KI&QkFXNUpO7ISYzejvjBn zb@JiXeLmkJR`H!|IXrJ^b=|L-W+{U8j|KN%JaVz9iaBrk_FlCGdfS+Xb?Vmg)8r5QA2M= z*U38#!zV|$@lKXC@@90OJkuh4GKV|wzn3aq zjaac=Ci~BWcGrhCtqXLGHVejH$TB><^V0s7|MrHQ&e9SaJX$$cnXydX>nSmr)ykVO zaB`qk_~Z&N-pO07ycvThGg^mF*6`+?oNDdO7&@8LCVcV@Z{EqZA>NGPlV6&LPww#H zo$PDj%@{d(qIvja3t!&Jxi;R6(UW)Dgin6q%R5=t)|)YQa;9zgyK@X0^?c_-`Idow0auCx!Iydr>i@?Cpx#?;B04&jp{0(mF* zI(RdtPd*tEKA9tkck*6qZ^q2YHyy$mvnCrlhBIbQ?sN>FoDs}B`K_ZjWA0>2r|`)V zA-t34I(akZPgV>KpL`;OcXFwbw=bv#1`ji)6{(z+b)UGdoIkxvSo)`>b;2_1smhGO zo+Yo_&+Pp-$7*%@lDD7gl8?EB^BrMNI8^kYZ;QwM@4Wxit3Gc#Yr11{Zm7iMr(xcV z#gjY3!zXKm@lJjl?##)^07>?xlMTcDCo6M^Onwlv$l;csqC)7z8FaMxF+Va>0f{`2{A|MNXRB5W&MVOK5UmL=x*gVFm`i&5e=E z89@psKSah7dswJ`hIc zgW4saninJnYaWBzCZMhsNDS0OhNGVaw*R>qzynvHp(O@TQyOF* z$Z=+%kx2#yh6=D=1@J&EXzT(shRy(MYp;NeVlqsqn7pq>NfK-@SUqHfw4Z^2VFCjK z!^FvVtNiQ57{J5K;AZa>1_p5Zx|xB20W|2^!N9=K2^v3TU|;~b78C&>-+_jWL0y^{ z2Jo;9186K1)KiILU;uZWf*2SWf*BYXBpAR$;tWy@;307aQ1e=rfq_AefdTB;ZcsBH z)B~txU|;~n4XB?5>a_VXfCp8;U0YQK1_m_-1_pHo1_lji_gH~}fkBahfdSO3XkcJq z0QDX~eKJtF0cwl7F)%QIy0v=H?k}h-4C+3Eq6^fa0JTp&7#J8lK|M}THy0F7hS0b% zhQ6c*P>eD#Fo;xx;tbpm1*I8Kyn{j<gC;)9Y2Xv`phfq?;*_(5qI zlmtLg1Zw>!FfcI0GcYiKGD$Q80|O}YfYgJM3MjLH)Fm@8Fo1>t>OqrjAWfiL1)7Bd z6+R#hAOk?T52P3r*Pyfp$~Yi{Kxqvm24aJ9DkwdIaxJJ!4oXL$G-$=Zz~BMudxJ6@ zsEZ6LPi&%%%wkI|>=ca56pAyeQuEU$@2_`d%%1$Y&x6fO&wzoUda`4`u{5Yi08O`XVvIA^Gu8u5yi6`^l$^Yx--y|aVdmub4U*EZxtiCxM`lMn4?NAp7-ynq zpl89rux7I01WCqSle-%w#b8rb#ho7iwk%btVPcFk)H9s?phaYI-UN0Q6LW_1llM1D z`opHCVDleP(+m-lT>d`~Z{5R^Gz%(YgqSCbgifk3F~*tc8G(GmG5O&H5jjJU!(p|N zhT+ol(l1KCgB@UGq-QaCeuKDzsh$BtEp!@gw&?a(QmuQknHb{?AfBGWIJu<5h-oh4 zb$iUFcG})z7 zi|ZN_WR3``UIjWqDJdkB6Qaz21svLjAorWFOkUF|!DP!a`B{kO4eN%fQg^;Ye-4oa<*UGcm4W0q52R*fef2+l1QW8CDfcj4;z+ z^SF`w-?7fQ_f~_6G0sR2WEcZ%W;Z>mdoc@|^@`f&ndOmi@tk{oV)ftB(rYvP*{LTihh2Yb{vPut?4{$2&WMYgn zhgfUN4w;Edi9CF09{&PXCPoc*$iymW3K*0uLodHksmd3KWMYi7&;!}VkiiZvs2U8p zKEBp+6JcRu3}I(r5MgCtXjsoa*{54V1~%RNH-~XUfs)HDaHN3s%CS#w=$0^p%}Yly z&bjUOeSQHGW1K0(L9mJCHz`q}cWn;1fUN}wLp#UhbKMe5U=u{;4H+04Pyf&&$y0|#z-blijMr?F19~LPVAJH7;ey41W)KH{=bAjP zN5Tw?J1zB$K#3o>p}ahk-}Fc@N$^Z&>osDU&okMo*T)Pt1J3m_T-JU;&o6L-1bY}Z z#r^Q#*>xHwyfdIF!jOSs65r%Ky%I98x$(-6TW{R9-vh}U#-Pk0%0Kx-uY?S2()_1j zYW6>qvopaanCKarFa!!r{?jZW1Dh>x-jI4;%hDf|Fbwq!^voF;G6g0Zv>1uOrp;&1 zJSA0_yMr5Sg5l)2iE>OHLX+zzN-%=6JtL@ekz_QQEY~MzioN*JGh|>07KWr8?W>E6 zE@p3v0h?w7l9@TVw5gHt=w#bQNg1rF3_wA9Qgrf}79$zhRQb}*1l3~aL8_Va;_rvywhz&XGWWb-D8$$qU8#<1CQ zEq#waKcYoGffazwgw317CU?Qf45W2(V1u*_Y?8Y>Wy;1qw@%&%%Yq$yP-^m=Rtd(w z$y-~E85d1{*=o#$&oSUq0OFX*S#45GdNPyS+I+B>4=OM~RquP5$?w`En5<E#$qyz5SjjU&R)^dUtzHtoT0v5a5mH7siih(37cseK#t13N zj#|bC+m~5g&j1ZKgE|mvC$E~+U2kZrSCU@kkeHK`pR8Y4nwgxfn^RPpl9^JOUz(em zmR;gdaQ*G%S~ zZMS*Cya?vWQk(TAOU<*{{9<7dBiI6u&H9tqOmmoAwpmLM9Kt#IDT&1ydf;^+lb3E* zQMa=-)-%vEP^hU<$jmFwOi3*&2C?mIL5&at-Nc-Nj6^*ngUJ`CDNpv?qCaV?`sDPj PWndR8Y*n2+F>(d~u=5UO delta 10794 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 I$&O(&0L9^Md;kCd diff --git a/styles/package.json b/styles/package.json index bff8cab..e1348de 100644 --- a/styles/package.json +++ b/styles/package.json @@ -1,10 +1,10 @@ { "scripts": { - "build": "tailwindcss build -i ./base.css -o ../public/styles.css --minify", - "dev": "tailwindcss build -i ./base.css -o ../public/styles.css --watch --minify" + "build": "tailwindcss build -i ./base.css -o ../static/styles.css --minify", + "dev": "tailwindcss build -i ./base.css -o ../static/styles.css --watch --minify" }, "dependencies": { - "@tailwindcss/typography": "^0.5.10", - "tailwindcss": "^3.4.1" + "@tailwindcss/typography": "^0.5.15", + "tailwindcss": "^3.4.15" } -} \ No newline at end of file +} diff --git a/styles/tailwind.config.js b/styles/tailwind.config.js index f415ac4..8a4c268 100644 --- a/styles/tailwind.config.js +++ b/styles/tailwind.config.js @@ -2,7 +2,7 @@ const defaultTheme = require("tailwindcss/defaultTheme"); /** @type {import('tailwindcss').Config} */ module.exports = { - content: ["../templates/**/*.tmpl", "../lib/codeparser.go"], + content: ["../templates/**/*.tera"], safelist: [ { pattern: /theme+/ }, ], diff --git a/templates/components/usercard.tmpl b/templates/components/usercard.tmpl deleted file mode 100644 index 3c400ad..0000000 --- a/templates/components/usercard.tmpl +++ /dev/null @@ -1,6 +0,0 @@ -{{define "usercard"}} - - {{.Name}}'s icon - {{.Name}} - -{{end}} diff --git a/templates/layouts/base.tmpl b/templates/layouts/base.tera similarity index 72% rename from templates/layouts/base.tmpl rename to templates/layouts/base.tera index 60a0d28..cbcc27e 100644 --- a/templates/layouts/base.tmpl +++ b/templates/layouts/base.tera @@ -1,4 +1,6 @@ -{{define "base"}} +{% block title %}{% set title = "Isabel Roses" %}{% endblock %} +{% block description %}{% set description = "A website documenting my development journey" %}{% endblock %} + @@ -7,193 +9,192 @@ - {{template "title" .}} + {{ title }} - + - + - - + + - + - + - + - + - {{template "additionalhead" .}} + {% block additionalhead %} {% endblock %} - {{template "header" .}} + {% include "partials/header.tera" %}
- {{template "main" .}} + {% block main %} {% endblock %}
-{{end}} diff --git a/templates/layouts/stripped.tmpl b/templates/layouts/stripped.tera similarity index 59% rename from templates/layouts/stripped.tmpl rename to templates/layouts/stripped.tera index 85c60aa..59bda8b 100644 --- a/templates/layouts/stripped.tmpl +++ b/templates/layouts/stripped.tera @@ -1,4 +1,6 @@ -{{define "stripped"}} +{% block title %}{% set title = "Isabel Roses" %}{% endblock %} +{% block description %}{% set description = "A website documenting my development journey" %}{% endblock %} + @@ -7,34 +9,33 @@ - {{template "title" .}} + {{ title }} - + - + - - + + - + - + - {{template "additionalhead" .}} + {% block additionalhead %} {% endblock %}
- {{template "main" .}} + {% block main %} {% endblock %}
-{{end}} diff --git a/templates/main.go b/templates/main.go deleted file mode 100644 index 38623c8..0000000 --- a/templates/main.go +++ /dev/null @@ -1,55 +0,0 @@ -package templates - -import ( - "embed" - "html/template" - "io/fs" - "log" - "net/http" - "path/filepath" - "runtime" -) - -//go:embed layouts/* pages/* components/* -var templatesFS embed.FS - -// RenderTemplate renders a template with the given layout, components, and properties. -func RenderTemplate(w http.ResponseWriter, layout string, components []string, props interface{}) error { - _, filename, _, _ := runtime.Caller(1) - page := filepath.Base(filename) - page = page[:len(page)-len(filepath.Ext(page))] - - // Collect the template paths - templatePaths := []string{ - "layouts/" + layout + ".tmpl", - "pages/" + page + ".tmpl", - } - - for _, component := range components { - templatePaths = append(templatePaths, "components/"+component+".tmpl") - } - - // Create a new template and parse the files from the embedded file system - ts := template.New("") - for _, path := range templatePaths { - content, err := fs.ReadFile(templatesFS, path) - if err != nil { - log.Print(err.Error()) - return err - } - ts, err = ts.Parse(string(content)) - if err != nil { - log.Print(err.Error()) - return err - } - } - - // Execute the template - err := ts.ExecuteTemplate(w, layout, props) - if err != nil { - log.Print(err.Error()) - return err - } - - return nil -} diff --git a/templates/pages/2004.tmpl b/templates/pages/2004.tera similarity index 94% rename from templates/pages/2004.tmpl rename to templates/pages/2004.tera index 62068cb..87c655f 100644 --- a/templates/pages/2004.tmpl +++ b/templates/pages/2004.tera @@ -1,7 +1,9 @@ -{{define "title"}}2004{{end}} -{{define "description"}}The time when the internet was good{{end}} +{% extends "layouts/stripped.tera" %} -{{define "additionalhead"}} +{% block title %}{% set title = "2004" %}{% endblock %} +{% block description %}{% set description = "The time when the internet was good" %}{% endblock %} + +{% block additionalhead %} -{{end}} - -{{define "main"}} +{% endblock %} +{% block main %} - -{{end}} +{% endblock %} diff --git a/templates/pages/blog.tera b/templates/pages/blog.tera new file mode 100644 index 0000000..cafc3c4 --- /dev/null +++ b/templates/pages/blog.tera @@ -0,0 +1,44 @@ +{% extends "layouts/base.tera" %} + +{% block title %}{% set title = "Blog" %}{% endblock %} +{% block description %}{% set description = "The parts of my life that I put out into the world" %}{% endblock %} + +{% block main %} +
+
+

+ {{ title }} + {% if tag %} > 🏷️ + {{ tag }} + {% endif %} +

+

{{ description }}

+
+ +
+ + +
+ + {% for post in posts %} + {% include "partials/blogpreview.tera" %} + {% endfor %} +
+{% endblock %} diff --git a/templates/pages/blog.tmpl b/templates/pages/blog.tmpl deleted file mode 100644 index b84a89d..0000000 --- a/templates/pages/blog.tmpl +++ /dev/null @@ -1,39 +0,0 @@ -{{define "title"}}Blog{{end}} -{{define "description"}}The parts of my life that I put out into the world{{end}} -{{define "additionalhead"}}{{end}} - -{{define "main"}} -
-
-

- {{template "title" .}} {{if .Tag}} > 🏷️ - {{.Tag}}{{end}} -

-

{{template "description" .}}

-
- -
- - -
- - {{range .Posts}} {{template "blogpreview" .}} {{end}} -
-{{end}} diff --git a/templates/pages/donos.tera b/templates/pages/donos.tera new file mode 100644 index 0000000..e895d98 --- /dev/null +++ b/templates/pages/donos.tera @@ -0,0 +1,35 @@ +{% extends "layouts/base.tera" %} + +{% block title %}{% set title = "Donations" %}{% endblock %} +{% block description %}{% set description = "A special thanks to all those who support me!" %}{% endblock %} + +{% block main %} +
+
+

{{ title }}

+

{{ description }}

+
+ +
+

Subscribers

+
+ {% for donor in donors %} + {% if donor.tier != "OneTime" %} + {% include "partials/usercard.tera" %} + {% endif %} + {% endfor %} +
+
+ +
+

One Time Donors

+
+ {% for donor in donors %} + {% if donor.tier == "OneTime" %} + {% include "partials/usercard.tera" %} + {% endif %} + {% endfor %} +
+
+
+{% endblock %} diff --git a/templates/pages/donos.tmpl b/templates/pages/donos.tmpl deleted file mode 100644 index 235ae9e..0000000 --- a/templates/pages/donos.tmpl +++ /dev/null @@ -1,36 +0,0 @@ -{{define "title"}}Donations{{end}} -{{define "description"}}A special thanks to all those who support me!{{end}} -{{define "additionalhead"}}{{end}} - -{{define "main"}} -
-
-

{{template "title" .}}

-

{{template "description" .}}

-
- -
- {{if .Subscribers | len}} -

Subscribers

-
- {{range .Subscribers}} - {{template "usercard" .}} - {{end}} -
- {{end}} -
- -
-

One Time Donors

-
- {{if .OneTimeDonors | len}} - {{range .OneTimeDonors}} - {{template "usercard" .}} - {{end}} - {{else}} -

No one time donors yet

- {{end}} -
-
-
-{{end}} diff --git a/templates/pages/error.tera b/templates/pages/error.tera new file mode 100644 index 0000000..cb87c88 --- /dev/null +++ b/templates/pages/error.tera @@ -0,0 +1,14 @@ +{% extends "layouts/base.tera" %} + +{% block title %}{% set title = "Error " ~ code %}{% endblock %} +{% block description %}{% set description = title %}{% endblock %} + +{% block main %} +
+ Error {{ code }} +
+{% endblock %} diff --git a/templates/pages/error.tmpl b/templates/pages/error.tmpl deleted file mode 100644 index 4f254ab..0000000 --- a/templates/pages/error.tmpl +++ /dev/null @@ -1,13 +0,0 @@ -{{define "title"}}Error {{.Code}}{{end}} -{{define "description"}}{{template "title" .}}{{end}} -{{define "additionalhead"}}{{end}} - -{{define "main"}} -
- {{.Code}} -
-{{end}} diff --git a/templates/pages/home.tmpl b/templates/pages/home.tera similarity index 89% rename from templates/pages/home.tmpl rename to templates/pages/home.tera index 63c7056..6df23b5 100644 --- a/templates/pages/home.tmpl +++ b/templates/pages/home.tera @@ -1,8 +1,6 @@ -{{define "title"}}Isabel Roses{{end}} -{{define "description"}}A website documenting my development journey{{end}} -{{define "additionalhead"}}{{end}} +{% extends "layouts/base.tera" %} -{{define "main"}} +{% block main %}

Hello, I'm @@ -40,4 +38,4 @@

-{{end}} +{% endblock %} diff --git a/templates/pages/post.tmpl b/templates/pages/post.tera similarity index 89% rename from templates/pages/post.tmpl rename to templates/pages/post.tera index 6c2ec8e..ccc9b6e 100644 --- a/templates/pages/post.tmpl +++ b/templates/pages/post.tera @@ -1,36 +1,39 @@ -{{define "title"}}Blog > {{.Title}}{{end}} -{{define "description"}}{{.Description}}{{end}} -{{define "additionalhead"}} +{% extends "layouts/base.tera" %} + +{% block title %}{% set title = "Blog > " ~ post.title %}{% endblock %} +{% block description %}{% set description = post.description %}{% endblock %} + +{% block additionalhead %} -{{end}} +{% endblock %} -{{define "main"}} +{% block main %}
-

{{.Title}}

-

{{.Description}}

-

{{.Date}}

+

{{ post.title }}

+

{{ post.description }}

+

{{ post.date }}

-
{{.Content}}
+
{{ post.content }}