A little app to serve my photography from my personal website

Scroll spy

+69 -9
+1
Cargo.lock
··· 3066 3066 "init-tracing-opentelemetry", 3067 3067 "minijinja", 3068 3068 "minijinja-autoreload", 3069 + "regex", 3069 3070 "serde", 3070 3071 "serde_html_form", 3071 3072 "serde_valid",
+1
Cargo.toml
··· 31 31 comrak = "0.48.0" 32 32 arborium = { version = "2.3.2", features = ["all-languages"] } 33 33 slug = "0.1.6" 34 + regex = "1.12.2"
+26 -1
src/blog.rs
··· 16 16 nodes::{AstNode, NodeValue, Sourcepos}, 17 17 options, parse_document, 18 18 }; 19 + use regex::Regex; 19 20 use serde::Serialize; 20 21 21 22 use crate::{ ··· 175 176 176 177 write!( 177 178 output, 178 - "<div class=\"blog__heading\" id=\"{}\">\n<h{}>", 179 + "</section>\n<section id=\"{}\" class=\"blog__section\"><div class=\"blog__heading\">\n<h{}>", 179 180 id, heading.level 180 181 ) 181 182 } ··· 248 249 toc.into() 249 250 } 250 251 252 + fn fix_sections(body: &str) -> String { 253 + let re = Regex::new(r"<\/?section>").unwrap(); 254 + let mut result = body.to_string(); 255 + for (i, m) in re.find_iter(body).enumerate() { 256 + if m.as_str() == "<section>" && i == 0 { 257 + break; 258 + } 259 + if m.as_str() == "</section>" && i == 0 { 260 + result = body.replacen("</section>", "", 1); 261 + break; 262 + } 263 + } 264 + 265 + let close = "</section>"; 266 + if let Some(i) = body.rfind("<section class=\"footnotes\"") { 267 + result.insert_str(i - close.len(), close); 268 + } else { 269 + result += close; 270 + } 271 + 272 + result 273 + } 274 + 251 275 pub fn render_post(path: &PathBuf) -> anyhow::Result<(String, Vec<Section>)> { 252 276 let md = read_to_string(path)?; 253 277 let syntax_adapter = SyntaxAdapter::new(); ··· 279 303 280 304 let mut body = String::new(); 281 305 format_document_with_plugins(root, &options, &mut body, &plugins)?; 306 + body = fix_sections(&body); 282 307 283 308 Ok((body, toc)) 284 309 }
+26
src/views/blog/show/script.js
··· 1 + const initToc = () => { 2 + const intersections = {}; 3 + 4 + document.querySelectorAll(".blog__section").forEach((el) => { 5 + intersections[el.id] = document.querySelector(`.blog__toc a[href="#${el.id}"]`); 6 + }); 7 + 8 + 9 + observer = new IntersectionObserver((entries) => { 10 + entries.forEach((entry) => { 11 + const id = entry.target.id; 12 + intersections[id].classList.toggle("active", entry.isIntersecting); 13 + }); 14 + }, 15 + { 16 + rootMargin: "-50% 0px", 17 + }); 18 + 19 + document.querySelectorAll(".blog__section").forEach((el) => { 20 + observer.observe(el); 21 + }); 22 + }; 23 + 24 + document.addEventListener("DOMContentLoaded", () => { 25 + initToc(); 26 + });
+15 -8
src/views/blog/show/template.jinja
··· 8 8 {% endif %} 9 9 {% endblock %} 10 10 {% block body %} 11 - <div class="blog__container"> 12 - <h1 class="blog__title">{{ post.title }}</h1> 13 - <p> 14 - <time datetime="{{ post.published_at }}">{{ post.published_at }}</time> 15 - </p> 16 - {{ toc_html }} 17 - <div class="blog__body"> 18 - {{ body }} 11 + <div class="blog__layout"> 12 + <div class="blog__header"> 13 + <h1 class="blog__title">{{ post.title }}</h1> 14 + <p> 15 + <time datetime="{{ post.published_at }}">{{ post.published_at }}</time> 16 + </p> 17 + </div> 18 + <div class="blog__container"> 19 + <div class="blog__body"> 20 + {{ body }} 21 + </div> 22 + <div class="blog__toc"> 23 + {{ toc_html }} 24 + </div> 19 25 </div> 20 26 </div> 27 + {{ inline_script("src/views/blog/show/script.js") }} 21 28 {% endblock %}