diff --git a/README.md b/README.md index 00657b0..a21375a 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ in the `docs/content` folder of the repository and the community can use [its fo | Syntax highlighting | ✔ | ✔ | ✔ | ✔ | | Sass compilation | ✔ | ✔ | ✔ | ✔ | | Assets co-location | ✔ | ✔ | ✔ | ✔ | -| i18n | ✕ | ✕ | ✔ | ✔ | +| Multilingual site | ✔ | ✕ | ✔ | ✔ | | Image processing | ✔ | ✕ | ✔ | ✔ | | Sane & powerful template engine | ✔ | ~ | ~ | ✔ | | Themes | ✔ | ✕ | ✔ | ✔ | diff --git a/components/config/src/lib.rs b/components/config/src/lib.rs index 74564ac..b7a4ebc 100644 --- a/components/config/src/lib.rs +++ b/components/config/src/lib.rs @@ -12,7 +12,7 @@ extern crate syntect; mod config; pub mod highlighting; mod theme; -pub use config::{Config, Taxonomy, Language}; +pub use config::{Config, Language, Taxonomy}; use std::path::Path; diff --git a/components/library/src/content/file_info.rs b/components/library/src/content/file_info.rs index dc887a8..2b8fc88 100644 --- a/components/library/src/content/file_info.rs +++ b/components/library/src/content/file_info.rs @@ -119,7 +119,7 @@ impl FileInfo { // Go with the assumption that no one is using `.` in filenames when using i18n // We can document that - let mut parts: Vec = self.name.splitn(2,'.').map(|s| s.to_string()).collect(); + let mut parts: Vec = self.name.splitn(2, '.').map(|s| s.to_string()).collect(); // The language code is not present in the config: typo or the user forgot to add it to the // config @@ -155,7 +155,7 @@ mod tests { use config::{Config, Language}; - use super::{FileInfo, find_content_components}; + use super::{find_content_components, FileInfo}; #[test] fn can_find_content_components() { @@ -165,17 +165,19 @@ mod tests { } #[test] fn can_find_components_in_page_with_assets() { - let file = - FileInfo::new_page(&Path::new("/home/vincent/code/site/content/posts/tutorials/python/index.md")); + let file = FileInfo::new_page(&Path::new( + "/home/vincent/code/site/content/posts/tutorials/python/index.md", + )); assert_eq!(file.components, ["posts".to_string(), "tutorials".to_string()]); } #[test] fn can_find_valid_language_in_page() { let mut config = Config::default(); - config.languages.push(Language {code: String::from("fr"), rss: false}); - let mut file = - FileInfo::new_page(&Path::new("/home/vincent/code/site/content/posts/tutorials/python.fr.md")); + config.languages.push(Language { code: String::from("fr"), rss: false }); + let mut file = FileInfo::new_page(&Path::new( + "/home/vincent/code/site/content/posts/tutorials/python.fr.md", + )); let res = file.find_language(&config); assert!(res.is_ok()); assert_eq!(res.unwrap(), Some(String::from("fr"))); @@ -184,9 +186,10 @@ mod tests { #[test] fn can_find_valid_language_in_page_with_assets() { let mut config = Config::default(); - config.languages.push(Language {code: String::from("fr"), rss: false}); - let mut file = - FileInfo::new_page(&Path::new("/home/vincent/code/site/content/posts/tutorials/python/index.fr.md")); + config.languages.push(Language { code: String::from("fr"), rss: false }); + let mut file = FileInfo::new_page(&Path::new( + "/home/vincent/code/site/content/posts/tutorials/python/index.fr.md", + )); assert_eq!(file.components, ["posts".to_string(), "tutorials".to_string()]); let res = file.find_language(&config); assert!(res.is_ok()); @@ -196,8 +199,9 @@ mod tests { #[test] fn do_nothing_on_unknown_language_in_page_with_i18n_off() { let config = Config::default(); - let mut file = - FileInfo::new_page(&Path::new("/home/vincent/code/site/content/posts/tutorials/python.fr.md")); + let mut file = FileInfo::new_page(&Path::new( + "/home/vincent/code/site/content/posts/tutorials/python.fr.md", + )); let res = file.find_language(&config); assert!(res.is_ok()); assert!(res.unwrap().is_none()); @@ -206,9 +210,10 @@ mod tests { #[test] fn errors_on_unknown_language_in_page_with_i18n_on() { let mut config = Config::default(); - config.languages.push(Language {code: String::from("it"), rss: false}); - let mut file = - FileInfo::new_page(&Path::new("/home/vincent/code/site/content/posts/tutorials/python.fr.md")); + config.languages.push(Language { code: String::from("it"), rss: false }); + let mut file = FileInfo::new_page(&Path::new( + "/home/vincent/code/site/content/posts/tutorials/python.fr.md", + )); let res = file.find_language(&config); assert!(res.is_err()); } @@ -216,9 +221,10 @@ mod tests { #[test] fn can_find_valid_language_in_section() { let mut config = Config::default(); - config.languages.push(Language {code: String::from("fr"), rss: false}); - let mut file = - FileInfo::new_section(&Path::new("/home/vincent/code/site/content/posts/tutorials/_index.fr.md")); + config.languages.push(Language { code: String::from("fr"), rss: false }); + let mut file = FileInfo::new_section(&Path::new( + "/home/vincent/code/site/content/posts/tutorials/_index.fr.md", + )); let res = file.find_language(&config); assert!(res.is_ok()); assert_eq!(res.unwrap(), Some(String::from("fr"))); diff --git a/components/library/src/content/page.rs b/components/library/src/content/page.rs index dea402d..5736e17 100644 --- a/components/library/src/content/page.rs +++ b/components/library/src/content/page.rs @@ -74,6 +74,8 @@ pub struct Page { /// The language of that page. `None` if the user doesn't setup `languages` in config. /// Corresponds to the lang in the {slug}.{lang}.md file scheme pub lang: Option, + /// Contains all the translated version of that page + pub translations: Vec, } impl Page { @@ -101,6 +103,7 @@ impl Page { word_count: None, reading_time: None, lang: None, + translations: Vec::new(), } } @@ -300,6 +303,7 @@ impl Default for Page { word_count: None, reading_time: None, lang: None, + translations: Vec::new(), } } } @@ -577,7 +581,7 @@ Hello world #[test] fn can_specify_language_in_filename() { let mut config = Config::default(); - config.languages.push(Language {code: String::from("fr"), rss: false}); + config.languages.push(Language { code: String::from("fr"), rss: false }); let content = r#" +++ +++ @@ -594,7 +598,7 @@ Bonjour le monde"# #[test] fn can_specify_language_in_filename_with_date() { let mut config = Config::default(); - config.languages.push(Language {code: String::from("fr"), rss: false}); + config.languages.push(Language { code: String::from("fr"), rss: false }); let content = r#" +++ +++ @@ -612,7 +616,7 @@ Bonjour le monde"# #[test] fn i18n_frontmatter_path_overrides_default_permalink() { let mut config = Config::default(); - config.languages.push(Language {code: String::from("fr"), rss: false}); + config.languages.push(Language { code: String::from("fr"), rss: false }); let content = r#" +++ path = "bonjour" diff --git a/components/library/src/content/section.rs b/components/library/src/content/section.rs index c711bbc..041b9ac 100644 --- a/components/library/src/content/section.rs +++ b/components/library/src/content/section.rs @@ -54,6 +54,8 @@ pub struct Section { /// The language of that section. `None` if the user doesn't setup `languages` in config. /// Corresponds to the lang in the _index.{lang}.md file scheme pub lang: Option, + /// Contains all the translated version of that section + pub translations: Vec, } impl Section { @@ -78,6 +80,7 @@ impl Section { word_count: None, reading_time: None, lang: None, + translations: Vec::new(), } } @@ -235,6 +238,7 @@ impl Default for Section { reading_time: None, word_count: None, lang: None, + translations: Vec::new(), } } } @@ -302,7 +306,7 @@ mod tests { #[test] fn can_specify_language_in_filename() { let mut config = Config::default(); - config.languages.push(Language {code: String::from("fr"), rss: false}); + config.languages.push(Language { code: String::from("fr"), rss: false }); let content = r#" +++ +++ diff --git a/components/library/src/library.rs b/components/library/src/library.rs index 3ff6f2f..5917588 100644 --- a/components/library/src/library.rs +++ b/components/library/src/library.rs @@ -80,12 +80,8 @@ impl Library { /// Find out the direct subsections of each subsection if there are some /// as well as the pages for each section pub fn populate_sections(&mut self) { - let root_path= self - .sections - .values() - .find(|s| s.is_index()) - .map(|s| s.file.parent.clone()) - .unwrap(); + let root_path = + self.sections.values().find(|s| s.is_index()).map(|s| s.file.parent.clone()).unwrap(); // We are going to get both the ancestors and grandparents for each section in one go let mut ancestors: HashMap> = HashMap::new(); let mut subsections: HashMap> = HashMap::new(); @@ -119,7 +115,9 @@ impl Library { if path == section.file.parent { continue; } - if let Some(section_key) = self.paths_to_sections.get(&path.join(§ion.file.filename)) { + if let Some(section_key) = + self.paths_to_sections.get(&path.join(§ion.file.filename)) + { parents.push(*section_key); } } diff --git a/components/rebuild/src/lib.rs b/components/rebuild/src/lib.rs index 7e43166..1f59ba5 100644 --- a/components/rebuild/src/lib.rs +++ b/components/rebuild/src/lib.rs @@ -288,7 +288,7 @@ pub fn after_content_rename(site: &mut Site, old: &Path, new: &Path) -> Result<( old.to_path_buf() }; site.library.remove_page(&old_path); - return handle_page_editing(site, &new_path); + handle_page_editing(site, &new_path) } /// What happens when a section or a page is created/edited diff --git a/components/rebuild/tests/rebuild.rs b/components/rebuild/tests/rebuild.rs index 9e84bdb..621e8a8 100644 --- a/components/rebuild/tests/rebuild.rs +++ b/components/rebuild/tests/rebuild.rs @@ -267,9 +267,5 @@ Edite let res = after_content_change(&mut site, &file_path); println!("{:?}", res); assert!(res.is_ok()); - assert!(file_contains!( - site_path, - "public/fr/blog/with-assets/index.html", - "Edite" - )); + assert!(file_contains!(site_path, "public/fr/blog/with-assets/index.html", "Edite")); } diff --git a/components/site/src/lib.rs b/components/site/src/lib.rs index 9ae418f..efe889d 100644 --- a/components/site/src/lib.rs +++ b/components/site/src/lib.rs @@ -152,7 +152,10 @@ impl Site { fn index_section_paths(&self) -> Vec<(PathBuf, Option)> { let mut res = vec![(self.content_path.join("_index.md"), None)]; for language in &self.config.languages { - res.push((self.content_path.join(format!("_index.{}.md", language.code)), Some(language.code.clone()))); + res.push(( + self.content_path.join(format!("_index.{}.md", language.code)), + Some(language.code.clone()), + )); } res } @@ -189,7 +192,9 @@ impl Site { .unwrap() .filter_map(|e| e.ok()) .filter(|e| !e.as_path().file_name().unwrap().to_str().unwrap().starts_with('.')) - .partition(|entry| entry.as_path().file_name().unwrap().to_str().unwrap().starts_with("_index.")); + .partition(|entry| { + entry.as_path().file_name().unwrap().to_str().unwrap().starts_with("_index.") + }); self.library = Library::new(page_entries.len(), section_entries.len()); @@ -241,7 +246,8 @@ impl Site { let mut index_section = Section::default(); index_section.file.parent = self.content_path.clone(); index_section.file.name = "_index".to_string(); - index_section.file.filename = index_path.file_name().unwrap().to_string_lossy().to_string(); + index_section.file.filename = + index_path.file_name().unwrap().to_string_lossy().to_string(); if let Some(ref l) = lang { index_section.permalink = self.config.make_permalink(l); let filename = format!("_index.{}.md", l); @@ -353,7 +359,8 @@ impl Site { pub fn add_page(&mut self, mut page: Page, render: bool) -> Result> { self.permalinks.insert(page.file.relative.clone(), page.permalink.clone()); if render { - let insert_anchor = self.find_parent_section_insert_anchor(&page.file.parent, &page.lang); + let insert_anchor = + self.find_parent_section_insert_anchor(&page.file.parent, &page.lang); page.render_markdown(&self.permalinks, &self.tera, &self.config, insert_anchor)?; } let prev = self.library.remove_page(&page.file.path); @@ -379,7 +386,11 @@ impl Site { /// Finds the insert_anchor for the parent section of the directory at `path`. /// Defaults to `AnchorInsert::None` if no parent section found - pub fn find_parent_section_insert_anchor(&self, parent_path: &PathBuf, lang: &Option) -> InsertAnchor { + pub fn find_parent_section_insert_anchor( + &self, + parent_path: &PathBuf, + lang: &Option, + ) -> InsertAnchor { let parent = if let Some(ref l) = lang { parent_path.join(format!("_index.{}.md", l)) } else { @@ -746,7 +757,7 @@ impl Site { let number_pagers = (section.pages.len() as f64 / section.meta.paginate_by.unwrap() as f64) .ceil() as isize; - for i in 1..number_pagers + 1 { + for i in 1..=number_pagers { let permalink = format!("{}{}/{}/", section.permalink, section.meta.paginate_path, i); sections.push(SitemapEntry::new(permalink, None)) @@ -770,7 +781,7 @@ impl Site { let number_pagers = (item.pages.len() as f64 / taxonomy.kind.paginate_by.unwrap() as f64) .ceil() as isize; - for i in 1..number_pagers + 1 { + for i in 1..=number_pagers { let permalink = self.config.make_permalink(&format!( "{}/{}/{}/{}", name, @@ -822,7 +833,7 @@ impl Site { context.insert("last_build_date", &pages[0].meta.date.clone()); // limit to the last n elements if the limit is set; otherwise use all. - let num_entries = self.config.rss_limit.unwrap_or(pages.len()); + let num_entries = self.config.rss_limit.unwrap_or_else(|| pages.len()); let p = pages .iter() .take(num_entries) diff --git a/components/site/tests/common.rs b/components/site/tests/common.rs index 90bd48c..34c6a45 100644 --- a/components/site/tests/common.rs +++ b/components/site/tests/common.rs @@ -1,5 +1,5 @@ -extern crate tempfile; extern crate site; +extern crate tempfile; use std::env; use std::path::PathBuf; @@ -50,7 +50,10 @@ pub fn build_site(name: &str) -> (Site, TempDir, PathBuf) { } /// Same as `build_site` but has a hook to setup some config options -pub fn build_site_with_setup(name: &str, mut setup_cb: F) -> (Site, TempDir, PathBuf) where F: FnMut(Site) -> (Site, bool) { +pub fn build_site_with_setup(name: &str, mut setup_cb: F) -> (Site, TempDir, PathBuf) +where + F: FnMut(Site) -> (Site, bool), +{ let mut path = env::current_dir().unwrap().parent().unwrap().parent().unwrap().to_path_buf(); path.push(name); let site = Site::new(&path, "config.toml").unwrap(); diff --git a/components/site/tests/site.rs b/components/site/tests/site.rs index cf973bd..f347bc7 100644 --- a/components/site/tests/site.rs +++ b/components/site/tests/site.rs @@ -6,9 +6,9 @@ use std::collections::HashMap; use std::env; use std::path::Path; +use common::{build_site, build_site_with_setup}; use config::Taxonomy; use site::Site; -use common::{build_site, build_site_with_setup}; #[test] fn can_parse_site() { @@ -425,7 +425,10 @@ fn can_build_site_with_pagination_for_index() { let (_, _tmp_dir, public) = build_site_with_setup("test_site", |mut site| { site.load().unwrap(); { - let index = site.library.get_section_mut(&site.base_path.join("content").join("_index.md")).unwrap(); + let index = site + .library + .get_section_mut(&site.base_path.join("content").join("_index.md")) + .unwrap(); index.meta.paginate_by = Some(2); index.meta.template = Some("index_paginated.html".to_string()); } @@ -482,8 +485,10 @@ fn can_build_site_with_pagination_for_taxonomy() { for (i, (_, page)) in site.library.pages_mut().iter_mut().enumerate() { page.meta.taxonomies = { let mut taxonomies = HashMap::new(); - taxonomies - .insert("tags".to_string(), vec![if i % 2 == 0 { "A" } else { "B" }.to_string()]); + taxonomies.insert( + "tags".to_string(), + vec![if i % 2 == 0 { "A" } else { "B" }.to_string()], + ); taxonomies }; } diff --git a/components/site/tests/site_i18n.rs b/components/site/tests/site_i18n.rs index 975818a..0b5e93b 100644 --- a/components/site/tests/site_i18n.rs +++ b/components/site/tests/site_i18n.rs @@ -3,8 +3,8 @@ mod common; use std::env; -use site::Site; use common::build_site; +use site::Site; #[test] fn can_parse_multilingual_site() { @@ -17,11 +17,13 @@ fn can_parse_multilingual_site() { assert_eq!(site.library.sections().len(), 4); // default index sections - let default_index_section = site.library.get_section(&path.join("content").join("_index.md")).unwrap(); + let default_index_section = + site.library.get_section(&path.join("content").join("_index.md")).unwrap(); assert_eq!(default_index_section.pages.len(), 1); assert!(default_index_section.ancestors.is_empty()); - let fr_index_section = site.library.get_section(&path.join("content").join("_index.fr.md")).unwrap(); + let fr_index_section = + site.library.get_section(&path.join("content").join("_index.fr.md")).unwrap(); assert_eq!(fr_index_section.pages.len(), 1); assert!(fr_index_section.ancestors.is_empty()); @@ -31,7 +33,10 @@ fn can_parse_multilingual_site() { let default_blog = site.library.get_section(&blog_path.join("_index.md")).unwrap(); assert_eq!(default_blog.subsections.len(), 0); assert_eq!(default_blog.pages.len(), 4); - assert_eq!(default_blog.ancestors, vec![*site.library.get_section_key(&default_index_section.file.path).unwrap()]); + assert_eq!( + default_blog.ancestors, + vec![*site.library.get_section_key(&default_index_section.file.path).unwrap()] + ); for key in &default_blog.pages { let page = site.library.get_page_by_key(*key); assert_eq!(page.lang, None); @@ -40,7 +45,10 @@ fn can_parse_multilingual_site() { let fr_blog = site.library.get_section(&blog_path.join("_index.fr.md")).unwrap(); assert_eq!(fr_blog.subsections.len(), 0); assert_eq!(fr_blog.pages.len(), 3); - assert_eq!(fr_blog.ancestors, vec![*site.library.get_section_key(&fr_index_section.file.path).unwrap()]); + assert_eq!( + fr_blog.ancestors, + vec![*site.library.get_section_key(&fr_index_section.file.path).unwrap()] + ); for key in &fr_blog.pages { let page = site.library.get_page_by_key(*key); assert_eq!(page.lang, Some("fr".to_string())); diff --git a/components/templates/src/global_fns/load_data.rs b/components/templates/src/global_fns/load_data.rs index 1161d01..e6ad349 100644 --- a/components/templates/src/global_fns/load_data.rs +++ b/components/templates/src/global_fns/load_data.rs @@ -50,24 +50,24 @@ impl FromStr for OutputFormat { type Err = Error; fn from_str(output_format: &str) -> Result { - return match output_format { + match output_format { "toml" => Ok(OutputFormat::Toml), "csv" => Ok(OutputFormat::Csv), "json" => Ok(OutputFormat::Json), "plain" => Ok(OutputFormat::Plain), format => Err(format!("Unknown output format {}", format).into()), - }; + } } } impl OutputFormat { fn as_accept_header(&self) -> header::HeaderValue { - return header::HeaderValue::from_static(match self { + header::HeaderValue::from_static(match self { OutputFormat::Json => "application/json", OutputFormat::Csv => "text/csv", OutputFormat::Toml => "application/toml", OutputFormat::Plain => "text/plain", - }); + }) } } @@ -91,18 +91,18 @@ impl DataSource { if let Some(url) = url_arg { return Url::parse(&url) - .map(|parsed_url| DataSource::Url(parsed_url)) + .map(DataSource::Url) .map_err(|e| format!("Failed to parse {} as url: {}", url, e).into()); } - return Err(GET_DATA_ARGUMENT_ERROR_MESSAGE.into()); + Err(GET_DATA_ARGUMENT_ERROR_MESSAGE.into()) } fn get_cache_key(&self, format: &OutputFormat) -> u64 { let mut hasher = DefaultHasher::new(); format.hash(&mut hasher); self.hash(&mut hasher); - return hasher.finish(); + hasher.finish() } } @@ -123,10 +123,9 @@ fn get_data_source_from_args( args: &HashMap, ) -> Result { let path_arg = optional_arg!(String, args.get("path"), GET_DATA_ARGUMENT_ERROR_MESSAGE); - let url_arg = optional_arg!(String, args.get("url"), GET_DATA_ARGUMENT_ERROR_MESSAGE); - return DataSource::from_args(path_arg, url_arg, content_path); + DataSource::from_args(path_arg, url_arg, content_path) } fn read_data_file(base_path: &PathBuf, full_path: PathBuf) -> Result { @@ -140,9 +139,9 @@ fn read_data_file(base_path: &PathBuf, full_path: PathBuf) -> Result { ) .into()); } - return read_file(&full_path).map_err(|e| { + read_file(&full_path).map_err(|e| { format!("`load_data`: error {} loading file {}", full_path.to_str().unwrap(), e).into() - }); + }) } fn get_output_format_from_args( @@ -161,14 +160,14 @@ fn get_output_format_from_args( let from_extension = if let DataSource::Path(path) = data_source { let extension_result: Result<&str> = - path.extension().map(|extension| extension.to_str().unwrap()).ok_or( - format!("Could not determine format for {} from extension", path.display()).into(), - ); + path.extension().map(|extension| extension.to_str().unwrap()).ok_or_else(|| { + format!("Could not determine format for {} from extension", path.display()).into() + }); extension_result? } else { "plain" }; - return OutputFormat::from_str(from_extension); + OutputFormat::from_str(from_extension) } /// A global function to load data from a file or from a URL @@ -231,7 +230,7 @@ pub fn make_load_data(content_path: PathBuf, base_path: PathBuf) -> GlobalFn { fn load_json(json_data: String) -> Result { let json_content: Value = serde_json::from_str(json_data.as_str()).map_err(|e| format!("{:?}", e))?; - return Ok(json_content); + Ok(json_content) } /// Parse a TOML string and convert it to a Tera Value diff --git a/components/templates/src/global_fns/mod.rs b/components/templates/src/global_fns/mod.rs index 83cffb7..bf58931 100644 --- a/components/templates/src/global_fns/mod.rs +++ b/components/templates/src/global_fns/mod.rs @@ -142,9 +142,11 @@ pub fn make_get_taxonomy(all_taxonomies: &[Taxonomy], library: &Library) -> Glob let container = match taxonomies.get(&kind) { Some(c) => c, None => { - return Err( - format!("`get_taxonomy` received an unknown taxonomy as kind: {}", kind).into() - ); + return Err(format!( + "`get_taxonomy` received an unknown taxonomy as kind: {}", + kind + ) + .into()); } }; diff --git a/src/cmd/serve.rs b/src/cmd/serve.rs index d675834..1f22eea 100644 --- a/src/cmd/serve.rs +++ b/src/cmd/serve.rs @@ -502,12 +502,9 @@ fn detect_change_kind(pwd: &Path, path: &Path) -> (ChangeKind, PathBuf) { /// Check if the directory at path contains any file fn is_folder_empty(dir: &Path) -> bool { // Can panic if we don't have the rights I guess? - for _ in read_dir(dir).expect("Failed to read a directory to see if it was empty") { - // If we get there, that means we have a file - return false; - } - - true + let files: Vec<_> = + read_dir(dir).expect("Failed to read a directory to see if it was empty").collect(); + files.is_empty() } #[cfg(test)]