You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

289 lines
10KB

  1. use std::path::{Path, PathBuf};
  2. use config::Config;
  3. use errors::{bail, Result};
  4. /// Takes a full path to a file and returns only the components after the first `content` directory
  5. /// Will not return the filename as last component
  6. pub fn find_content_components<P: AsRef<Path>>(path: P) -> Vec<String> {
  7. let path = path.as_ref();
  8. let mut is_in_content = false;
  9. let mut components = vec![];
  10. for section in path.parent().unwrap().components() {
  11. let component = section.as_os_str().to_string_lossy();
  12. if is_in_content {
  13. components.push(component.to_string());
  14. continue;
  15. }
  16. if component == "content" {
  17. is_in_content = true;
  18. }
  19. }
  20. components
  21. }
  22. /// Struct that contains all the information about the actual file
  23. #[derive(Debug, Clone, PartialEq)]
  24. pub struct FileInfo {
  25. /// The full path to the .md file
  26. pub path: PathBuf,
  27. /// The on-disk filename, will differ from the `name` when there is a language code in it
  28. pub filename: String,
  29. /// The name of the .md file without the extension, always `_index` for sections
  30. /// Doesn't contain the language if there was one in the filename
  31. pub name: String,
  32. /// The .md path, starting from the content directory, with `/` slashes
  33. pub relative: String,
  34. /// Path of the directory containing the .md file
  35. pub parent: PathBuf,
  36. /// Path of the grand parent directory for that file. Only used in sections to find subsections.
  37. pub grand_parent: Option<PathBuf>,
  38. /// The folder names to this section file, starting from the `content` directory
  39. /// For example a file at content/kb/solutions/blabla.md will have 2 components:
  40. /// `kb` and `solutions`
  41. pub components: Vec<String>,
  42. /// This is `parent` + `name`, used to find content referring to the same content but in
  43. /// various languages.
  44. pub canonical: PathBuf,
  45. }
  46. impl FileInfo {
  47. pub fn new_page(path: &Path, base_path: &PathBuf) -> FileInfo {
  48. let file_path = path.to_path_buf();
  49. let mut parent = file_path.parent().expect("Get parent of page").to_path_buf();
  50. let name = path.file_stem().unwrap().to_string_lossy().to_string();
  51. let canonical = parent.join(&name);
  52. let mut components =
  53. find_content_components(&file_path.strip_prefix(base_path).unwrap_or(&file_path));
  54. let relative = if !components.is_empty() {
  55. format!("{}/{}.md", components.join("/"), name)
  56. } else {
  57. format!("{}.md", name)
  58. };
  59. // If we have a folder with an asset, don't consider it as a component
  60. // Splitting on `.` as we might have a language so it isn't *only* index but also index.fr
  61. // etc
  62. if !components.is_empty() && name.split('.').collect::<Vec<_>>()[0] == "index" {
  63. components.pop();
  64. // also set parent_path to grandparent instead
  65. parent = parent.parent().unwrap().to_path_buf();
  66. }
  67. FileInfo {
  68. filename: file_path.file_name().unwrap().to_string_lossy().to_string(),
  69. path: file_path,
  70. // We don't care about grand parent for pages
  71. grand_parent: None,
  72. canonical,
  73. parent,
  74. name,
  75. components,
  76. relative,
  77. }
  78. }
  79. pub fn new_section(path: &Path, base_path: &PathBuf) -> FileInfo {
  80. let file_path = path.to_path_buf();
  81. let parent = path.parent().expect("Get parent of section").to_path_buf();
  82. let name = path.file_stem().unwrap().to_string_lossy().to_string();
  83. let components =
  84. find_content_components(&file_path.strip_prefix(base_path).unwrap_or(&file_path));
  85. let relative = if !components.is_empty() {
  86. format!("{}/{}.md", components.join("/"), name)
  87. } else {
  88. format!("{}.md", name)
  89. };
  90. let grand_parent = parent.parent().map(|p| p.to_path_buf());
  91. FileInfo {
  92. filename: file_path.file_name().unwrap().to_string_lossy().to_string(),
  93. path: file_path,
  94. canonical: parent.join(&name),
  95. parent,
  96. grand_parent,
  97. name,
  98. components,
  99. relative,
  100. }
  101. }
  102. /// Look for a language in the filename.
  103. /// If a language has been found, update the name of the file in this struct to
  104. /// remove it and return the language code
  105. pub fn find_language(&mut self, config: &Config) -> Result<String> {
  106. // No languages? Nothing to do
  107. if !config.is_multilingual() {
  108. return Ok(config.default_language.clone());
  109. }
  110. if !self.name.contains('.') {
  111. return Ok(config.default_language.clone());
  112. }
  113. // Go with the assumption that no one is using `.` in filenames when using i18n
  114. // We can document that
  115. let mut parts: Vec<String> = self.name.splitn(2, '.').map(|s| s.to_string()).collect();
  116. // The language code is not present in the config: typo or the user forgot to add it to the
  117. // config
  118. if !config.languages_codes().contains(&parts[1].as_ref()) {
  119. bail!("File {:?} has a language code of {} which isn't present in the config.toml `languages`", self.path, parts[1]);
  120. }
  121. self.name = parts.swap_remove(0);
  122. self.canonical = self.path.parent().expect("Get parent of page path").join(&self.name);
  123. let lang = parts.swap_remove(0);
  124. Ok(lang)
  125. }
  126. }
  127. #[doc(hidden)]
  128. impl Default for FileInfo {
  129. fn default() -> FileInfo {
  130. FileInfo {
  131. path: PathBuf::new(),
  132. parent: PathBuf::new(),
  133. grand_parent: None,
  134. filename: String::new(),
  135. name: String::new(),
  136. components: vec![],
  137. relative: String::new(),
  138. canonical: PathBuf::new(),
  139. }
  140. }
  141. }
  142. #[cfg(test)]
  143. mod tests {
  144. use std::path::{Path, PathBuf};
  145. use config::{Config, Language};
  146. use super::{find_content_components, FileInfo};
  147. #[test]
  148. fn can_find_content_components() {
  149. let res =
  150. find_content_components("/home/vincent/code/site/content/posts/tutorials/python.md");
  151. assert_eq!(res, ["posts".to_string(), "tutorials".to_string()]);
  152. }
  153. #[test]
  154. fn can_find_components_in_page_with_assets() {
  155. let file = FileInfo::new_page(
  156. &Path::new("/home/vincent/code/site/content/posts/tutorials/python/index.md"),
  157. &PathBuf::new(),
  158. );
  159. assert_eq!(file.components, ["posts".to_string(), "tutorials".to_string()]);
  160. }
  161. #[test]
  162. fn doesnt_fail_with_multiple_content_directories() {
  163. let file = FileInfo::new_page(
  164. &Path::new("/home/vincent/code/content/site/content/posts/tutorials/python/index.md"),
  165. &PathBuf::from("/home/vincent/code/content/site"),
  166. );
  167. assert_eq!(file.components, ["posts".to_string(), "tutorials".to_string()]);
  168. }
  169. #[test]
  170. fn can_find_valid_language_in_page() {
  171. let mut config = Config::default();
  172. config.languages.push(Language { code: String::from("fr"), rss: false, search: false });
  173. let mut file = FileInfo::new_page(
  174. &Path::new("/home/vincent/code/site/content/posts/tutorials/python.fr.md"),
  175. &PathBuf::new(),
  176. );
  177. let res = file.find_language(&config);
  178. assert!(res.is_ok());
  179. assert_eq!(res.unwrap(), "fr");
  180. }
  181. #[test]
  182. fn can_find_valid_language_in_page_with_assets() {
  183. let mut config = Config::default();
  184. config.languages.push(Language { code: String::from("fr"), rss: false, search: false });
  185. let mut file = FileInfo::new_page(
  186. &Path::new("/home/vincent/code/site/content/posts/tutorials/python/index.fr.md"),
  187. &PathBuf::new(),
  188. );
  189. assert_eq!(file.components, ["posts".to_string(), "tutorials".to_string()]);
  190. let res = file.find_language(&config);
  191. assert!(res.is_ok());
  192. assert_eq!(res.unwrap(), "fr");
  193. }
  194. #[test]
  195. fn do_nothing_on_unknown_language_in_page_with_i18n_off() {
  196. let config = Config::default();
  197. let mut file = FileInfo::new_page(
  198. &Path::new("/home/vincent/code/site/content/posts/tutorials/python.fr.md"),
  199. &PathBuf::new(),
  200. );
  201. let res = file.find_language(&config);
  202. assert!(res.is_ok());
  203. assert_eq!(res.unwrap(), config.default_language);
  204. }
  205. #[test]
  206. fn errors_on_unknown_language_in_page_with_i18n_on() {
  207. let mut config = Config::default();
  208. config.languages.push(Language { code: String::from("it"), rss: false, search: false });
  209. let mut file = FileInfo::new_page(
  210. &Path::new("/home/vincent/code/site/content/posts/tutorials/python.fr.md"),
  211. &PathBuf::new(),
  212. );
  213. let res = file.find_language(&config);
  214. assert!(res.is_err());
  215. }
  216. #[test]
  217. fn can_find_valid_language_in_section() {
  218. let mut config = Config::default();
  219. config.languages.push(Language { code: String::from("fr"), rss: false, search: false });
  220. let mut file = FileInfo::new_section(
  221. &Path::new("/home/vincent/code/site/content/posts/tutorials/_index.fr.md"),
  222. &PathBuf::new(),
  223. );
  224. let res = file.find_language(&config);
  225. assert!(res.is_ok());
  226. assert_eq!(res.unwrap(), "fr");
  227. }
  228. /// Regression test for https://github.com/getzola/zola/issues/854
  229. #[test]
  230. fn correct_canonical_for_index() {
  231. let file = FileInfo::new_page(
  232. &Path::new("/home/vincent/code/site/content/posts/tutorials/python/index.md"),
  233. &PathBuf::new(),
  234. );
  235. assert_eq!(
  236. file.canonical,
  237. Path::new("/home/vincent/code/site/content/posts/tutorials/python/index")
  238. );
  239. }
  240. /// Regression test for https://github.com/getzola/zola/issues/854
  241. #[test]
  242. fn correct_canonical_after_find_language() {
  243. let mut config = Config::default();
  244. config.languages.push(Language { code: String::from("fr"), rss: false, search: false });
  245. let mut file = FileInfo::new_page(
  246. &Path::new("/home/vincent/code/site/content/posts/tutorials/python/index.fr.md"),
  247. &PathBuf::new(),
  248. );
  249. let res = file.find_language(&config);
  250. assert!(res.is_ok());
  251. assert_eq!(
  252. file.canonical,
  253. Path::new("/home/vincent/code/site/content/posts/tutorials/python/index")
  254. );
  255. }
  256. }