WIP: ToC implementation #69
2 changed files with 99 additions and 9 deletions
|
@ -1,8 +1,10 @@
|
||||||
use crate::misc::date::Date;
|
use crate::misc::date::Date;
|
||||||
use base64::engine::general_purpose;
|
use base64::engine::general_purpose;
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use comrak::nodes::{AstNode, NodeValue};
|
use comrak::nodes::{AstNode, NodeCode, NodeMath, NodeValue};
|
||||||
use comrak::{format_html, parse_document, Arena, ComrakOptions, ListStyleType, Options};
|
use comrak::{
|
||||||
|
format_html, parse_document, Anchorizer, Arena, ComrakOptions, ListStyleType, Options,
|
||||||
|
};
|
||||||
use lol_html::html_content::ContentType;
|
use lol_html::html_content::ContentType;
|
||||||
use lol_html::{element, rewrite_str, HtmlRewriter, RewriteStrSettings, Settings};
|
use lol_html::{element, rewrite_str, HtmlRewriter, RewriteStrSettings, Settings};
|
||||||
use ramhorns::Content;
|
use ramhorns::Content;
|
||||||
|
@ -19,7 +21,7 @@ pub struct FileMetadataBlog {
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
pub publish: Option<bool>,
|
pub publish: Option<bool>,
|
||||||
pub tags: Option<Vec<Tag>>,
|
pub tags: Option<Vec<Tag>>,
|
||||||
pub toc: Option<bool>,
|
pub toc: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A tag, related to post blog
|
/// A tag, related to post blog
|
||||||
|
@ -338,10 +340,15 @@ pub fn get_metadata<'a>(root: &'a AstNode<'a>, mtype: &TypeFileMetadata) -> File
|
||||||
.find_map(|node| match &node.data.borrow().value {
|
.find_map(|node| match &node.data.borrow().value {
|
||||||
// Extract metadata from frontmatter
|
// Extract metadata from frontmatter
|
||||||
NodeValue::FrontMatter(text) => Some(match mtype {
|
NodeValue::FrontMatter(text) => Some(match mtype {
|
||||||
TypeFileMetadata::Blog => FileMetadata {
|
TypeFileMetadata::Blog => {
|
||||||
blog: Some(deserialize_metadata(text)),
|
let mut metadata: FileMetadataBlog = deserialize_metadata(text);
|
||||||
|
metadata.toc = toc_to_html(&generate_toc(root));
|
||||||
|
|
||||||
|
FileMetadata {
|
||||||
|
blog: Some(metadata),
|
||||||
..FileMetadata::default()
|
..FileMetadata::default()
|
||||||
},
|
}
|
||||||
|
}
|
||||||
TypeFileMetadata::Contact => {
|
TypeFileMetadata::Contact => {
|
||||||
let mut metadata: FileMetadataContact = deserialize_metadata(text);
|
let mut metadata: FileMetadataContact = deserialize_metadata(text);
|
||||||
|
|
||||||
|
@ -508,3 +515,87 @@ fn mail_obfuscation(html: &str) -> (String, bool) {
|
||||||
(new_html, modified)
|
(new_html, modified)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct TOCEntry {
|
||||||
|
id: String,
|
||||||
|
title: String,
|
||||||
|
depth: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_toc<'a>(root: &'a AstNode<'a>) -> Vec<TOCEntry> {
|
||||||
|
/// See <https://github.com/kivikakk/comrak/blob/b67d406d3b101b93539c37a1ca75bff81ff8c149/src/html.rs#L446>
|
||||||
|
fn collect_text<'a>(node: &'a AstNode<'a>, output: &mut String) {
|
||||||
|
match node.data.borrow().value {
|
||||||
|
NodeValue::Text(ref literal)
|
||||||
|
| NodeValue::Code(NodeCode { ref literal, .. })
|
||||||
|
| NodeValue::Math(NodeMath { ref literal, .. }) => {
|
||||||
|
*output = literal.to_string();
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
for n in node.children() {
|
||||||
|
if !output.is_empty() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
collect_text(n, output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut toc = vec![];
|
||||||
|
|
||||||
|
let mut anchorizer = Anchorizer::new();
|
||||||
|
|
||||||
|
// Collect headings first to avoid mutable borrow conflicts
|
||||||
|
let headings: Vec<_> = root
|
||||||
|
.children()
|
||||||
|
.filter_map(|node| {
|
||||||
|
if let NodeValue::Heading(ref nch) = &node.data.borrow().value {
|
||||||
|
Some((*nch, node))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Now process each heading
|
||||||
|
for (nch, node) in headings {
|
||||||
|
let mut title = String::with_capacity(20);
|
||||||
|
collect_text(node, &mut title);
|
||||||
|
|
||||||
|
toc.push(TOCEntry {
|
||||||
|
id: anchorizer.anchorize(title.clone()),
|
||||||
|
title,
|
||||||
|
depth: nch.level,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
toc
|
||||||
|
}
|
||||||
|
|
||||||
|
fn toc_to_html(toc: &[TOCEntry]) -> Option<String> {
|
||||||
|
if toc.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut html = Vec::with_capacity(20 + 20 * toc.len());
|
||||||
|
|
||||||
|
html.extend_from_slice(b"<ul>");
|
||||||
|
|
||||||
|
for entry in toc {
|
||||||
|
// TODO: Use depth
|
||||||
|
html.extend_from_slice(
|
||||||
|
format!(
|
||||||
|
"<li><a href=\"{}\">{} (dbg/depth/{})</a></li>",
|
||||||
|
entry.id, entry.title, entry.depth
|
||||||
|
)
|
||||||
|
.as_bytes(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.extend_from_slice(b"</ul>");
|
||||||
|
|
||||||
|
Some(String::from_utf8(html).unwrap())
|
||||||
|
}
|
||||||
|
|
|
@ -255,8 +255,7 @@ fn get_post(
|
||||||
None => default.2,
|
None => default.2,
|
||||||
},
|
},
|
||||||
match &data.metadata.info.blog.as_ref().unwrap().toc {
|
match &data.metadata.info.blog.as_ref().unwrap().toc {
|
||||||
// TODO: Generate TOC
|
Some(toc) => toc.into(),
|
||||||
Some(true) => String::new(),
|
|
||||||
_ => default.3,
|
_ => default.3,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
Loading…
Reference in a new issue