Handle naked anchor link for internal links

Closes #1676
This commit is contained in:
Vincent Prouillet 2021-12-03 23:30:29 +01:00
parent 769264e3e5
commit 51784aa60f
8 changed files with 99 additions and 84 deletions

View File

@ -10,6 +10,7 @@
- Shorcodes and `anchor-link.html` can now access the `lang` context - 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 - 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 - 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) ## 0.14.1 (2021-08-24)

View File

@ -236,7 +236,7 @@ impl Page {
anchor_insert, anchor_insert,
); );
context.set_shortcode_definitions(shortcode_definitions); 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)); context.tera_context.insert("page", &SerializingPage::from_page_basic(self, None));
let res = render_content(&self.raw_content, &context).map_err(|e| { let res = render_content(&self.raw_content, &context).map_err(|e| {

View File

@ -10,7 +10,7 @@ use front_matter::{split_section_content, SectionFrontMatter};
use rendering::{render_content, Heading, RenderContext}; use rendering::{render_content, Heading, RenderContext};
use utils::fs::read_file; use utils::fs::read_file;
use utils::site::get_reading_analytics; 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::file_info::FileInfo;
use crate::content::ser::SerializingSection; use crate::content::ser::SerializingSection;
@ -147,6 +147,7 @@ impl Section {
permalinks: &HashMap<String, String>, permalinks: &HashMap<String, String>,
tera: &Tera, tera: &Tera,
config: &Config, config: &Config,
shortcode_definitions: &HashMap<String, ShortcodeDefinition>,
) -> Result<()> { ) -> Result<()> {
let mut context = RenderContext::new( let mut context = RenderContext::new(
tera, tera,
@ -156,7 +157,8 @@ impl Section {
permalinks, permalinks,
self.meta.insert_anchor_links, 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)); context.tera_context.insert("section", &SerializingSection::from_section_basic(self, None));
let res = render_content(&self.raw_content, &context).map_err(|e| { let res = render_content(&self.raw_content, &context).map_err(|e| {

View File

@ -12,6 +12,7 @@ pub struct RenderContext<'a> {
pub tera: Cow<'a, Tera>, pub tera: Cow<'a, Tera>,
pub config: &'a Config, pub config: &'a Config,
pub tera_context: Context, pub tera_context: Context,
pub current_page_path: Option<&'a str>,
pub current_page_permalink: &'a str, pub current_page_permalink: &'a str,
pub permalinks: Cow<'a, HashMap<String, String>>, pub permalinks: Cow<'a, HashMap<String, String>>,
pub insert_anchor: InsertAnchor, pub insert_anchor: InsertAnchor,
@ -35,6 +36,7 @@ impl<'a> RenderContext<'a> {
Self { Self {
tera: Cow::Borrowed(tera), tera: Cow::Borrowed(tera),
tera_context, tera_context,
current_page_path: None,
current_page_permalink, current_page_permalink,
permalinks: Cow::Borrowed(permalinks), permalinks: Cow::Borrowed(permalinks),
insert_anchor, insert_anchor,
@ -50,6 +52,11 @@ impl<'a> RenderContext<'a> {
self.shortcode_definitions = Cow::Borrowed(def); 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 // In use in the markdown filter
// NOTE: This RenderContext is not i18n-aware, see MarkdownFilter::filter for details // 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 // If this function is ever used outside of MarkdownFilter, take this into consideration
@ -57,6 +64,7 @@ impl<'a> RenderContext<'a> {
Self { Self {
tera: Cow::Owned(Tera::default()), tera: Cow::Owned(Tera::default()),
tera_context: Context::new(), tera_context: Context::new(),
current_page_path: None,
current_page_permalink: "", current_page_permalink: "",
permalinks: Cow::Owned(HashMap::new()), permalinks: Cow::Owned(HashMap::new()),
insert_anchor: InsertAnchor::None, insert_anchor: InsertAnchor::None,

View File

@ -10,6 +10,7 @@ use errors::Result;
pub use context::RenderContext; pub use context::RenderContext;
use markdown::markdown_to_html; use markdown::markdown_to_html;
pub use markdown::Rendered;
pub use table_of_contents::Heading; pub use table_of_contents::Heading;
pub fn render_content(content: &str, context: &RenderContext) -> Result<markdown::Rendered> { pub fn render_content(content: &str, context: &RenderContext) -> Result<markdown::Rendered> {

View File

@ -92,9 +92,20 @@ fn fix_link(
} else { } else {
if is_external_link(link) { if is_external_link(link) {
external_links.push(link.to_owned()); 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) Ok(result)
} }
@ -121,8 +132,7 @@ fn get_heading_refs(events: &[Event]) -> Vec<HeadingRef> {
heading_refs.push(HeadingRef::new(i, *level)); heading_refs.push(HeadingRef::new(i, *level));
} }
Event::End(Tag::Heading(_)) => { Event::End(Tag::Heading(_)) => {
let msg = "Heading end before start?"; heading_refs.last_mut().expect("Heading end before start?").end_idx = i;
heading_refs.last_mut().expect(msg).end_idx = i;
} }
_ => (), _ => (),
} }

View File

@ -1,82 +1,62 @@
use std::collections::HashMap;
use errors::Result;
use rendering::Rendered;
mod common; mod common;
macro_rules! test_links { fn render_content(content: &str, permalinks: HashMap<String, String>) -> Result<Rendered> {
( let config = config::Config::default_for_test();
$in_str:literal, let tera = tera::Tera::default();
[$($id:literal => $perma_link:literal),*], let mut context = rendering::RenderContext::new(
[$($abs_path:literal),*], &tera,
[$($rel_path:literal:$opt_anchor:expr),*], &config,
[$($shortcodes:ident),*] &config.default_language,
) => { "http://mypage.com",
let config = config::Config::default_for_test(); &permalinks,
front_matter::InsertAnchor::None,
#[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":<Option<&str>>::None],
[]
); );
context.set_current_page_path("mine.md");
rendering::render_content(content, &context)
} }
#[test] #[test]
#[should_panic] fn can_detect_links() {
fn relative_links_no_perma() { // no links
test_links!("[abc](@/def/123.md)", [], [], ["def/123.md": <Option<&str>>::None], []); 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());
} }

View File

@ -380,7 +380,9 @@ impl Site {
.values_mut() .values_mut()
.collect::<Vec<_>>() .collect::<Vec<_>>()
.par_iter_mut() .par_iter_mut()
.map(|section| section.render_markdown(permalinks, tera, config)) .map(|section| {
section.render_markdown(permalinks, tera, config, &self.shortcode_definitions)
})
.collect::<Result<()>>()?; .collect::<Result<()>>()?;
Ok(()) Ok(())
@ -426,7 +428,12 @@ impl Site {
pub fn add_section(&mut self, mut section: Section, render_md: bool) -> Result<()> { pub fn add_section(&mut self, mut section: Section, render_md: bool) -> Result<()> {
self.permalinks.insert(section.file.relative.clone(), section.permalink.clone()); self.permalinks.insert(section.file.relative.clone(), section.permalink.clone());
if render_md { 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"); let mut library = self.library.write().expect("Get lock for add_section");
library.remove_section(&section.file.path); library.remove_section(&section.file.path);
@ -847,7 +854,13 @@ impl Site {
if taxonomy.kind.is_paginated() { if taxonomy.kind.is_paginated() {
self.render_paginated( self.render_paginated(
comp.clone(), 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 { } else {
let single_output = let single_output =