From 80bcc51fd227a659ef2f5141f92c72e362eb08b9 Mon Sep 17 00:00:00 2001 From: SumDonkuS Date: Sun, 4 Feb 2024 14:39:18 -0700 Subject: [PATCH] Issue 2359 get section by lang (#2410) * adding optional `lang` arugment to `get_section` global function * Add handling of default language passed in `lang` argument of `get_section` * Remove clones for path. Change "?" to an explicit check for error * lint changes * Clean up error handling for add_lang_to_path call * fix format * Add optional parameter "lang" to get_page template function. Add check for language available in config. * Modify helper function name from calculate_path to get_path_with_lang. Modify documentation for get_section and get_page to include equivalent calls without using lang argument to demostrate how lang argument effects pathing. --- components/site/src/tpls.rs | 17 +- .../templates/src/global_fns/content.rs | 274 ++++++++++++++++-- .../documentation/templates/overview.md | 24 ++ 3 files changed, 290 insertions(+), 25 deletions(-) diff --git a/components/site/src/tpls.rs b/components/site/src/tpls.rs index d1e2eacd5..c51d0de17 100644 --- a/components/site/src/tpls.rs +++ b/components/site/src/tpls.rs @@ -1,5 +1,6 @@ use crate::Site; use libs::tera::Result as TeraResult; +use std::sync::Arc; use templates::{filters, global_fns}; /// Adds global fns that are to be available to shortcodes while rendering markdown @@ -74,13 +75,25 @@ pub fn register_early_global_fns(site: &mut Site) -> TeraResult<()> { /// Functions filled once we have parsed all the pages/sections only, so not available in shortcodes pub fn register_tera_global_fns(site: &mut Site) { + let language_list: Arc> = + Arc::new(site.config.languages.keys().map(|s| s.to_string()).collect()); site.tera.register_function( "get_page", - global_fns::GetPage::new(site.base_path.clone(), site.library.clone()), + global_fns::GetPage::new( + site.base_path.clone(), + &site.config.default_language, + Arc::clone(&language_list), + site.library.clone(), + ), ); site.tera.register_function( "get_section", - global_fns::GetSection::new(site.base_path.clone(), site.library.clone()), + global_fns::GetSection::new( + site.base_path.clone(), + &site.config.default_language, + Arc::clone(&language_list), + site.library.clone(), + ), ); site.tera.register_function( "get_taxonomy", diff --git a/components/templates/src/global_fns/content.rs b/components/templates/src/global_fns/content.rs index 84c155fd7..17183bb63 100644 --- a/components/templates/src/global_fns/content.rs +++ b/components/templates/src/global_fns/content.rs @@ -1,5 +1,6 @@ use content::{Library, Taxonomy, TaxonomyTerm}; use libs::tera::{from_value, to_value, Function as TeraFn, Result, Value}; +use std::borrow::Cow; use std::collections::HashMap; use std::path::PathBuf; use std::sync::{Arc, RwLock}; @@ -71,14 +72,60 @@ impl TeraFn for GetTaxonomyUrl { } } +fn add_lang_to_path<'a>(path: &str, lang: &str) -> Result> { + match path.rfind('.') { + Some(period_offset) => { + let prefix = path.get(0..period_offset); + let suffix = path.get(period_offset..); + if prefix.is_none() || suffix.is_none() { + Err(format!("Error adding language code to {}", path).into()) + } else { + Ok(Cow::Owned(format!("{}.{}{}", prefix.unwrap(), lang, suffix.unwrap()))) + } + } + None => Ok(Cow::Owned(format!("{}.{}", path, lang))), + } +} + +fn get_path_with_lang<'a>( + path: &'a String, + lang: &Option, + default_lang: &str, + supported_languages: &[String], +) -> Result> { + if supported_languages.contains(&default_lang.to_string()) { + lang.as_ref().map_or_else( + || Ok(Cow::Borrowed(path)), + |lang_code| match default_lang == lang_code { + true => Ok(Cow::Borrowed(path)), + false => add_lang_to_path(path, lang_code), + }, + ) + } else { + Err(format!("Unsupported language {}", default_lang).into()) + } +} + #[derive(Debug)] pub struct GetPage { base_path: PathBuf, + default_lang: String, + supported_languages: Arc>, library: Arc>, } impl GetPage { - pub fn new(base_path: PathBuf, library: Arc>) -> Self { - Self { base_path: base_path.join("content"), library } + pub fn new( + base_path: PathBuf, + default_lang: &str, + supported_languages: Arc>, + library: Arc>, + ) -> Self { + Self { + base_path: base_path.join("content"), + default_lang: default_lang.to_string(), + supported_languages, + library, + } } } impl TeraFn for GetPage { @@ -88,23 +135,50 @@ impl TeraFn for GetPage { args.get("path"), "`get_page` requires a `path` argument with a string value" ); - let full_path = self.base_path.join(&path); - let library = self.library.read().unwrap(); - match library.pages.get(&full_path) { - Some(p) => Ok(to_value(p.serialize(&library)).unwrap()), - None => Err(format!("Page `{}` not found.", path).into()), - } + + let lang = + optional_arg!(String, args.get("lang"), "`get_section`: `lang` must be a string"); + + get_path_with_lang(&path, &lang, &self.default_lang, &self.supported_languages).and_then( + |path_with_lang| { + let full_path = self.base_path.join(path_with_lang.as_ref()); + let library = self.library.read().unwrap(); + + match library.pages.get(&full_path) { + Some(p) => Ok(to_value(p.serialize(&library)).unwrap()), + None => match lang { + Some(lang_code) => { + Err(format!("Page `{}` not found for language `{}`.", path, lang_code) + .into()) + } + None => Err(format!("Page `{}` not found.", path).into()), + }, + } + }, + ) } } #[derive(Debug)] pub struct GetSection { base_path: PathBuf, + default_lang: String, + supported_languages: Arc>, library: Arc>, } impl GetSection { - pub fn new(base_path: PathBuf, library: Arc>) -> Self { - Self { base_path: base_path.join("content"), library } + pub fn new( + base_path: PathBuf, + default_lang: &str, + supported_languages: Arc>, + library: Arc>, + ) -> Self { + Self { + base_path: base_path.join("content"), + default_lang: default_lang.to_string(), + supported_languages, + library, + } } } impl TeraFn for GetSection { @@ -119,19 +193,32 @@ impl TeraFn for GetSection { .get("metadata_only") .map_or(false, |c| from_value::(c.clone()).unwrap_or(false)); - let full_path = self.base_path.join(&path); - let library = self.library.read().unwrap(); - - match library.sections.get(&full_path) { - Some(s) => { - if metadata_only { - Ok(to_value(s.serialize_basic(&library)).unwrap()) - } else { - Ok(to_value(s.serialize(&library)).unwrap()) + let lang = + optional_arg!(String, args.get("lang"), "`get_section`: `lang` must be a string"); + + get_path_with_lang(&path, &lang, self.default_lang.as_str(), &self.supported_languages) + .and_then(|path_with_lang| { + let full_path = self.base_path.join(path_with_lang.as_ref()); + let library = self.library.read().unwrap(); + + match library.sections.get(&full_path) { + Some(s) => { + if metadata_only { + Ok(to_value(s.serialize_basic(&library)).unwrap()) + } else { + Ok(to_value(s.serialize(&library)).unwrap()) + } + } + None => match lang { + Some(lang_code) => Err(format!( + "Section `{}` not found for language `{}`.", + path, lang_code + ) + .into()), + None => Err(format!("Section `{}` not found.", path).into()), + }, } - } - None => Err(format!("Section `{}` not found.", path).into()), - } + }) } } @@ -273,7 +360,148 @@ impl TeraFn for GetTaxonomyTerm { mod tests { use super::*; use config::{Config, TaxonomyConfig}; - use content::TaxonomyTerm; + use content::{FileInfo, Library, Page, Section, SortBy, TaxonomyTerm}; + use std::path::Path; + use std::sync::{Arc, RwLock}; + + fn create_page(title: &str, file_path: &str, lang: &str) -> Page { + let mut page = Page { lang: lang.to_owned(), ..Page::default() }; + page.file = FileInfo::new_page( + Path::new(format!("/test/base/path/{}", file_path).as_str()), + &PathBuf::new(), + ); + page.meta.title = Some(title.to_string()); + page.meta.weight = Some(1); + page.file.find_language("en", &["fr"]).unwrap(); + page + } + + #[test] + fn can_get_page() { + let mut library = Library::default(); + let pages = vec![ + ("Homepage", "content/homepage.md", "en"), + ("Page D'Accueil", "content/homepage.fr.md", "fr"), + ("Blog", "content/blog.md", "en"), + ("Wiki", "content/wiki.md", "en"), + ("Wiki", "content/wiki.fr.md", "fr"), + ("Recipes", "content/wiki/recipes.md", "en"), + ("Recettes", "content/wiki/recipes.fr.md", "fr"), + ("Programming", "content/wiki/programming.md", "en"), + ("La Programmation", "content/wiki/programming.fr.md", "fr"), + ("Novels", "content/novels.md", "en"), + ("Des Romans", "content/novels.fr.md", "fr"), + ]; + for (t, f, l) in pages.clone() { + library.insert_page(create_page(t, f, l)); + } + let base_path = "/test/base/path".into(); + let lang_list = vec!["en".to_string(), "fr".to_string()]; + + let static_fn = + GetPage::new(base_path, "en", Arc::new(lang_list), Arc::new(RwLock::new(library))); + + // Find with lang argument + let mut args = HashMap::new(); + args.insert("path".to_string(), to_value("wiki/recipes.md").unwrap()); + args.insert("lang".to_string(), to_value("fr").unwrap()); + let res = static_fn.call(&args).unwrap(); + let res_obj = res.as_object().unwrap(); + assert_eq!(res_obj["title"], to_value("Recettes").unwrap()); + + // Find with lang in path for legacy support + args = HashMap::new(); + args.insert("path".to_string(), to_value("wiki/recipes.fr.md").unwrap()); + let res = static_fn.call(&args).unwrap(); + let res_obj = res.as_object().unwrap(); + assert_eq!(res_obj["title"], to_value("Recettes").unwrap()); + + // Find with default lang + args = HashMap::new(); + args.insert("path".to_string(), to_value("wiki/recipes.md").unwrap()); + let res = static_fn.call(&args).unwrap(); + let res_obj = res.as_object().unwrap(); + assert_eq!(res_obj["title"], to_value("Recipes").unwrap()); + + // Find with default lang when default lang passed + args = HashMap::new(); + args.insert("path".to_string(), to_value("wiki/recipes.md").unwrap()); + args.insert("lang".to_string(), to_value("en").unwrap()); + let res = static_fn.call(&args).unwrap(); + let res_obj = res.as_object().unwrap(); + assert_eq!(res_obj["title"], to_value("Recipes").unwrap()); + } + + fn create_section(title: &str, file_path: &str, lang: &str) -> Section { + let mut section = Section { lang: lang.to_owned(), ..Section::default() }; + section.file = FileInfo::new_section( + Path::new(format!("/test/base/path/{}", file_path).as_str()), + &PathBuf::new(), + ); + section.meta.title = Some(title.to_string()); + section.meta.weight = 1; + section.meta.transparent = false; + section.meta.sort_by = SortBy::None; + section.meta.page_template = Some("new_page.html".to_owned()); + section.file.find_language("en", &["fr"]).unwrap(); + section + } + + #[test] + fn can_get_section() { + let mut library = Library::default(); + let sections = vec![ + ("Homepage", "content/_index.md", "en"), + ("Page D'Accueil", "content/_index.fr.md", "fr"), + ("Blog", "content/blog/_index.md", "en"), + ("Wiki", "content/wiki/_index.md", "en"), + ("Wiki", "content/wiki/_index.fr.md", "fr"), + ("Recipes", "content/wiki/recipes/_index.md", "en"), + ("Recettes", "content/wiki/recipes/_index.fr.md", "fr"), + ("Programming", "content/wiki/programming/_index.md", "en"), + ("La Programmation", "content/wiki/programming/_index.fr.md", "fr"), + ("Novels", "content/novels/_index.md", "en"), + ("Des Romans", "content/novels/_index.fr.md", "fr"), + ]; + for (t, f, l) in sections.clone() { + library.insert_section(create_section(t, f, l)); + } + let base_path = "/test/base/path".into(); + let lang_list = vec!["en".to_string(), "fr".to_string()]; + + let static_fn = + GetSection::new(base_path, "en", Arc::new(lang_list), Arc::new(RwLock::new(library))); + + // Find with lang argument + let mut args = HashMap::new(); + args.insert("path".to_string(), to_value("wiki/recipes/_index.md").unwrap()); + args.insert("lang".to_string(), to_value("fr").unwrap()); + let res = static_fn.call(&args).unwrap(); + let res_obj = res.as_object().unwrap(); + assert_eq!(res_obj["title"], to_value("Recettes").unwrap()); + + // Find with lang in path for legacy support + args = HashMap::new(); + args.insert("path".to_string(), to_value("wiki/recipes/_index.fr.md").unwrap()); + let res = static_fn.call(&args).unwrap(); + let res_obj = res.as_object().unwrap(); + assert_eq!(res_obj["title"], to_value("Recettes").unwrap()); + + // Find with default lang + args = HashMap::new(); + args.insert("path".to_string(), to_value("wiki/recipes/_index.md").unwrap()); + let res = static_fn.call(&args).unwrap(); + let res_obj = res.as_object().unwrap(); + assert_eq!(res_obj["title"], to_value("Recipes").unwrap()); + + // Find with default lang when default lang passed + args = HashMap::new(); + args.insert("path".to_string(), to_value("wiki/recipes/_index.md").unwrap()); + args.insert("lang".to_string(), to_value("en").unwrap()); + let res = static_fn.call(&args).unwrap(); + let res_obj = res.as_object().unwrap(); + assert_eq!(res_obj["title"], to_value("Recipes").unwrap()); + } #[test] fn can_get_taxonomy() { diff --git a/docs/content/documentation/templates/overview.md b/docs/content/documentation/templates/overview.md index 688ff985c..0437beeb2 100644 --- a/docs/content/documentation/templates/overview.md +++ b/docs/content/documentation/templates/overview.md @@ -141,6 +141,18 @@ Takes a path to an `.md` file and returns the associated page. The base path is {% set page = get_page(path="blog/page2.md") %} ``` +If selecting a specific language for the page, you can pass `lang` with the language code to the function: + +```jinja2 +{% set page = get_page(path="blog/page2.md", lang="fr") %} + +{# If "fr" is the default language, this is equivalent to #} +{% set page = get_page(path="blog/page2.md") %} + +{# If "fr" is not the default language, this is equivalent to #} +{% set page = get_page(path="blog/page2.fr.md") %} +``` + ### `get_section` Takes a path to an `_index.md` file and returns the associated section. The base path is the `content` directory. @@ -154,6 +166,18 @@ If you only need the metadata of the section, you can pass `metadata_only=true` {% set section = get_section(path="blog/_index.md", metadata_only=true) %} ``` +If selecting a specific language for the section, you can pass `lang` with the language code to the function: + +```jinja2 +{% set section = get_section(path="blog/_index.md", lang="fr") %} + +{# If "fr" is the default language, this is equivalent to #} +{% set section = get_section(path="blog/_index.md") %} + +{# If "fr" is not the default language, this is equivalent to #} +{% set section = get_section(path="blog/_index.fr.md") %} +``` + ### `get_taxonomy_url` Gets the permalink for the taxonomy item found.