implement automatic anchor generation for headings
This commit is contained in:
parent
f36bfb7afa
commit
9b018b559d
57
src/blog.rs
57
src/blog.rs
|
@ -120,7 +120,7 @@ impl Post {
|
|||
// library updates. Updaing this value invalidates all
|
||||
// existing cache and they will be recompiled when someone
|
||||
// visits.
|
||||
const CACHE_VERSION: &'static str = "0003";
|
||||
const CACHE_VERSION: &'static str = "0007";
|
||||
|
||||
// The prefix path used for caching remote images
|
||||
pub const IMG_CACHE_PREFIX: &'static str = "/imgcache/";
|
||||
|
@ -203,6 +203,60 @@ impl PostContentCache {
|
|||
}
|
||||
}
|
||||
|
||||
// Do some HTML-level transformations to the compiled result
|
||||
// Because the Markdown parser doesn't always allow us to do
|
||||
// everything, like adding `id` attributes to tags
|
||||
fn transform_html(html: &mut String) {
|
||||
// Generate IDs for all headings in article
|
||||
// This allows navigation via the hash part of the URL
|
||||
let mut last_idx: usize = 0;
|
||||
loop {
|
||||
let len = html.len();
|
||||
|
||||
let idx = match (&html[last_idx..len]).find("<h") {
|
||||
Some(i) => i + last_idx,
|
||||
None => break,
|
||||
};
|
||||
|
||||
if idx >= len - 4 {
|
||||
break;
|
||||
}
|
||||
|
||||
last_idx = idx + 3;
|
||||
|
||||
if &html[idx + 3..idx + 4] != ">" {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Now we have found a <h*> tag
|
||||
let htype = &html[idx + 1..idx + 3];
|
||||
|
||||
// Find the closing tag for this one
|
||||
// Since it's generated by the Markdown engine,
|
||||
// we can assume it's correct HTML
|
||||
let end_idx = match (&html[idx + 3..len]).find(&format!("</{}>", htype)) {
|
||||
Some(i) => i + idx + 3,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
if end_idx >= len - 4 {
|
||||
break;
|
||||
}
|
||||
|
||||
let heading = &html[idx + 4..end_idx];
|
||||
// We also assume there should be no other HTML tags in the heading
|
||||
// This should be fine for me but I don't know about others
|
||||
// However it's really tedious to do anything better...
|
||||
let heading_anchor = filter_non_ascii_alphanumeric(
|
||||
&heading.to_lowercase()).replace(" ", "-");
|
||||
let inserted_id = format!(" id=\"{}\"", heading_anchor);
|
||||
|
||||
html.insert_str(idx + 3, &inserted_id);
|
||||
|
||||
last_idx = idx + 3 + inserted_id.len();
|
||||
}
|
||||
}
|
||||
|
||||
// Only renders the content and spits out a cache object
|
||||
// can be used to display the page or to write to cache
|
||||
// Despite the signature, this function BLOCKS
|
||||
|
@ -224,6 +278,7 @@ impl PostContentCache {
|
|||
}
|
||||
let mut html_output = String::new();
|
||||
html::push_html(&mut html_output, parser.into_iter());
|
||||
Self::transform_html(&mut html_output);
|
||||
PostContentCache {
|
||||
uuid: post.uuid.clone(),
|
||||
version: CACHE_VERSION.to_owned(),
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
<div class="page-wrapper">
|
||||
{{> sidebar.hbs }}
|
||||
<article class="content">
|
||||
<h1>{{ title }}</h1>
|
||||
<h1 id="paprika-post-title">{{ title }}</h1>
|
||||
<span class="date">{{ format_date timestamp "%e %b, %Y" }}</span>
|
||||
{{{ content }}}
|
||||
</article>
|
||||
|
|
|
@ -11,4 +11,114 @@ avatar.onanimationend = (ev) => {
|
|||
// Trigger progress bar when loading new page
|
||||
window.onbeforeunload = (ev) => {
|
||||
document.getElementsByClassName("loading-progress")[0].className = "loading-progress force-visible";
|
||||
};
|
||||
};
|
||||
|
||||
window.onload = function() {
|
||||
let content = document.getElementsByClassName("content");
|
||||
if (content.length == 0) return;
|
||||
|
||||
let level = 0;
|
||||
let maxLevel = 0;
|
||||
let toc = "";
|
||||
content[0].querySelectorAll("h1, h2, h3, h4, h5").forEach((elem) => {
|
||||
let openLevel = parseInt(elem.tagName.toLowerCase().replace("h", ""));
|
||||
if (openLevel > level) {
|
||||
toc += (new Array(openLevel - level + 1)).join("<ul>");
|
||||
} else if (openLevel < level) {
|
||||
toc += (new Array(level - openLevel + 1)).join("</ul>");
|
||||
}
|
||||
|
||||
level = openLevel;
|
||||
if (level > maxLevel) maxLevel = level;
|
||||
|
||||
let anchor = elem.getAttribute("id");
|
||||
let titleText = elem.innerText;
|
||||
toc += "<li><a href=\"#" + anchor + "\">" + titleText + "</a></li>";
|
||||
});
|
||||
|
||||
if (level) {
|
||||
toc += (new Array(level + 1)).join("</ul>");
|
||||
}
|
||||
|
||||
if (maxLevel > 1) {
|
||||
document.getElementsByClassName("toc")[0].innerHTML = toc;
|
||||
document.getElementsByClassName("toc-wrapper")[0].className = "toc-wrapper"; // remove hidden
|
||||
|
||||
// Get rid of ul layers that have only one ul child
|
||||
removeTrivialUlLayer(document.getElementsByClassName("toc")[0]);
|
||||
|
||||
var curAnchorLink = null;
|
||||
window.onscroll = (ev) => {
|
||||
let anchor = findClosestAnchor(content[0].querySelectorAll("h1, h2, h3, h4, h5"));
|
||||
let name = anchor.getAttribute("id");
|
||||
let tocLink = document.querySelector("a[href=\"#" + name + "\"").parentElement;
|
||||
if (tocLink != curAnchorLink) {
|
||||
tocLink.className = "current";
|
||||
if (curAnchorLink != null) {
|
||||
curAnchorLink.className = "";
|
||||
}
|
||||
curAnchorLink = tocLink;
|
||||
}
|
||||
};
|
||||
|
||||
window.onscroll();
|
||||
}
|
||||
};
|
||||
|
||||
function removeTrivialUlLayer(elem) {
|
||||
let children = elem.getElementsByTagName("ul");
|
||||
if (elem.childNodes.length == 1 && children.length == 1) {
|
||||
// Every child is a ul
|
||||
elem.innerHTML = children[0].innerHTML;
|
||||
removeTrivialUlLayer(elem);
|
||||
} else {
|
||||
for (const child of children) {
|
||||
removeTrivialUlLayer(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// <https://stackoverflow.com/questions/10642587/finding-closest-anchor-href-via-scrolloffset>
|
||||
// findPos : courtesy of @ppk - see http://www.quirksmode.org/js/findpos.html
|
||||
var findPos = function (obj) {
|
||||
var curleft = 0,
|
||||
curtop = 0;
|
||||
if (obj.offsetParent) {
|
||||
curleft = obj.offsetLeft;
|
||||
curtop = obj.offsetTop;
|
||||
while ((obj = obj.offsetParent)) {
|
||||
curleft += obj.offsetLeft;
|
||||
curtop += obj.offsetTop;
|
||||
}
|
||||
}
|
||||
return [curleft, curtop];
|
||||
};
|
||||
|
||||
var findClosestAnchor = function (anchors) {
|
||||
var sortByDistance = function (element1, element2) {
|
||||
var pos1 = findPos(element1),
|
||||
pos2 = findPos(element2);
|
||||
|
||||
// vect1 & vect2 represent 2d vectors going from the top left extremity of each element to the point positionned at the scrolled offset of the window
|
||||
var vect1 = [
|
||||
window.scrollX - pos1[0],
|
||||
window.scrollY - pos1[1]
|
||||
],
|
||||
vect2 = [
|
||||
window.scrollX - pos2[0],
|
||||
window.scrollY - pos2[1]
|
||||
];
|
||||
|
||||
// we compare the length of the vectors using only the sum of their components squared
|
||||
// no need to find the magnitude of each (this was inspired by Mageek’s answer)
|
||||
var sqDist1 = vect1[0] * vect1[0] + vect1[1] * vect1[1],
|
||||
sqDist2 = vect2[0] * vect2[0] + vect2[1] * vect2[1];
|
||||
|
||||
if (sqDist1 < sqDist2) return -1;
|
||||
else if (sqDist1 > sqDist2) return 1;
|
||||
else return 0;
|
||||
};
|
||||
|
||||
// Convert the nodelist to an array, then returns the first item of the elements sorted by distance
|
||||
return Array.prototype.slice.call(anchors).sort(sortByDistance)[0];
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue