diff --git a/docs/static/style.css b/docs/static/style.css
index 9ad2c87..a1106ec 100644
--- a/docs/static/style.css
+++ b/docs/static/style.css
@@ -193,6 +193,31 @@ h3 {
margin: 1.5rem 0 0.75rem;
}
+/* Heading anchor links (¶) - hover reveal for deep-linking */
+.heading-anchor {
+ visibility: hidden;
+ margin-left: 0.5em;
+ font-size: 0.75em;
+ color: var(--fg-muted);
+ text-decoration: none;
+ opacity: 0.5;
+}
+
+.heading-anchor:hover {
+ color: var(--accent);
+ opacity: 1;
+}
+
+/* Show anchor on heading hover */
+h1:hover .heading-anchor,
+h2:hover .heading-anchor,
+h3:hover .heading-anchor,
+h4:hover .heading-anchor,
+h5:hover .heading-anchor,
+h6:hover .heading-anchor {
+ visibility: visible;
+}
+
p {
margin-bottom: 1rem;
}
diff --git a/docs/templates/base.html b/docs/templates/base.html
index d6b679e..53886e7 100644
--- a/docs/templates/base.html
+++ b/docs/templates/base.html
@@ -41,7 +41,7 @@
{% if page_path == item.path and page.toc and anchors %}
{% endif %}
@@ -53,7 +53,7 @@
{% if page_path == child.path and page.toc and anchors %}
{% endif %}
diff --git a/src/render.rs b/src/render.rs
index 56e8040..f6fdd51 100644
--- a/src/render.rs
+++ b/src/render.rs
@@ -172,7 +172,11 @@ pub fn markdown_to_html(markdown: &str) -> (String, Vec) {
let insert_pos = pos + format!("", id));
}
- html_output.push_str(&format!("\n", level_num));
+ // Add pilcrow anchor link for deep-linking (hover-reveal via CSS)
+ html_output.push_str(&format!(
+ "¶\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("Hello
"));
+ // Heading includes pilcrow anchor for deep-linking
+ assert!(html.contains(
+ "Hello¶
"
+ ));
assert!(html.contains("test"));
}
diff --git a/src/template_engine.rs b/src/template_engine.rs
index 41b675d..03fd020 100644
--- a/src/template_engine.rs
+++ b/src/template_engine.rs
@@ -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 {
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) -> tera::Result {
+ 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::*;