Initial blog setup

This commit is contained in:
2024-10-01 19:24:07 +08:00
committed by guus
commit 02a27a43a5
13 changed files with 2009 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
/target
/.nvim.lua
/.nvim.workspace.lua

999
Cargo.lock generated Normal file
View File

@@ -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"

14
Cargo.toml Normal file
View File

@@ -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"

173
project_templates/.nvim.lua Normal file
View File

@@ -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', '<F6>', r)
if dap_ok then
-- Shift-F5 to launch default config
vim.keymap.set('n', '<F17>', function()
MakeAnd(function()
dap.run(dap_def_cfg)
end)
end)
end
end

61
src/git_tracker.rs Normal file
View File

@@ -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<GitVersion, String> {
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());
}
}
}

160
src/main.rs Normal file
View File

@@ -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<RwLock<post_manager::PostManager>>,
}
#[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<AppState>) -> 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<AppState>, Path(page): Path<String>) -> 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<AppState>,
Path(post_name): Path<String>,
) -> 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<AppState>) -> 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<String, String> {
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<String, String> {
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))
}

204
src/post_manager.rs Normal file
View File

@@ -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<Utc>,
pub modified_at: DateTime<Utc>,
}
struct Entry {
#[allow(dead_code)]
short_name: String,
file_path: String,
created_at: DateTime<Utc>,
modified_at: DateTime<Utc>,
status: i32,
}
pub struct PostManager {
root_dir: PathBuf,
posts_dir_rel: String,
posts: HashMap<String, Post>,
}
impl PostManager {
pub fn new(root_dir: &str) -> Result<Self, String> {
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<HashMap<String, Entry>, 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<String, Entry> = HashMap::new();
let mut current_timestamp: Option<DateTime<Utc>> = 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::<i64>() {
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<DateTime<Utc>, 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
}

270
src/template_engine.rs Normal file
View File

@@ -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<String, String> {
parse_template(template, post_manager, current_post, 0)
}
fn parse_template(
template: &str,
post_manager: &PostManager,
current_post: Option<&str>,
depth: usize,
) -> Result<String, String> {
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<char> = 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<String, String>, 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<String, String>,
post_manager: &PostManager,
current_post: Option<&str>,
depth: usize,
) -> Result<String, String> {
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::<usize>().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("<ul class=\"post-list\">\n");
for post in posts {
list_html.push_str(&format!(
" <li><a href=\"/p/{}\">{}</a> <span class=\"date\">{}</span></li>\n",
post.name,
post.name,
post.created_at.format("%Y-%m-%d")
));
}
list_html.push_str("</ul>");
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<char> = "$<post-list/>".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<char> = "$<post-list />".chars().collect();
let result = parse_tag(&chars, 0);
assert!(result.is_some());
let chars: Vec<char> = r#"$<post-list limit="50"/>"#.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<char> = r#"$<include src="header.html"/>"#.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<char> = r#"$<include src="header.html" />"#.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<char> = "$<post-html/>".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());
}
}

8
templates/footer.html Normal file
View File

@@ -0,0 +1,8 @@
<footer style="margin-top: 50px; padding-top: 20px; border-top: 1px solid #ddd; color: #666; text-align: center;">
<p>
Guus Waals - updated on $<updated/> </br>
$<version/>
</p>
</footer>
</body>
</html>

103
templates/header.html Normal file
View File

@@ -0,0 +1,103 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Blog</title>
<style>
body {
font-family: system-ui, -apple-system, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
line-height: 1.6;
color: #333;
}
nav {
border-bottom: 2px solid #333;
padding-bottom: 10px;
margin-bottom: 30px;
}
nav span {
font-size: 1.2em;
font-weight: bold;
margin-right: 20px;
}
nav a {
margin-right: 20px;
text-decoration: none;
color: #0066cc;
}
nav a:hover {
text-decoration: underline;
}
.post-list {
list-style: none;
padding: 0;
}
.post-list li {
margin-bottom: 10px;
padding: 10px;
border-left: 3px solid #0066cc;
padding-left: 15px;
}
.post-list a {
text-decoration: none;
color: #333;
font-weight: bold;
}
.post-list a:hover {
color: #0066cc;
}
.date {
color: #666;
font-size: 0.9em;
float: right;
}
article {
margin-top: 30px;
}
h1, h2, h3 {
color: #222;
}
code {
background: #f4f4f4;
padding: 2px 6px;
border-radius: 3px;
}
pre {
background: #f4f4f4;
padding: 15px;
border-radius: 5px;
overflow-x: auto;
}
pre code {
padding: 0;
}
.github-link {
float: right;
display: inline-block;
width: 24px;
height: 24px;
vertical-align: middle;
}
.github-link svg {
fill: #333;
transition: fill 0.2s;
}
.github-link:hover svg {
fill: #0066cc;
}
</style>
</head>
<body>
<nav>
<span>(dis) gus' things</span>
<a href="/">Home</a>
<a href="/all">All Posts</a>
<a href="https://github.com/guusw" class="github-link" target="_blank" aria-label="View source on Git">
<svg viewBox="0 0 16 16" width="24" height="24" aria-hidden="true">
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/>
</svg>
</a>
</nav>

4
templates/index.html Normal file
View File

@@ -0,0 +1,4 @@
$<include src="header.html"/>
<h1>Recent Posts</h1>
$<post-list limit="10"/>
$<include src="footer.html"/>

4
templates/page_all.html Normal file
View File

@@ -0,0 +1,4 @@
$<include src="header.html"/>
<h1>All Posts</h1>
$<post-list/>
$<include src="footer.html"/>

6
templates/post.html Normal file
View File

@@ -0,0 +1,6 @@
$<include src="header.html"/>
<article>
$<post-html/>
</article>
<p><a href="/">← Back to home</a></p>
$<include src="footer.html"/>