240 lines
8.4 KiB
Rust
240 lines
8.4 KiB
Rust
use std::path::{Path, PathBuf};
|
|
|
|
use config::Config;
|
|
use errors::Result;
|
|
|
|
/// Takes a full path to a file and returns only the components after the first `content` directory
|
|
/// Will not return the filename as last component
|
|
pub fn find_content_components<P: AsRef<Path>>(path: P) -> Vec<String> {
|
|
let path = path.as_ref();
|
|
let mut is_in_content = false;
|
|
let mut components = vec![];
|
|
|
|
for section in path.parent().unwrap().components() {
|
|
let component = section.as_os_str().to_string_lossy();
|
|
|
|
if is_in_content {
|
|
components.push(component.to_string());
|
|
continue;
|
|
}
|
|
|
|
if component == "content" {
|
|
is_in_content = true;
|
|
}
|
|
}
|
|
|
|
components
|
|
}
|
|
|
|
/// Struct that contains all the information about the actual file
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub struct FileInfo {
|
|
/// The full path to the .md file
|
|
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
|
|
/// Doesn't contain the language if there was one in the filename
|
|
pub name: String,
|
|
/// The .md path, starting from the content directory, with `/` slashes
|
|
pub relative: String,
|
|
/// Path of the directory containing the .md file
|
|
pub parent: PathBuf,
|
|
/// Path of the grand parent directory for that file. Only used in sections to find subsections.
|
|
pub grand_parent: Option<PathBuf>,
|
|
/// The folder names to this section file, starting from the `content` directory
|
|
/// 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 {
|
|
pub fn new_page(path: &Path) -> FileInfo {
|
|
let file_path = path.to_path_buf();
|
|
let mut parent = file_path.parent().unwrap().to_path_buf();
|
|
let name = path.file_stem().unwrap().to_string_lossy().to_string();
|
|
let mut components = find_content_components(&file_path);
|
|
let relative = if !components.is_empty() {
|
|
format!("{}/{}.md", components.join("/"), name)
|
|
} else {
|
|
format!("{}.md", name)
|
|
};
|
|
|
|
// If we have a folder with an asset, don't consider it as a component
|
|
// Splitting on `.` as we might have a language so it isn't *only* index but also index.fr
|
|
// etc
|
|
if !components.is_empty() && name.split('.').collect::<Vec<_>>()[0] == "index" {
|
|
components.pop();
|
|
// also set parent_path to grandparent instead
|
|
parent = parent.parent().unwrap().to_path_buf();
|
|
}
|
|
|
|
FileInfo {
|
|
filename: file_path.file_name().unwrap().to_string_lossy().to_string(),
|
|
path: file_path,
|
|
// We don't care about grand parent for pages
|
|
grand_parent: None,
|
|
canonical: parent.join(&name),
|
|
parent,
|
|
name,
|
|
components,
|
|
relative,
|
|
}
|
|
}
|
|
|
|
pub fn new_section(path: &Path) -> FileInfo {
|
|
let file_path = path.to_path_buf();
|
|
let parent = path.parent().unwrap().to_path_buf();
|
|
let name = path.file_stem().unwrap().to_string_lossy().to_string();
|
|
let components = find_content_components(path);
|
|
let relative = if !components.is_empty() {
|
|
format!("{}/{}.md", components.join("/"), name)
|
|
} else {
|
|
format!("{}.md", name)
|
|
};
|
|
let grand_parent = parent.parent().map(|p| p.to_path_buf());
|
|
|
|
FileInfo {
|
|
filename: file_path.file_name().unwrap().to_string_lossy().to_string(),
|
|
path: file_path,
|
|
canonical: parent.join(&name),
|
|
parent,
|
|
grand_parent,
|
|
name,
|
|
components,
|
|
relative,
|
|
}
|
|
}
|
|
|
|
/// Look for a language in the filename.
|
|
/// If a language has been found, update the name of the file in this struct to
|
|
/// remove it and return the language code
|
|
pub fn find_language(&mut self, config: &Config) -> Result<Option<String>> {
|
|
// No languages? Nothing to do
|
|
if !config.is_multilingual() {
|
|
return Ok(None);
|
|
}
|
|
|
|
if !self.name.contains('.') {
|
|
return Ok(None);
|
|
}
|
|
|
|
// Go with the assumption that no one is using `.` in filenames when using i18n
|
|
// We can document that
|
|
let mut parts: Vec<String> = 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
|
|
if !config.languages_codes().contains(&parts[1].as_ref()) {
|
|
bail!("File {:?} has a language code of {} which isn't present in the config.toml `languages`", self.path, parts[1]);
|
|
}
|
|
|
|
self.name = parts.swap_remove(0);
|
|
self.canonical = self.parent.join(&self.name);
|
|
let lang = parts.swap_remove(0);
|
|
|
|
Ok(Some(lang))
|
|
}
|
|
}
|
|
|
|
#[doc(hidden)]
|
|
impl Default for FileInfo {
|
|
fn default() -> FileInfo {
|
|
FileInfo {
|
|
path: PathBuf::new(),
|
|
parent: PathBuf::new(),
|
|
grand_parent: None,
|
|
filename: String::new(),
|
|
name: String::new(),
|
|
components: vec![],
|
|
relative: String::new(),
|
|
canonical: PathBuf::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use std::path::Path;
|
|
|
|
use config::{Config, Language};
|
|
|
|
use super::{find_content_components, FileInfo};
|
|
|
|
#[test]
|
|
fn can_find_content_components() {
|
|
let res =
|
|
find_content_components("/home/vincent/code/site/content/posts/tutorials/python.md");
|
|
assert_eq!(res, ["posts".to_string(), "tutorials".to_string()]);
|
|
}
|
|
#[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",
|
|
));
|
|
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",
|
|
));
|
|
let res = file.find_language(&config);
|
|
assert!(res.is_ok());
|
|
assert_eq!(res.unwrap(), Some(String::from("fr")));
|
|
}
|
|
|
|
#[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",
|
|
));
|
|
assert_eq!(file.components, ["posts".to_string(), "tutorials".to_string()]);
|
|
let res = file.find_language(&config);
|
|
assert!(res.is_ok());
|
|
assert_eq!(res.unwrap(), Some(String::from("fr")));
|
|
}
|
|
|
|
#[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 res = file.find_language(&config);
|
|
assert!(res.is_ok());
|
|
assert!(res.unwrap().is_none());
|
|
}
|
|
|
|
#[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",
|
|
));
|
|
let res = file.find_language(&config);
|
|
assert!(res.is_err());
|
|
}
|
|
|
|
#[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",
|
|
));
|
|
let res = file.find_language(&config);
|
|
assert!(res.is_ok());
|
|
assert_eq!(res.unwrap(), Some(String::from("fr")));
|
|
}
|
|
}
|