diff --git a/Cargo.lock b/Cargo.lock index 8d7a4a1..f690e92 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -369,7 +369,6 @@ dependencies = [ "comrak", "console-subscriber", "fronma", - "futures-util", "notify", "scc", "serde", @@ -844,17 +843,6 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" -[[package]] -name = "futures-macro" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.60", -] - [[package]] name = "futures-sink" version = "0.3.30" @@ -874,11 +862,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-core", - "futures-macro", "futures-task", "pin-project-lite", "pin-utils", - "slab", ] [[package]] @@ -2126,9 +2112,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.9" +version = "0.22.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e40bb779c5187258fd7aad0eb68cb8706a0a81fa712fbea808ab43c4b8374c4" +checksum = "d3328d4f68a705b2a4498da1d580585d39a6510f98318a2cec3018a7ec61ddef" dependencies = [ "indexmap 2.2.6", "serde", diff --git a/Cargo.toml b/Cargo.toml index d2a150d..e84de71 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,6 @@ color-eyre = "0.6.3" comrak = { version = "0.22.0", features = ["syntect"] } console-subscriber = { version = "0.2.0", optional = true } fronma = "0.2.0" -futures-util = "0.3.30" notify = "6.1.1" scc = "2.1.0" serde = { version = "1.0.197", features = ["derive"] } diff --git a/README.md b/README.md index e560b13..f065b99 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ title: "README" description: "the README.md file of this project" author: "slonkazoid" -created_at: 2024-04-18T01:15:26Z +created_at: 2024-04-18T04:15:26+03:00 --- # bingus-blog @@ -11,10 +11,123 @@ blazingly fast markdown blog software written in rust memory safe ## TODO -- [ ] finish writing this document -- [ ] document config +- [ ] RSS +- [x] finish writing this document +- [x] document config - [ ] extend syntect options - [ ] general cleanup of code +- [ ] make `compress.rs` not suck +- [ ] better error reporting and pages +- [ ] better tracing +- [ ] cache cleanup task +- [ ] (de)compress cache with zstd on startup/shutdown +- [ ] make date parsing less strict +- [ ] make date formatting better +- [ ] clean up imports and require less features - [x] be blazingly fast - [x] 100+ MiB binary size +## Configuration + +the default configuration with comments looks like this + +```toml +# main settings +host = "0.0.0.0" # ip to listen on +port = 3000 # port to listen on +title = "bingus-blog" # title of the website +description = "blazingly fast markdown blog software written in rust memory safe" # description of the website +posts_dir = "posts" # where posts are stored +#cache_file = "..." # file to serialize the cache into on shutdown, and + # to deserialize from on startup. uncomment to enable +markdown_access = true # allow users to see the raw markdown of a post + +[render] # rendering-specific settings +syntect.load_defaults = false # include default syntect themes +syntect.themes_dir = "themes" # directory to include themes from +syntect.theme = "Catppuccin Mocha" # theme file name (without `.tmTheme`) + +[precompression] # precompression settings +enable = false # gzip every file in static/ on startup +watch = true # keep watching and gzip files as they change +``` + +you don't have to copy it from here, it's generated if it doesn't exist + +## Usage + +build the application with `cargo`: + +```sh +cargo build --release +``` + +the executable will be located at `target/release/bingus-blog`. + +### Building for another architecture + +you can use the `--target` flag in `cargo build` for this purpose + +building for `aarch64-unknown-linux-musl` (for example, a Redmi 5 Plus running postmarketOS): + +```sh +# install the required packages to compile and link aarch64 binaries +sudo pacman -S aarch64-linux-gnu-gcc +export CC=aarch64-linux-gnu-gcc +export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER=$CC +cargo build --release --target=aarch64-unknown-linux-musl +``` + +your executable will be located at `target//release/bingus-blog` this time. + +## Writing Posts + +posts are written in markdown. the requirements for a file to count as a post are: + +1. the file must be in the root of the `posts` directory you configured +2. the file's name must end with the extension `.md` +3. the file's contents must begin with a valid [front matter](#front-matter) + +this file counts as a valid post, and will show up if you just `git clone` and +`cargo r`. there is a symlink to this file from the default posts directory + +## Front Matter + +every post **must** begin with a **valid** front matter. else it wont be listed +in / & /posts, and when you navigate to it, you will be met with an error page. +the error page will tell you what the problem is. + +example: + +```md +--- +title: "README" +description: "the README.md file of this project" +author: "slonkazoid" +created_at: 2024-04-18T04:15:26+03:00 +#modified_at: ... # see above +--- +``` + +only first 3 fields are required. if it can't find the other 2 fields, it will +get them from filesystem metadata. if you are on musl and you omit the +`created_at` field, it will just not show up + +the dates must follow the [RFC 3339](https://datatracker.ietf.org/doc/html/rfc3339) +standard. examples of valid and invalid dates: + +```diff ++ 2024-04-18T01:15:26Z # valid ++ 2024-04-18T04:15:26+03:00 # valid (with timezone) +- 2024-04-18T04:15:26Z # invalid (missing Z) +- 2024-04-18T04:15Z # invalid (missing seconds) +- # everything else is also invalid +``` + +## Routes + +- `GET /`: index page, lists posts +- `GET /posts`: returns a list of all posts with metadata in JSON format +- `GET /posts/`: view a post +- `GET /posts/.md`: view the raw markdown of a post +- `GET /post/*`: redirects to `/posts/*` diff --git a/src/config.rs b/src/config.rs index 789761e..6ed5327 100644 --- a/src/config.rs +++ b/src/config.rs @@ -9,12 +9,17 @@ use serde::{Deserialize, Serialize}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tracing::{error, info}; +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)] +pub struct SyntectConfig { + pub load_defaults: bool, + pub themes_dir: Option, + pub theme: Option, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)] #[serde(default)] pub struct RenderConfig { - pub syntect_load_defaults: bool, - pub syntect_themes_dir: Option, - pub syntect_theme: Option, + pub syntect: SyntectConfig, } #[cfg(feature = "precompression")] @@ -37,6 +42,7 @@ pub struct Config { #[cfg(feature = "precompression")] pub precompression: PrecompressionConfig, pub cache_file: Option, + pub markdown_access: bool, } impl Default for Config { @@ -51,6 +57,7 @@ impl Default for Config { #[cfg(feature = "precompression")] precompression: Default::default(), cache_file: None, + markdown_access: true, } } } @@ -58,9 +65,11 @@ impl Default for Config { impl Default for RenderConfig { fn default() -> Self { Self { - syntect_load_defaults: false, - syntect_themes_dir: Some("themes".into()), - syntect_theme: Some("Catppuccin Mocha".into()), + syntect: SyntectConfig { + load_defaults: false, + themes_dir: Some("themes".into()), + theme: Some("Catppuccin Mocha".into()), + }, } } } diff --git a/src/main.rs b/src/main.rs index 13db8a2..1d07e44 100644 --- a/src/main.rs +++ b/src/main.rs @@ -63,9 +63,10 @@ struct ViewPostTemplate { meta: PostMetadata, rendered: String, rendered_in: RenderStats, + markdown_access: bool, } -type AppResult = Result; +type AppResult = Result; #[derive(Error, Debug)] enum AppError { @@ -107,16 +108,27 @@ async fn index(State(state): State) -> AppResult { } async fn post(State(state): State, Path(name): Path) -> AppResult { - let post = state.posts.get_post(&name).await?; + if name.ends_with(".md") && state.config.markdown_access { + let mut file = tokio::fs::OpenOptions::new() + .read(true) + .open(state.config.posts_dir.join(&name)) + .await?; - let post = ViewPostTemplate { - meta: post.0, - rendered: post.1, - rendered_in: post.2, + let mut buf = Vec::new(); + file.read_to_end(&mut buf).await?; + + Ok(([("content-type", "text/plain")], buf).into_response()) + } else { + let post = state.posts.get_post(&name).await?; + let page = ViewPostTemplate { + meta: post.0, + rendered: post.1, + rendered_in: post.2, + markdown_access: state.config.markdown_access, + }; + + Ok(page.into_response()) } - .into_response(); - - Ok(post) } async fn all_posts(State(state): State) -> AppResult>> { diff --git a/src/markdown_render.rs b/src/markdown_render.rs index cc80677..e128fc7 100644 --- a/src/markdown_render.rs +++ b/src/markdown_render.rs @@ -3,7 +3,6 @@ use std::sync::{Arc, OnceLock, RwLock}; use comrak::markdown_to_html_with_plugins; use comrak::plugins::syntect::{SyntectAdapter, SyntectAdapterBuilder}; use comrak::ComrakOptions; -use comrak::Plugins; use comrak::RenderPlugins; use syntect::highlighting::ThemeSet; @@ -18,22 +17,22 @@ fn syntect_adapter(config: &RenderConfig) -> Arc { } fn build_syntect(config: &RenderConfig) -> Arc { - let mut theme_set = if config.syntect_load_defaults { + let mut theme_set = if config.syntect.load_defaults { ThemeSet::load_defaults() } else { ThemeSet::new() }; - if let Some(path) = config.syntect_themes_dir.as_ref() { + if let Some(path) = config.syntect.themes_dir.as_ref() { theme_set.add_from_folder(path).unwrap(); } let mut builder = SyntectAdapterBuilder::new().theme_set(theme_set); - if let Some(theme) = config.syntect_theme.as_ref() { + if let Some(theme) = config.syntect.theme.as_ref() { builder = builder.theme(theme); } Arc::new(builder.build()) } -pub fn render_with_config(markdown: &str, config: &RenderConfig, front_matter: bool) -> String { +pub fn render(markdown: &str, config: &RenderConfig, front_matter: bool) -> String { let mut options = ComrakOptions::default(); options.extension.table = true; options.extension.autolink = true; @@ -55,10 +54,5 @@ pub fn render_with_config(markdown: &str, config: &RenderConfig, front_matter: b .build() .unwrap(); - render(markdown, &options, &plugins) -} - -pub fn render(markdown: &str, options: &ComrakOptions, plugins: &Plugins) -> String { - // TODO: post-processing - markdown_to_html_with_plugins(markdown, options, plugins) + markdown_to_html_with_plugins(markdown, &options, &plugins) } diff --git a/src/post/cache.rs b/src/post/cache.rs index 328a6c7..3b4ef8b 100644 --- a/src/post/cache.rs +++ b/src/post/cache.rs @@ -134,7 +134,7 @@ impl<'de> Deserialize<'de> for Cache { type Value = Cache; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(formatter, "meow") + write!(formatter, "expected a map") } fn visit_map(self, mut map: A) -> Result diff --git a/src/post/mod.rs b/src/post/mod.rs index a315fbd..a833b95 100644 --- a/src/post/mod.rs +++ b/src/post/mod.rs @@ -13,7 +13,7 @@ use tokio::io::AsyncReadExt; use tracing::warn; use crate::config::RenderConfig; -use crate::markdown_render; +use crate::markdown_render::render; use crate::post::cache::Cache; use crate::PostError; @@ -117,7 +117,7 @@ impl PostManager { let parsing = parsing_start.elapsed(); let before_render = Instant::now(); - let rendered_markdown = markdown_render::render_with_config(body, &self.config, false); + let rendered_markdown = render(body, &self.config, false); let post = Post { meta: &metadata, rendered_markdown, diff --git a/templates/view_post.html b/templates/view_post.html index 819a480..e469da5 100644 --- a/templates/view_post.html +++ b/templates/view_post.html @@ -30,6 +30,9 @@ {% when RenderStats::Cached(total) %} retrieved from cache in {{ total|duration }} {% endmatch %} + {% if markdown_access %} + - view raw + {% endif %}