feat(nav): add anchor TOC in sidebar for toc-enabled pages
- Pass extracted anchors through template context - Render anchor links under active page when page.toc is true - Hierarchical indentation by heading level (h2-h6) - CSS styling: smaller font, muted color, border-left indicator - Add toc: true to templates.md as example
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
title: Tera Templates
|
title: Tera Templates
|
||||||
description: Customizable templates without recompilation
|
description: Customizable templates without recompilation
|
||||||
weight: 1
|
weight: 1
|
||||||
|
toc: true
|
||||||
---
|
---
|
||||||
|
|
||||||
sukr uses [Tera](https://tera.netlify.app/), a Jinja2-like templating engine. Templates are loaded at runtime, so you can modify them without recompiling sukr.
|
sukr uses [Tera](https://tera.netlify.app/), a Jinja2-like templating engine. Templates are loaded at runtime, so you can modify them without recompiling sukr.
|
||||||
|
|||||||
39
docs/static/style.css
vendored
39
docs/static/style.css
vendored
@@ -111,6 +111,45 @@ body {
|
|||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Anchor navigation (TOC in sidebar) */
|
||||||
|
.sidebar .nav-anchors {
|
||||||
|
margin-left: 1rem;
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
border-left: 1px solid var(--border);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.125rem;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .nav-anchors .anchor-link {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--fg-muted);
|
||||||
|
padding: 0.125rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .nav-anchors .anchor-link:hover {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Indent anchors by heading level */
|
||||||
|
.sidebar .nav-anchors .level-3 {
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .nav-anchors .level-4 {
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .nav-anchors .level-5 {
|
||||||
|
margin-left: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .nav-anchors .level-6 {
|
||||||
|
margin-left: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar-footer {
|
.sidebar-footer {
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
padding-top: 1rem;
|
padding-top: 1rem;
|
||||||
|
|||||||
14
docs/templates/base.html
vendored
14
docs/templates/base.html
vendored
@@ -38,11 +38,25 @@
|
|||||||
{% set section_prefix = item.path | replace(from="index.html", to="") %}
|
{% set section_prefix = item.path | replace(from="index.html", to="") %}
|
||||||
{% set is_current_section = page_path is starting_with(section_prefix) %}
|
{% set is_current_section = page_path is starting_with(section_prefix) %}
|
||||||
<a href="{{ prefix }}{{ item.path }}" {% if page_path==item.path %}class="active" {% endif %}>{{ item.label }}</a>
|
<a href="{{ prefix }}{{ item.path }}" {% if page_path==item.path %}class="active" {% endif %}>{{ item.label }}</a>
|
||||||
|
{% if page_path == item.path and page.toc and anchors %}
|
||||||
|
<div class="nav-anchors">
|
||||||
|
{% for anchor in anchors %}
|
||||||
|
<a href="#{{ anchor.id }}" class="anchor-link level-{{ anchor.level }}">{{ anchor.label }}</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% if config.nested_nav and item.children %}
|
{% if config.nested_nav and item.children %}
|
||||||
<div class="nav-children{% if is_current_section %} expanded{% endif %}">
|
<div class="nav-children{% if is_current_section %} expanded{% endif %}">
|
||||||
{% for child in item.children %}
|
{% for child in item.children %}
|
||||||
<a href="{{ prefix }}{{ child.path }}" {% if page_path==child.path %}class="active" {% endif %}>{{ child.label
|
<a href="{{ prefix }}{{ child.path }}" {% if page_path==child.path %}class="active" {% endif %}>{{ child.label
|
||||||
}}</a>
|
}}</a>
|
||||||
|
{% if page_path == child.path and page.toc and anchors %}
|
||||||
|
<div class="nav-anchors">
|
||||||
|
{% for anchor in anchors %}
|
||||||
|
<a href="#{{ anchor.id }}" class="anchor-link level-{{ anchor.level }}">{{ anchor.label }}</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
20
src/main.rs
20
src/main.rs
@@ -142,10 +142,16 @@ fn run(config_path: &Path) -> Result<()> {
|
|||||||
// Render individual content pages for all sections
|
// Render individual content pages for all sections
|
||||||
for item in &items {
|
for item in &items {
|
||||||
eprintln!(" processing: {}", item.slug);
|
eprintln!(" processing: {}", item.slug);
|
||||||
let (html_body, _anchors) = render::markdown_to_html(&item.body);
|
let (html_body, anchors) = render::markdown_to_html(&item.body);
|
||||||
let page_path = format!("/{}", item.output_path(&content_dir).display());
|
let page_path = format!("/{}", item.output_path(&content_dir).display());
|
||||||
let html =
|
let html = engine.render_content(
|
||||||
engine.render_content(item, &html_body, &page_path, &config, &manifest.nav)?;
|
item,
|
||||||
|
&html_body,
|
||||||
|
&page_path,
|
||||||
|
&config,
|
||||||
|
&manifest.nav,
|
||||||
|
&anchors,
|
||||||
|
)?;
|
||||||
write_output(&output_dir, &content_dir, item, html)?;
|
write_output(&output_dir, &content_dir, item, html)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,9 +265,10 @@ fn process_pages(
|
|||||||
eprintln!("processing: {}", path.display());
|
eprintln!("processing: {}", path.display());
|
||||||
|
|
||||||
let content = Content::from_path(&path, ContentKind::Page)?;
|
let content = Content::from_path(&path, ContentKind::Page)?;
|
||||||
let (html_body, _anchors) = render::markdown_to_html(&content.body);
|
let (html_body, anchors) = render::markdown_to_html(&content.body);
|
||||||
let page_path = format!("/{}", content.output_path(content_dir).display());
|
let page_path = format!("/{}", content.output_path(content_dir).display());
|
||||||
let html = engine.render_page(&content, &html_body, &page_path, config, nav)?;
|
let html =
|
||||||
|
engine.render_page(&content, &html_body, &page_path, config, nav, &anchors)?;
|
||||||
|
|
||||||
write_output(output_dir, content_dir, &content, html)?;
|
write_output(output_dir, content_dir, &content, html)?;
|
||||||
}
|
}
|
||||||
@@ -278,13 +285,14 @@ fn generate_homepage(
|
|||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
eprintln!("generating: homepage");
|
eprintln!("generating: homepage");
|
||||||
|
|
||||||
let (html_body, _anchors) = render::markdown_to_html(&manifest.homepage.body);
|
let (html_body, anchors) = render::markdown_to_html(&manifest.homepage.body);
|
||||||
let html = engine.render_page(
|
let html = engine.render_page(
|
||||||
&manifest.homepage,
|
&manifest.homepage,
|
||||||
&html_body,
|
&html_body,
|
||||||
"/index.html",
|
"/index.html",
|
||||||
config,
|
config,
|
||||||
&manifest.nav,
|
&manifest.nav,
|
||||||
|
&anchors,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let out_path = output_dir.join("index.html");
|
let out_path = output_dir.join("index.html");
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use tera::{Context, Tera};
|
|||||||
use crate::config::SiteConfig;
|
use crate::config::SiteConfig;
|
||||||
use crate::content::{Content, NavItem};
|
use crate::content::{Content, NavItem};
|
||||||
use crate::error::{Error, Result};
|
use crate::error::{Error, Result};
|
||||||
|
use crate::render::Anchor;
|
||||||
|
|
||||||
/// Runtime template engine wrapping Tera.
|
/// Runtime template engine wrapping Tera.
|
||||||
pub struct TemplateEngine {
|
pub struct TemplateEngine {
|
||||||
@@ -40,11 +41,13 @@ impl TemplateEngine {
|
|||||||
page_path: &str,
|
page_path: &str,
|
||||||
config: &SiteConfig,
|
config: &SiteConfig,
|
||||||
nav: &[NavItem],
|
nav: &[NavItem],
|
||||||
|
anchors: &[Anchor],
|
||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
let mut ctx = self.base_context(page_path, config, nav);
|
let mut ctx = self.base_context(page_path, config, nav);
|
||||||
ctx.insert("title", &content.frontmatter.title);
|
ctx.insert("title", &content.frontmatter.title);
|
||||||
ctx.insert("page", &FrontmatterContext::from(&content.frontmatter));
|
ctx.insert("page", &FrontmatterContext::from(&content.frontmatter));
|
||||||
ctx.insert("content", html_body);
|
ctx.insert("content", html_body);
|
||||||
|
ctx.insert("anchors", anchors);
|
||||||
self.render("page.html", &ctx)
|
self.render("page.html", &ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,6 +59,7 @@ impl TemplateEngine {
|
|||||||
page_path: &str,
|
page_path: &str,
|
||||||
config: &SiteConfig,
|
config: &SiteConfig,
|
||||||
nav: &[NavItem],
|
nav: &[NavItem],
|
||||||
|
anchors: &[Anchor],
|
||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
let template = content
|
let template = content
|
||||||
.frontmatter
|
.frontmatter
|
||||||
@@ -66,6 +70,7 @@ impl TemplateEngine {
|
|||||||
ctx.insert("title", &content.frontmatter.title);
|
ctx.insert("title", &content.frontmatter.title);
|
||||||
ctx.insert("page", &FrontmatterContext::from(&content.frontmatter));
|
ctx.insert("page", &FrontmatterContext::from(&content.frontmatter));
|
||||||
ctx.insert("content", html_body);
|
ctx.insert("content", html_body);
|
||||||
|
ctx.insert("anchors", anchors);
|
||||||
self.render(template, &ctx)
|
self.render(template, &ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,6 +150,8 @@ pub struct FrontmatterContext {
|
|||||||
pub tags: Vec<String>,
|
pub tags: Vec<String>,
|
||||||
pub weight: Option<i64>,
|
pub weight: Option<i64>,
|
||||||
pub link_to: Option<String>,
|
pub link_to: Option<String>,
|
||||||
|
/// Enable table of contents (anchor nav in sidebar)
|
||||||
|
pub toc: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&crate::content::Frontmatter> for FrontmatterContext {
|
impl From<&crate::content::Frontmatter> for FrontmatterContext {
|
||||||
@@ -156,6 +163,7 @@ impl From<&crate::content::Frontmatter> for FrontmatterContext {
|
|||||||
tags: fm.tags.clone(),
|
tags: fm.tags.clone(),
|
||||||
weight: fm.weight,
|
weight: fm.weight,
|
||||||
link_to: fm.link_to.clone(),
|
link_to: fm.link_to.clone(),
|
||||||
|
toc: fm.toc.unwrap_or(false),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user