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:
25
docs/static/style.css
vendored
25
docs/static/style.css
vendored
@@ -193,6 +193,31 @@ h3 {
|
|||||||
margin: 1.5rem 0 0.75rem;
|
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 {
|
p {
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|||||||
4
docs/templates/base.html
vendored
4
docs/templates/base.html
vendored
@@ -41,7 +41,7 @@
|
|||||||
{% if page_path == item.path and page.toc and anchors %}
|
{% if page_path == item.path and page.toc and anchors %}
|
||||||
<div class="nav-anchors">
|
<div class="nav-anchors">
|
||||||
{% for anchor in anchors %}
|
{% for anchor in anchors %}
|
||||||
<a href="#{{ anchor.id }}" class="anchor-link level-{{ anchor.level }}">{{ anchor.label }}</a>
|
<a href="#{{ anchor.id }}" class="anchor-link level-{{ anchor.level }}">{{ anchor.label | strip_parens }}</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
{% if page_path == child.path and page.toc and anchors %}
|
{% if page_path == child.path and page.toc and anchors %}
|
||||||
<div class="nav-anchors">
|
<div class="nav-anchors">
|
||||||
{% for anchor in anchors %}
|
{% for anchor in anchors %}
|
||||||
<a href="#{{ anchor.id }}" class="anchor-link level-{{ anchor.level }}">{{ anchor.label }}</a>
|
<a href="#{{ anchor.id }}" class="anchor-link level-{{ anchor.level }}">{{ anchor.label | strip_parens }}</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -172,7 +172,11 @@ pub fn markdown_to_html(markdown: &str) -> (String, Vec<Anchor>) {
|
|||||||
let insert_pos = pos + format!("<h{}", level_num).len();
|
let insert_pos = pos + format!("<h{}", level_num).len();
|
||||||
html_output.insert_str(insert_pos, &format!(" id=\"{}\">", id));
|
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)
|
// Extract anchor for h2-h6 (skip h1)
|
||||||
if level_num >= 2 {
|
if level_num >= 2 {
|
||||||
@@ -339,7 +343,10 @@ mod tests {
|
|||||||
fn test_basic_markdown() {
|
fn test_basic_markdown() {
|
||||||
let md = "# Hello\n\nThis is a *test*.";
|
let md = "# Hello\n\nThis is a *test*.";
|
||||||
let (html, _) = markdown_to_html(md);
|
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>"));
|
assert!(html.contains("<em>test</em>"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
//! Tera-based template engine for runtime HTML generation.
|
//! Tera-based template engine for runtime HTML generation.
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use tera::{Context, Tera};
|
use tera::{Context, Tera, Value};
|
||||||
|
|
||||||
use crate::config::SiteConfig;
|
use crate::config::SiteConfig;
|
||||||
use crate::content::{Content, NavItem};
|
use crate::content::{Content, NavItem};
|
||||||
@@ -19,7 +20,11 @@ impl TemplateEngine {
|
|||||||
/// Load templates from a directory (glob pattern: `templates/**/*`).
|
/// Load templates from a directory (glob pattern: `templates/**/*`).
|
||||||
pub fn new(template_dir: &Path) -> Result<Self> {
|
pub fn new(template_dir: &Path) -> Result<Self> {
|
||||||
let pattern = template_dir.join("**/*").display().to_string();
|
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 })
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
Reference in New Issue
Block a user