From a41218e0d982f0bae7c05e68800bba2ed1255222 Mon Sep 17 00:00:00 2001 From: Pantonshire Date: Tue, 17 May 2022 03:40:12 +0100 Subject: [PATCH] Initial commit --- .gitignore | 3 + Cargo.lock | 1611 ++++++++++++++++++++++++++++++ Cargo.toml | 6 + blog_server/Cargo.toml | 32 + blog_server/src/codeblock.rs | 67 ++ blog_server/src/fs_watcher.rs | 35 + blog_server/src/handlers.rs | 1 + blog_server/src/html_response.rs | 211 ++++ blog_server/src/main.rs | 221 ++++ blog_server/src/post.rs | 295 ++++++ blog_server/src/posts_store.rs | 159 +++ blog_server/src/render.rs | 262 +++++ static/style/code.css | 344 +++++++ static/style/main.css | 0 utils/css_gen/Cargo.toml | 7 + utils/css_gen/src/main.rs | 30 + 16 files changed, 3284 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 blog_server/Cargo.toml create mode 100644 blog_server/src/codeblock.rs create mode 100644 blog_server/src/fs_watcher.rs create mode 100644 blog_server/src/handlers.rs create mode 100644 blog_server/src/html_response.rs create mode 100644 blog_server/src/main.rs create mode 100644 blog_server/src/post.rs create mode 100644 blog_server/src/posts_store.rs create mode 100644 blog_server/src/render.rs create mode 100644 static/style/code.css create mode 100644 static/style/main.css create mode 100644 utils/css_gen/Cargo.toml create mode 100644 utils/css_gen/src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10166c2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +/target/ +/config.kdl \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..8dd4d25 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1611 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "async-trait" +version = "0.1.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed6aa3524a2dfcf9fe180c51eae2b58738348d819517ceadf95789c51fff7600" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "axum" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab2504b827a8bef941ba3dd64bdffe9cf56ca182908a147edd6189c95fbcae7d" +dependencies = [ + "async-trait", + "axum-core", + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "hyper", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-http", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da31c0ed7b4690e2c78fe4b880d21cd7db04a346ebc658b4270251b695437f17" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "mime", +] + +[[package]] +name = "backtrace" +version = "0.3.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11a17d453482a265fd5f8479f2a3f405566e6ca627837aaddb85af8b1ab8ef61" +dependencies = [ + "addr2line", + "cc", + "cfg-if 1.0.0", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "blog_server" +version = "0.1.0" +dependencies = [ + "axum", + "chrono", + "knuffel", + "libshire", + "maud", + "miette", + "notify", + "pulldown-cmark", + "syntect", + "tokio", + "tower", + "tower-http", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "bytes" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" + +[[package]] +name = "cc" +version = "1.0.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[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.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "libc", + "num-integer", + "num-traits", + "time 0.1.44", + "winapi 0.3.9", +] + +[[package]] +name = "chumsky" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d02796e4586c6c41aeb68eae9bfb4558a522c35f1430c14b40136c3706e09e4" + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "css_gen" +version = "0.1.0" +dependencies = [ + "syntect", +] + +[[package]] +name = "filetime" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0408e2626025178a6a7f7ffc05a25bc47103229f19c113755de7bf63816290c" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "redox_syscall", + "winapi 0.3.9", +] + +[[package]] +name = "flate2" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39522e96686d38f4bc984b9198e3a0613264abaebaff2c5c918bfa6b6da09af" +dependencies = [ + "cfg-if 1.0.0", + "crc32fast", + "libc", + "miniz_oxide", +] + +[[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.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +dependencies = [ + "matches", + "percent-encoding", +] + +[[package]] +name = "fsevent" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ab7d1bd1bd33cc98b0889831b72da23c0aa4df9cec7e0702f46ecea04b35db6" +dependencies = [ + "bitflags", + "fsevent-sys", +] + +[[package]] +name = "fsevent-sys" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f41b048a94555da0f42f1d632e2e19510084fb8e303b0daa2816e733fb3644a0" +dependencies = [ + "libc", +] + +[[package]] +name = "fuchsia-zircon" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" +dependencies = [ + "bitflags", + "fuchsia-zircon-sys", +] + +[[package]] +name = "fuchsia-zircon-sys" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" + +[[package]] +name = "futures-channel" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" + +[[package]] +name = "futures-sink" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" + +[[package]] +name = "futures-task" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" + +[[package]] +name = "futures-util" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "pin-utils", +] + +[[package]] +name = "getopts" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "gimli" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78cc372d058dcf6d5ecd98510e7fbc9e5aec4d21de70f65fea8fecebcd881bd4" + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" + +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "http" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff8670570af52249509a86f5e3e18a08c60b177071826898fde8997cf5f6bfbb" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ff4f84919677303da5f147645dbea6b1881f368d03ac84e1dc09031ebd7b2c6" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "http-range-header" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfe8eed0a9285ef776bb792479ea3834e8b94e13d615c2f66d03dd50a435a29" + +[[package]] +name = "httparse" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "496ce29bb5a52785b44e0f7ca2847ae0bb839c9bd28f69acac9b99d461c0c04c" + +[[package]] +name = "httpdate" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" + +[[package]] +name = "hyper" +version = "0.14.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b26ae0a80afebe130861d90abf98e3814a4f28a4c6ffeb5ab8ebb2be311e0ef2" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "indexmap" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f647032dfaa1f8b6dc29bd3edb7bbef4861b8b8007ebb118d6db284fd59f6ee" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "inotify" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4816c66d2c8ae673df83366c18341538f234a26d65a9ecea5c348b453ac1d02f" +dependencies = [ + "bitflags", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + +[[package]] +name = "iovec" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" +dependencies = [ + "libc", +] + +[[package]] +name = "is_ci" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616cde7c720bb2bb5824a224687d8f77bfd38922027f01d825cd7453be5099fb" + +[[package]] +name = "itoa" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" + +[[package]] +name = "kernel32-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + +[[package]] +name = "knuffel" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f9f7a07459e9dc5d07f5dabfc2c2a965bf39195451eb785b61460c65f386eef" +dependencies = [ + "base64", + "chumsky", + "knuffel-derive", + "miette", + "thiserror", + "unicode-width", +] + +[[package]] +name = "knuffel-derive" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbcbdb0b6f26a4e5ecb0dd9074a430398a41b2c1624c205bcc202541ddc15488" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "libc" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5916d2ae698f6de9bfb891ad7a8d65c09d232dc58cc4ac433c7da3b2fd84bc2b" + +[[package]] +name = "libshire" +version = "0.1.0" +source = "git+https://github.com/pantonshire/libshire#70fa5daf6c11229d88687324475c016cb49789f1" + +[[package]] +name = "line-wrap" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f30344350a2a51da54c1d53be93fade8a237e545dbcc4bdbe635413f2117cab9" +dependencies = [ + "safemem", +] + +[[package]] +name = "linked-hash-map" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" + +[[package]] +name = "lock_api" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "matches" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" + +[[package]] +name = "matchit" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73cbba799671b762df5a175adf59ce145165747bb891505c43d09aefbbf38beb" + +[[package]] +name = "maud" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7384febb4f8fc970cc2efab1650a6f48cac2e79b0b29587c90497b646fb10e1e" +dependencies = [ + "maud_macros", +] + +[[package]] +name = "maud_macros" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "423430ac467408136d7de93f2929debd8a7bc3e795c92476f45e259b158e3355" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "miette" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c90329e44f9208b55f45711f9558cec15d7ef8295cc65ecd6d4188ae8edc58c" +dependencies = [ + "atty", + "backtrace", + "miette-derive", + "once_cell", + "owo-colors", + "supports-color", + "supports-hyperlinks", + "supports-unicode", + "terminal_size", + "textwrap", + "thiserror", + "unicode-width", +] + +[[package]] +name = "miette-derive" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b5bc45b761bcf1b5e6e6c4128cd93b84c218721a8d9b894aa0aff4ed180174c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" + +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "miniz_oxide" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2b29bd4bc3f33391105ebee3589c19197c4271e3e5a9ec9bfe8127eeff8f082" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.6.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4afd66f5b91bf2a3bc13fad0e21caedac168ca4c707504e75585648ae80e4cc4" +dependencies = [ + "cfg-if 0.1.10", + "fuchsia-zircon", + "fuchsia-zircon-sys", + "iovec", + "kernel32-sys", + "libc", + "log", + "miow", + "net2", + "slab", + "winapi 0.2.8", +] + +[[package]] +name = "mio" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "713d550d9b44d89174e066b7a6217ae06234c10cb47819a88290d2b353c31799" +dependencies = [ + "libc", + "log", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys", +] + +[[package]] +name = "mio-extras" +version = "2.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52403fe290012ce777c4626790c8951324a2b9e3316b3143779c72b029742f19" +dependencies = [ + "lazycell", + "log", + "mio 0.6.23", + "slab", +] + +[[package]] +name = "miow" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d" +dependencies = [ + "kernel32-sys", + "net2", + "winapi 0.2.8", + "ws2_32-sys", +] + +[[package]] +name = "net2" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "391630d12b68002ae1e25e8f974306474966550ad82dac6886fb8910c19568ae" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "notify" +version = "4.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae03c8c853dba7bfd23e571ff0cff7bc9dceb40a4cd684cd1681824183f45257" +dependencies = [ + "bitflags", + "filetime", + "fsevent", + "fsevent-sys", + "inotify", + "libc", + "mio 0.6.23", + "mio-extras", + "walkdir", + "winapi 0.3.9", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "num_threads" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +dependencies = [ + "libc", +] + +[[package]] +name = "object" +version = "0.28.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42c982f2d955fac81dd7e1d0e1426a7d702acd9c98d19ab01083a6a0328c424" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9" + +[[package]] +name = "onig" +version = "6.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67ddfe2c93bb389eea6e6d713306880c7f6dcc99a75b659ce145d962c861b225" +dependencies = [ + "bitflags", + "lazy_static", + "libc", + "onig_sys", +] + +[[package]] +name = "onig_sys" +version = "69.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd3eee045c84695b53b20255bb7317063df090b68e18bfac0abb6c39cf7f33e" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] +name = "owo-colors" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "decf7381921fea4dcb2549c5667eda59b3ec297ab7e2b5fc33eac69d2e7da87b" + +[[package]] +name = "parking_lot" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "redox_syscall", + "smallvec", + "windows-sys", +] + +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" + +[[package]] +name = "pin-project" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58ad3879ad3baf4e44784bc6a718a8698867bb991f8ce24d1bcbe2cfb4c3a75e" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "744b6f092ba29c3650faf274db506afd39944f48420f6c86b17cfe0ee1cb36bb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" + +[[package]] +name = "plist" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd39bc6cdc9355ad1dc5eeedefee696bb35c34caf21768741e81826c0bbd7225" +dependencies = [ + "base64", + "indexmap", + "line-wrap", + "serde", + "time 0.3.9", + "xml-rs", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pulldown-cmark" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34f197a544b0c9ab3ae46c359a7ec9cbbb5c7bf97054266fecb7ead794a181d6" +dependencies = [ + "bitflags", + "getopts", + "memchr", + "unicase", +] + +[[package]] +name = "quote" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + +[[package]] +name = "rustc-demangle" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" + +[[package]] +name = "ryu" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" + +[[package]] +name = "safemem" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "serde" +version = "1.0.137" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1" + +[[package]] +name = "serde_derive" +version = "1.0.137" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c" +dependencies = [ + "itoa", + "ryu", + "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 = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32" + +[[package]] +name = "smallvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" + +[[package]] +name = "smawk" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043" + +[[package]] +name = "socket2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" +dependencies = [ + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "supports-color" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4872ced36b91d47bae8a214a683fe54e7078875b399dfa251df346c9b547d1f9" +dependencies = [ + "atty", + "is_ci", +] + +[[package]] +name = "supports-hyperlinks" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "590b34f7c5f01ecc9d78dba4b3f445f31df750a67621cf31626f3b7441ce6406" +dependencies = [ + "atty", +] + +[[package]] +name = "supports-unicode" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8b945e45b417b125a8ec51f1b7df2f8df7920367700d1f98aedd21e5735f8b2" +dependencies = [ + "atty", +] + +[[package]] +name = "syn" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbaf6116ab8924f39d52792136fb74fd60a80194cf1b1c6ffa6453eef1c3f942" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20518fe4a4c9acf048008599e464deb21beeae3d3578418951a189c235a7a9a8" + +[[package]] +name = "syntect" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b20815bbe80ee0be06e6957450a841185fcf690fe0178f14d77a05ce2caa031" +dependencies = [ + "bincode", + "bitflags", + "flate2", + "fnv", + "lazy_static", + "lazycell", + "onig", + "plist", + "regex-syntax", + "serde", + "serde_derive", + "serde_json", + "walkdir", + "yaml-rust", +] + +[[package]] +name = "terminal_size" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" +dependencies = [ + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "textwrap" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" +dependencies = [ + "smawk", + "unicode-linebreak", + "unicode-width", +] + +[[package]] +name = "thiserror" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" +dependencies = [ + "once_cell", +] + +[[package]] +name = "time" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi 0.3.9", +] + +[[package]] +name = "time" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2702e08a7a860f005826c6815dcac101b19b5eb330c27fe4a5928fec1d20ddd" +dependencies = [ + "itoa", + "libc", + "num_threads", +] + +[[package]] +name = "tokio" +version = "1.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4903bf0427cf68dddd5aa6a93220756f8be0c34fcfa9f5e6191e103e15a31395" +dependencies = [ + "bytes", + "libc", + "memchr", + "mio 0.8.3", + "num_cpus", + "once_cell", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "winapi 0.3.9", +] + +[[package]] +name = "tokio-macros" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-util" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f988a1a1adc2fb21f9c12aa96441da33a1728193ae0b95d2be22dbd17fcb4e5c" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a89fd63ad6adf737582df5db40d286574513c69a11dac5214dc3b5603d6713e" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d342c6d58709c0a6d48d48dabbb62d4ef955cf5f0f3bbfd845838e7ae88dbae" +dependencies = [ + "bitflags", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-range-header", + "httpdate", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "tokio", + "tokio-util", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "343bc9466d3fe6b0f960ef45960509f84480bf4fd96f92901afe7ff3df9d3a62" + +[[package]] +name = "tower-service" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" + +[[package]] +name = "tracing" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0ecdcb44a79f0fe9844f0c4f33a342cbcbb5117de8001e6ba0dc2351327d09" +dependencies = [ + "cfg-if 1.0.0", + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6b8ad3567499f98a1db7a752b07a7c8c7c7c34c332ec00effb2b0027974b7c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f54c8ca710e81886d498c2fd3331b56c93aa248d49de2222ad2742247c60072f" +dependencies = [ + "lazy_static", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +dependencies = [ + "lazy_static", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bc28f93baff38037f64e6f43d34cfa1605f27a49c34e8a04c5e78b0babf2596" +dependencies = [ + "ansi_term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" + +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-ident" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee" + +[[package]] +name = "unicode-linebreak" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a52dcaab0c48d931f7cc8ef826fa51690a08e1ea55117ef26f89864f532383f" +dependencies = [ + "regex", +] + +[[package]] +name = "unicode-width" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "walkdir" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +dependencies = [ + "same-file", + "winapi 0.3.9", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +dependencies = [ + "log", + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-build" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" +dependencies = [ + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" + +[[package]] +name = "windows_i686_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" + +[[package]] +name = "windows_i686_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" + +[[package]] +name = "ws2_32-sys" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + +[[package]] +name = "xml-rs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..697cf69 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,6 @@ +[workspace] + +members = [ + "blog_server", + "utils/css_gen" +] diff --git a/blog_server/Cargo.toml b/blog_server/Cargo.toml new file mode 100644 index 0000000..8936e29 --- /dev/null +++ b/blog_server/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "blog_server" +version = "0.1.0" +edition = "2021" + +[dependencies] +# My own utilities library +libshire = { git = "https://github.com/pantonshire/libshire" } +# Async runtime for Axum +tokio = { version = "1", features = ["full"] } +# Web server framework +axum = "0.5" +# Middleware for the web server +tower = { version = "0.4", features = ["limit"] } +tower-http = { version = "0.3", features = ["fs", "trace"] } +# Compile-time HTTP templating +maud = "0.23" +# KDL parsing +knuffel = "2" +# CommonMark parsing +pulldown-cmark = "0.9" +# Syntax highlighting +syntect = "4" +# Filesystem event watcher +notify = "4" +# Time library +chrono = "0.4" +# Logging for observability +tracing = "0.1" +tracing-subscriber = "0.3" +# Pretty errors +miette = { version = "4", features = ["fancy"] } diff --git a/blog_server/src/codeblock.rs b/blog_server/src/codeblock.rs new file mode 100644 index 0000000..3e21c4b --- /dev/null +++ b/blog_server/src/codeblock.rs @@ -0,0 +1,67 @@ +use maud::{html, Markup, PreEscaped}; +use syntect::html::{ClassedHTMLGenerator, ClassStyle}; +use syntect::parsing::SyntaxSet; +use syntect::util::LinesWithEndings; + +const CLASS_STYLE: ClassStyle = ClassStyle::SpacedPrefixed { prefix: "cb_" }; + +pub struct CodeBlockRenderer { + syntax_set: SyntaxSet, +} + +impl CodeBlockRenderer { + pub fn new() -> Self { + // Load Syntect's default syntax set from Sublime syntax definitions embedded in the + // binary. + let default_syntax_set = SyntaxSet::load_defaults_newlines(); + Self::new_with_syntax_set(default_syntax_set) + } + + pub fn new_with_syntax_set(syntax_set: SyntaxSet) -> Self { + Self { + syntax_set, + } + } + + pub fn render(&self, lang: &str, source: &str) -> Markup { + const CONTEXT_DELIM: &str = "@@"; + + // Grab the optional context information between @@s from the first line of the code block. + let (context, source) = source.split_once('\n') + .and_then(|(context, source)| context + .trim() + .strip_prefix(CONTEXT_DELIM) + .and_then(|context| context.strip_suffix(CONTEXT_DELIM)) + .map(|context| (Some(context.trim()), source))) + .unwrap_or((None, source)); + + // Search the syntax set for the syntax definition for the language specified for the code + // block (after the triple backtick), and default to plaintext if no syntax definition is + // found. + let syntax = self.syntax_set + .find_syntax_by_token(lang) + .unwrap_or_else(|| self.syntax_set.find_syntax_plain_text()); + + let mut html_gen = ClassedHTMLGenerator::new_with_class_style( + syntax, + &self.syntax_set, + CLASS_STYLE + ); + + for line in LinesWithEndings::from(source) { + html_gen.parse_html_for_line_which_includes_newline(line); + } + + let html_out = html_gen.finalize(); + + //TODO: show context & language + + html! { + pre .codeblock { + code { + (PreEscaped(html_out)) + } + } + } + } +} diff --git a/blog_server/src/fs_watcher.rs b/blog_server/src/fs_watcher.rs new file mode 100644 index 0000000..62e2e92 --- /dev/null +++ b/blog_server/src/fs_watcher.rs @@ -0,0 +1,35 @@ +use std::{ + path::Path, + sync::mpsc, + time::Duration, +}; + +use miette::{IntoDiagnostic, WrapErr}; +use notify::{ + DebouncedEvent, + FsEventWatcher, + RecursiveMode, + Watcher, + watcher, +}; +use tracing::info; + +pub fn start_watching( + tx: mpsc::Sender, + watch_path: &Path +) -> miette::Result +{ + let mut watcher = watcher(tx, Duration::from_secs(2)) + .into_diagnostic() + .wrap_err("Failed to create filesystem watcher")?; + + // Watch the path in non-recursive mode, so events are not generated for nodes in + // sub-directories. + watcher.watch(watch_path, RecursiveMode::NonRecursive) + .into_diagnostic() + .wrap_err_with(|| format!("Failed to watch directory {}", watch_path.to_string_lossy()))?; + + info!(path = %watch_path.to_string_lossy(), "Watching directory"); + + Ok(watcher) +} diff --git a/blog_server/src/handlers.rs b/blog_server/src/handlers.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/blog_server/src/handlers.rs @@ -0,0 +1 @@ + diff --git a/blog_server/src/html_response.rs b/blog_server/src/html_response.rs new file mode 100644 index 0000000..07c095b --- /dev/null +++ b/blog_server/src/html_response.rs @@ -0,0 +1,211 @@ +use std::borrow::Cow; +use std::fmt::{self, Write}; + +use axum::response::{IntoResponse, Response}; +use axum::http::{self, StatusCode}; +use maud::{html, Markup, Render, Escaper, DOCTYPE}; + +pub struct HtmlResponse { + status: StatusCode, + title: Cow<'static, str>, + head: Option, + body: Option, + crawler_hints: CrawlerHints, +} + +impl HtmlResponse { + pub fn new() -> Self { + Self { + status: StatusCode::OK, + title: Cow::Borrowed("untitled"), + head: None, + body: None, + crawler_hints: CrawlerHints::restrictive(), + } + } + + pub fn with_status(self, status: StatusCode) -> Self { + Self { status, ..self } + } + + pub fn with_title(self, title: Cow<'static, str>) -> Self { + Self { title, ..self } + } + + pub fn with_title_static(self, title: &'static str) -> Self { + self.with_title(Cow::Borrowed(title)) + } + + pub fn with_title_owned(self, title: String) -> Self { + self.with_title(Cow::Owned(title)) + } + + pub fn with_head(self, head: Markup) -> Self { + Self { head: Some(head), ..self } + } + + pub fn with_body(self, body: Markup) -> Self { + Self { body: Some(body), ..self } + } + + pub fn with_crawler_hints(self, crawler_hints: CrawlerHints) -> Self { + Self { crawler_hints, ..self } + } + + pub fn with_crawler_restrictive(self) -> Self { + self.with_crawler_hints(CrawlerHints::restrictive()) + } + + pub fn with_crawler_permissive(self) -> Self { + self.with_crawler_hints(CrawlerHints::permissive()) + } +} + +impl Default for HtmlResponse { + fn default() -> Self { + Self::new() + } +} + +impl IntoResponse for HtmlResponse { + fn into_response(self) -> Response { + let html_doc = html! { + (DOCTYPE) + html { + head { + meta charset="utf-8"; + meta name="robots" content=(self.crawler_hints); + title { (self.title) } + @if let Some(head) = self.head { + (head) + } + } + body { + @if let Some(body) = self.body { + (body) + } + } + } + }; + + let mut response = (self.status, html_doc.into_string()) + .into_response(); + + response.headers_mut() + .append("Content-Type", http::HeaderValue::from_static("text/html; charset=utf-8")); + + response + } +} + +#[derive(Clone, Copy, Debug)] +pub struct CrawlerHints { + index: bool, + follow: bool, + archive: bool, + snippet: bool, + image_index: bool, +} + +impl CrawlerHints { + pub const fn restrictive() -> Self { + Self { + index: false, + follow: false, + archive: false, + snippet: false, + image_index: false, + } + } + + pub const fn permissive() -> Self { + Self { + index: true, + follow: true, + archive: true, + snippet: true, + image_index: true, + } + } + + pub const fn with_index(self, index: bool) -> Self { + Self { index, ..self } + } + + pub const fn with_follow(self, follow: bool) -> Self { + Self { follow, ..self } + } + + pub const fn with_archive(self, archive: bool) -> Self { + Self { archive, ..self } + } + + pub const fn with_snippet(self, snippet: bool) -> Self { + Self { snippet, ..self } + } + + pub const fn with_image_index(self, image_index: bool) -> Self { + Self { image_index, ..self } + } + + fn index_str(self) -> &'static str { + if self.index { + "index" + } else { + "noindex" + } + } + + fn follow_str(self) -> &'static str { + if self.follow { + "follow" + } else { + "nofollow" + } + } + + fn archive_strs(self) -> Option<[&'static str; 2]> { + if self.archive { + None + } else { + Some(["noarchive", "nocache"]) + } + } + + fn snippet_str(self) -> Option<&'static str> { + if self.snippet { + None + } else { + Some("nosnippet") + } + } + + fn image_index_str(self) -> Option<&'static str> { + if self.image_index { + None + } else { + Some("noimageindex") + } + } + + fn write_meta_list_to(self, mut buf: W) -> fmt::Result { + write!(buf, "{},{}", self.index_str(), self.follow_str())?; + if let Some([archive_str, cache_str]) = self.archive_strs() { + write!(buf, ",{},{}", archive_str, cache_str)?; + } + if let Some(snippet_str) = self.snippet_str() { + write!(buf, ",{}", snippet_str)?; + } + if let Some(image_index_str) = self.image_index_str() { + write!(buf, ",{}", image_index_str)?; + } + Ok(()) + } +} + +impl Render for CrawlerHints { + fn render_to(&self, buf: &mut String) { + let escaper = Escaper::new(buf); + let _result = self.write_meta_list_to(escaper); + } +} diff --git a/blog_server/src/main.rs b/blog_server/src/main.rs new file mode 100644 index 0000000..8b13258 --- /dev/null +++ b/blog_server/src/main.rs @@ -0,0 +1,221 @@ +mod codeblock; +mod fs_watcher; +mod handlers; +mod html_response; +mod post; +mod posts_store; +mod render; + +use std::{env, fs, io, path::PathBuf, thread}; + +use axum::{ + {routing::{get, get_service}, Router}, + extract::{Extension, Path}, + response::{IntoResponse, Response}, + handler::Handler, + http::StatusCode +}; +use libshire::convert::infallible_elim; +use maud::html; +use miette::{IntoDiagnostic, Context}; +use tower::{ + limit::ConcurrencyLimitLayer, + ServiceExt, +}; +use tower_http::{services::ServeDir, trace::TraceLayer}; +use tracing::info; + +use codeblock::CodeBlockRenderer; +use html_response::HtmlResponse; +use posts_store::ConcurrentPostsStore; +use render::Renderer; + +#[derive(knuffel::Decode)] +struct Config { + #[knuffel(child, unwrap(argument))] + bind: String, + #[knuffel(child, unwrap(argument))] + posts_dir: PathBuf, + #[knuffel(child, unwrap(argument))] + static_dir: PathBuf, + #[knuffel(child, unwrap(argument))] + concurrency_limit: usize, +} + +fn main() -> miette::Result<()> { + tracing_subscriber::fmt::init(); + + // Load the configuration from the KDL config file specified by the first command-line + // argument. + let config = { + let config_path = env::args().nth(1) + .ok_or_else(|| miette::Error::msg("No config file specified"))?; + + info!(path = %config_path, "Loading config"); + + let contents = fs::read_to_string(&config_path) + .into_diagnostic() + .wrap_err_with(|| format!("Failed to read config file {}", config_path))?; + + knuffel::parse::(&config_path, &contents) + .wrap_err_with(|| format!("Failed to parse config file {}", config_path))? + }; + + // Create the data structure used to store the rendered posts. This uses an `Arc` internally, + // so clones will point to the same underlying data. + let posts_store = ConcurrentPostsStore::new(); + + let code_renderer = CodeBlockRenderer::new(); + + // Create the post renderer and the mpsc channel that will be used to communicate with it. + let (renderer, tx) = Renderer::new( + posts_store.clone(), + code_renderer, + config.posts_dir.clone() + ); + + // Dropping the watcher stops its thread, so keep it alive until `main` returns. + let _watcher = fs_watcher::start_watching(tx, &config.posts_dir)?; + + thread::spawn(move || { + renderer.handle_events(); + }); + + info!("Started renderer thread"); + + // To run the web server, we need to be in an async context, so create a new Tokio runtime and + // pass control to it. + tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .into_diagnostic() + .wrap_err("Failed to create async runtime")? + .block_on(run(config, posts_store)) +} + +async fn run(config: Config, posts_store: ConcurrentPostsStore) -> miette::Result<()> { + let static_service = get_service(ServeDir::new(&config.static_dir) + .fallback(handle_fallback + .into_service() + .map_err(infallible_elim::))) + .handle_error(handle_static_io_error); + + let router = Router::new() + .route("/", get(handle_index)) + .route("/posts/:post_id", get(handle_post_page)) + .nest("/static", static_service) + .fallback(handle_fallback.into_service()) + .layer(ConcurrencyLimitLayer::new(config.concurrency_limit)) + .layer(TraceLayer::new_for_http()) + .layer(Extension(posts_store)); + + let bind_address = &config.bind + .parse() + .into_diagnostic() + .wrap_err_with(|| format!("Failed to parse socket address \"{}\"", config.bind))?; + + info!(address = %bind_address, "Starting server"); + + axum::Server::try_bind(bind_address) + .into_diagnostic() + .wrap_err_with(|| format!("Failed to bind {}", bind_address))? + .serve(router.into_make_service()) + .await + .into_diagnostic() + .wrap_err("Fatal error while running the server") +} + +async fn handle_fallback() -> Error { + Error::NotFound +} + +async fn handle_static_io_error(_err: io::Error) -> Error { + Error::Internal +} + +async fn handle_index(Extension(posts): Extension) -> HtmlResponse { + HtmlResponse::new() + .with_title_static("Placeholder title") + .with_crawler_permissive() + .with_body(html! { + h1 { "Here is my great heading" } + p { "Hello world" } + ul { + @for post in posts.read().await.iter_by_created().rev() { + li { + a href={ "/posts/" (post.id_str()) } { + (post.title()) + }; + } + } + } + }) +} + +async fn handle_post_page( + Path(post_id): Path, + Extension(posts): Extension +) -> Result +{ + let post = posts.get(&post_id) + .await + .ok_or(Error::NotFound)?; + + Ok(HtmlResponse::new() + .with_crawler_permissive() + .with_title_owned(post.title().to_owned()) + .with_head(html! { + link href="/static/style/code.css" rel="stylesheet"; + }) + .with_body(html! { + h1 { (post.title()) } + p { "by " (post.author()) } + article { + (post.html()) + } + })) +} + +// TODO: store diagnostic information in Error struct which is output to trace +#[derive(Debug)] +enum Error { + Internal, + NotFound, +} + +impl Error { + fn status_code(&self) -> StatusCode { + match self { + Error::Internal => StatusCode::INTERNAL_SERVER_ERROR, + Error::NotFound => StatusCode::NOT_FOUND, + } + } +} + +impl IntoResponse for Error { + fn into_response(self) -> Response { + let status_code = self.status_code(); + + // Create a string buffer containing the full error text, e.g. "404 Not Found". + let status_text = { + let status_code_str = status_code.as_str(); + let reason = status_code.canonical_reason(); + let mut buf = String::with_capacity( + status_code_str.len() + reason.map(|reason| reason.len() + 1).unwrap_or(0)); + buf.push_str(status_code_str); + if let Some(reason) = reason { + buf.push(' '); + buf.push_str(reason); + } + buf + }; + + HtmlResponse::new() + .with_status(status_code) + .with_body(html! { + p { (status_text) } + }) + .with_title_owned(status_text) + .into_response() + } +} diff --git a/blog_server/src/post.rs b/blog_server/src/post.rs new file mode 100644 index 0000000..dc62a85 --- /dev/null +++ b/blog_server/src/post.rs @@ -0,0 +1,295 @@ +use std::{borrow, error, fmt, ops}; + +use chrono::{DateTime, Utc}; +use libshire::strings::ShString22; +use maud::{Markup, PreEscaped}; + +use crate::codeblock::CodeBlockRenderer; + +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +pub struct PostId(ShString22); + +impl PostId { + pub fn from_file_name(file_name: &str) -> Option { + const POST_FILE_EXTENSION: &str = ".kdl.md"; + + fn is_invalid_char(c: char) -> bool { + c == '/' || c == '\\' || c == '.' + } + + let prefix = file_name + .strip_suffix(POST_FILE_EXTENSION)?; + + if prefix.contains(is_invalid_char) { + return None; + } + + Some(Self(ShString22::new_from_str(prefix))) + } +} + +impl ops::Deref for PostId { + type Target = str; + + fn deref(&self) -> &Self::Target { + &*self.0 + } +} + +impl ops::DerefMut for PostId { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut *self.0 + } +} + +impl AsRef for PostId { + fn as_ref(&self) -> &str { + self + } +} + +impl AsMut for PostId { + fn as_mut(&mut self) -> &mut str { + self + } +} + +impl borrow::Borrow for PostId { + fn borrow(&self) -> &str { + self + } +} + +impl borrow::BorrowMut for PostId { + fn borrow_mut(&mut self) -> &mut str { + self + } +} + +impl fmt::Display for PostId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(&**self, f) + } +} + +pub struct Post { + id: PostId, + title: String, + author: String, + html: Markup, + tags: Vec, + created: DateTime, + updated: DateTime, +} + +impl Post { + pub fn id_str(&self) -> &str { + &self.id + } + + pub fn id(&self) -> &PostId { + &self.id + } + + pub fn title(&self) -> &str { + &self.title + } + + pub fn author(&self) -> &str { + &self.author + } + + pub fn html(&self) -> PreEscaped<&str> { + PreEscaped(&self.html.0) + } + + pub fn tags(&self) -> &[ShString22] { + &self.tags + } + + pub fn created(&self) -> DateTime { + self.created + } + + pub fn updated(&self) -> DateTime { + self.updated + } + + pub fn parse( + code_renderer: &CodeBlockRenderer, + post_id: PostId, + file_name: &str, + created: DateTime, + updated: DateTime, + source: &str, + ) -> Result + { + let mdpost = MdPost::parse(file_name, source)?; + Ok(Self::from_mdpost(post_id, code_renderer, created, updated, mdpost)) + } + + fn from_mdpost( + id: PostId, + code_renderer: &CodeBlockRenderer, + created: DateTime, + updated: DateTime, + mdpost: MdPost, + ) -> Self + { + use pulldown_cmark::{Options, Parser, html::push_html}; + + const PARSER_OPTIONS: Options = Options::ENABLE_TABLES + .union(Options::ENABLE_FOOTNOTES) + .union(Options::ENABLE_STRIKETHROUGH); + + let mut parser = PostMdParser::new( + Parser::new_ext(&mdpost.markdown, PARSER_OPTIONS), + code_renderer + ); + + let mut html_buf = String::new(); + push_html(&mut html_buf, parser.by_ref()); + + Self { + id, + title: mdpost.title, + author: mdpost.author, + html: PreEscaped(html_buf), + tags: mdpost.tags, + created, + updated, + } + } +} + +/// Iterator struct which wraps another event iterator in order to render code blocks, collect the links +/// encountered and generate a summary of the text content. +struct PostMdParser<'p, I> { + iter: I, + code_renderer: &'p CodeBlockRenderer, + links: Vec, + summary: String, +} + +impl<'p, I> PostMdParser<'p, I> { + fn new(iter: I, code_renderer: &'p CodeBlockRenderer) -> Self { + Self { + iter, + code_renderer, + links: Vec::new(), + summary: String::new(), + } + } +} + +impl<'e, 'p, I> Iterator for PostMdParser<'p, I> where I: Iterator> { + type Item = pulldown_cmark::Event<'e>; + + fn next(&mut self) -> Option { + use pulldown_cmark::{CodeBlockKind, CowStr, Event, LinkType, Tag}; + + self.iter.next().map(|event| match event { + // When we reach a code block, we want to collect the text content until the code block finishes + // and have the `CodeBlockRenderer` render it + Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(lang))) => { + let mut code_buf = String::new(); + + for event in self.iter.by_ref() { + match event { + // The code block has finished, so break out of the loop + Event::End(Tag::CodeBlock(_)) => break, + // All text events until the end of the code block should be considered as code, so + // add the text to the `code_buf` to be rendered later + Event::Text(text) => code_buf.push_str(&text), + // Ignore all other events + _ => (), + } + } + + let highlighted = self.code_renderer.render(&lang, &code_buf); + Event::Html(CowStr::Boxed(highlighted.into_string().into_boxed_str())) + }, + + event => { + match &event { + Event::Start(Tag::Link(LinkType::Inline | LinkType::Autolink, destination, _title)) => { + self.links.push(destination.clone().into_string()); + }, + + //TODO: better way of generating a summary + Event::Text(text) => { + if self.summary.is_empty() { + self.summary = text.clone().into_string(); + } + }, + + _ => (), + } + + event + }, + }) + } +} + +#[derive(knuffel::Decode)] +struct HeaderNode { + #[knuffel(child, unwrap(argument))] + title: String, + #[knuffel(child, unwrap(argument))] + author: String, + #[knuffel(children(name="tag"))] + tags: Vec, +} + +#[derive(knuffel::Decode)] +struct TagNode { + #[knuffel(argument)] + tag: String, +} + +#[derive(Debug)] +struct MdPost { + markdown: String, + title: String, + author: String, + tags: Vec, +} + +impl MdPost { + fn parse(file_name: &str, source: &str) -> Result { + const END_OF_HEADER_DELIM: &str = "\n---\n"; + + let (header, md) = source.split_once(END_OF_HEADER_DELIM) + .ok_or(ParseError::MissingHeader)?; + + let header = knuffel::parse::(file_name, header) + .map_err(|err| ParseError::InvalidHeader(Box::new(err)))?; + + let md = md.trim_start(); + + Ok(Self { + markdown: md.to_owned(), + title: header.title, + author: header.author, + tags: header.tags.into_iter().map(|tag| tag.tag.into()).collect(), + }) + } +} + +#[derive(Debug)] +pub enum ParseError { + MissingHeader, + InvalidHeader(Box), +} + +impl fmt::Display for ParseError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ParseError::MissingHeader => write!(f, "Post file has no header"), + ParseError::InvalidHeader(err) => fmt::Display::fmt(err, f), + } + } +} + +impl error::Error for ParseError {} diff --git a/blog_server/src/posts_store.rs b/blog_server/src/posts_store.rs new file mode 100644 index 0000000..ac243ae --- /dev/null +++ b/blog_server/src/posts_store.rs @@ -0,0 +1,159 @@ +use std::{ + collections::{BTreeSet, hash_map, HashMap, HashSet}, + iter::FusedIterator, + sync::Arc, +}; + +use chrono::{DateTime, Utc}; +use libshire::strings::ShString22; +use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard}; + +use crate::post::{Post, PostId}; + +#[derive(Clone)] +pub struct ConcurrentPostsStore { + inner: Arc>, +} + +impl ConcurrentPostsStore { + pub fn new() -> Self { + Self { inner: Arc::new(RwLock::new(PostsStore::new())) } + } + + pub async fn read(&self) -> RwLockReadGuard<'_, PostsStore> { + self.inner.read().await + } + + pub fn write_blocking(&self) -> RwLockWriteGuard<'_, PostsStore> { + self.inner.blocking_write() + } + + pub async fn get(&self, id: &str) -> Option> { + self.read().await.get(id) + } +} + +impl Default for ConcurrentPostsStore { + fn default() -> Self { + Self::new() + } +} + +pub struct PostsStore { + posts: HashMap>, + created_ix: BTreeSet, + tags_ix: HashMap>, +} + +// TODO: shrink the various collections on removal to deallocate unneeded space + +impl PostsStore { + pub fn new() -> Self { + Self { + posts: HashMap::new(), + created_ix: BTreeSet::new(), + tags_ix: HashMap::new(), + } + } + + pub fn get(&self, id: &str) -> Option> { + self.posts.get(id).cloned() + } + + pub fn insert(&mut self, post: Post) -> Option> { + let old_post = self.remove(post.id_str()); + + // Insert the post into each of the tag indexes. + for tag in post.tags() { + // First, get the existing `HashSet` for the tag, or create a new one if one does not + // already exist. Then, insert the post's ID into the `HashSet`. + match self.tags_ix.entry(tag.clone()) { + hash_map::Entry::Occupied(entry) => entry.into_mut(), + hash_map::Entry::Vacant(entry) => entry.insert(HashSet::new()), + }.insert(post.id().clone()); + } + + // Insert the post into the correct position of the created BTree index. + self.created_ix.insert(CreatedIxEntry::new(&post)); + + // Wrap the post with an atomic reference counter and insert it into the main posts + // `HashMap`. + self.posts.insert(post.id().clone(), Arc::new(post)); + + old_post + } + + pub fn remove(&mut self, id: &str) -> Option> { + match self.posts.remove(id) { + Some(post) => { + // Remove the post's entry in the created index. + self.created_ix + .remove(&CreatedIxEntry::new(&post)); + + // Remove every occurence of the post from the tags index. + for tag in post.tags() { + if let Some(tag_ix) = self.tags_ix.get_mut(tag) { + tag_ix.remove(id); + } + } + + Some(post) + }, + None => None, + } + } + + pub fn clear(&mut self) { + self.tags_ix.clear(); + self.created_ix.clear(); + self.posts.clear(); + } + + pub fn iter(&self) + -> impl '_ + + Iterator> + + ExactSizeIterator + + FusedIterator + + Clone + { + self.posts.values().cloned() + } + + pub fn iter_by_created(&self) + -> impl '_ + + Iterator> + + DoubleEndedIterator + + ExactSizeIterator + + FusedIterator + + Clone + { + // For each entry of the created index, look up the corresponding post in the posts map and + // return the post. Every entry in the created index should contain the ID of a post in the + // posts map, so the `expect` should never fail. + self.created_ix + .iter() + .map(|entry| self.get(&entry.id) + .expect("invalid entry in `created_ix` pointing to a post that does not exist")) + } +} + +impl Default for PostsStore { + fn default() -> Self { + Self::new() + } +} + +#[derive(PartialEq, Eq, PartialOrd, Ord)] +struct CreatedIxEntry { + created: DateTime, + id: PostId, +} + +impl CreatedIxEntry { + fn new(post: &Post) -> Self { + Self { + created: post.created(), + id: post.id().clone(), + } + } +} diff --git a/blog_server/src/render.rs b/blog_server/src/render.rs new file mode 100644 index 0000000..bff7232 --- /dev/null +++ b/blog_server/src/render.rs @@ -0,0 +1,262 @@ +use std::{ + fmt, + fs, + io::{self, Read}, + path::PathBuf, + sync::mpsc, +}; + +use chrono::{DateTime, Utc}; +use notify::DebouncedEvent; +use tracing::{info, warn, error}; + +use crate::codeblock::CodeBlockRenderer; +use crate::post::{ParseError, Post, PostId}; +use crate::posts_store::ConcurrentPostsStore; + +pub struct Renderer { + posts: ConcurrentPostsStore, + code_renderer: CodeBlockRenderer, + posts_dir_path: PathBuf, + rx: mpsc::Receiver, +} + +impl Renderer { + pub fn new( + posts: ConcurrentPostsStore, + code_renderer: CodeBlockRenderer, + posts_dir_path: PathBuf, + ) -> (Self, mpsc::Sender) + { + let (tx, rx) = mpsc::channel(); + + // Buffer a rescan event here so that it will be the first event received when + // `handle_events` is called. This will cause the `Renderer` to perform an "initial scan" + // of the post files. + tx.send(DebouncedEvent::Rescan).unwrap(); + + (Self { + posts, + code_renderer, + posts_dir_path, + rx, + }, tx) + } + + #[tracing::instrument(skip(self))] + pub fn handle_events(self) { + while let Ok(notify_event) = self.rx.recv() { + let fs_event = match notify_event { + // Convert create & write events for valid post file names to update events. + DebouncedEvent::Create(path) | DebouncedEvent::Write(path) => { + EventTarget::from_path(path) + .map(Event::Update) + }, + + // Convert remove events for valid post file names. + DebouncedEvent::Remove(path) => { + EventTarget::from_path(path) + .map(Event::Remove) + }, + + // Convert rename events depending on whether the old / new paths are valid post + // file names. + DebouncedEvent::Rename(old_path, new_path) => { + match (EventTarget::from_path(old_path), EventTarget::from_path(new_path)) { + (Some(old_target), Some(new_target)) => Some(Event::Rename(old_target, new_target)), + (None, Some(new_target)) => Some(Event::Update(new_target)), + (Some(old_target), None) => Some(Event::Remove(old_target)), + (None, None) => None, + } + }, + + // Convert rescan events, where it is necessary to read the directory's contents. + DebouncedEvent::Rescan => Some(Event::Scan), + + // Ignore all other events. + _ => None, + }; + + if let Some(fs_event) = fs_event { + self.handle_event(&fs_event); + } + } + + info!("Filesystem events channel closed, exiting"); + } + + fn handle_event(&self, event: &Event) { + info!(event = ?event); + match event { + Event::Update(target) => self.update(target), + Event::Rename(old_target, new_target) => self.rename(old_target, new_target), + Event::Remove(target) => self.remove(target), + Event::Scan => self.scan(), + } + } + + #[tracing::instrument(skip(self))] + fn update(&self, target: &EventTarget) { + match self.parse_post_from_target(target) { + Ok(post) => { + let mut guard = self.posts.write_blocking(); + guard.insert(post); + }, + Err(err) => { + err.log(); + } + }; + } + + #[tracing::instrument(skip(self))] + fn rename(&self, old_target: &EventTarget, new_target: &EventTarget) { + let post_res = self.parse_post_from_target(new_target); + let mut guard = self.posts.write_blocking(); + guard.remove(&old_target.id); + match post_res { + Ok(post) => { + guard.insert(post); + }, + Err(err) => { + err.log(); + }, + } + } + + #[tracing::instrument(skip(self))] + fn remove(&self, target: &EventTarget) { + self.posts.write_blocking().remove(&target.id); + } + + #[tracing::instrument(skip(self))] + fn scan(&self) { + let posts_dir = match fs::read_dir(&self.posts_dir_path) { + Ok(posts_dir) => posts_dir, + Err(err) => { + Error::Io(Box::new(err)).log(); + return; + }, + }; + + let mut posts = Vec::new(); + + for dir_entry in posts_dir { + let dir_entry = match dir_entry { + Ok(dir_entry) => dir_entry, + Err(err) => { + Error::Io(Box::new(err)).log(); + continue; + }, + }; + + if let Some(target) = EventTarget::from_path(dir_entry.path()) { + posts.push(match self.parse_post_from_target(&target) { + Ok(post) => post, + Err(err) => { + err.log(); + continue; + }, + }); + } + } + + let mut guard = self.posts.write_blocking(); + guard.clear(); + for post in posts { + guard.insert(post); + } + } + + fn parse_post_from_target(&self, target: &EventTarget) -> Result { + let mut fd = fs::OpenOptions::new() + .read(true) + .open(&target.path) + .map_err(|err| Error::Io(Box::new(err)))?; + + let metadata = fd.metadata() + .map_err(|err| Error::Io(Box::new(err)))?; + + if !metadata.file_type().is_file() { + return Err(Error::NotAFile); + } + + let (created, updated) = metadata.created() + .and_then(|created| metadata.modified() + .map(|modified| (DateTime::::from(created), DateTime::::from(modified)))) + .unwrap_or_else(|_| { + let now = Utc::now(); + (now, now) + }); + + let contents = { + let mut buf = String::new(); + fd.read_to_string(&mut buf) + .map_err(|err| Error::Io(Box::new(err)))?; + buf + }; + + drop(fd); + + Post::parse( + &self.code_renderer, + target.id.clone(), + &target.path.to_string_lossy(), + created, + updated, + &contents + ).map_err(|err| Error::Parsing(Box::new(err))) + } +} + +#[derive(Debug)] +enum Event { + Update(EventTarget), + Rename(EventTarget, EventTarget), + Remove(EventTarget), + Scan, +} + +struct EventTarget { + pub path: PathBuf, + pub id: PostId, +} + +impl fmt::Debug for EventTarget { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}@{}", self.id, self.path.to_string_lossy()) + } +} + +impl EventTarget { + pub fn from_path(path: PathBuf) -> Option { + path.file_name() + .and_then(|file_name| file_name.to_str()) + .and_then(PostId::from_file_name) + .map(|id| Self { + path, + id, + }) + } +} + +pub enum Error { + Io(Box), + NotAFile, + Parsing(Box), +} + +impl Error { + fn log(&self) { + match self { + Error::Io(err) => { + error!(error = %err, "IO error while processing event"); + }, + Error::NotAFile => { + warn!("Event target is not a regular file"); + }, + Error::Parsing(err) => { + warn!(error = %err, "Parsing error while processing event"); + }, + } + } +} diff --git a/static/style/code.css b/static/style/code.css new file mode 100644 index 0000000..85c1544 --- /dev/null +++ b/static/style/code.css @@ -0,0 +1,344 @@ +.cb_code { + color: #323232; + background-color: #ffffff; +} + +.cb_comment { + color: #969896; + font-style: italic; +} + +.cb_string { + color: #183691; +} + +.cb_regexp-operator { + color: #a71d5d; +} + +.cb_string.cb_regexp.cb_characterclass .cb_punctuation.cb_definition.cb_string.cb_begin, +.cb_string.cb_regexp.cb_characterclass .cb_punctuation.cb_definition.cb_string.cb_end { + color: #a71d5d; +} + +.cb_constant.cb_numeric { + color: #0086b3; +} + +.cb_constant.cb_language { + color: #0086b3; +} + +.cb_constant.cb_character, +.cb_constant.cb_other, +.cb_variable.cb_other.cb_constant { + color: #0086b3; +} + +.cb_variable { + color: #323232; +} + +.cb_keyword { + color: #a71d5d; + font-weight: bold; +} + +.cb_bitwise-operator { + color: #a71d5d; + font-weight: bold; +} + +.cb_storage { + color: #a71d5d; + font-weight: bold; +} + +.cb_storage.cb_type { + color: #a71d5d; + font-weight: bold; +} + +.cb_entity.cb_name.cb_class { + color: #0086b3; +} + +.cb_entity.cb_other.cb_inherited-class { + color: #0086b3; +} + +.cb_entity.cb_name.cb_function { + color: #795da3; + font-weight: bold; +} + +.cb_variable.cb_parameter { + color: #323232; +} + +.cb_entity.cb_name.cb_tag { + color: #63a35c; +} + +.cb_entity.cb_other.cb_attribute-name { + color: #795da3; +} + +.cb_support.cb_function { + color: #62a35c; +} + +.cb_support.cb_constant { + color: #0086b3; +} + +.cb_support.cb_type, +.cb_support.cb_class { + color: #0086b3; +} + +.cb_support.cb_other.cb_variable { + color: #323232; +} + +.cb_invalid, +.cb_invalid.cb_illegal, +.cb_invalid.cb_deprecated { + color: #b52a1d; + background-color: #f5f5f5; + font-weight: bold; +} + +.cb_entity.cb_name.cb_filename.cb_find-in-files { + color: #323232; + font-weight: bold; +} + +.cb_constant.cb_numeric.cb_line-number.cb_find-in-files, +.cb_constant.cb_numeric.cb_line-number.cb_match.cb_find-in-files { + color: #b3b3b3; +} + +.cb_meta.cb_diff.cb_header { + color: #969896; + background-color: #ffffff; + font-style: italic; +} + +.cb_meta.cb_diff.cb_header .cb_punctuation.cb_definition.cb_from-file.cb_diff { + color: #bd2c00; + background-color: #ffecec; + font-weight: bold; + font-style: italic; +} + +.cb_meta.cb_diff.cb_header .cb_punctuation.cb_definition.cb_to-file.cb_diff { + color: #55a532; + background-color: #eaffea; + font-weight: bold; + font-style: italic; +} + +.cb_meta.cb_diff.cb_range { + color: #969896; + font-weight: bold; + font-style: italic; +} + +.cb_markup.cb_deleted { + background-color: #ffecec; +} + +.cb_markup.cb_deleted .cb_punctuation.cb_definition.cb_inserted { + color: #bd2c00; + font-weight: bold; +} + +.cb_markup.cb_inserted { + background-color: #eaffea; +} + +.cb_markup.cb_inserted .cb_punctuation.cb_definition.cb_inserted { + color: #55a532; + font-weight: bold; +} + +.cb_markup.cb_deleted.cb_git_gutter { + color: #bd2c00; +} + +.cb_markup.cb_inserted.cb_git_gutter { + color: #55a532; +} + +.cb_markup.cb_changed.cb_git_gutter { + color: #0086b3; +} + +.cb_markup.cb_ignored.cb_git_gutter { + color: #b3b3b3; +} + +.cb_markup.cb_untracked.cb_git_gutter { + color: #b3b3b3; +} + +.cb_source.cb_css .cb_punctuation.cb_definition.cb_entity { + color: #323232; +} + +.cb_source.cb_css .cb_entity.cb_other.cb_attribute-name.cb_pseudo-class, +.cb_source.cb_css .cb_entity.cb_other.cb_attribute-name.cb_pseudo-element { + color: #a71d5d; +} + +.cb_source.cb_css .cb_meta.cb_value, +.cb_source.cb_css .cb_support.cb_constant, +.cb_source.cb_css .cb_support.cb_function { + color: #323232; +} + +.cb_source.cb_css .cb_constant.cb_other.cb_color { + color: #ed6a43; +} + +.cb_source.cb_scss .cb_punctuation.cb_definition.cb_entity { + color: #323232; +} + +.cb_source.cb_scss .cb_entity.cb_other.cb_attribute-name.cb_pseudo-class, +.cb_source.cb_scss .cb_entity.cb_other.cb_attribute-name.cb_pseudo-element { + color: #a71d5d; +} + +.cb_source.cb_scss .cb_support.cb_constant.cb_property-value, +.cb_source.cb_scss .cb_support.cb_function { + color: #323232; +} + +.cb_source.cb_scss .cb_variable { + color: #a71d5d; +} + +.cb_variable.cb_language.cb_this.cb_js { + color: #ed6a43; +} + +.cb_source.cb_js .cb_entity.cb_name.cb_function { + color: #323232; +} + +.cb_source.cb_js .cb_meta.cb_function .cb_entity.cb_name.cb_function, +.cb_source.cb_js .cb_entity.cb_name.cb_function .cb_meta.cb_function { + color: #795da3; + font-weight: bold; +} + +.cb_entity.cb_name.cb_type.cb_new.cb_js { + color: #795da3; +} + +.cb_variable.cb_language.cb_prototype.cb_js { + color: #0086b3; +} + +.cb_source.cb_js .cb_support.cb_function { + color: #0086b3; +} + +.cb_support.cb_type.cb_object.cb_console.cb_js { + color: #795da3; +} + +.cb_source.cb_python .cb_keyword { + font-weight: bold; +} + +.cb_source.cb_python .cb_storage { + font-weight: bold; +} + +.cb_source.cb_python .cb_storage.cb_type { + font-weight: bold; +} + +.cb_source.cb_python .cb_entity.cb_name.cb_function { + color: #323232; + font-weight: bold; +} + +.cb_source.cb_php .cb_entity.cb_name.cb_type.cb_class { + color: #323232; + font-weight: bold; +} + +.cb_variable.cb_language.cb_ruby { + color: #ed6a43; +} + +.cb_entity.cb_name.cb_type.cb_module.cb_ruby { + color: #795da3; + font-weight: bold; +} + +.cb_entity.cb_name.cb_type.cb_class.cb_ruby { + color: #795da3; + font-weight: bold; +} + +.cb_entity.cb_other.cb_inherited-class.cb_ruby { + color: #795da3; + font-weight: bold; +} + +.cb_text.cb_html.cb_markdown .cb_punctuation.cb_definition { + color: #a71d5d; +} + +.cb_text.cb_html.cb_markdown .cb_meta.cb_separator { + color: #b3b3b3; +} + +.cb_text.cb_html.cb_markdown .cb_markup.cb_heading { + font-weight: bold; +} + +.cb_text.cb_html.cb_markdown .cb_markup.cb_raw.cb_block { + color: #323232; +} + +.cb_text.cb_html.cb_markdown .cb_markup.cb_raw.cb_inline { + color: #323232; +} + +.cb_text.cb_html.cb_markdown .cb_meta.cb_link, +.cb_text.cb_html.cb_markdown .cb_meta.cb_image { + color: #4183c4; +} + +.cb_text.cb_html.cb_markdown .cb_markup.cb_underline.cb_link, +.cb_text.cb_html.cb_markdown .cb_constant.cb_other.cb_reference { + font-style: italic; +} + +.cb_text.cb_html.cb_markdown .cb_markup.cb_list { + color: #ed6a43; +} + +.cb_text.cb_html.cb_markdown .cb_markup.cb_bold { + font-weight: bold; +} + +.cb_text.cb_html.cb_markdown .cb_markup.cb_italic { + font-style: italic; +} + +.cb_text.cb_html.cb_markdown .cb_markup.cb_bold .cb_markup.cb_italic { + font-weight: bold; + font-style: italic; +} + +.cb_text.cb_html.cb_markdown .cb_markup.cb_italic .cb_markup.cb_bold { + font-weight: bold; + font-style: italic; +} diff --git a/static/style/main.css b/static/style/main.css new file mode 100644 index 0000000..e69de29 diff --git a/utils/css_gen/Cargo.toml b/utils/css_gen/Cargo.toml new file mode 100644 index 0000000..fcb1c4d --- /dev/null +++ b/utils/css_gen/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "css_gen" +version = "0.1.0" +edition = "2021" + +[dependencies] +syntect = "4" diff --git a/utils/css_gen/src/main.rs b/utils/css_gen/src/main.rs new file mode 100644 index 0000000..c9b8d87 --- /dev/null +++ b/utils/css_gen/src/main.rs @@ -0,0 +1,30 @@ +use std::env; +use std::process; + +use syntect::highlighting::ThemeSet; +use syntect::html::{css_for_theme_with_class_style, ClassStyle}; + +const CLASS_STYLE: ClassStyle = ClassStyle::SpacedPrefixed { prefix: "cb_" }; + +fn main() { + let theme_set = ThemeSet::load_defaults(); + let theme_name = env::args().nth(1).unwrap_or_else(|| { + eprintln!("No theme specified"); + eprint_available_themes(&theme_set); + process::exit(1) + }); + let theme = theme_set.themes.get(&theme_name).unwrap_or_else(|| { + eprintln!("Theme not found: {}", theme_name); + eprint_available_themes(&theme_set); + process::exit(1) + }); + let css = css_for_theme_with_class_style(theme, CLASS_STYLE); + println!("{}", css); +} + +fn eprint_available_themes(theme_set: &ThemeSet) { + eprintln!("Available themes:"); + for key in theme_set.themes.keys() { + eprintln!(" {}", key); + } +}