commit 29502f68164ddfdd68b963a19904ae19a5abb9c9 Author: Guus Waals <_@guusw.nl> Date: Tue Oct 1 19:24:07 2024 +0800 Initial commit Initial commit Add lua template diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..742e653 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +/.nvim.lua +/.nvim.workspace.lua diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..dfa58e1 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1038 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[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.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +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", + "tokio", + "tower 0.5.2", + "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", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "blog-server" +version = "0.1.0" +dependencies = [ + "axum", + "chrono", + "once_cell", + "pulldown-cmark", + "regex", + "serde", + "tokio", + "tower 0.4.13", + "tower-http", +] + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "cc" +version = "1.2.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac9fe6cdbb24b6ade63616c0a0688e45bb56732262c158df3c0c4bea4ca47cb7" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "find-msvc-tools" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +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-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[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 = "getopts" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "http-range-header" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "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 = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "js-sys" +version = "0.3.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[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 = "mio" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[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.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pulldown-cmark" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f86ba2052aebccc42cbbb3ed234b8b13ce76f75c3551a303cb2bcffcff12bb14" +dependencies = [ + "bitflags", + "getopts", + "memchr", + "pulldown-cmark-escape", + "unicase", +] + +[[package]] +name = "pulldown-cmark-escape" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" + +[[package]] +name = "quote" +version = "1.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[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 = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +dependencies = [ + "libc", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "syn" +version = "2.0.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "tokio" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-util" +version = "0.7.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "http-range-header", + "httpdate", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "tokio", + "tokio-util", + "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.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", +] + +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + +[[package]] +name = "unicode-ident" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "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.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..20dbf91 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "blog-server" +version = "0.1.0" +edition = "2021" + +[dependencies] +axum = "0.7" +tokio = { version = "1", features = ["full"] } +tower = "0.4" +tower-http = { version = "0.5", features = ["fs"] } +pulldown-cmark = "0.12" +serde = { version = "1.0", features = ["derive"] } +chrono = "0.4" +regex = "1.10" +once_cell = "1.19" diff --git a/PROMPT.md b/PROMPT.md new file mode 100644 index 0000000..5722008 --- /dev/null +++ b/PROMPT.md @@ -0,0 +1,9 @@ +> Can you create a rust web server application for my static blog, that includes cmark and parses inside the posts/ folder, it should generate an + index page that lists top say 50 posts, also serve an /all page that lists the same but not limited. each links to a /p/ where post name + matches the filename. The app should keep some internal state that is refreshed upon git tag changes, check the git version from the data folder + (which is also this project folder. Do this like in ~/Projects/lemons/src/tools/git_info/git_info.cpp) The internal state keeps track of all + articles and their creation dates (mock dates for now as we add this later). Feel free to create some example posts. Also the page serving should + be data driven like, all templates inside the templates/ folder act as a page, but we parse the and whenever we encounter $ we treat this as some built in functionality and generate stuff, things this should support are $ for generated post html + $ with optional $ for the main page and $ for including other files inline. Thats all + diff --git a/posts/async-rust.md b/posts/async-rust.md new file mode 100644 index 0000000..eccb8f0 --- /dev/null +++ b/posts/async-rust.md @@ -0,0 +1,62 @@ +# Async Programming in Rust + +Asynchronous programming in Rust allows you to write concurrent code that's both fast and safe. + +## What is Async? + +Async programming is about doing multiple things at once without blocking. Instead of waiting for one operation to complete before starting another, async code can switch between tasks. + +## Key Concepts + +### Futures + +A `Future` represents a value that may not be available yet: + +```rust +async fn fetch_data() -> String { + // Simulate async work + tokio::time::sleep(Duration::from_secs(1)).await; + "Data fetched!".to_string() +} +``` + +### Async/Await + +The `async` keyword makes a function asynchronous, and `await` pauses execution until a future is ready: + +```rust +async fn process_data() { + let data = fetch_data().await; + println!("Got: {}", data); +} +``` + +### Runtimes + +Rust doesn't have a built-in async runtime. Popular choices include: + +- **Tokio**: Full-featured runtime +- **async-std**: Standard library approach +- **smol**: Minimal runtime + +## Example: Concurrent HTTP Requests + +```rust +use tokio::task; + +async fn fetch_all() { + let task1 = task::spawn(fetch_url("https://api1.example.com")); + let task2 = task::spawn(fetch_url("https://api2.example.com")); + let task3 = task::spawn(fetch_url("https://api3.example.com")); + + let (result1, result2, result3) = tokio::join!(task1, task2, task3); +} +``` + +## Benefits + +- **Performance**: Handle thousands of connections efficiently +- **Resource Usage**: Lower memory and CPU usage +- **Scalability**: Build systems that scale + +Async Rust might seem complex at first, but it's worth learning for building high-performance applications! diff --git a/posts/git-workflow.md b/posts/git-workflow.md new file mode 100644 index 0000000..8734b0f --- /dev/null +++ b/posts/git-workflow.md @@ -0,0 +1,91 @@ +# Git Workflow Best Practices + +Git is an essential tool for modern software development. Here are some best practices for an effective Git workflow. + +## Branching Strategy + +### Main Branches + +- **main/master**: Production-ready code +- **develop**: Integration branch for features + +### Supporting Branches + +- **feature/**: New features +- **bugfix/**: Bug fixes +- **hotfix/**: Urgent production fixes + +## Commit Messages + +Good commit messages are crucial: + +``` +Add user authentication system + +- Implement JWT token generation +- Add login and logout endpoints +- Create user session middleware +``` + +Follow the 50/72 rule: +- First line: 50 characters or less +- Body: Wrap at 72 characters + +## Commands + +### Creating a Feature Branch + +```bash +git checkout -b feature/new-feature develop +``` + +### Committing Changes + +```bash +git add . +git commit -m "Add feature description" +``` + +### Merging + +```bash +git checkout develop +git merge --no-ff feature/new-feature +git branch -d feature/new-feature +``` + +## Tips + +1. **Commit Often**: Make small, logical commits +2. **Pull Regularly**: Stay in sync with the team +3. **Review Before Commit**: Use `git diff` to check changes +4. **Use .gitignore**: Don't commit build artifacts or secrets + +## Advanced Features + +### Interactive Rebase + +Clean up commit history: + +```bash +git rebase -i HEAD~3 +``` + +### Stashing + +Save work in progress: + +```bash +git stash +git stash pop +``` + +### Cherry-pick + +Apply specific commits: + +```bash +git cherry-pick +``` + +Master Git, and you'll be a more effective developer! diff --git a/posts/hello-world.md b/posts/hello-world.md new file mode 100644 index 0000000..6bdad65 --- /dev/null +++ b/posts/hello-world.md @@ -0,0 +1,38 @@ +# Hello World + +Welcome to my blog! This is my first post written in Markdown and served by a custom Rust web server. + +## Features + +This blog supports: + +- **CommonMark** markdown parsing +- Git-based cache invalidation +- Template-driven rendering +- Custom template tags + +## Code Example + +Here's a simple Rust function: + +```rust +fn greet(name: &str) -> String { + format!("Hello, {}!", name) +} +``` + +## Lists + +Unordered list: +- Item one +- Item two +- Item three + +Ordered list: +1. First item +2. Second item +3. Third item + +--- + +Thanks for reading! diff --git a/posts/markdown-guide.md b/posts/markdown-guide.md new file mode 100644 index 0000000..c2bb5ed --- /dev/null +++ b/posts/markdown-guide.md @@ -0,0 +1,62 @@ +# Markdown Guide + +This blog uses CommonMark for rendering markdown content. Here's a quick guide to the syntax. + +## Headers + +Use `#` for headers: + +```markdown +# H1 +## H2 +### H3 +``` + +## Emphasis + +- *Italic* with `*asterisks*` +- **Bold** with `**double asterisks**` +- ~~Strikethrough~~ with `~~tildes~~` + +## Links and Images + +Links: `[Link text](https://example.com)` + +Images: `![Alt text](image.jpg)` + +## Blockquotes + +> This is a blockquote. +> It can span multiple lines. + +## Code + +Inline `code` with backticks. + +Code blocks with triple backticks: + +```python +def hello(): + print("Hello, world!") +``` + +## Tables + +| Column 1 | Column 2 | Column 3 | +|----------|----------|----------| +| Data 1 | Data 2 | Data 3 | +| More | Data | Here | + +## Task Lists + +- [x] Completed task +- [ ] Incomplete task +- [ ] Another task + +## Horizontal Rule + +Use three or more hyphens: + +--- + +That's the basics of Markdown! diff --git a/posts/rust-web-servers.md b/posts/rust-web-servers.md new file mode 100644 index 0000000..9557197 --- /dev/null +++ b/posts/rust-web-servers.md @@ -0,0 +1,57 @@ +# Building Web Servers in Rust + +Rust has become a popular choice for building web servers due to its performance and safety guarantees. + +## Why Rust for Web Development? + +1. **Memory Safety**: No null pointer exceptions or data races +2. **Performance**: Comparable to C/C++ +3. **Concurrency**: Fearless concurrency with async/await +4. **Type System**: Catch errors at compile time + +## Popular Frameworks + +### Axum + +Axum is a modern web framework built on top of Tokio. It provides: + +- Type-safe routing +- Extractors for parsing requests +- Middleware support +- Great ergonomics + +### Actix Web + +Actix Web is a powerful, pragmatic framework that: + +- Uses the actor model +- Provides excellent performance +- Has a mature ecosystem + +### Rocket + +Rocket focuses on: + +- Developer ergonomics +- Type-safe routing +- Easy to learn + +## Example Server + +```rust +use axum::{routing::get, Router}; + +#[tokio::main] +async fn main() { + let app = Router::new() + .route("/", get(|| async { "Hello, World!" })); + + let listener = tokio::net::TcpListener::bind("127.0.0.1:3000") + .await + .unwrap(); + + axum::serve(listener, app).await.unwrap(); +} +``` + +This is just the beginning of what you can build with Rust! diff --git a/project_templates/.nvim.lua b/project_templates/.nvim.lua new file mode 100644 index 0000000..a942c38 --- /dev/null +++ b/project_templates/.nvim.lua @@ -0,0 +1,158 @@ +-- Load persistent configuration from .workspace.lua +local ws_file = "./.nvim.workspace.lua" +local loaded, workspace = pcall(dofile, ws_file) +if not loaded then + workspace = { args = { }, build_type = "debug", binary = "blog-server" } +end + +local build_folder +local bin_target +local bin_name + +local function updateBuildEnv() + build_folder = './target/' .. workspace.build_type + bin_target = workspace.binary + bin_name = build_folder .. '/' .. bin_target + + -- The run (F6) arguments + vim.opt.makeprg = "cargo build" + if workspace.build_type == "release" then + vim.opt.makeprg = vim.opt.makeprg .. " --release" + end + + -- Rust compiler error format + vim.opt.errorformat = { + -- Main error/warning line with file:line:col format + '%E%>error%m,' .. -- Start of error block + '%W%>warning: %m,' .. -- Start of warning block + '%-G%>help: %m,' .. -- Ignore help lines + '%-G%>note: %m,' .. -- Ignore note lines + '%C%> --> %f:%l:%c,' .. -- Continuation: file location + '%Z%>%p^%m,' .. -- End: column pointer (^^^) + '%C%>%s%#|%.%#,' .. -- Continuation: context lines with | + '%C%>%s%#%m,' .. -- Continuation: other context + '%-G%.%#' -- Ignore everything else + } +end + +updateBuildEnv() + +local function writeWorkspace() + -- A very minimal serializer for workspace configuration + local s = { l = "", ls = {}, i = "" } + local function w(v) s.l = s.l .. v end + local function nl() + s.ls[#s.ls + 1] = s.l; s.l = s.i; + end + local function wv(v) + local t = type(v) + if t == 'table' then + w('{'); local pi = s.i; s.i = s.i .. " " + for k1, v1 in pairs(v) do + nl(); w('['); wv(k1); w('] = '); wv(v1); w(',') + end + s.i = pi; nl(); w('}'); + elseif t == 'number' then + w(tostring(v)) + elseif t == 'string' then + w('"' .. v .. '"') + else + w(tostring(v)) + end + end + + -- Write the workspace file + w("return "); wv(workspace); nl() + vim.fn.writefile(s.ls, ws_file) +end + +-- nvim-dap configuration +local dap_ok, dap = pcall(require, "dap") +local dap_def_cfg +local dap_configs + +-- Update args for both run and debug configs +local function updateArgs(args) + workspace.args = args + if dap_configs ~= nil then dap_configs[1].args = args end + writeWorkspace() +end + +-- Find terminal tab or create new one, then run command +local function TermRun(cmd) + local found = false + for i = 1, vim.fn.tabpagenr('$') do + vim.cmd('tabnext ' .. i) + if vim.bo.buftype == 'terminal' then + found = true + break + end + end + if not found then vim.cmd('tabnew | terminal') end + vim.fn.chansend(vim.b.terminal_job_id, '' .. cmd .. '\n') + vim.fn.feedkeys("G", "n") +end + +-- The Configure command +vim.api.nvim_create_user_command("Configure", function(a) + local args = {} + local bt = "debug" + if #a.args > 0 then bt = a.args end + workspace.build_type = bt + updateBuildEnv() + writeWorkspace() +end, { nargs = '?', desc = "Update run/debug arguments" }) + +vim.api.nvim_create_user_command("Args", function(a) updateArgs(a.fargs) end, + { nargs = '*', desc = "Update run/debug arguments" }) + +if dap_ok then + local lldb_init = { } + dap_configs = { + { + name = 'test all', + type = 'codelldb', + request = 'launch', + program = bin_name, + args = workspace.args, + cwd = '${workspaceFolder}', + stopOnEntry = false, + initCommands = lldb_init + } + } + dap_def_cfg = dap_configs[1] + dap.providers.configs["project"] = function() + return dap_configs + end + + -- DebugArgs to set debugger arguments and run immediately + vim.api.nvim_create_user_command("DebugArgs", function(a) + updateArgs(a.fargs) + dap.run(dap_configs[1]) + end, { nargs = '*', desc = "Starts debugging with specified arguments" }) +end + +if MakeAnd then + local r = function() + MakeAnd(function() + TermRun(bin_name .. " " .. table.concat(workspace.args, " ")) + end) + end + -- RunArgs sets the run arguments that F6 uses and reruns immediately + vim.api.nvim_create_user_command("RunArgs", function(a) + updateArgs(a.fargs) + r() + end, { nargs = '*', desc = "Starts debugging with specified arguments" }) + + -- F6 to run the application + vim.keymap.set('n', '', r) + + if dap_ok then + -- Shift-F5 to launch default config + vim.keymap.set('n', '', function() + MakeAnd(function() + dap.run(dap_def_cfg) + end) + end) + end +end diff --git a/src/git_tracker.rs b/src/git_tracker.rs new file mode 100644 index 0000000..07fb615 --- /dev/null +++ b/src/git_tracker.rs @@ -0,0 +1,68 @@ +use std::fs; +use std::path::Path; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct GitVersion { + pub branch: String, + pub commit: String, +} + +impl GitVersion { + pub fn key(&self) -> String { + format!("{}/{}", self.branch, self.commit) + } +} + +pub fn get_git_version(git_dir: &str) -> Result { + let git_path = Path::new(git_dir); + let head_path = git_path.join(".git/HEAD"); + + // Read HEAD file + let head_content = fs::read_to_string(&head_path) + .map_err(|e| format!("Failed to read .git/HEAD: {}", e))?; + + let head_content = head_content.trim(); + + // Check if HEAD points to a ref + if head_content.starts_with("ref:") { + let ref_path = head_content.strip_prefix("ref:").unwrap().trim(); + let full_ref_path = git_path.join(".git").join(ref_path); + + // Read the ref file to get the commit hash + let commit_hash = fs::read_to_string(&full_ref_path) + .map_err(|e| format!("Failed to read ref file: {}", e))? + .trim() + .to_string(); + + // Extract branch name from ref path + let branch = ref_path + .strip_prefix("refs/heads/") + .unwrap_or(ref_path) + .to_string(); + + Ok(GitVersion { + branch, + commit: commit_hash, + }) + } else { + // Detached HEAD state - just use the commit hash + Ok(GitVersion { + branch: "detached".to_string(), + commit: head_content.to_string(), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_git_version() { + // This test will only work if run in a git repository + if let Ok(version) = get_git_version(".") { + assert!(!version.commit.is_empty()); + assert!(!version.branch.is_empty()); + } + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..9a3a9fc --- /dev/null +++ b/src/main.rs @@ -0,0 +1,116 @@ +mod git_tracker; +mod post_manager; +mod template_engine; + +use axum::{ + extract::{Path, State}, + http::StatusCode, + response::{Html, IntoResponse}, + routing::get, + Router, +}; +use std::sync::Arc; +use tokio::sync::RwLock; + +#[derive(Clone)] +struct AppState { + post_manager: Arc>, +} + +#[tokio::main] +async fn main() { + let post_manager = Arc::new(RwLock::new( + post_manager::PostManager::new(".").expect("Failed to initialize post manager"), + )); + + let app_state = AppState { + post_manager: post_manager.clone(), + }; + + // Spawn background task to watch for git changes + tokio::spawn(async move { + let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(5)); + loop { + interval.tick().await; + let mut manager = post_manager.write().await; + if let Err(e) = manager.refresh_if_needed() { + eprintln!("Error refreshing posts: {}", e); + } + } + }); + + let app = Router::new() + .route("/", get(index_handler)) + .route("/:page", get(all_handler)) + .route("/p/:post_name", get(post_handler)) + .with_state(app_state); + + let listener = tokio::net::TcpListener::bind("127.0.0.1:3000") + .await + .expect("Failed to bind to address"); + + println!("Server running on http://127.0.0.1:3000"); + + axum::serve(listener, app) + .await + .expect("Failed to start server"); +} + +async fn index_handler(State(state): State) -> impl IntoResponse { + match render_template("index.html", &state, Some(50)).await { + Ok(html) => Html(html).into_response(), + Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e).into_response(), + } +} + +async fn all_handler(State(state): State, Path(page): Path) -> impl IntoResponse { + if page.contains("..") { + return (StatusCode::NOT_FOUND, "Invalid path").into_response(); + } + match render_template(&format!("page_{}.html", page), &state, None).await { + Ok(html) => Html(html).into_response(), + Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e).into_response(), + } +} + +async fn post_handler( + State(state): State, + Path(post_name): Path, +) -> impl IntoResponse { + let manager = state.post_manager.read().await; + + if post_name.contains("..") || post_name.contains("/") { + return (StatusCode::NOT_FOUND, "Invalid path").into_response(); + } + match manager.get_post(&post_name) { + Some(_post) => { + drop(manager); + match render_post_template(&state, post_name).await { + Ok(html) => Html(html).into_response(), + Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e).into_response(), + } + } + None => (StatusCode::NOT_FOUND, "Post not found").into_response(), + } +} + +async fn render_template( + template_name: &str, + state: &AppState, + limit: Option, +) -> Result { + let template_path = format!("templates/{}", template_name); + let template_content = std::fs::read_to_string(&template_path) + .map_err(|e| format!("Failed to read template: {}", e))?; + + let manager = state.post_manager.read().await; + template_engine::render_template(&template_content, &*manager, None, limit) +} + +async fn render_post_template(state: &AppState, post_name: String) -> Result { + let template_content = std::fs::read_to_string("templates/post.html") + .map_err(|e| format!("Failed to read post template: {}", e))?; + + let manager = state.post_manager.read().await; + template_engine::render_template(&template_content, &*manager, Some(&post_name), None) +} diff --git a/src/post_manager.rs b/src/post_manager.rs new file mode 100644 index 0000000..9fa156a --- /dev/null +++ b/src/post_manager.rs @@ -0,0 +1,148 @@ +use crate::git_tracker::{get_git_version, GitVersion}; +use chrono::{DateTime, Utc}; +use pulldown_cmark::{html, Options, Parser}; +use std::collections::HashMap; +use std::fs; +use std::path::PathBuf; + +#[derive(Debug, Clone)] +pub struct Post { + pub name: String, + pub filename: String, + pub markdown_content: String, + pub html_content: String, + pub created_at: DateTime, +} + +pub struct PostManager { + git_dir: PathBuf, + posts_dir: PathBuf, + posts: HashMap, + last_git_version: Option, +} + +impl PostManager { + pub fn new(root_dir: &str) -> Result { + let git_dir = PathBuf::from(root_dir); + let posts_dir = git_dir.join("posts"); + + // Create posts directory if it doesn't exist + if !posts_dir.exists() { + fs::create_dir_all(&posts_dir) + .map_err(|e| format!("Failed to create posts directory: {}", e))?; + } + + let mut manager = PostManager { + git_dir, + posts_dir, + posts: HashMap::new(), + last_git_version: None, + }; + + manager.refresh_posts()?; + Ok(manager) + } + + pub fn refresh_if_needed(&mut self) -> Result { + let current_version = get_git_version(self.git_dir.to_str().unwrap())?; + + if self.last_git_version.as_ref() != Some(¤t_version) { + println!( + "Git version changed: {} -> {}", + self.last_git_version + .as_ref() + .map(|v| v.key()) + .unwrap_or_else(|| "none".to_string()), + current_version.key() + ); + self.refresh_posts()?; + self.last_git_version = Some(current_version); + Ok(true) + } else { + Ok(false) + } + } + + fn refresh_posts(&mut self) -> Result<(), String> { + self.posts.clear(); + + let entries = fs::read_dir(&self.posts_dir) + .map_err(|e| format!("Failed to read posts directory: {}", e))?; + + for entry in entries { + let entry = entry.map_err(|e| format!("Failed to read directory entry: {}", e))?; + let path = entry.path(); + + if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("md") { + let filename = path + .file_name() + .and_then(|s| s.to_str()) + .ok_or_else(|| "Invalid filename".to_string())? + .to_string(); + + let name = path + .file_stem() + .and_then(|s| s.to_str()) + .ok_or_else(|| "Invalid file stem".to_string())? + .to_string(); + + let markdown_content = fs::read_to_string(&path) + .map_err(|e| format!("Failed to read post file: {}", e))?; + + let html_content = markdown_to_html(&markdown_content); + + // Mock creation date for now + let metadata = fs::metadata(&path) + .map_err(|e| format!("Failed to read file metadata: {}", e))?; + let created_at = metadata + .created() + .or_else(|_| metadata.modified()) + .map(|t| DateTime::::from(t)) + .unwrap_or_else(|_| Utc::now()); + + let post = Post { + name: name.clone(), + filename, + markdown_content, + html_content, + created_at, + }; + + self.posts.insert(name, post); + } + } + + println!("Loaded {} posts", self.posts.len()); + Ok(()) + } + + pub fn get_post(&self, name: &str) -> Option<&Post> { + self.posts.get(name) + } + + pub fn get_all_posts(&self) -> Vec<&Post> { + let mut posts: Vec<&Post> = self.posts.values().collect(); + posts.sort_by(|a, b| b.created_at.cmp(&a.created_at)); + posts + } + + pub fn get_posts_limited(&self, limit: usize) -> Vec<&Post> { + let mut posts = self.get_all_posts(); + posts.truncate(limit); + posts + } +} + +fn markdown_to_html(markdown: &str) -> String { + let mut options = Options::empty(); + options.insert(Options::ENABLE_STRIKETHROUGH); + options.insert(Options::ENABLE_TABLES); + options.insert(Options::ENABLE_FOOTNOTES); + options.insert(Options::ENABLE_TASKLISTS); + options.insert(Options::ENABLE_SMART_PUNCTUATION); + + let parser = Parser::new_ext(markdown, options); + let mut html_output = String::new(); + html::push_html(&mut html_output, parser); + html_output +} diff --git a/src/template_engine.rs b/src/template_engine.rs new file mode 100644 index 0000000..a3efb02 --- /dev/null +++ b/src/template_engine.rs @@ -0,0 +1,140 @@ +use crate::post_manager::PostManager; +use regex::Regex; +use std::fs; + +pub fn render_template( + template: &str, + post_manager: &PostManager, + current_post: Option<&str>, + limit_override: Option, +) -> Result { + let mut result = template.to_string(); + + // Process includes first + result = process_includes(&result)?; + + // Process post-html tag + if let Some(post_name) = current_post { + result = process_post_html(&result, post_manager, post_name)?; + } + + // Process post-list tag + result = process_post_list(&result, post_manager, limit_override)?; + + Ok(result) +} + +fn process_includes(template: &str) -> Result { + let include_regex = Regex::new(r#"\$"#).unwrap(); + let mut result = template.to_string(); + let mut iteration = 0; + const MAX_ITERATIONS: usize = 10; + + loop { + iteration += 1; + if iteration > MAX_ITERATIONS { + return Err("Too many nested includes (max 10)".to_string()); + } + + let result_copy = result.clone(); + let captures: Vec<_> = include_regex.captures_iter(&result_copy).collect(); + + if captures.is_empty() { + break; + } + + for cap in captures { + let full_match = cap.get(0).unwrap().as_str(); + let src = cap.get(1).unwrap().as_str(); + + let include_path = format!("templates/{}", src); + let content = fs::read_to_string(&include_path) + .map_err(|e| format!("Failed to read include file '{}': {}", src, e))?; + + result = result.replace(full_match, &content); + } + } + + Ok(result) +} + +fn process_post_html( + template: &str, + post_manager: &PostManager, + post_name: &str, +) -> Result { + let post_html_regex = Regex::new(r"\$").unwrap(); + + let post = post_manager + .get_post(post_name) + .ok_or_else(|| format!("Post '{}' not found", post_name))?; + + Ok(post_html_regex.replace_all(template, post.html_content.as_str()).to_string()) +} + +fn process_post_list( + template: &str, + post_manager: &PostManager, + limit_override: Option, +) -> Result { + let post_list_regex = Regex::new(r#"\$"#).unwrap(); + + let mut result = template.to_string(); + + for cap in post_list_regex.captures_iter(template) { + let full_match = cap.get(0).unwrap().as_str(); + let limit_attr = cap.get(1).and_then(|m| m.as_str().parse::().ok()); + + let limit = limit_override.or(limit_attr); + + let posts = if let Some(l) = limit { + post_manager.get_posts_limited(l) + } else { + post_manager.get_all_posts() + }; + + let mut list_html = String::from("
    \n"); + for post in posts { + list_html.push_str(&format!( + "
  • {} {}
  • \n", + post.name, + post.name, + post.created_at.format("%Y-%m-%d") + )); + } + list_html.push_str("
