commit 02a27a43a598d11fc52ee558d84edc045a0dd31c Author: Guus Waals <_@guusw.nl> Date: Tue Oct 1 19:24:07 2024 +0800 Initial blog setup 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..0f855f2 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,999 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[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", + "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 = "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..12461c5 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,14 @@ +[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" +once_cell = "1.19" diff --git a/project_templates/.nvim.lua b/project_templates/.nvim.lua new file mode 100644 index 0000000..e0df6ba --- /dev/null +++ b/project_templates/.nvim.lua @@ -0,0 +1,173 @@ +-- 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" + vim.g.cargo_makeprg_params = 'build' + if workspace.build_type == "release" then + vim.opt.makeprg = vim.opt.makeprg .. " --release" + vim.g.cargo_makeprg_params = vim.g.cargo_makeprg_params .. " --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() + +-- Prevent Vim's built-in rust ftplugin from loading the cargo compiler +vim.api.nvim_create_autocmd("FileType", { + pattern = "rust", + callback = function() + vim.b.current_compiler = 'custom' + end, +}) + +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 rust_path = vim.fn.system("rustc --print sysroot") -- We need to query this to get the sysroot + rust_path = string.sub(rust_path, 1, #rust_path - 1) -- trim trailing newline + local lldb_init = { + "command script import " .. rust_path .. "/lib/rustlib/etc/lldb_lookup.py", + "command source " .. rust_path .. "/lib/rustlib/etc/lldb_commands", + } + 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..78afd5e --- /dev/null +++ b/src/git_tracker.rs @@ -0,0 +1,61 @@ +use std::fs; +use std::path::PathBuf; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct GitVersion { + pub branch: String, + pub commit: String, +} + +pub fn get_git_version(git_path: &PathBuf) -> Result { + 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..e8414fa --- /dev/null +++ b/src/main.rs @@ -0,0 +1,160 @@ +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::process::Command; +use tokio::sync::RwLock; +use tower_http::services::ServeDir; + +#[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(), + }; + + let p_router = Router::new() + .route("/:post_name", get(post_handler)) + .fallback_service(ServeDir::new("posts")); + + let app = Router::new() + .route("/", get(index_handler)) + .route("/update", get(update_handler)) + .route("/:page", get(all_handler)) + .nest("/p", p_router) + .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).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).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 { + if post_name.contains("..") { + return (StatusCode::NOT_FOUND, "Invalid path").into_response(); + } + + let manager = state.post_manager.read().await; + 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 update_handler(State(state): State) -> impl IntoResponse { + // Run git pull --autostash + let output = Command::new("git") + .arg("pull") + .arg("--autostash") + .output() + .await; + + match output { + Ok(output) => { + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + eprintln!("Git pull failed: {}", stderr); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Git pull failed: {}", stderr), + ) + .into_response(); + } + + let stdout = String::from_utf8_lossy(&output.stdout); + println!("Git pull output: {}", stdout); + + // Refresh posts + let mut manager = state.post_manager.write().await; + match manager.refresh_posts() { + Ok(_) => { + println!("Successfully refreshed log pages"); + (StatusCode::OK, "Update successful: pulled changes and refreshed log pages") + .into_response() + } + Err(e) => { + eprintln!("Failed to refresh posts: {}", e); + ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Failed to refresh posts: {}", e), + ) + .into_response() + } + } + } + Err(e) => { + eprintln!("Failed to execute git pull: {}", e); + ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Failed to execute git pull: {}", e), + ) + .into_response() + } + } +} + + +async fn render_template(template_name: &str, state: &AppState) -> 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) +} + +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)) +} diff --git a/src/post_manager.rs b/src/post_manager.rs new file mode 100644 index 0000000..cd88250 --- /dev/null +++ b/src/post_manager.rs @@ -0,0 +1,204 @@ +use chrono::{DateTime, Utc}; +use pulldown_cmark::{html, Options, Parser}; +use std::collections::HashMap; +use std::fs; +use std::path::PathBuf; +use std::process::Command; + +#[derive(Debug, Clone)] +pub struct Post { + pub name: String, + #[allow(dead_code)] + pub filename: String, + #[allow(dead_code)] + pub markdown_content: String, + pub html_content: String, + pub created_at: DateTime, + pub modified_at: DateTime, +} + +struct Entry { + #[allow(dead_code)] + short_name: String, + file_path: String, + created_at: DateTime, + modified_at: DateTime, + status: i32, +} + +pub struct PostManager { + root_dir: PathBuf, + posts_dir_rel: String, + posts: HashMap, +} + +impl PostManager { + pub fn new(root_dir: &str) -> Result { + let root_dir = PathBuf::from(root_dir); + let posts_dir_rel = "posts"; + + let mut manager = PostManager { + root_dir, + posts_dir_rel: posts_dir_rel.to_string(), + posts: HashMap::new(), + }; + + manager.refresh_posts()?; + Ok(manager) + } + + pub fn get_root_dir(&self) -> &PathBuf { + &self.root_dir + } + + fn query_posts(&self) -> Result, String> { + let output = Command::new("git") + .arg("log") + .arg("--raw") + .arg("--no-merges") + .arg("--pretty=%h - %ad - %s") + .arg("--date=unix") + .arg("--") + .arg(self.posts_dir_rel.clone()) + .current_dir(&self.root_dir) + .output() + .map_err(|e| format!("Failed to execute git whatchanged: {}", e))?; + + if !output.status.success() { + return Err(format!( + "Git whatchanged command failed: {}", + String::from_utf8_lossy(&output.stderr) + )); + } + + let log_output = String::from_utf8_lossy(&output.stdout); + let mut result: HashMap = HashMap::new(); + let mut current_timestamp: Option> = None; + + for line in log_output.lines() { + let line = line.trim(); + + // Skip empty lines + if line.is_empty() { + continue; + } + + // Parse commit header line: "f4fcf0e - 1761305168 - New post" + if !line.starts_with(':') { + if let Some(dash_pos) = line.find(" - ") { + let after_first_dash = &line[dash_pos + 3..]; + if let Some(second_dash_pos) = after_first_dash.find(" - ") { + let timestamp_str = after_first_dash[..second_dash_pos].trim(); + if let Ok(timestamp) = timestamp_str.parse::() { + current_timestamp = DateTime::from_timestamp(timestamp, 0); + eprintln!("Timestamp: {:?} ({})", current_timestamp, timestamp); + } + } + } + continue; + } + + // Parse file change line: ":000000 100644 0000000 6bdad65 A posts/hello-world-2.md" + if line.starts_with(':') { + eprintln!("Parsing line: {}", line); + if let Some(timestamp) = current_timestamp { + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.len() >= 6 { + let status = parts[4]; // A (add), D (delete), M (modify) + let file_path = parts[5]; + + let fn_ = file_path + .strip_prefix("posts/") + .and_then(|s| s.strip_suffix(".md")); + if let Some(name) = fn_ { + let entry = result.entry(name.to_string()).or_insert(Entry { + short_name: name.to_string(), + file_path: file_path.to_string(), + created_at: timestamp, + modified_at: timestamp, + status: if status == "D" { -1 } else { 1 }, + }); + + // Always use the oldest timestamp for posts creation dates + entry.created_at = timestamp; + } + } + } else { + eprintln!("Invalid git log output, expected prior timestamp: {}", line); + } + } + } + + Ok(result) + } + + pub fn refresh_posts(&mut self) -> Result<(), String> { + self.posts.clear(); + + // Get timestamps from git history + let query_result = self.query_posts()?; + for (name, entry) in query_result { + if entry.status != 1 { + continue; + } + let markdown_content = fs::read_to_string(&entry.file_path) + .map_err(|e| format!("Failed to read post file: {}", e))?; + let html_content = markdown_to_html(&markdown_content); + let post = Post { + name: name.clone(), + filename: entry.file_path, + markdown_content, + html_content, + created_at: entry.created_at, + modified_at: entry.modified_at, + }; + eprintln!("Loaded post: {} ({})", name, entry.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) + } + + // Get all posts, sorted by creation date + 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 + } + + // Get the timstamp of when the blog was most recently updated + // derived from latest post update + pub fn get_update_timestamp(&self) -> Result, String> { + let mut posts: Vec<&Post> = self.posts.values().collect(); + posts.sort_by(|a, b| b.modified_at.cmp(&a.modified_at)); + Ok(posts + .first() + .ok_or("No posts found".to_string())? + .created_at) + } + + 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..cddc9e3 --- /dev/null +++ b/src/template_engine.rs @@ -0,0 +1,270 @@ +use crate::post_manager::PostManager; +use crate::git_tracker::get_git_version; +use std::collections::HashMap; +use std::fs; + +pub fn render_template( + template: &str, + post_manager: &PostManager, + current_post: Option<&str>, +) -> Result { + parse_template(template, post_manager, current_post, 0) +} + +fn parse_template( + template: &str, + post_manager: &PostManager, + current_post: Option<&str>, + depth: usize, +) -> Result { + const MAX_DEPTH: usize = 10; + if depth > MAX_DEPTH { + return Err("Too many nested includes (max 10)".to_string()); + } + + let mut result = String::new(); + let chars: Vec = template.chars().collect(); + let mut i = 0; + + while i < chars.len() { + // Check for $< tag start + if i + 1 < chars.len() && chars[i] == '$' && chars[i + 1] == '<' { + // Try to parse tag + if let Some((tag_name, attrs, end_pos)) = parse_tag(&chars, i) { + // Process tag immediately based on type + let content = process_tag(&tag_name, &attrs, post_manager, current_post, depth)?; + result.push_str(&content); + i = end_pos; + continue; + } + } + + // Regular character + result.push(chars[i]); + i += 1; + } + + Ok(result) +} + +fn parse_tag(chars: &[char], start: usize) -> Option<(String, HashMap, usize)> { + let mut i = start + 2; // Skip $< + + // Skip whitespace + while i < chars.len() && chars[i].is_whitespace() { + i += 1; + } + + // Parse tag name + let mut tag_name = String::new(); + while i < chars.len() && !chars[i].is_whitespace() && chars[i] != '/' && chars[i] != '>' { + tag_name.push(chars[i]); + i += 1; + } + + if tag_name.is_empty() { + return None; + } + + // Parse attributes + let mut attrs = HashMap::new(); + + loop { + // Skip whitespace + while i < chars.len() && chars[i].is_whitespace() { + i += 1; + } + + // Check for end of tag + if i >= chars.len() { + return None; + } + + if chars[i] == '/' { + i += 1; + if i < chars.len() && chars[i] == '>' { + return Some((tag_name, attrs, i + 1)); + } + return None; + } + + if chars[i] == '>' { + return Some((tag_name, attrs, i + 1)); + } + + // Parse attribute name + let mut attr_name = String::new(); + while i < chars.len() && chars[i] != '=' && !chars[i].is_whitespace() { + attr_name.push(chars[i]); + i += 1; + } + + if attr_name.is_empty() { + return None; + } + + // Skip whitespace + while i < chars.len() && chars[i].is_whitespace() { + i += 1; + } + + // Expect = + if i >= chars.len() || chars[i] != '=' { + return None; + } + i += 1; + + // Skip whitespace + while i < chars.len() && chars[i].is_whitespace() { + i += 1; + } + + // Parse attribute value (quoted) + if i >= chars.len() || chars[i] != '"' { + return None; + } + i += 1; // Skip opening quote + + let mut attr_value = String::new(); + while i < chars.len() && chars[i] != '"' { + attr_value.push(chars[i]); + i += 1; + } + + if i >= chars.len() { + return None; + } + i += 1; // Skip closing quote + + attrs.insert(attr_name, attr_value); + } +} + +fn process_tag( + tag_name: &str, + attrs: &HashMap, + post_manager: &PostManager, + current_post: Option<&str>, + depth: usize, +) -> Result { + match tag_name { + "include" => { + let src = attrs.get("src") + .ok_or_else(|| "include tag missing 'src' attribute".to_string())?; + + 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))?; + + // Recursively parse the included content + parse_template(&content, post_manager, current_post, depth + 1) + } + + "post-html" => { + let post_name = current_post + .ok_or_else(|| "post-html tag used outside of post context".to_string())?; + + let post = post_manager + .get_post(post_name) + .ok_or_else(|| format!("Post '{}' not found", post_name))?; + + Ok(post.html_content.clone()) + } + + "updated" => { + // Insert the last updated time of the current post (or most recent post) + let post_ts = if let Some(p) = current_post { + post_manager.get_post(p).ok_or_else(|| format!("Post '{}' not found", p))?.modified_at + } else { + post_manager.get_update_timestamp()? + }; + + Ok(post_ts.format("%Y-%m-%d").to_string()) + } + + "version" => { + let git_dir = post_manager.get_root_dir().clone(); + let git_version = get_git_version(&git_dir)?; + Ok(format!("{}/{}", git_version.branch, git_version.commit)) + } + + "post-list" => { + let limit = attrs.get("limit") + .and_then(|s| s.parse::().ok()); + + 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("
"); + + Ok(list_html) + } + + _ => Err(format!("Unknown tag: {}", tag_name)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_tag_post_list() { + let chars: Vec = "$".chars().collect(); + let result = parse_tag(&chars, 0); + assert!(result.is_some()); + let (tag_name, attrs, _) = result.unwrap(); + assert_eq!(tag_name, "post-list"); + assert!(attrs.is_empty()); + + let chars: Vec = "$".chars().collect(); + let result = parse_tag(&chars, 0); + assert!(result.is_some()); + + let chars: Vec = r#"$"#.chars().collect(); + let result = parse_tag(&chars, 0); + assert!(result.is_some()); + let (tag_name, attrs, _) = result.unwrap(); + assert_eq!(tag_name, "post-list"); + assert_eq!(attrs.get("limit"), Some(&"50".to_string())); + } + + #[test] + fn test_parse_tag_include() { + let chars: Vec = r#"$"#.chars().collect(); + let result = parse_tag(&chars, 0); + assert!(result.is_some()); + let (tag_name, attrs, _) = result.unwrap(); + assert_eq!(tag_name, "include"); + assert_eq!(attrs.get("src"), Some(&"header.html".to_string())); + + let chars: Vec = r#"$"#.chars().collect(); + let result = parse_tag(&chars, 0); + assert!(result.is_some()); + let (tag_name, attrs, _) = result.unwrap(); + assert_eq!(tag_name, "include"); + assert_eq!(attrs.get("src"), Some(&"header.html".to_string())); + } + + #[test] + fn test_parse_tag_post_html() { + let chars: Vec = "$".chars().collect(); + let result = parse_tag(&chars, 0); + assert!(result.is_some()); + let (tag_name, attrs, _) = result.unwrap(); + assert_eq!(tag_name, "post-html"); + assert!(attrs.is_empty()); + } +} diff --git a/templates/footer.html b/templates/footer.html new file mode 100644 index 0000000..d5ed8fb --- /dev/null +++ b/templates/footer.html @@ -0,0 +1,8 @@ +
+

+ Guus Waals - updated on $
+ $ +

+
+ + diff --git a/templates/header.html b/templates/header.html new file mode 100644 index 0000000..66980ce --- /dev/null +++ b/templates/header.html @@ -0,0 +1,103 @@ + + + + + + My Blog + + + + diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..8cf440f --- /dev/null +++ b/templates/index.html @@ -0,0 +1,4 @@ +$ +

Recent Posts

+ $ +$ diff --git a/templates/page_all.html b/templates/page_all.html new file mode 100644 index 0000000..022fbed --- /dev/null +++ b/templates/page_all.html @@ -0,0 +1,4 @@ +$ +

All 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

+$