From 9229313f9b86b300f65feeef5527fd041322a7a6 Mon Sep 17 00:00:00 2001 From: Guus Waals <_@guusw.nl> Date: Wed, 29 Oct 2025 20:29:52 +0800 Subject: [PATCH] Initial blog setup --- .cargo/config.toml | 9 + .gitignore | 4 + Cargo.lock | 1126 +++++++++++++++++++++++++++++++++++ Cargo.toml | 15 + lua/lemon.lua | 153 +++++ posts/blog-server.md | 106 ++++ project_templates/.nvim.lua | 136 +++++ scripts/deploy | 9 + src/git_tracker.rs | 61 ++ src/main.rs | 180 ++++++ src/post_manager.rs | 264 ++++++++ src/template_engine.rs | 308 ++++++++++ templates/footer.html | 8 + templates/header.html | 109 ++++ templates/index.html | 4 + templates/page_all.html | 4 + templates/post.html | 6 + 17 files changed, 2502 insertions(+) create mode 100644 .cargo/config.toml create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 lua/lemon.lua create mode 100644 posts/blog-server.md create mode 100644 project_templates/.nvim.lua create mode 100755 scripts/deploy create mode 100644 src/git_tracker.rs create mode 100644 src/main.rs create mode 100644 src/post_manager.rs create mode 100644 src/template_engine.rs create mode 100644 templates/footer.html create mode 100644 templates/header.html create mode 100644 templates/index.html create mode 100644 templates/page_all.html create mode 100644 templates/post.html diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..601a39b --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,9 @@ +[profile.small] +inherits = "release" +opt-level = "z" +codegen-units = 1 +lto = true + +[profile.extra-small] +inherits = "small" +panic = "abort" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..db0c223 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/target +/.nvim.lua +/.nvim.workspace.lua +/blog-server diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..34beb46 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1126 @@ +# 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 = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +dependencies = [ + "windows-sys 0.60.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.60.2", +] + +[[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", + "clap", + "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 = "clap" +version = "4.5.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c2cfd7bf8a6017ddaa4e32ffe7403d547790db06bd171c1c53926faab501623" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a4c05b9e80c5ccd3a7ef080ad7b6ba7d6fc00a985b8b157197075677c82c7a0" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[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 = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[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 = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[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 = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[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 = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[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 = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[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..0cffcfb --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "blog-server" +version = "0.1.0" +edition = "2021" + +[dependencies] +axum = "0.7" +tokio = { version = "1", features = ["full"] } +tower = "0.4" +tower-http = { version = "0.5", features = ["fs"] } +pulldown-cmark = "0.12" +serde = { version = "1.0", features = ["derive"] } +chrono = "0.4" +once_cell = "1.19" +clap = { version = "4.5", features = ["derive"] } diff --git a/lua/lemon.lua b/lua/lemon.lua new file mode 100644 index 0000000..d9b414b --- /dev/null +++ b/lua/lemon.lua @@ -0,0 +1,153 @@ +Lemon = { + ws = {}, + ws_file = '.nvim.workspace.lua', + term_buf = nil, + term_win_cmd = 'belowright 12split', +} + +function LoadWorkspace() + -- Load persistent configuration from .workspace.lua + local loaded, workspace = pcall(dofile, Lemon.ws_file) + if not loaded then return nil end + return workspace +end + +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(Lemon.ws); nl() + vim.fn.writefile(s.ls, Lemon.ws_file) +end + +-- Loads the workspace from the file, or return the default +---@param default table +---@return table + +function InitWorkspace(default) + Lemon.ws = LoadWorkspace() + if Lemon.ws == nil then + Lemon.ws = default + end + return Lemon.ws +end + +function TermShow() + local info = GetTermInfo() + + if info == nil then + -- Create new terminal buffer + vim.cmd(Lemon.term_win_cmd) + vim.cmd('terminal') + Lemon.term_buf = vim.api.nvim_get_current_buf() + -- Mark buffer so we can identify it later + vim.api.nvim_buf_set_var(Lemon.term_buf, 'lemon_terminal', true) + info = GetTermInfo() + elseif info.win == nil then + -- Buffer exists but not visible, open it + vim.cmd(Lemon.term_win_cmd) + vim.api.nvim_win_set_buf(0, Lemon.term_buf) + else + -- Window is visible, switch to it + vim.api.nvim_set_current_win(info.win) + end +return info +end + +-- Find or create persistent terminal buffer, open window, and run command +function TermRun(cmd) + local info = TermShow() + + -- Send command to terminal + vim.fn.chansend(info.job_id, '\021' .. cmd .. '\n') + vim.fn.feedkeys("G", "n") +end + +-- Get terminal buffer and job_id if valid, returns {buf, job_id, win} +-- win is nil if terminal is not currently visible +function GetTermInfo() + if Lemon.term_buf == nil or not vim.api.nvim_buf_is_valid(Lemon.term_buf) then + return nil + end + + local job_id = vim.api.nvim_buf_get_var(Lemon.term_buf, 'terminal_job_id') + + -- Find window showing the terminal buffer + local win = nil + for _, w in ipairs(vim.api.nvim_list_wins()) do + if vim.api.nvim_win_get_buf(w) == Lemon.term_buf then + win = w + break + end + end + + return { buf = Lemon.term_buf, job_id = job_id, win = win } +end + +-- Compatibility wrapper - returns window ID if terminal is visible +function SwitchToExistingTerm() + local info = GetTermInfo() + return info and info.win or nil +end + +-- Runs the make command and runs the callback when it completes +function MakeAnd(run_callback) + -- Create a one-time autocmd that fires when make completes + local group = vim.api.nvim_create_augroup('MakeAnd', { clear = false }) + + vim.api.nvim_create_autocmd('QuickFixCmdPost', { + group = group, + pattern = 'make', + once = true, + callback = function() + local qf_list = vim.fn.getqflist() + local has_errors = false + + for _, item in ipairs(qf_list) do + if item.valid == 1 then + has_errors = true + break + end + end + + vim.schedule(function() + if not has_errors then + run_callback() + else + vim.api.nvim_echo({ { "Build failed", "ErrorMsg" } }, false, {}) + end + end) + end + }) + + vim.cmd('silent make') +end + +function TabCurrent() + return vim.fn.tabpagenr() +end + +function TabSwitch(tab) + vim.cmd('tabnext ' .. tab) +end diff --git a/posts/blog-server.md b/posts/blog-server.md new file mode 100644 index 0000000..b7d6dd0 --- /dev/null +++ b/posts/blog-server.md @@ -0,0 +1,106 @@ +# Blog setup with markdown, rust & git + +Hey everyone. This is the first post I write for the blog I'm starting, which is Coincidentally about how I've structured the software surrounding my blog. + +First off, I've been wanting to do a lot recently: + +- Moving away from Windows to using Linux and MacOS for development and gaming +- Switching from VSCode/Cursor to neovim +- Switching from working for an employer to becoming a solo dev/freelancer +- Dedicate time to solo game development + +Because of this, I figured it would be a good time to set up a blog, both for documenting whatever I'm working on and to get the word out about my work. + +## The blog design + +I wanted to have a setup that generates static html from something like markdown. Then recently I stumbled across this [https://gaultier.github.io/blog/making_my_static_blog_generator_11_times_faster.html](https://gaultier.github.io/blog/making_my_static_blog_generator_11_times_faster.html) + +I decided to do something similar, however I used rust as it has some popular existing libraries for web servers and parsing markdown. The reason for writing an application to host the blog is that I wanted to have it automatically respond to a git webhook which would automatically pull the latest git repo and then rebuild the articles from their markdown files. + +## Parsing the git output + +As mentioned in [this article](https://gaultier.github.io/blog/making_my_static_blog_generator_11_times_faster.html) they use the git log command to retrieve the blog files, which I decided to also use. I found out there is a way to retreive the output from git in the following predictable format: + +``` +c584d39 - 1761735775 - Update server post and fix post titles + +:100644 100644 d5e614a 678eb09 M posts/blog-server.md +9d5f86a - 1761415068 - Template + +:000000 100644 0000000 d5e614a A posts/blog-server.md +``` + +Retrieved with the following command line: `git log --raw --no-merges --pretty="%h - %ad - %s" --date=unix -- posts` +Where the first entry will always be a commit hash followed by unix time stamp and then the commit name. After that it will list files affected by this commit, prefixed with a colon (:). + +Then we can simply parse this by reading line by line and attributing the following file changes to the commit that was above it. Then I use the first encounter of a given file to determine the creation date, and the last encounter to determine the update date. + +### (Bonus) Parsing the git version + +There's a nice trick to retrieve the current git branch and commit hash by just reading 2 files, which I grabbed from one of my CMake scripts that uses it to inject the git version into my code on every compile. It's super fast since it doesn't actually run any git commands to retrieve this information. + +The process is, read .git/HEAD, which will either contain just the hash, or something like `ref: refs/heads/main`.\ +In the later case, you can just read from .git/refs/heads/main which will then contain your git hash. + +This version is inserted at the bottom of the page so I can tell which version it's at. + +## HTML + +Of course there is also some styling and markup required for the shell, like the navigation bar and footers.\ +I decided to go with a simple list of html templates that get included by the application and scan it for tags to inject certain magic values, like the list of posts or timestamps. + +For example, here is what the page you're looking at looks like: + +```html +$ +
+ $ +
+

