@@ -1,6 +1,7 @@ | |||||
target | target | ||||
.idea/ | .idea/ | ||||
test_site/public | test_site/public | ||||
test_site_i18n/public | |||||
docs/public | docs/public | ||||
small-blog | small-blog | ||||
@@ -31,6 +31,8 @@ pub fn find_content_components<P: AsRef<Path>>(path: P) -> Vec<String> { | |||||
pub struct FileInfo { | pub struct FileInfo { | ||||
/// The full path to the .md file | /// The full path to the .md file | ||||
pub path: PathBuf, | pub path: PathBuf, | ||||
/// The on-disk filename, will differ from the `name` when there is a language code in it | |||||
pub filename: String, | |||||
/// The name of the .md file without the extension, always `_index` for sections | /// The name of the .md file without the extension, always `_index` for sections | ||||
/// Doesn't contain the language if there was one in the filename | /// Doesn't contain the language if there was one in the filename | ||||
pub name: String, | pub name: String, | ||||
@@ -68,6 +70,7 @@ impl FileInfo { | |||||
} | } | ||||
FileInfo { | FileInfo { | ||||
filename: file_path.file_name().unwrap().to_string_lossy().to_string(), | |||||
path: file_path, | path: file_path, | ||||
// We don't care about grand parent for pages | // We don't care about grand parent for pages | ||||
grand_parent: None, | grand_parent: None, | ||||
@@ -79,6 +82,7 @@ impl FileInfo { | |||||
} | } | ||||
pub fn new_section(path: &Path) -> FileInfo { | pub fn new_section(path: &Path) -> FileInfo { | ||||
let file_path = path.to_path_buf(); | |||||
let parent = path.parent().unwrap().to_path_buf(); | let parent = path.parent().unwrap().to_path_buf(); | ||||
let name = path.file_stem().unwrap().to_string_lossy().to_string(); | let name = path.file_stem().unwrap().to_string_lossy().to_string(); | ||||
let components = find_content_components(path); | let components = find_content_components(path); | ||||
@@ -90,7 +94,8 @@ impl FileInfo { | |||||
let grand_parent = parent.parent().map(|p| p.to_path_buf()); | let grand_parent = parent.parent().map(|p| p.to_path_buf()); | ||||
FileInfo { | FileInfo { | ||||
path: path.to_path_buf(), | |||||
filename: file_path.file_name().unwrap().to_string_lossy().to_string(), | |||||
path: file_path, | |||||
parent, | parent, | ||||
grand_parent, | grand_parent, | ||||
name, | name, | ||||
@@ -136,6 +141,7 @@ impl Default for FileInfo { | |||||
path: PathBuf::new(), | path: PathBuf::new(), | ||||
parent: PathBuf::new(), | parent: PathBuf::new(), | ||||
grand_parent: None, | grand_parent: None, | ||||
filename: String::new(), | |||||
name: String::new(), | name: String::new(), | ||||
components: vec![], | components: vec![], | ||||
relative: String::new(), | relative: String::new(), | ||||
@@ -80,14 +80,12 @@ impl Library { | |||||
/// Find out the direct subsections of each subsection if there are some | /// Find out the direct subsections of each subsection if there are some | ||||
/// as well as the pages for each section | /// as well as the pages for each section | ||||
pub fn populate_sections(&mut self) { | pub fn populate_sections(&mut self) { | ||||
let (root_path, index_path) = self | |||||
let root_path= self | |||||
.sections | .sections | ||||
.values() | .values() | ||||
.find(|s| s.is_index()) | .find(|s| s.is_index()) | ||||
.map(|s| (s.file.parent.clone(), s.file.path.clone())) | |||||
.map(|s| s.file.parent.clone()) | |||||
.unwrap(); | .unwrap(); | ||||
let root_key = self.paths_to_sections[&index_path]; | |||||
// We are going to get both the ancestors and grandparents for each section in one go | // We are going to get both the ancestors and grandparents for each section in one go | ||||
let mut ancestors: HashMap<PathBuf, Vec<_>> = HashMap::new(); | let mut ancestors: HashMap<PathBuf, Vec<_>> = HashMap::new(); | ||||
let mut subsections: HashMap<PathBuf, Vec<_>> = HashMap::new(); | let mut subsections: HashMap<PathBuf, Vec<_>> = HashMap::new(); | ||||
@@ -99,7 +97,8 @@ impl Library { | |||||
if let Some(ref grand_parent) = section.file.grand_parent { | if let Some(ref grand_parent) = section.file.grand_parent { | ||||
subsections | subsections | ||||
.entry(grand_parent.join("_index.md")) | |||||
// Using the original filename to work for multi-lingual sections | |||||
.entry(grand_parent.join(§ion.file.filename)) | |||||
.or_insert_with(|| vec![]) | .or_insert_with(|| vec![]) | ||||
.push(section.file.path.clone()); | .push(section.file.path.clone()); | ||||
} | } | ||||
@@ -111,6 +110,7 @@ impl Library { | |||||
} | } | ||||
let mut path = root_path.clone(); | let mut path = root_path.clone(); | ||||
let root_key = self.paths_to_sections[&root_path.join(§ion.file.filename)]; | |||||
// Index section is the first ancestor of every single section | // Index section is the first ancestor of every single section | ||||
let mut parents = vec![root_key]; | let mut parents = vec![root_key]; | ||||
for component in §ion.file.components { | for component in §ion.file.components { | ||||
@@ -119,7 +119,7 @@ impl Library { | |||||
if path == section.file.parent { | if path == section.file.parent { | ||||
continue; | continue; | ||||
} | } | ||||
if let Some(section_key) = self.paths_to_sections.get(&path.join("_index.md")) { | |||||
if let Some(section_key) = self.paths_to_sections.get(&path.join(§ion.file.filename)) { | |||||
parents.push(*section_key); | parents.push(*section_key); | ||||
} | } | ||||
} | } | ||||
@@ -127,7 +127,12 @@ impl Library { | |||||
} | } | ||||
for (key, page) in &mut self.pages { | for (key, page) in &mut self.pages { | ||||
let mut parent_section_path = page.file.parent.join("_index.md"); | |||||
let parent_filename = if let Some(ref lang) = page.lang { | |||||
format!("_index.{}.md", lang) | |||||
} else { | |||||
"_index.md".to_string() | |||||
}; | |||||
let mut parent_section_path = page.file.parent.join(&parent_filename); | |||||
while let Some(section_key) = self.paths_to_sections.get(&parent_section_path) { | while let Some(section_key) = self.paths_to_sections.get(&parent_section_path) { | ||||
let parent_is_transparent; | let parent_is_transparent; | ||||
// We need to get a reference to a section later so keep the scope of borrowing small | // We need to get a reference to a section later so keep the scope of borrowing small | ||||
@@ -158,9 +163,9 @@ impl Library { | |||||
break; | break; | ||||
} | } | ||||
// We've added `_index.md` so if we are here so we need to go up twice | |||||
// We've added `_index(.{LANG})?.md` so if we are here so we need to go up twice | |||||
match parent_section_path.clone().parent().unwrap().parent() { | match parent_section_path.clone().parent().unwrap().parent() { | ||||
Some(parent) => parent_section_path = parent.join("_index.md"), | |||||
Some(parent) => parent_section_path = parent.join(&parent_filename), | |||||
None => break, | None => break, | ||||
} | } | ||||
} | } | ||||
@@ -147,9 +147,14 @@ impl Site { | |||||
Ok(site) | Ok(site) | ||||
} | } | ||||
/// The index section is ALWAYS at that path | |||||
pub fn index_section_path(&self) -> PathBuf { | |||||
self.content_path.join("_index.md") | |||||
/// The index sections are ALWAYS at those paths | |||||
/// There are one index section for the basic language + 1 per language | |||||
fn index_section_paths(&self) -> Vec<(PathBuf, Option<String>)> { | |||||
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 | |||||
} | } | ||||
/// We avoid the port the server is going to use as it's not bound yet | /// We avoid the port the server is going to use as it's not bound yet | ||||
@@ -184,7 +189,7 @@ impl Site { | |||||
.unwrap() | .unwrap() | ||||
.filter_map(|e| e.ok()) | .filter_map(|e| e.ok()) | ||||
.filter(|e| !e.as_path().file_name().unwrap().to_str().unwrap().starts_with('.')) | .filter(|e| !e.as_path().file_name().unwrap().to_str().unwrap().starts_with('.')) | ||||
.partition(|entry| entry.as_path().file_name().unwrap() == "_index.md"); | |||||
.partition(|entry| entry.as_path().file_name().unwrap().to_str().unwrap().starts_with("_index.")); | |||||
self.library = Library::new(page_entries.len(), section_entries.len()); | self.library = Library::new(page_entries.len(), section_entries.len()); | ||||
@@ -219,26 +224,37 @@ impl Site { | |||||
self.add_section(s, false)?; | self.add_section(s, false)?; | ||||
} | } | ||||
// Insert a default index section if necessary so we don't need to create | |||||
// Insert a default index section for each language if necessary so we don't need to create | |||||
// a _index.md to render the index page at the root of the site | // a _index.md to render the index page at the root of the site | ||||
let index_path = self.index_section_path(); | |||||
if let Some(ref index_section) = self.library.get_section(&index_path) { | |||||
if self.config.build_search_index && !index_section.meta.in_search_index { | |||||
bail!( | |||||
for (index_path, lang) in self.index_section_paths() { | |||||
if let Some(ref index_section) = self.library.get_section(&index_path) { | |||||
if self.config.build_search_index && !index_section.meta.in_search_index { | |||||
bail!( | |||||
"You have enabled search in the config but disabled it in the index section: \ | "You have enabled search in the config but disabled it in the index section: \ | ||||
either turn off the search in the config or remote `in_search_index = true` from the \ | either turn off the search in the config or remote `in_search_index = true` from the \ | ||||
section front-matter." | section front-matter." | ||||
) | |||||
) | |||||
} | |||||
} | |||||
// Not in else because of borrow checker | |||||
if !self.library.contains_section(&index_path) { | |||||
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(); | |||||
if let Some(ref l) = lang { | |||||
index_section.permalink = self.config.make_permalink(l); | |||||
let filename = format!("_index.{}.md", l); | |||||
index_section.file.path = self.content_path.join(&filename); | |||||
index_section.file.relative = filename; | |||||
index_section.lang = Some(l.clone()); | |||||
} else { | |||||
index_section.permalink = self.config.make_permalink(""); | |||||
index_section.file.path = self.content_path.join("_index.md"); | |||||
index_section.file.relative = "_index.md".to_string(); | |||||
} | |||||
self.library.insert_section(index_section); | |||||
} | } | ||||
} | |||||
// Not in else because of borrow checker | |||||
if !self.library.contains_section(&index_path) { | |||||
let mut index_section = Section::default(); | |||||
index_section.permalink = self.config.make_permalink(""); | |||||
index_section.file.path = self.content_path.join("_index.md"); | |||||
index_section.file.parent = self.content_path.clone(); | |||||
index_section.file.relative = "_index.md".to_string(); | |||||
self.library.insert_section(index_section); | |||||
} | } | ||||
let mut pages_insert_anchors = HashMap::new(); | let mut pages_insert_anchors = HashMap::new(); | ||||
@@ -246,7 +262,7 @@ impl Site { | |||||
let p = page?; | let p = page?; | ||||
pages_insert_anchors.insert( | pages_insert_anchors.insert( | ||||
p.file.path.clone(), | p.file.path.clone(), | ||||
self.find_parent_section_insert_anchor(&p.file.parent.clone()), | |||||
self.find_parent_section_insert_anchor(&p.file.parent.clone(), &p.lang), | |||||
); | ); | ||||
self.add_page(p, false)?; | self.add_page(p, false)?; | ||||
} | } | ||||
@@ -274,7 +290,7 @@ impl Site { | |||||
for (_, p) in self.library.pages() { | for (_, p) in self.library.pages() { | ||||
pages_insert_anchors.insert( | pages_insert_anchors.insert( | ||||
p.file.path.clone(), | p.file.path.clone(), | ||||
self.find_parent_section_insert_anchor(&p.file.parent.clone()), | |||||
self.find_parent_section_insert_anchor(&p.file.parent.clone(), &p.lang), | |||||
); | ); | ||||
} | } | ||||
@@ -337,7 +353,7 @@ impl Site { | |||||
pub fn add_page(&mut self, mut page: Page, render: bool) -> Result<Option<Page>> { | pub fn add_page(&mut self, mut page: Page, render: bool) -> Result<Option<Page>> { | ||||
self.permalinks.insert(page.file.relative.clone(), page.permalink.clone()); | self.permalinks.insert(page.file.relative.clone(), page.permalink.clone()); | ||||
if render { | if render { | ||||
let insert_anchor = self.find_parent_section_insert_anchor(&page.file.parent); | |||||
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)?; | page.render_markdown(&self.permalinks, &self.tera, &self.config, insert_anchor)?; | ||||
} | } | ||||
let prev = self.library.remove_page(&page.file.path); | let prev = self.library.remove_page(&page.file.path); | ||||
@@ -363,8 +379,13 @@ impl Site { | |||||
/// Finds the insert_anchor for the parent section of the directory at `path`. | /// Finds the insert_anchor for the parent section of the directory at `path`. | ||||
/// Defaults to `AnchorInsert::None` if no parent section found | /// Defaults to `AnchorInsert::None` if no parent section found | ||||
pub fn find_parent_section_insert_anchor(&self, parent_path: &PathBuf) -> InsertAnchor { | |||||
match self.library.get_section(&parent_path.join("_index.md")) { | |||||
pub fn find_parent_section_insert_anchor(&self, parent_path: &PathBuf, lang: &Option<String>) -> InsertAnchor { | |||||
let parent = if let Some(ref l) = lang { | |||||
parent_path.join(format!("_index.{}.md", l)) | |||||
} else { | |||||
parent_path.join("_index.md") | |||||
}; | |||||
match self.library.get_section(&parent) { | |||||
Some(s) => s.meta.insert_anchor_links, | Some(s) => s.meta.insert_anchor_links, | ||||
None => InsertAnchor::None, | None => InsertAnchor::None, | ||||
} | } | ||||
@@ -0,0 +1,46 @@ | |||||
extern crate site; | |||||
use std::env; | |||||
use site::Site; | |||||
#[test] | |||||
fn can_parse_multilingual_site() { | |||||
let mut path = env::current_dir().unwrap().parent().unwrap().parent().unwrap().to_path_buf(); | |||||
path.push("test_site_i18n"); | |||||
let mut site = Site::new(&path, "config.toml").unwrap(); | |||||
site.load().unwrap(); | |||||
assert_eq!(site.library.pages().len(), 9); | |||||
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(); | |||||
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(); | |||||
assert_eq!(fr_index_section.pages.len(), 1); | |||||
assert!(fr_index_section.ancestors.is_empty()); | |||||
// blog sections get only their own language pages | |||||
let blog_path = path.join("content").join("blog"); | |||||
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()]); | |||||
for key in &default_blog.pages { | |||||
let page = site.library.get_page_by_key(*key); | |||||
assert_eq!(page.lang, None); | |||||
} | |||||
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()]); | |||||
for key in &fr_blog.pages { | |||||
let page = site.library.get_page_by_key(*key); | |||||
assert_eq!(page.lang, Some("fr".to_string())); | |||||
} | |||||
} |
@@ -0,0 +1,19 @@ | |||||
# The URL the site will be built for | |||||
base_url = "https://example.com" | |||||
# Whether to automatically compile all Sass files in the sass directory | |||||
compile_sass = false | |||||
# Whether to do syntax highlighting | |||||
# Theme can be customised by setting the `highlight_theme` variable to a theme supported by Zola | |||||
highlight_code = false | |||||
# Whether to build a search index to be used later on by a JavaScript library | |||||
build_search_index = false | |||||
languages = [ | |||||
{code = "fr"}, | |||||
] | |||||
[extra] | |||||
# Put all your custom variables here |
@@ -0,0 +1,5 @@ | |||||
+++ | |||||
title = "Une page" | |||||
+++ | |||||
Une page en Français |
@@ -0,0 +1,5 @@ | |||||
+++ | |||||
title = "A page" | |||||
+++ | |||||
A page in english |
@@ -0,0 +1,4 @@ | |||||
+++ | |||||
sort_by = "date" | |||||
insert_anchors = "right" | |||||
+++ |
@@ -0,0 +1,4 @@ | |||||
+++ | |||||
sort_by = "date" | |||||
insert_anchors = "left" | |||||
+++ |
@@ -0,0 +1,7 @@ | |||||
+++ | |||||
title = "Un slug fix" | |||||
slug = "something-else" | |||||
date = 2017-01-01 | |||||
+++ | |||||
Une page qui definit son slug dans le front-matter |
@@ -0,0 +1,12 @@ | |||||
+++ | |||||
title = "Fixed slug" | |||||
slug = "something-else" | |||||
date = 2017-01-01 | |||||
+++ | |||||
A simple page with a slug defined | |||||
# Title | |||||
Hey | |||||
@@ -0,0 +1,5 @@ | |||||
+++ | |||||
date = 2018-08-19 | |||||
+++ | |||||
Something not translated |
@@ -0,0 +1,6 @@ | |||||
+++ | |||||
title = "Quelque chose" | |||||
date = 2018-10-09 | |||||
+++ | |||||
Un article |
@@ -0,0 +1,6 @@ | |||||
+++ | |||||
title = "Something" | |||||
date = 2018-10-09 | |||||
+++ | |||||
A blog post |
@@ -0,0 +1,5 @@ | |||||
+++ | |||||
date = 2018-11-10 | |||||
+++ | |||||
Avec des fichiers |
@@ -0,0 +1,5 @@ | |||||
+++ | |||||
date = 2018-11-10 | |||||
+++ | |||||
With assets |
@@ -0,0 +1,3 @@ | |||||
{% for page in section.pages %} | |||||
{{page.title}} | |||||
{% endfor %} |