From beb93f2b0a149afe02d802cb742cf07942ad4aee Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Sun, 8 May 2022 14:10:29 +0200 Subject: [PATCH] Expose backlinks --- CHANGELOG.md | 3 + components/content/src/library.rs | 79 ++++++++++++++++--- components/content/src/ser.rs | 32 ++++++++ components/markdown/tests/common.rs | 2 +- components/site/src/lib.rs | 4 + components/templates/src/filters.rs | 19 ++--- docs/content/documentation/content/section.md | 14 ++-- .../documentation/templates/pages-sections.md | 12 +-- 8 files changed, 125 insertions(+), 40 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5151df51..ccf50b8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ - Switch to pulldown-cmark anchor rather than ours, some (very niche) edge cases are not supported anymore, you can also specify classes on headers now - Now outputs empty taxonomies instead of ignoring them +- Unify all sorting variable names in templates to `lower`/`higher` in order to make it easy to re-use templates and it +was becoming hard to come up with names ### Other - Fix markup for fenced code with linenos @@ -20,6 +22,7 @@ any pages related to that taxonomy - Serialize `transparent` field from front-matter of sections - Use Zola Tera instance for markdown filter: this means you have access to the same Tera functions as in shortcodes - Ignore sections with `render=false` when looking for path collisions +- Add support for backlinks ## 0.15.3 (2022-01-23) diff --git a/components/content/src/library.rs b/components/content/src/library.rs index 67875e5e..aa433966 100644 --- a/components/content/src/library.rs +++ b/components/content/src/library.rs @@ -8,6 +8,20 @@ use crate::sorting::sort_pages; use crate::taxonomies::{Taxonomy, TaxonomyFound}; use crate::{Page, Section, SortBy}; +macro_rules! set { + ($($key:expr,)+) => (set!($($key),+)); + + ( $($key:expr),* ) => { + { + let mut _set = AHashSet::new(); + $( + _set.insert($key); + )* + _set + } + }; +} + #[derive(Debug, Default)] pub struct Library { pub pages: AHashMap, @@ -15,8 +29,10 @@ pub struct Library { // aliases -> files, so we can easily check for conflicts pub reverse_aliases: AHashMap>, pub translations: AHashMap>, + pub backlinks: AHashMap>, // A mapping of {lang -> vec}>>} taxonomies_def: AHashMap>>>, + // All the taxonomies from config.toml in their slugifiedv ersion // So we don't need to pass the Config when adding a page to know how to slugify and we only // slugify once taxo_name_to_slug: AHashMap, @@ -44,11 +60,7 @@ impl Library { .and_modify(|s| { s.insert(file_path.to_path_buf()); }) - .or_insert_with(|| { - let mut s = AHashSet::new(); - s.insert(file_path.to_path_buf()); - s - }); + .or_insert_with(|| set! {file_path.to_path_buf()}); } } @@ -105,6 +117,33 @@ impl Library { self.sections.insert(file_path, section); } + /// Fills a map of target -> {content mentioning it} + /// This can only be called _after_ rendering markdown as we need to have accumulated all + /// the links first + pub fn fill_backlinks(&mut self) { + self.backlinks.clear(); + + let mut add_backlink = |target: &str, source: &Path| { + self.backlinks + .entry(target.to_owned()) + .and_modify(|s| { + s.insert(source.to_path_buf()); + }) + .or_insert(set! {source.to_path_buf()}); + }; + + for (_, page) in &self.pages { + for (internal_link, _) in &page.internal_links { + add_backlink(internal_link, &page.file.path); + } + } + for (_, section) in &self.sections { + for (internal_link, _) in §ion.internal_links { + add_backlink(internal_link, §ion.file.path); + } + } + } + /// This is called _before_ rendering the markdown the pages/sections pub fn find_taxonomies(&self, config: &Config) -> Vec { let mut taxonomies = Vec::new(); @@ -179,11 +218,7 @@ impl Library { .and_modify(|trans| { trans.insert(path.to_path_buf()); }) - .or_insert({ - let mut s = AHashSet::new(); - s.insert(path.to_path_buf()); - s - }); + .or_insert(set! {path.to_path_buf()}); } }; @@ -720,4 +755,28 @@ mod tests { // under the safe slugify strategy all terms should be distinct assert_eq!(tax.items.len(), 4); } + + #[test] + fn can_fill_backlinks() { + let mut page1 = create_page("page1.md", "en", PageSort::None); + page1.internal_links.push(("page2.md".to_owned(), None)); + let mut page2 = create_page("page2.md", "en", PageSort::None); + page2.internal_links.push(("_index.md".to_owned(), None)); + let mut section1 = create_section("_index.md", "en", 10, false, SortBy::None); + section1.internal_links.push(("page1.md".to_owned(), None)); + section1.internal_links.push(("page2.md".to_owned(), None)); + let mut library = Library::default(); + library.insert_page(page1); + library.insert_page(page2); + library.insert_section(section1); + library.fill_backlinks(); + + assert_eq!(library.backlinks.len(), 3); + assert_eq!(library.backlinks["page1.md"], set! {"_index.md".to_owned()}); + assert_eq!( + library.backlinks["page2.md"], + set! {"page1.md".to_owned(), "_index.md".to_owned()} + ); + assert_eq!(library.backlinks["_index.md"], set! {"page2.md".to_owned()}); + } } diff --git a/components/content/src/ser.rs b/components/content/src/ser.rs index 021b901d..0afb638b 100644 --- a/components/content/src/ser.rs +++ b/components/content/src/ser.rs @@ -8,6 +8,12 @@ use crate::{Page, Section}; use libs::tera::{Map, Value}; use utils::table_of_contents::Heading; +#[derive(Clone, Debug, PartialEq, Serialize)] +pub struct BackLink<'a> { + pub permalink: &'a str, + pub title: &'a Option, +} + #[derive(Clone, Debug, PartialEq, Serialize)] pub struct TranslatedContent<'a> { pub lang: &'a str, @@ -17,6 +23,22 @@ pub struct TranslatedContent<'a> { pub path: &'a Path, } +fn find_backlinks<'a>(relative_path: &str, library: &'a Library) -> Vec> { + let mut backlinks = Vec::new(); + if let Some(b) = library.backlinks.get(relative_path) { + for backlink in b { + if let Some(p) = library.pages.get(backlink) { + backlinks.push(BackLink { permalink: &p.permalink, title: &p.meta.title }); + } + if let Some(s) = library.sections.get(backlink) { + backlinks.push(BackLink { permalink: &s.permalink, title: &s.meta.title }); + } + } + backlinks.sort_by_key(|b| b.permalink); + } + backlinks +} + #[derive(Clone, Debug, PartialEq, Serialize)] pub struct SerializingPage<'a> { relative_path: &'a str, @@ -45,6 +67,7 @@ pub struct SerializingPage<'a> { lower: Option>>, higher: Option>>, translations: Vec>, + backlinks: Vec>, } impl<'a> SerializingPage<'a> { @@ -60,6 +83,7 @@ impl<'a> SerializingPage<'a> { let mut lower = None; let mut higher = None; let mut translations = vec![]; + let mut backlinks = vec![]; if let Some(lib) = library { translations = lib.find_translations(&page.file.canonical); @@ -74,6 +98,8 @@ impl<'a> SerializingPage<'a> { .as_ref() .map(|p| Box::new(Self::new(&lib.pages[p], Some(lib), false))); } + + backlinks = find_backlinks(&page.file.relative, &lib); } Self { @@ -103,6 +129,7 @@ impl<'a> SerializingPage<'a> { lower, higher, translations, + backlinks, } } } @@ -127,6 +154,7 @@ pub struct SerializingSection<'a> { pages: Vec>, subsections: Vec<&'a str>, translations: Vec>, + backlinks: Vec>, } #[derive(Debug)] @@ -145,6 +173,7 @@ impl<'a> SerializingSection<'a> { let mut pages = Vec::with_capacity(section.pages.len()); let mut subsections = Vec::with_capacity(section.subsections.len()); let mut translations = Vec::new(); + let mut backlinks = Vec::new(); match mode { SectionSerMode::ForMarkdown => {} @@ -162,6 +191,8 @@ impl<'a> SerializingSection<'a> { pages.push(SerializingPage::new(&lib.pages[p], Some(lib), true)); } } + + backlinks = find_backlinks(§ion.file.relative, &lib); } } @@ -184,6 +215,7 @@ impl<'a> SerializingSection<'a> { pages, subsections, translations, + backlinks, } } } diff --git a/components/markdown/tests/common.rs b/components/markdown/tests/common.rs index fff4b714..a8513d8f 100644 --- a/components/markdown/tests/common.rs +++ b/components/markdown/tests/common.rs @@ -57,7 +57,7 @@ fn configurable_render( tera.register_filter( "markdown", - templates::filters::MarkdownFilter::new(config.clone(), permalinks.clone(), tera.clone()) + templates::filters::MarkdownFilter::new(config.clone(), permalinks.clone(), tera.clone()), ); let mut context = RenderContext::new( &tera, diff --git a/components/site/src/lib.rs b/components/site/src/lib.rs index e3e38d64..3872cc1a 100644 --- a/components/site/src/lib.rs +++ b/components/site/src/lib.rs @@ -288,6 +288,10 @@ impl Site { tpls::register_early_global_fns(self)?; self.populate_sections(); self.render_markdown()?; + { + let mut lib = self.library.write().unwrap(); + lib.fill_backlinks(); + } tpls::register_tera_global_fns(self); // Needs to be done after rendering markdown as we only get the anchors at that point diff --git a/components/templates/src/filters.rs b/components/templates/src/filters.rs index 36a41baa..4360b46a 100644 --- a/components/templates/src/filters.rs +++ b/components/templates/src/filters.rs @@ -10,7 +10,6 @@ use libs::tera::{ }; use markdown::{render_content, RenderContext}; - #[derive(Debug)] pub struct MarkdownFilter { config: Config, @@ -19,11 +18,7 @@ pub struct MarkdownFilter { } impl MarkdownFilter { - pub fn new( - config: Config, - permalinks: HashMap, - tera: Tera, - ) -> Self { + pub fn new(config: Config, permalinks: HashMap, tera: Tera) -> Self { Self { config, permalinks, tera } } } @@ -110,9 +105,9 @@ impl TeraFilter for NumFormatFilter { #[cfg(test)] mod tests { - use std::{collections::HashMap}; + use std::collections::HashMap; - use libs::tera::{to_value, Tera, Filter}; + use libs::tera::{to_value, Filter, Tera}; use super::{base64_decode, base64_encode, MarkdownFilter, NumFormatFilter}; use config::Config; @@ -146,8 +141,8 @@ mod tests { fn markdown_filter_inline() { let mut args = HashMap::new(); args.insert("inline".to_string(), to_value(true).unwrap()); - let result = - MarkdownFilter::new(Config::default(), HashMap::new(), Tera::default()).filter( + let result = MarkdownFilter::new(Config::default(), HashMap::new(), Tera::default()) + .filter( &to_value(&"Using `map`, `filter`, and `fold` instead of `for`").unwrap(), &args, ); @@ -160,8 +155,8 @@ mod tests { fn markdown_filter_inline_tables() { let mut args = HashMap::new(); args.insert("inline".to_string(), to_value(true).unwrap()); - let result = - MarkdownFilter::new(Config::default(), HashMap::new(), Tera::default()).filter( + let result = MarkdownFilter::new(Config::default(), HashMap::new(), Tera::default()) + .filter( &to_value( &r#" |id|author_id| timestamp_created|title |content | diff --git a/docs/content/documentation/content/section.md b/docs/content/documentation/content/section.md index 5d075573..e0669ff4 100644 --- a/docs/content/documentation/content/section.md +++ b/docs/content/documentation/content/section.md @@ -159,14 +159,14 @@ The `sort_by` front-matter variable can have the following values: ### `date` This will sort all pages by their `date` field, from the most recent (at the -top of the list) to the oldest (at the bottom of the list). Each page will -get `page.earlier` and `page.later` variables that contain the pages with +top of the list) to the oldest (at the bottom of the list). Each page will +get `page.lower` and `page.higher` variables that contain the pages with earlier and later dates, respectively. ### `title` This will sort all pages by their `title` field in natural lexical order, as defined by `natural_lexical_cmp` in the [lexical-sort] crate. Each page will -get `page.title_prev` and `page.title_next` variables that contain the pages +get `page.lower` and `page.higher` variables that contain the pages with previous and next titles, respectively. For example, here is a natural lexical ordering: "bachata, BART, bolero, @@ -180,7 +180,7 @@ bolero, meter, underground, μ-kernel". ### `weight` This will be sort all pages by their `weight` field, from lightest weight (at the top of the list) to heaviest (at the bottom of the list). Each -page gets `page.lighter` and `page.heavier` variables that contain the +page gets `page.lower` and `page.higher` variables that contain the pages with lighter and heavier weights, respectively. ### Reversed sorting @@ -190,11 +190,7 @@ pages sorted by weight will be sorted from lightest (at the top) to heaviest (at the bottom); pages sorted by date will be sorted from oldest (at the top) to newest (at the bottom). -`reverse` has no effect on: - -* `page.later` / `page.earlier`, -* `page.title_prev` / `page.title_next`, or -* `page.heavier` / `page.lighter`. +`reverse` has no effect on `page.lower` / `page.higher`. If the section is paginated the `paginate_reversed=true` in the front matter of the relevant section should be set instead of using the filter. diff --git a/docs/content/documentation/templates/pages-sections.md b/docs/content/documentation/templates/pages-sections.md index 17fa0f04..8486cd6a 100644 --- a/docs/content/documentation/templates/pages-sections.md +++ b/docs/content/documentation/templates/pages-sections.md @@ -34,14 +34,10 @@ toc: Array
, word_count: Number; // Based on https://help.medium.com/hc/en-us/articles/214991667-Read-time reading_time: Number; -// `earlier` and `later` are only populated if the section variable `sort_by` is set to `date` -// and only set when rendering the page itself -earlier: Page?; -later: Page?; -// `heavier` and `lighter` are only populated if the section variable `sort_by` is set to `weight` -// and only set when rendering the page itself -heavier: Page?; -lighter: Page?; +// earlier / ligher +lower: Page?; +// later / heavier +higher: Page?; // Year/month/day is only set if the page has a date and month/day are 1-indexed year: Number?; month: Number?;