A little app to serve my photography from my personal website

anchors in blog headings

ericwood.org 3d749f71 523fea6e

Waiting for spindle ...
+84 -6
+17
Cargo.lock
··· 1814 1814 ] 1815 1815 1816 1816 [[package]] 1817 + name = "deunicode" 1818 + version = "1.6.2" 1819 + source = "registry+https://github.com/rust-lang/crates.io-index" 1820 + checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04" 1821 + 1822 + [[package]] 1817 1823 name = "digest" 1818 1824 version = "0.10.7" 1819 1825 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3064 3070 "serde_html_form", 3065 3071 "serde_valid", 3066 3072 "serde_yaml", 3073 + "slug", 3067 3074 "sqlx", 3068 3075 "thiserror 2.0.17", 3069 3076 "tokio", ··· 3820 3827 version = "0.4.11" 3821 3828 source = "registry+https://github.com/rust-lang/crates.io-index" 3822 3829 checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" 3830 + 3831 + [[package]] 3832 + name = "slug" 3833 + version = "0.1.6" 3834 + source = "registry+https://github.com/rust-lang/crates.io-index" 3835 + checksum = "882a80f72ee45de3cc9a5afeb2da0331d58df69e4e7d8eeb5d3c7784ae67e724" 3836 + dependencies = [ 3837 + "deunicode", 3838 + "wasm-bindgen", 3839 + ] 3823 3840 3824 3841 [[package]] 3825 3842 name = "smallvec"
+1
Cargo.toml
··· 30 30 serde_yaml = "0.9.34" 31 31 comrak = "0.48.0" 32 32 arborium = { version = "2.3.2", features = ["all-languages"] } 33 + slug = "0.1.6"
+42 -6
src/blog.rs
··· 1 1 use std::{ 2 2 collections::HashMap, 3 + fmt::{self, Write}, 3 4 fs::{self, File, read_to_string}, 4 - io::{BufRead, BufReader, Write}, 5 + io::{BufRead, BufReader, Write as IoWrite}, 5 6 path::{Path, PathBuf}, 6 7 }; 7 8 8 9 use arborium::Highlighter; 9 - use comrak::{adapters::SyntaxHighlighterAdapter, markdown_to_html_with_plugins, options}; 10 + use comrak::{ 11 + adapters::{HeadingAdapter, HeadingMeta, SyntaxHighlighterAdapter}, 12 + markdown_to_html_with_plugins, 13 + nodes::Sourcepos, 14 + options, 15 + }; 10 16 use minijinja::context; 11 17 12 18 use crate::{AppState, config::Config, date_time::DateTime, templates::render}; ··· 98 104 .collect(); 99 105 100 106 for file_name in names { 101 - let mut slug = file_name.replace("_", "-"); 102 - slug.replace_last(".md", ""); 107 + let mut slug = slug::slugify(&file_name); 108 + slug.replace_last("-md", ""); 103 109 let path = root_path.join(file_name).clone(); 104 110 105 111 let maybe_post = extract_frontmatter(&path)?; ··· 143 149 Ok(Some(post)) 144 150 } 145 151 152 + struct LinkedHeadingAdapter; 153 + 154 + impl HeadingAdapter for LinkedHeadingAdapter { 155 + fn enter( 156 + &self, 157 + output: &mut dyn Write, 158 + heading: &HeadingMeta, 159 + _sourcepos: Option<Sourcepos>, 160 + ) -> fmt::Result { 161 + let id = slug::slugify(&heading.content); 162 + 163 + write!( 164 + output, 165 + "<div class=\"blog__heading\" id=\"{}\">\n<h{}>", 166 + id, heading.level 167 + ) 168 + } 169 + 170 + fn exit(&self, output: &mut dyn Write, heading: &HeadingMeta) -> fmt::Result { 171 + let id = slug::slugify(&heading.content); 172 + write!( 173 + output, 174 + "</h{}><a href=\"#{}\" aria-label=\"Permalink: {}\">#</a></div>", 175 + heading.level, id, heading.content 176 + ) 177 + } 178 + } 179 + 146 180 pub fn render_post(path: &PathBuf) -> anyhow::Result<String> { 147 181 let md = read_to_string(path)?; 148 - let adapter = SyntaxAdapter::new(); 182 + let syntax_adapter = SyntaxAdapter::new(); 183 + let heading_adapter = LinkedHeadingAdapter; 149 184 let mut plugins = options::Plugins::default(); 150 - plugins.render.codefence_syntax_highlighter = Some(&adapter); 185 + plugins.render.codefence_syntax_highlighter = Some(&syntax_adapter); 186 + plugins.render.heading_adapter = Some(&heading_adapter); 151 187 let body = markdown_to_html_with_plugins( 152 188 &md, 153 189 &comrak::Options {
+24
src/views/blog/show/style.css
··· 13 13 display: block; 14 14 } 15 15 16 + .blog__heading { 17 + position: relative; 18 + scroll-margin-top: 20px; 19 + } 20 + 21 + .blog__heading a { 22 + position: absolute; 23 + top: 50%; 24 + left: 0; 25 + transform: translateX(calc(-100% - 5px)) translateY(-50%); 26 + color: black; 27 + text-decoration: none; 28 + opacity: 0; 29 + transition: opacity 0.3s; 30 + } 31 + 32 + .blog__heading:hover a, 33 + .blog__heading:hover a, 34 + .blog__heading:hover a, 35 + .blog__heading:hover a, 36 + .blog__heading:hover a { 37 + opacity: 0.75; 38 + } 39 +