feat(ux): add hover-reveal ¶ anchors, strip parens from nav

- Add pilcrow (¶) anchor link after heading text for deep-linking
- CSS: hidden by default, visible on heading hover, user-overridable
- Add strip_parens Tera filter for cleaner nav anchor labels
- Update test expectations for new heading format
This commit is contained in:
Timothy DeHerrera
2026-02-01 09:57:47 -07:00
parent fc9e75d687
commit ce8a9e8f00
4 changed files with 60 additions and 6 deletions

View File

@@ -172,7 +172,11 @@ pub fn markdown_to_html(markdown: &str) -> (String, Vec<Anchor>) {
let insert_pos = pos + format!("<h{}", level_num).len();
html_output.insert_str(insert_pos, &format!(" id=\"{}\">", id));
}
html_output.push_str(&format!("</h{}>\n", level_num));
// Add pilcrow anchor link for deep-linking (hover-reveal via CSS)
html_output.push_str(&format!(
"<a class=\"heading-anchor\" href=\"#{}\">¶</a></h{}>\n",
id, level_num
));
// Extract anchor for h2-h6 (skip h1)
if level_num >= 2 {
@@ -339,7 +343,10 @@ mod tests {
fn test_basic_markdown() {
let md = "# Hello\n\nThis is a *test*.";
let (html, _) = markdown_to_html(md);
assert!(html.contains("<h1 id=\"hello\">Hello</h1>"));
// Heading includes pilcrow anchor for deep-linking
assert!(html.contains(
"<h1 id=\"hello\">Hello<a class=\"heading-anchor\" href=\"#hello\">¶</a></h1>"
));
assert!(html.contains("<em>test</em>"));
}

View File

@@ -1,9 +1,10 @@
//! Tera-based template engine for runtime HTML generation.
use std::collections::HashMap;
use std::path::Path;
use serde::Serialize;
use tera::{Context, Tera};
use tera::{Context, Tera, Value};
use crate::config::SiteConfig;
use crate::content::{Content, NavItem};
@@ -19,7 +20,11 @@ impl TemplateEngine {
/// Load templates from a directory (glob pattern: `templates/**/*`).
pub fn new(template_dir: &Path) -> Result<Self> {
let pattern = template_dir.join("**/*").display().to_string();
let tera = Tera::new(&pattern).map_err(Error::TemplateLoad)?;
let mut tera = Tera::new(&pattern).map_err(Error::TemplateLoad)?;
// Register custom filters
tera.register_filter("strip_parens", strip_parens_filter);
Ok(Self { tera })
}
@@ -198,6 +203,23 @@ impl ContentContext {
}
}
/// Tera filter to strip parenthetical text from strings.
/// E.g., "Content (in Section)" → "Content"
fn strip_parens_filter(value: &Value, _args: &HashMap<String, Value>) -> tera::Result<Value> {
match value {
Value::String(s) => {
// Find opening paren and trim everything from there
let result = if let Some(pos) = s.find('(') {
s[..pos].trim().to_string()
} else {
s.clone()
};
Ok(Value::String(result))
}
_ => Ok(value.clone()),
}
}
#[cfg(test)]
mod tests {
use super::*;