← Back to home

+$ +``` + +Which can be parsed very quickly by a simple scanner that triggers on $< and then reads the key and map of `` parameters. + +This allows me to just write template code like this: +```html +$<title default="Guus' blog" pre="Guus - "> +``` + +and process it in rust like this: +```rs +"title" => { + if let Some(post_name) = current_post { + let post = post_manager + .get_post(post_name) + .await + .ok_or_else(|| format!("Post '{}' not found", post_name))?; + + let none: String = "".to_string(); + let pre = attrs.get("pre").unwrap_or(&none); + let postfix = attrs.get("post").unwrap_or(&none); + + Ok(format!("{}{}{}", pre, post.title, postfix)) + } else { + let def = attrs + .get("default") + .and_then(|s| Some(s.clone())) + .unwrap_or("".to_string()); + Ok(def.to_string()) + } +} +``` + +All the HTML code that has been generated like this is then cached until the next the blog update is triggered by my pushing a new post to git. + +## Styling + +For the styling, I added some minimal CSS to center the contents, limit the width and change the font sizes.\ +I wanted to keep it simple so the layout works on both a desktop browser, and on mobile phone screens. This also allows you to dock the window to a side of your screen or second vertical monitor in case you wanted to reference it for some code which I find useful. + +## Conclusion + +I like this design as it's easy to deploy locally for previewing, and adding new posts or making edits is as simple as `git commit && git push`. I can keep posts I'm working on in separate branches and progressively work on them like that. + +If you're interested in the source code, or would like to use it for yourself feel free to check it out [here](https://git.bakje.coffee/guus/blog). + diff --git a/project_templates/.nvim.lua b/project_templates/.nvim.lua new file mode 100644 index 0000000..06a469f --- /dev/null +++ b/project_templates/.nvim.lua @@ -0,0 +1,136 @@ +require("lua/lemon") + +local def_workspace = { args = {}, build_type = "debug", binary = "blog-server" } +local workspace = InitWorkspace(def_workspace) + +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, +}) + +-- 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 + +-- 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 = 'default', + 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 + +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) +vim.keymap.set('n', '<F18>', function() + local info = GetTermInfo() + if info then + -- Send interrupt to terminal without switching + vim.fn.chansend(info.job_id, '\003') + -- Close window if it's open + vim.print("Stopped program") + else + vim.print("No terminal buffer found") + end +end) + +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 diff --git a/scripts/deploy b/scripts/deploy new file mode 100755 index 0000000..67af07b --- /dev/null +++ b/scripts/deploy @@ -0,0 +1,9 @@ +#!/bin/bash + +ROOT=$(dirname $0)/.. +pushd $ROOT + +set -ex + +cargo build --profile extra-small +cp -f target/extra-small/blog-server ./ 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<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()); + } + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..ec8fbb0 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,180 @@ +mod git_tracker; +mod post_manager; +mod template_engine; + +use axum::{ + extract::{Path, State}, + http::StatusCode, + response::{Html, IntoResponse, Response}, + routing::get, + Router, +}; +use clap::Parser; +use std::sync::Arc; +use tokio::process::Command; +use tokio::sync::{Mutex, RwLock}; +use tower_http::services::ServeDir; + +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +struct Args { + /// Disable caching of markdown rendering + #[arg(long)] + no_cache: bool, +} + +#[derive(Clone)] +struct AppState { + post_manager: Arc<RwLock<post_manager::PostManager>>, + update_lock: Arc<Mutex<()>>, +} + +#[tokio::main] +async fn main() { + let args = Args::parse(); + + let post_manager = Arc::new(RwLock::new( + post_manager::PostManager::new(".", args.no_cache).expect("Failed to initialize post manager"), + )); + + let app_state = AppState { + post_manager: post_manager.clone(), + update_lock: Arc::new(Mutex::new(())), + }; + + if args.no_cache { + println!("Running with caching disabled"); + } + + 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>) -> Response { + 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>) -> Response { + 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>, +) -> Response { + 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).await { + 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 { + // Acquire lock to prevent concurrent updates + let _lock = state.update_lock.lock().await; + + // 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).await +} + +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)).await +} diff --git a/src/post_manager.rs b/src/post_manager.rs new file mode 100644 index 0000000..7a9023f --- /dev/null +++ b/src/post_manager.rs @@ -0,0 +1,264 @@ +use chrono::{DateTime, Utc}; +use pulldown_cmark::{html, Event, HeadingLevel, Options, Parser, Tag, TagEnd}; +use std::collections::HashMap; +use std::fs; +use std::path::PathBuf; +use std::process::Command; +use std::sync::Arc; +use tokio::sync::RwLock; + +#[derive(Debug, Clone)] +pub struct Post { + pub name: String, + pub title: 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, Arc<RwLock<Post>>>, + no_cache: bool, +} + +impl PostManager { + pub fn new(root_dir: &str, no_cache: bool) -> 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(), + no_cache, + }; + + 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, title) = markdown_to_html(&markdown_content); + let post = Post { + name: name.clone(), + title: title, + 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, Arc::new(RwLock::new(post))); + } + + println!("Loaded {} posts", self.posts.len()); + Ok(()) + } + + pub async fn get_post(&self, name: &str) -> Option<Post> { + let post_lock = self.posts.get(name)?; + + // If no_cache is enabled, always regenerate + if self.no_cache { + self.refresh_post_cache(name, post_lock).await; + } + + let post = post_lock.read().await; + Some(post.clone()) + } + + async fn refresh_post_cache(&self, name: &str, post_lock: &Arc<RwLock<Post>>) { + let mut post = post_lock.write().await; + let filename = post.filename.clone(); + + if let Ok(markdown_content) = fs::read_to_string(&filename) { + let (html_content, title) = markdown_to_html(&markdown_content); + post.html_content = html_content; + post.title = title; + post.markdown_content = markdown_content; + eprintln!("Refreshed post '{}'", name); + } + } + + // Get all posts, sorted by creation date + pub async fn get_all_posts(&self) -> Vec<Post> { + let mut posts = Vec::new(); + for (name, post_lock) in &self.posts { + // If no_cache is enabled, always regenerate + if self.no_cache { + self.refresh_post_cache(name, post_lock).await; + } + + let post = post_lock.read().await; + posts.push(post.clone()); + } + 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 async fn get_update_timestamp(&self) -> Result<DateTime<Utc>, String> { + let posts = self.get_all_posts().await; + let mut posts_sorted: Vec<&Post> = posts.iter().collect(); + posts_sorted.sort_by(|a, b| b.modified_at.cmp(&a.modified_at)); + Ok(posts_sorted + .first() + .ok_or("No posts found".to_string())? + .created_at) + } + + pub async fn get_posts_limited(&self, limit: usize) -> Vec<Post> { + let mut posts = self.get_all_posts().await; + posts.truncate(limit); + posts + } +} + +fn markdown_title(markdown: &str) -> Option<String> { + let parser = Parser::new(markdown); + let mut in_tag = false; + for event in parser { + match event { + Event::Start(Tag::Heading { + level: HeadingLevel::H1, + .. + }) => in_tag = true, + Event::End(TagEnd::Heading(HeadingLevel::H1)) => in_tag = false, + Event::Text(txt) => { + if in_tag { + return Some(txt.to_string()); + } + } + _ => {}, + } + } + None +} + +fn markdown_to_html(markdown: &str) -> (String, 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 title = markdown_title(markdown).unwrap_or("unknown".to_string()); + + let parser = Parser::new_ext(markdown, options); + let mut html_output = String::new(); + html::push_html(&mut html_output, parser); + (html_output, title) +} diff --git a/src/template_engine.rs b/src/template_engine.rs new file mode 100644 index 0000000..f88801d --- /dev/null +++ b/src/template_engine.rs @@ -0,0 +1,308 @@ +use crate::git_tracker::get_git_version; +use crate::post_manager::PostManager; +use std::collections::HashMap; +use std::fs; +use std::future::Future; +use std::pin::Pin; + +pub async fn render_template( + template: &str, + post_manager: &PostManager, + current_post: Option<&str>, +) -> Result<String, String> { + parse_template(template, post_manager, current_post, 0).await +} + +fn parse_template<'a>( + template: &'a str, + post_manager: &'a PostManager, + current_post: Option<&'a str>, + depth: usize, +) -> Pin<Box<dyn Future<Output = Result<String, String>> + Send + 'a>> { + Box::pin(async move { parse_template_impl(template, post_manager, current_post, depth).await }) +} + +async fn parse_template_impl( + 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).await?; + 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); + } +} + +async 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).await + } + + "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) + .await + .ok_or_else(|| format!("Post '{}' not found", post_name))?; + + Ok(post.html_content.clone()) + } + + "title" => { + if let Some(post_name) = current_post { + let post = post_manager + .get_post(post_name) + .await + .ok_or_else(|| format!("Post '{}' not found", post_name))?; + + let none: String = "".to_string(); + let pre = attrs.get("pre").unwrap_or(&none); + let postfix = attrs.get("post").unwrap_or(&none); + + Ok(format!("{}{}{}", pre, post.title, postfix)) + } else { + let def = attrs + .get("default") + .and_then(|s| Some(s.clone())) + .unwrap_or("<title not set>".to_string()); + Ok(def.to_string()) + } + } + + "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) + .await + .ok_or_else(|| format!("Post '{}' not found", p))? + .modified_at + } else { + post_manager.get_update_timestamp().await? + }; + + 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).await + } else { + post_manager.get_all_posts().await + }; + + 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.title, + 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()); + } +} 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 @@ + <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> diff --git a/templates/header.html b/templates/header.html new file mode 100644 index 0000000..cbfc3d8 --- /dev/null +++ b/templates/header.html @@ -0,0 +1,109 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>$<title default="Guus' blog" pre="Guus - "> + + + + 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

+$