Skip to content

Commit

Permalink
Issue 2359 get section by lang (#2410)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
SumDonkuS authored and Keats committed Jun 20, 2024
1 parent 7d18ddf commit 80bcc51
Show file tree
Hide file tree
Showing 3 changed files with 290 additions and 25 deletions.
17 changes: 15 additions & 2 deletions components/site/src/tpls.rs
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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<Vec<String>> =
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",
Expand Down
274 changes: 251 additions & 23 deletions components/templates/src/global_fns/content.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -71,14 +72,60 @@ impl TeraFn for GetTaxonomyUrl {
}
}

fn add_lang_to_path<'a>(path: &str, lang: &str) -> Result<Cow<'a, String>> {
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<String>,
default_lang: &str,
supported_languages: &[String],
) -> Result<Cow<'a, String>> {
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<Vec<String>>,
library: Arc<RwLock<Library>>,
}
impl GetPage {
pub fn new(base_path: PathBuf, library: Arc<RwLock<Library>>) -> Self {
Self { base_path: base_path.join("content"), library }
pub fn new(
base_path: PathBuf,
default_lang: &str,
supported_languages: Arc<Vec<String>>,
library: Arc<RwLock<Library>>,
) -> Self {
Self {
base_path: base_path.join("content"),
default_lang: default_lang.to_string(),
supported_languages,
library,
}
}
}
impl TeraFn for GetPage {
Expand All @@ -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<Vec<String>>,
library: Arc<RwLock<Library>>,
}
impl GetSection {
pub fn new(base_path: PathBuf, library: Arc<RwLock<Library>>) -> Self {
Self { base_path: base_path.join("content"), library }
pub fn new(
base_path: PathBuf,
default_lang: &str,
supported_languages: Arc<Vec<String>>,
library: Arc<RwLock<Library>>,
) -> Self {
Self {
base_path: base_path.join("content"),
default_lang: default_lang.to_string(),
supported_languages,
library,
}
}
}
impl TeraFn for GetSection {
Expand All @@ -119,19 +193,32 @@ impl TeraFn for GetSection {
.get("metadata_only")
.map_or(false, |c| from_value::<bool>(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()),
}
})
}
}

Expand Down Expand Up @@ -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() {
Expand Down
24 changes: 24 additions & 0 deletions docs/content/documentation/templates/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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.

Expand Down

0 comments on commit 80bcc51

Please sign in to comment.