From 51784aa60f398bc32ab3898fc55fa7e7b371ac72 Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Fri, 3 Dec 2021 23:30:29 +0100 Subject: [PATCH] Handle naked anchor link for internal links Closes #1676 --- CHANGELOG.md | 1 + components/library/src/content/page.rs | 2 +- components/library/src/content/section.rs | 6 +- components/rendering/src/context.rs | 8 ++ components/rendering/src/lib.rs | 1 + components/rendering/src/markdown.rs | 16 ++- components/rendering/tests/links.rs | 130 +++++++++------------- components/site/src/lib.rs | 19 +++- 8 files changed, 99 insertions(+), 84 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 32b08113..8fa79876 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - Shorcodes and `anchor-link.html` can now access the `lang` context - Add prompt before replacing the output directory with `zola build` if the `output-dir` flag is given - Shortcode handling has been completely rewritten, solving many issues +- Also add internal links starting with `#` without any internal Zola link ## 0.14.1 (2021-08-24) diff --git a/components/library/src/content/page.rs b/components/library/src/content/page.rs index f3f5e429..364aa2e9 100644 --- a/components/library/src/content/page.rs +++ b/components/library/src/content/page.rs @@ -236,7 +236,7 @@ impl Page { anchor_insert, ); context.set_shortcode_definitions(shortcode_definitions); - + context.set_current_page_path(&self.file.relative); context.tera_context.insert("page", &SerializingPage::from_page_basic(self, None)); let res = render_content(&self.raw_content, &context).map_err(|e| { diff --git a/components/library/src/content/section.rs b/components/library/src/content/section.rs index 4aa3c05a..18bb830f 100644 --- a/components/library/src/content/section.rs +++ b/components/library/src/content/section.rs @@ -10,7 +10,7 @@ use front_matter::{split_section_content, SectionFrontMatter}; use rendering::{render_content, Heading, RenderContext}; use utils::fs::read_file; use utils::site::get_reading_analytics; -use utils::templates::render_template; +use utils::templates::{render_template, ShortcodeDefinition}; use crate::content::file_info::FileInfo; use crate::content::ser::SerializingSection; @@ -147,6 +147,7 @@ impl Section { permalinks: &HashMap, tera: &Tera, config: &Config, + shortcode_definitions: &HashMap, ) -> Result<()> { let mut context = RenderContext::new( tera, @@ -156,7 +157,8 @@ impl Section { permalinks, self.meta.insert_anchor_links, ); - + context.set_shortcode_definitions(shortcode_definitions); + context.set_current_page_path(&self.file.relative); context.tera_context.insert("section", &SerializingSection::from_section_basic(self, None)); let res = render_content(&self.raw_content, &context).map_err(|e| { diff --git a/components/rendering/src/context.rs b/components/rendering/src/context.rs index df0e6aa4..1bc23c63 100644 --- a/components/rendering/src/context.rs +++ b/components/rendering/src/context.rs @@ -12,6 +12,7 @@ pub struct RenderContext<'a> { pub tera: Cow<'a, Tera>, pub config: &'a Config, pub tera_context: Context, + pub current_page_path: Option<&'a str>, pub current_page_permalink: &'a str, pub permalinks: Cow<'a, HashMap>, pub insert_anchor: InsertAnchor, @@ -35,6 +36,7 @@ impl<'a> RenderContext<'a> { Self { tera: Cow::Borrowed(tera), tera_context, + current_page_path: None, current_page_permalink, permalinks: Cow::Borrowed(permalinks), insert_anchor, @@ -50,6 +52,11 @@ impl<'a> RenderContext<'a> { self.shortcode_definitions = Cow::Borrowed(def); } + /// Same as above + pub fn set_current_page_path(&mut self, path: &'a str) { + self.current_page_path = Some(path); + } + // In use in the markdown filter // NOTE: This RenderContext is not i18n-aware, see MarkdownFilter::filter for details // If this function is ever used outside of MarkdownFilter, take this into consideration @@ -57,6 +64,7 @@ impl<'a> RenderContext<'a> { Self { tera: Cow::Owned(Tera::default()), tera_context: Context::new(), + current_page_path: None, current_page_permalink: "", permalinks: Cow::Owned(HashMap::new()), insert_anchor: InsertAnchor::None, diff --git a/components/rendering/src/lib.rs b/components/rendering/src/lib.rs index cf0c1f55..11936102 100644 --- a/components/rendering/src/lib.rs +++ b/components/rendering/src/lib.rs @@ -10,6 +10,7 @@ use errors::Result; pub use context::RenderContext; use markdown::markdown_to_html; +pub use markdown::Rendered; pub use table_of_contents::Heading; pub fn render_content(content: &str, context: &RenderContext) -> Result { diff --git a/components/rendering/src/markdown.rs b/components/rendering/src/markdown.rs index 49c4641e..9e15ca42 100644 --- a/components/rendering/src/markdown.rs +++ b/components/rendering/src/markdown.rs @@ -92,9 +92,20 @@ fn fix_link( } else { if is_external_link(link) { external_links.push(link.to_owned()); + link.to_owned() + } else if link.starts_with("#") { + // local anchor without the internal zola path + if let Some(current_path) = context.current_page_path { + internal_links.push((current_path.to_owned(), Some(link[1..].to_owned()))); + format!("{}{}", context.current_page_permalink, &link) + } else { + link.to_string() + } + } else { + link.to_string() } - link.to_string() }; + Ok(result) } @@ -121,8 +132,7 @@ fn get_heading_refs(events: &[Event]) -> Vec { heading_refs.push(HeadingRef::new(i, *level)); } Event::End(Tag::Heading(_)) => { - let msg = "Heading end before start?"; - heading_refs.last_mut().expect(msg).end_idx = i; + heading_refs.last_mut().expect("Heading end before start?").end_idx = i; } _ => (), } diff --git a/components/rendering/tests/links.rs b/components/rendering/tests/links.rs index 99e386b5..bc735cf7 100644 --- a/components/rendering/tests/links.rs +++ b/components/rendering/tests/links.rs @@ -1,82 +1,62 @@ +use std::collections::HashMap; + +use errors::Result; +use rendering::Rendered; + mod common; -macro_rules! test_links { - ( - $in_str:literal, - [$($id:literal => $perma_link:literal),*], - [$($abs_path:literal),*], - [$($rel_path:literal:$opt_anchor:expr),*], - [$($shortcodes:ident),*] - ) => { - let config = config::Config::default_for_test(); - - #[allow(unused_mut)] - let mut tera = tera::Tera::default(); - - // Add all shortcodes - $( - tera.add_raw_template( - &format!("shortcodes/{}", $shortcodes.filename()), - $shortcodes.output - ).expect("Failed to add raw template"); - )* - - #[allow(unused_mut)] - let mut permalinks = std::collections::HashMap::new(); - - $( - permalinks.insert($id.to_string(), $perma_link.to_string()); - )* - - let context = rendering::RenderContext::new( - &tera, - &config, - &config.default_language, - "", - &permalinks, - front_matter::InsertAnchor::None, - ); - - let rendered = rendering::render_content($in_str, &context); - assert!(rendered.is_ok(), "Rendering failed"); - - let rendered = rendered.unwrap(); - - let asserted_int_links = vec![ - $( - ($rel_path.to_string(), $opt_anchor.map(|x| x.to_string())) - ),* - ]; - let asserted_ext_links: Vec<&str> = vec![$($abs_path),*]; - - assert_eq!(rendered.internal_links, asserted_int_links, "Internal links unequal"); - assert_eq!(rendered.external_links, asserted_ext_links, "External links unequal"); - } -} - -#[test] -fn basic_internal() { - test_links!("Hello World!", [], [], [], []); -} - -#[test] -fn absolute_links() { - test_links!("[abc](https://google.com/)", [], ["https://google.com/"], [], []); -} - -#[test] -fn relative_links() { - test_links!( - "[abc](@/def/123.md)", - ["def/123.md" => "https://xyz.com/def/123"], - [], - ["def/123.md":>::None], - [] +fn render_content(content: &str, permalinks: HashMap) -> Result { + let config = config::Config::default_for_test(); + let tera = tera::Tera::default(); + let mut context = rendering::RenderContext::new( + &tera, + &config, + &config.default_language, + "http://mypage.com", + &permalinks, + front_matter::InsertAnchor::None, ); + context.set_current_page_path("mine.md"); + + rendering::render_content(content, &context) } #[test] -#[should_panic] -fn relative_links_no_perma() { - test_links!("[abc](@/def/123.md)", [], [], ["def/123.md": >::None], []); +fn can_detect_links() { + // no links + let rendered = render_content("Hello World!", HashMap::new()).unwrap(); + assert_eq!(rendered.internal_links.len(), 0); + assert_eq!(rendered.external_links.len(), 0); + + // external + let rendered = render_content("[abc](https://google.com/)", HashMap::new()).unwrap(); + assert_eq!(rendered.internal_links.len(), 0); + assert_eq!(rendered.external_links.len(), 1); + assert_eq!(rendered.external_links[0], "https://google.com/"); + + // internal + let mut permalinks = HashMap::new(); + permalinks.insert("def/123.md".to_owned(), "https://xyz.com/def/123".to_owned()); + let rendered = render_content("[abc](@/def/123.md)", permalinks).unwrap(); + assert_eq!(rendered.internal_links.len(), 1); + assert_eq!(rendered.internal_links[0], ("def/123.md".to_owned(), None)); + assert_eq!(rendered.external_links.len(), 0); + + // internal with anchors + let mut permalinks = HashMap::new(); + permalinks.insert("def/123.md".to_owned(), "https://xyz.com/def/123".to_owned()); + let rendered = render_content("[abc](@/def/123.md#hello)", permalinks).unwrap(); + assert_eq!(rendered.internal_links.len(), 1); + assert_eq!(rendered.internal_links[0], ("def/123.md".to_owned(), Some("hello".to_owned()))); + assert_eq!(rendered.external_links.len(), 0); + + // internal link referring to self + let rendered = render_content("[abc](#hello)", HashMap::new()).unwrap(); + assert_eq!(rendered.internal_links.len(), 1); + assert_eq!(rendered.internal_links[0], ("mine.md".to_owned(), Some("hello".to_owned()))); + assert_eq!(rendered.external_links.len(), 0); + + // Not pointing to anything so that's an error + let res = render_content("[abc](@/def/123.md)", HashMap::new()); + assert!(res.is_err()); } diff --git a/components/site/src/lib.rs b/components/site/src/lib.rs index 23427f50..2d80a09d 100644 --- a/components/site/src/lib.rs +++ b/components/site/src/lib.rs @@ -380,7 +380,9 @@ impl Site { .values_mut() .collect::>() .par_iter_mut() - .map(|section| section.render_markdown(permalinks, tera, config)) + .map(|section| { + section.render_markdown(permalinks, tera, config, &self.shortcode_definitions) + }) .collect::>()?; Ok(()) @@ -426,7 +428,12 @@ impl Site { pub fn add_section(&mut self, mut section: Section, render_md: bool) -> Result<()> { self.permalinks.insert(section.file.relative.clone(), section.permalink.clone()); if render_md { - section.render_markdown(&self.permalinks, &self.tera, &self.config)?; + section.render_markdown( + &self.permalinks, + &self.tera, + &self.config, + &self.shortcode_definitions, + )?; } let mut library = self.library.write().expect("Get lock for add_section"); library.remove_section(§ion.file.path); @@ -847,7 +854,13 @@ impl Site { if taxonomy.kind.is_paginated() { self.render_paginated( comp.clone(), - &Paginator::from_taxonomy(taxonomy, item, &library, &self.tera, &self.config.theme), + &Paginator::from_taxonomy( + taxonomy, + item, + &library, + &self.tera, + &self.config.theme, + ), )?; } else { let single_output =