"); + + result = result.replace(full_match, &list_html); + } + + Ok(result) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_post_list_regex() { + let regex = Regex::new(r#"\$"#).unwrap(); + + assert!(regex.is_match("$")); + assert!(regex.is_match("$")); + assert!(regex.is_match("$")); + assert!(regex.is_match("$")); + + let cap = regex.captures("$").unwrap(); + assert_eq!(cap.get(1).unwrap().as_str(), "50"); + } + + #[test] + fn test_include_regex() { + let regex = Regex::new(r#"\$"#).unwrap(); + + assert!(regex.is_match(r#"$"#)); + assert!(regex.is_match(r#"$"#)); + + let cap = regex.captures(r#"$"#).unwrap(); + assert_eq!(cap.get(1).unwrap().as_str(), "header.html"); + } +} diff --git a/templates/all.html b/templates/all.html new file mode 100644 index 0000000..022fbed --- /dev/null +++ b/templates/all.html @@ -0,0 +1,4 @@ +$ +

All Posts

+ $ +$ diff --git a/templates/footer.html b/templates/footer.html new file mode 100644 index 0000000..09baa70 --- /dev/null +++ b/templates/footer.html @@ -0,0 +1,5 @@ +
+

My Static Blog - Powered by Rust and CommonMark

+
+ + diff --git a/templates/header.html b/templates/header.html new file mode 100644 index 0000000..f43f7a9 --- /dev/null +++ b/templates/header.html @@ -0,0 +1,78 @@ + + + + + + My Blog + + + + diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..45a58fd --- /dev/null +++ b/templates/index.html @@ -0,0 +1,4 @@ +$ +

Recent Posts

+ $ +$ diff --git a/templates/post.html b/templates/post.html new file mode 100644 index 0000000..917b182 --- /dev/null +++ b/templates/post.html @@ -0,0 +1,6 @@ +$ +
+ $ +
+

← Back to home

+$