@@ -46,6 +46,9 @@ pub struct FileInfo { | |||
/// For example a file at content/kb/solutions/blabla.md will have 2 components: | |||
/// `kb` and `solutions` | |||
pub components: Vec<String>, | |||
/// This is `parent` + `name`, used to find content referring to the same content but in | |||
/// various languages. | |||
pub canonical: PathBuf, | |||
} | |||
impl FileInfo { | |||
@@ -74,6 +77,7 @@ impl FileInfo { | |||
path: file_path, | |||
// We don't care about grand parent for pages | |||
grand_parent: None, | |||
canonical: parent.join(&name), | |||
parent, | |||
name, | |||
components, | |||
@@ -96,6 +100,7 @@ impl FileInfo { | |||
FileInfo { | |||
filename: file_path.file_name().unwrap().to_string_lossy().to_string(), | |||
path: file_path, | |||
canonical: parent.join(&name), | |||
parent, | |||
grand_parent, | |||
name, | |||
@@ -128,6 +133,7 @@ impl FileInfo { | |||
} | |||
self.name = parts.swap_remove(0); | |||
self.canonical = self.parent.join(&self.name); | |||
let lang = parts.swap_remove(0); | |||
Ok(Some(lang)) | |||
@@ -145,6 +151,7 @@ impl Default for FileInfo { | |||
name: String::new(), | |||
components: vec![], | |||
relative: String::new(), | |||
canonical: PathBuf::new(), | |||
} | |||
} | |||
} | |||
@@ -7,6 +7,38 @@ use content::{Page, Section}; | |||
use library::Library; | |||
use rendering::Header; | |||
#[derive(Clone, Debug, PartialEq, Serialize)] | |||
pub struct TranslatedContent<'a> { | |||
lang: &'a Option<String>, | |||
permalink: &'a str, | |||
title: &'a Option<String>, | |||
} | |||
impl<'a> TranslatedContent<'a> { | |||
// copypaste eh, not worth creating an enum imo | |||
pub fn find_all_sections(section: &'a Section, library: &'a Library) -> Vec<Self> { | |||
let mut translations = vec![]; | |||
for key in §ion.translations { | |||
let other = library.get_section_by_key(*key); | |||
translations.push(TranslatedContent { lang: &other.lang, permalink: &other.permalink, title: &other.meta.title }); | |||
} | |||
translations | |||
} | |||
pub fn find_all_pages(page: &'a Page, library: &'a Library) -> Vec<Self> { | |||
let mut translations = vec![]; | |||
for key in &page.translations { | |||
let other = library.get_page_by_key(*key); | |||
translations.push(TranslatedContent { lang: &other.lang, permalink: &other.permalink, title: &other.meta.title }); | |||
} | |||
translations | |||
} | |||
} | |||
#[derive(Clone, Debug, PartialEq, Serialize)] | |||
pub struct SerializingPage<'a> { | |||
relative_path: &'a str, | |||
@@ -35,6 +67,7 @@ pub struct SerializingPage<'a> { | |||
heavier: Option<Box<SerializingPage<'a>>>, | |||
earlier: Option<Box<SerializingPage<'a>>>, | |||
later: Option<Box<SerializingPage<'a>>>, | |||
translations: Vec<TranslatedContent<'a>>, | |||
} | |||
impl<'a> SerializingPage<'a> { | |||
@@ -67,6 +100,8 @@ impl<'a> SerializingPage<'a> { | |||
.map(|k| library.get_section_by_key(*k).file.relative.clone()) | |||
.collect(); | |||
let translations = TranslatedContent::find_all_pages(page, library); | |||
SerializingPage { | |||
relative_path: &page.file.relative, | |||
ancestors, | |||
@@ -94,6 +129,7 @@ impl<'a> SerializingPage<'a> { | |||
heavier, | |||
earlier, | |||
later, | |||
translations, | |||
} | |||
} | |||
@@ -116,6 +152,12 @@ impl<'a> SerializingPage<'a> { | |||
vec![] | |||
}; | |||
let translations = if let Some(ref lib) = library { | |||
TranslatedContent::find_all_pages(page, lib) | |||
} else { | |||
vec![] | |||
}; | |||
SerializingPage { | |||
relative_path: &page.file.relative, | |||
ancestors, | |||
@@ -143,6 +185,7 @@ impl<'a> SerializingPage<'a> { | |||
heavier: None, | |||
earlier: None, | |||
later: None, | |||
translations, | |||
} | |||
} | |||
} | |||
@@ -165,6 +208,7 @@ pub struct SerializingSection<'a> { | |||
assets: &'a [String], | |||
pages: Vec<SerializingPage<'a>>, | |||
subsections: Vec<&'a str>, | |||
translations: Vec<TranslatedContent<'a>>, | |||
} | |||
impl<'a> SerializingSection<'a> { | |||
@@ -185,6 +229,7 @@ impl<'a> SerializingSection<'a> { | |||
.iter() | |||
.map(|k| library.get_section_by_key(*k).file.relative.clone()) | |||
.collect(); | |||
let translations = TranslatedContent::find_all_sections(section, library); | |||
SerializingSection { | |||
relative_path: §ion.file.relative, | |||
@@ -203,6 +248,7 @@ impl<'a> SerializingSection<'a> { | |||
lang: §ion.lang, | |||
pages, | |||
subsections, | |||
translations, | |||
} | |||
} | |||
@@ -218,6 +264,12 @@ impl<'a> SerializingSection<'a> { | |||
vec![] | |||
}; | |||
let translations = if let Some(ref lib) = library { | |||
TranslatedContent::find_all_sections(section, lib) | |||
} else { | |||
vec![] | |||
}; | |||
SerializingSection { | |||
relative_path: §ion.file.relative, | |||
ancestors, | |||
@@ -235,6 +287,7 @@ impl<'a> SerializingSection<'a> { | |||
lang: §ion.lang, | |||
pages: vec![], | |||
subsections: vec![], | |||
translations, | |||
} | |||
} | |||
} |
@@ -22,18 +22,21 @@ pub struct Library { | |||
/// All the sections of the site | |||
sections: DenseSlotMap<Section>, | |||
/// A mapping path -> key for pages so we can easily get their key | |||
paths_to_pages: HashMap<PathBuf, Key>, | |||
pub paths_to_pages: HashMap<PathBuf, Key>, | |||
/// A mapping path -> key for sections so we can easily get their key | |||
pub paths_to_sections: HashMap<PathBuf, Key>, | |||
/// Whether we need to look for translations | |||
is_multilingual: bool, | |||
} | |||
impl Library { | |||
pub fn new(cap_pages: usize, cap_sections: usize) -> Self { | |||
pub fn new(cap_pages: usize, cap_sections: usize, is_multilingual: bool) -> Self { | |||
Library { | |||
pages: DenseSlotMap::with_capacity(cap_pages), | |||
sections: DenseSlotMap::with_capacity(cap_sections), | |||
paths_to_pages: HashMap::with_capacity(cap_pages), | |||
paths_to_sections: HashMap::with_capacity(cap_sections), | |||
is_multilingual, | |||
} | |||
} | |||
@@ -116,10 +119,10 @@ impl Library { | |||
continue; | |||
} | |||
if let Some(section_key) = | |||
self.paths_to_sections.get(&path.join(§ion.file.filename)) | |||
{ | |||
parents.push(*section_key); | |||
} | |||
self.paths_to_sections.get(&path.join(§ion.file.filename)) | |||
{ | |||
parents.push(*section_key); | |||
} | |||
} | |||
ancestors.insert(section.file.path.clone(), parents); | |||
} | |||
@@ -169,6 +172,7 @@ impl Library { | |||
} | |||
} | |||
self.populate_translations(); | |||
self.sort_sections_pages(); | |||
let sections = self.paths_to_sections.clone(); | |||
@@ -188,7 +192,8 @@ impl Library { | |||
} | |||
} | |||
/// Sort all sections pages | |||
/// Sort all sections pages according to sorting method given | |||
/// Pages that cannot be sorted are set to the section.ignored_pages instead | |||
pub fn sort_sections_pages(&mut self) { | |||
let mut updates = HashMap::new(); | |||
for (key, section) in &self.sections { | |||
@@ -268,6 +273,52 @@ impl Library { | |||
} | |||
} | |||
/// Finds all the translations for each section/page and set the `translations` | |||
/// field of each as needed | |||
/// A no-op for sites without multiple languages | |||
fn populate_translations(&mut self) { | |||
if !self.is_multilingual { | |||
return; | |||
} | |||
// Sections first | |||
let mut sections_translations = HashMap::new(); | |||
for (key, section) in &self.sections { | |||
sections_translations | |||
.entry(section.file.canonical.clone()) // TODO: avoid this clone | |||
.or_insert_with(Vec::new) | |||
.push(key); | |||
} | |||
for (key, section) in self.sections.iter_mut() { | |||
let translations = §ions_translations[§ion.file.canonical]; | |||
if translations.len() == 1 { | |||
section.translations = vec![]; | |||
continue; | |||
} | |||
section.translations = translations.iter().filter(|k| **k != key).cloned().collect(); | |||
} | |||
// Same thing for pages | |||
let mut pages_translations = HashMap::new(); | |||
for (key, page) in &self.pages { | |||
pages_translations | |||
.entry(page.file.canonical.clone()) // TODO: avoid this clone | |||
.or_insert_with(Vec::new) | |||
.push(key); | |||
} | |||
for (key, page) in self.pages.iter_mut() { | |||
let translations = &pages_translations[&page.file.canonical]; | |||
if translations.len() == 1 { | |||
page.translations = vec![]; | |||
continue; | |||
} | |||
page.translations = translations.iter().filter(|k| **k != key).cloned().collect(); | |||
} | |||
} | |||
/// Find all the orphan pages: pages that are in a folder without an `_index.md` | |||
pub fn get_all_orphan_pages(&self) -> Vec<&Page> { | |||
let pages_in_sections = | |||
@@ -254,7 +254,7 @@ mod tests { | |||
} | |||
fn create_library(is_index: bool) -> (Section, Library) { | |||
let mut library = Library::new(3, 0); | |||
let mut library = Library::new(3, 0, false); | |||
library.insert_page(Page::default()); | |||
library.insert_page(Page::default()); | |||
library.insert_page(Page::default()); | |||
@@ -227,7 +227,7 @@ mod tests { | |||
#[test] | |||
fn can_make_taxonomies() { | |||
let mut config = Config::default(); | |||
let mut library = Library::new(2, 0); | |||
let mut library = Library::new(2, 0, false); | |||
config.taxonomies = vec![ | |||
TaxonomyConfig { name: "categories".to_string(), ..TaxonomyConfig::default() }, | |||
@@ -307,7 +307,7 @@ mod tests { | |||
#[test] | |||
fn errors_on_unknown_taxonomy() { | |||
let mut config = Config::default(); | |||
let mut library = Library::new(2, 0); | |||
let mut library = Library::new(2, 0, false); | |||
config.taxonomies = | |||
vec![TaxonomyConfig { name: "authors".to_string(), ..TaxonomyConfig::default() }]; | |||
@@ -141,7 +141,7 @@ impl Site { | |||
taxonomies: Vec::new(), | |||
permalinks: HashMap::new(), | |||
// We will allocate it properly later on | |||
library: Library::new(0, 0), | |||
library: Library::new(0, 0, false), | |||
}; | |||
Ok(site) | |||
@@ -173,7 +173,7 @@ impl Site { | |||
} | |||
pub fn set_base_url(&mut self, base_url: String) { | |||
let mut imageproc = self.imageproc.lock().unwrap(); | |||
let mut imageproc = self.imageproc.lock().expect("Couldn't lock imageproc (set_base_url)"); | |||
imageproc.set_base_url(&base_url); | |||
self.config.base_url = base_url; | |||
} | |||
@@ -189,14 +189,14 @@ impl Site { | |||
let content_glob = format!("{}/{}", base_path, "content/**/*.md"); | |||
let (section_entries, page_entries): (Vec<_>, Vec<_>) = glob(&content_glob) | |||
.unwrap() | |||
.expect("Invalid glob") | |||
.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.") | |||
}); | |||
self.library = Library::new(page_entries.len(), section_entries.len()); | |||
self.library = Library::new(page_entries.len(), section_entries.len(), self.config.is_multilingual()); | |||
let sections = { | |||
let config = &self.config; | |||
@@ -452,12 +452,12 @@ impl Site { | |||
} | |||
pub fn num_img_ops(&self) -> usize { | |||
let imageproc = self.imageproc.lock().unwrap(); | |||
let imageproc = self.imageproc.lock().expect("Couldn't lock imageproc (num_img_ops)"); | |||
imageproc.num_img_ops() | |||
} | |||
pub fn process_images(&self) -> Result<()> { | |||
let mut imageproc = self.imageproc.lock().unwrap(); | |||
let mut imageproc = self.imageproc.lock().expect("Couldn't lock imageproc (process_images)"); | |||
imageproc.prune()?; | |||
imageproc.do_process() | |||
} | |||
@@ -497,7 +497,7 @@ impl Site { | |||
// Copy any asset we found previously into the same directory as the index.html | |||
for asset in &page.assets { | |||
let asset_path = asset.as_path(); | |||
copy(&asset_path, ¤t_path.join(asset_path.file_name().unwrap()))?; | |||
copy(&asset_path, ¤t_path.join(asset_path.file_name().expect("Couldn't get filename from page asset")))?; | |||
} | |||
Ok(()) | |||
@@ -626,7 +626,7 @@ impl Site { | |||
) -> Result<Vec<(PathBuf, PathBuf)>> { | |||
let glob_string = format!("{}/**/*.{}", sass_path.display(), extension); | |||
let files = glob(&glob_string) | |||
.unwrap() | |||
.expect("Invalid glob for sass") | |||
.filter_map(|e| e.ok()) | |||
.filter(|entry| { | |||
!entry.as_path().file_name().unwrap().to_string_lossy().starts_with('_') | |||
@@ -920,7 +920,7 @@ impl Site { | |||
// Copy any asset we found previously into the same directory as the index.html | |||
for asset in §ion.assets { | |||
let asset_path = asset.as_path(); | |||
copy(&asset_path, &output_path.join(asset_path.file_name().unwrap()))?; | |||
copy(&asset_path, &output_path.join(asset_path.file_name().expect("Failed to get asset filename for section")))?; | |||
} | |||
if render_pages { | |||
@@ -957,7 +957,7 @@ impl Site { | |||
/// Used only on reload | |||
pub fn render_index(&self) -> Result<()> { | |||
self.render_section( | |||
&self.library.get_section(&self.content_path.join("_index.md")).unwrap(), | |||
&self.library.get_section(&self.content_path.join("_index.md")).expect("Failed to get index section"), | |||
false, | |||
) | |||
} | |||
@@ -27,10 +27,10 @@ macro_rules! file_contains { | |||
for component in $path.split("/") { | |||
path = path.join(component); | |||
} | |||
let mut file = std::fs::File::open(&path).unwrap(); | |||
let mut file = std::fs::File::open(&path).expect(&format!("Failed to open {:?}", $path)); | |||
let mut s = String::new(); | |||
file.read_to_string(&mut s).unwrap(); | |||
// println!("{}", s); | |||
println!("{}", s); | |||
s.contains($text) | |||
}}; | |||
} | |||
@@ -45,7 +45,7 @@ pub fn build_site(name: &str) -> (Site, TempDir, PathBuf) { | |||
let tmp_dir = tempdir().expect("create temp dir"); | |||
let public = &tmp_dir.path().join("public"); | |||
site.set_output_path(&public); | |||
site.build().unwrap(); | |||
site.build().expect("Couldn't build the site"); | |||
(site, tmp_dir, public.clone()) | |||
} | |||
@@ -64,6 +64,6 @@ where | |||
let tmp_dir = tempdir().expect("create temp dir"); | |||
let public = &tmp_dir.path().join("public"); | |||
site.set_output_path(&public); | |||
site.build().unwrap(); | |||
site.build().expect("Couldn't build the site"); | |||
(site, tmp_dir, public.clone()) | |||
} |
@@ -70,10 +70,21 @@ fn can_build_multilingual_site() { | |||
assert!(file_exists!(public, "base/index.html")); | |||
assert!(file_exists!(public, "fr/base/index.html")); | |||
// Sections are there as well | |||
// Sections are there as well, with translations info | |||
assert!(file_exists!(public, "blog/index.html")); | |||
assert!(file_contains!(public, "blog/index.html", "Translated in fr: Mon blog https://example.com/fr/blog/")); | |||
assert!(file_contains!(public, "blog/index.html", "Translated in it: Il mio blog https://example.com/it/blog/")); | |||
assert!(file_exists!(public, "fr/blog/index.html")); | |||
assert!(file_contains!(public, "fr/blog/index.html", "Language: fr")); | |||
assert!(file_contains!(public, "fr/blog/index.html", "Translated in : My blog https://example.com/blog/")); | |||
assert!(file_contains!(public, "fr/blog/index.html", "Translated in it: Il mio blog https://example.com/it/blog/")); | |||
// Normal pages are there with the translations | |||
assert!(file_exists!(public, "blog/something/index.html")); | |||
assert!(file_contains!(public, "blog/something/index.html", "Translated in fr: Quelque chose https://example.com/fr/blog/something/")); | |||
assert!(file_exists!(public, "fr/blog/something/index.html")); | |||
assert!(file_contains!(public, "fr/blog/something/index.html", "Language: fr")); | |||
assert!(file_contains!(public, "fr/blog/something/index.html", "Translated in : Something https://example.com/blog/something/")); | |||
// sitemap contains all languages | |||
assert!(file_exists!(public, "sitemap.xml")); | |||
@@ -296,7 +296,7 @@ mod tests { | |||
#[test] | |||
fn can_get_taxonomy() { | |||
let taxo_config = TaxonomyConfig { name: "tags".to_string(), ..TaxonomyConfig::default() }; | |||
let library = Library::new(0, 0); | |||
let library = Library::new(0, 0, false); | |||
let tag = TaxonomyItem::new("Programming", "tags", &Config::default(), vec![], &library); | |||
let tags = Taxonomy { kind: taxo_config, items: vec![tag] }; | |||
@@ -335,7 +335,7 @@ mod tests { | |||
#[test] | |||
fn can_get_taxonomy_url() { | |||
let taxo_config = TaxonomyConfig { name: "tags".to_string(), ..TaxonomyConfig::default() }; | |||
let library = Library::new(0, 0); | |||
let library = Library::new(0, 0, false); | |||
let tag = TaxonomyItem::new("Programming", "tags", &Config::default(), vec![], &library); | |||
let tags = Taxonomy { kind: taxo_config, items: vec![tag] }; | |||
@@ -52,7 +52,9 @@ ancestors: Array<String>; | |||
// The relative path from the `content` directory to the markdown file | |||
relative_path: String; | |||
// The language for the page if there is one | |||
lang: String? | |||
lang: String?; | |||
// Information about all the available languages for that content | |||
translations: Array<TranslatedContent>; | |||
``` | |||
## Section variables | |||
@@ -96,7 +98,9 @@ ancestors: Array<String>; | |||
// The relative path from the `content` directory to the markdown file | |||
relative_path: String; | |||
// The language for the section if there is one | |||
lang: String? | |||
lang: String?; | |||
// Information about all the available languages for that content | |||
translations: Array<TranslatedContent>; | |||
``` | |||
## Table of contents | |||
@@ -116,3 +120,19 @@ permalink: String; | |||
// All lower level headers below this header | |||
children: Array<Header>; | |||
``` | |||
## Translated content | |||
Both page and section have a `translations` field which corresponds to an array of `TranslatedContent`. If your site is not using multiple languages, | |||
this will always be an empty array. | |||
A `TranslatedContent` has the following fields: | |||
```ts | |||
// The language code for that content, empty if it is the default language | |||
lang: String?; | |||
// The title of that content if there is one | |||
title: String?; | |||
// A permalink to that content | |||
permalink: String; | |||
``` | |||
@@ -1,4 +1,5 @@ | |||
+++ | |||
title = "Mon blog" | |||
sort_by = "date" | |||
insert_anchors = "right" | |||
+++ |
@@ -1,4 +1,5 @@ | |||
+++ | |||
title = "Il mio blog" | |||
sort_by = "date" | |||
insert_anchors = "right" | |||
+++ |
@@ -1,4 +1,5 @@ | |||
+++ | |||
title = "My blog" | |||
sort_by = "date" | |||
insert_anchors = "left" | |||
+++ |
@@ -1,2 +1,8 @@ | |||
{{page.title}} | |||
{{page.content | safe}} | |||
Language: {{lang}} | |||
{% for t in page.translations %} | |||
Translated in {{t.lang|default(value=config.default_language)}}: {{t.title}} {{t.permalink|safe}} | |||
{% endfor %} | |||
@@ -2,3 +2,7 @@ | |||
{{page.title}} | |||
{% endfor %} | |||
Language: {{lang}} | |||
{% for t in section.translations %} | |||
Translated in {{t.lang|default(value=config.default_language)}}: {{t.title}} {{t.permalink|safe}} | |||
{% endfor %} |