markdown_render.rs, post/mod.rs: remove unnecessary function and rename render_with_config

main.rs, config.rs, view_post.html: add markdown_access
main.rs: change AppError to PostError
cache.rs: replace development string
config.rs: put syntect options in their own struct
view_post.html: add markdown_access
README.md: write the readme
This commit is contained in:
slonkazoid 2024-04-19 22:41:14 +03:00
parent c56a182d14
commit 2b3f935a98
Signed by: slonk
SSH key fingerprint: SHA256:tbZfJX4IOvZ0LGWOWu5Ijo8jfMPi78TU7x1VoEeCIjM
9 changed files with 165 additions and 49 deletions

18
Cargo.lock generated
View file

@ -369,7 +369,6 @@ dependencies = [
"comrak", "comrak",
"console-subscriber", "console-subscriber",
"fronma", "fronma",
"futures-util",
"notify", "notify",
"scc", "scc",
"serde", "serde",
@ -844,17 +843,6 @@ version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" 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]] [[package]]
name = "futures-sink" name = "futures-sink"
version = "0.3.30" version = "0.3.30"
@ -874,11 +862,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-macro",
"futures-task", "futures-task",
"pin-project-lite", "pin-project-lite",
"pin-utils", "pin-utils",
"slab",
] ]
[[package]] [[package]]
@ -2126,9 +2112,9 @@ dependencies = [
[[package]] [[package]]
name = "toml_edit" name = "toml_edit"
version = "0.22.9" version = "0.22.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e40bb779c5187258fd7aad0eb68cb8706a0a81fa712fbea808ab43c4b8374c4" checksum = "d3328d4f68a705b2a4498da1d580585d39a6510f98318a2cec3018a7ec61ddef"
dependencies = [ dependencies = [
"indexmap 2.2.6", "indexmap 2.2.6",
"serde", "serde",

View file

@ -32,7 +32,6 @@ color-eyre = "0.6.3"
comrak = { version = "0.22.0", features = ["syntect"] } comrak = { version = "0.22.0", features = ["syntect"] }
console-subscriber = { version = "0.2.0", optional = true } console-subscriber = { version = "0.2.0", optional = true }
fronma = "0.2.0" fronma = "0.2.0"
futures-util = "0.3.30"
notify = "6.1.1" notify = "6.1.1"
scc = "2.1.0" scc = "2.1.0"
serde = { version = "1.0.197", features = ["derive"] } serde = { version = "1.0.197", features = ["derive"] }

119
README.md
View file

@ -2,7 +2,7 @@
title: "README" title: "README"
description: "the README.md file of this project" description: "the README.md file of this project"
author: "slonkazoid" author: "slonkazoid"
created_at: 2024-04-18T01:15:26Z created_at: 2024-04-18T04:15:26+03:00
--- ---
# bingus-blog # bingus-blog
@ -11,10 +11,123 @@ blazingly fast markdown blog software written in rust memory safe
## TODO ## TODO
- [ ] finish writing this document - [ ] RSS
- [ ] document config - [x] finish writing this document
- [x] document config
- [ ] extend syntect options - [ ] extend syntect options
- [ ] general cleanup of code - [ ] 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] be blazingly fast
- [x] 100+ MiB binary size - [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/<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/<name>`: view a post
- `GET /posts/<name>.md`: view the raw markdown of a post
- `GET /post/*`: redirects to `/posts/*`

View file

@ -9,12 +9,17 @@ use serde::{Deserialize, Serialize};
use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tracing::{error, info}; use tracing::{error, info};
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
pub struct SyntectConfig {
pub load_defaults: bool,
pub themes_dir: Option<PathBuf>,
pub theme: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
#[serde(default)] #[serde(default)]
pub struct RenderConfig { pub struct RenderConfig {
pub syntect_load_defaults: bool, pub syntect: SyntectConfig,
pub syntect_themes_dir: Option<PathBuf>,
pub syntect_theme: Option<String>,
} }
#[cfg(feature = "precompression")] #[cfg(feature = "precompression")]
@ -37,6 +42,7 @@ pub struct Config {
#[cfg(feature = "precompression")] #[cfg(feature = "precompression")]
pub precompression: PrecompressionConfig, pub precompression: PrecompressionConfig,
pub cache_file: Option<PathBuf>, pub cache_file: Option<PathBuf>,
pub markdown_access: bool,
} }
impl Default for Config { impl Default for Config {
@ -51,6 +57,7 @@ impl Default for Config {
#[cfg(feature = "precompression")] #[cfg(feature = "precompression")]
precompression: Default::default(), precompression: Default::default(),
cache_file: None, cache_file: None,
markdown_access: true,
} }
} }
} }
@ -58,9 +65,11 @@ impl Default for Config {
impl Default for RenderConfig { impl Default for RenderConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {
syntect_load_defaults: false, syntect: SyntectConfig {
syntect_themes_dir: Some("themes".into()), load_defaults: false,
syntect_theme: Some("Catppuccin Mocha".into()), themes_dir: Some("themes".into()),
theme: Some("Catppuccin Mocha".into()),
},
} }
} }
} }

View file

@ -63,9 +63,10 @@ struct ViewPostTemplate {
meta: PostMetadata, meta: PostMetadata,
rendered: String, rendered: String,
rendered_in: RenderStats, rendered_in: RenderStats,
markdown_access: bool,
} }
type AppResult<T> = Result<T, AppError>; type AppResult<T> = Result<T, PostError>;
#[derive(Error, Debug)] #[derive(Error, Debug)]
enum AppError { enum AppError {
@ -107,16 +108,27 @@ async fn index(State(state): State<ArcState>) -> AppResult<IndexTemplate> {
} }
async fn post(State(state): State<ArcState>, Path(name): Path<String>) -> AppResult<Response> { async fn post(State(state): State<ArcState>, Path(name): Path<String>) -> AppResult<Response> {
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 { let mut buf = Vec::new();
meta: post.0, file.read_to_end(&mut buf).await?;
rendered: post.1,
rendered_in: post.2, 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<ArcState>) -> AppResult<Json<Vec<PostMetadata>>> { async fn all_posts(State(state): State<ArcState>) -> AppResult<Json<Vec<PostMetadata>>> {

View file

@ -3,7 +3,6 @@ use std::sync::{Arc, OnceLock, RwLock};
use comrak::markdown_to_html_with_plugins; use comrak::markdown_to_html_with_plugins;
use comrak::plugins::syntect::{SyntectAdapter, SyntectAdapterBuilder}; use comrak::plugins::syntect::{SyntectAdapter, SyntectAdapterBuilder};
use comrak::ComrakOptions; use comrak::ComrakOptions;
use comrak::Plugins;
use comrak::RenderPlugins; use comrak::RenderPlugins;
use syntect::highlighting::ThemeSet; use syntect::highlighting::ThemeSet;
@ -18,22 +17,22 @@ fn syntect_adapter(config: &RenderConfig) -> Arc<SyntectAdapter> {
} }
fn build_syntect(config: &RenderConfig) -> Arc<SyntectAdapter> { fn build_syntect(config: &RenderConfig) -> Arc<SyntectAdapter> {
let mut theme_set = if config.syntect_load_defaults { let mut theme_set = if config.syntect.load_defaults {
ThemeSet::load_defaults() ThemeSet::load_defaults()
} else { } else {
ThemeSet::new() 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(); theme_set.add_from_folder(path).unwrap();
} }
let mut builder = SyntectAdapterBuilder::new().theme_set(theme_set); 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); builder = builder.theme(theme);
} }
Arc::new(builder.build()) 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(); let mut options = ComrakOptions::default();
options.extension.table = true; options.extension.table = true;
options.extension.autolink = true; options.extension.autolink = true;
@ -55,10 +54,5 @@ pub fn render_with_config(markdown: &str, config: &RenderConfig, front_matter: b
.build() .build()
.unwrap(); .unwrap();
render(markdown, &options, &plugins) markdown_to_html_with_plugins(markdown, &options, &plugins)
}
pub fn render(markdown: &str, options: &ComrakOptions, plugins: &Plugins) -> String {
// TODO: post-processing
markdown_to_html_with_plugins(markdown, options, plugins)
} }

View file

@ -134,7 +134,7 @@ impl<'de> Deserialize<'de> for Cache {
type Value = Cache; type Value = Cache;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(formatter, "meow") write!(formatter, "expected a map")
} }
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error> fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>

View file

@ -13,7 +13,7 @@ use tokio::io::AsyncReadExt;
use tracing::warn; use tracing::warn;
use crate::config::RenderConfig; use crate::config::RenderConfig;
use crate::markdown_render; use crate::markdown_render::render;
use crate::post::cache::Cache; use crate::post::cache::Cache;
use crate::PostError; use crate::PostError;
@ -117,7 +117,7 @@ impl PostManager {
let parsing = parsing_start.elapsed(); let parsing = parsing_start.elapsed();
let before_render = Instant::now(); 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 { let post = Post {
meta: &metadata, meta: &metadata,
rendered_markdown, rendered_markdown,

View file

@ -30,6 +30,9 @@
{% when RenderStats::Cached(total) %} {% when RenderStats::Cached(total) %}
retrieved from cache in {{ total|duration }} retrieved from cache in {{ total|duration }}
{% endmatch %} {% endmatch %}
{% if markdown_access %}
- <a href="/posts/{{ meta.name }}.md">view raw</a>
{% endif %}
</footer> </footer>
</body> </body>
</html> </html>