diff --git a/src/blog.rs b/src/blog.rs index 788f070..eb5049a 100644 --- a/src/blog.rs +++ b/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(" 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 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(), diff --git a/theme/default/post.hbs b/theme/default/post.hbs index f33e3ea..912142c 100644 --- a/theme/default/post.hbs +++ b/theme/default/post.hbs @@ -9,7 +9,7 @@
{{> sidebar.hbs }}
-

{{ title }}

+

{{ title }}

{{ format_date timestamp "%e %b, %Y" }} {{{ content }}}
diff --git a/theme/default/static/script.js b/theme/default/static/script.js index a4435d6..48a90ef 100644 --- a/theme/default/static/script.js +++ b/theme/default/static/script.js @@ -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"; -}; \ No newline at end of file +}; + +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("
    "); + } else if (openLevel < level) { + toc += (new Array(level - openLevel + 1)).join("
"); + } + + level = openLevel; + if (level > maxLevel) maxLevel = level; + + let anchor = elem.getAttribute("id"); + let titleText = elem.innerText; + toc += "
  • " + titleText + "
  • "; + }); + + if (level) { + toc += (new Array(level + 1)).join(""); + } + + 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); + } + } +} + +// +// 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]; +};