Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue 2359 get section by lang #2410

Merged
merged 8 commits into from
Feb 4, 2024
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 =
Keats marked this conversation as resolved.
Show resolved Hide resolved
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:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would mention for pages and here that it's equivalent to {% set section = get_section(path="blog/_index.fr.md") %}


```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