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.

233 lines
8.1KB

  1. use std::path::{Path, PathBuf};
  2. use config::Config;
  3. use errors::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. }
  43. impl FileInfo {
  44. pub fn new_page(path: &Path) -> FileInfo {
  45. let file_path = path.to_path_buf();
  46. let mut parent = file_path.parent().unwrap().to_path_buf();
  47. let name = path.file_stem().unwrap().to_string_lossy().to_string();
  48. let mut components = find_content_components(&file_path);
  49. let relative = if !components.is_empty() {
  50. format!("{}/{}.md", components.join("/"), name)
  51. } else {
  52. format!("{}.md", name)
  53. };
  54. // If we have a folder with an asset, don't consider it as a component
  55. // Splitting on `.` as we might have a language so it isn't *only* index but also index.fr
  56. // etc
  57. if !components.is_empty() && name.split('.').collect::<Vec<_>>()[0] == "index" {
  58. components.pop();
  59. // also set parent_path to grandparent instead
  60. parent = parent.parent().unwrap().to_path_buf();
  61. }
  62. FileInfo {
  63. filename: file_path.file_name().unwrap().to_string_lossy().to_string(),
  64. path: file_path,
  65. // We don't care about grand parent for pages
  66. grand_parent: None,
  67. parent,
  68. name,
  69. components,
  70. relative,
  71. }
  72. }
  73. pub fn new_section(path: &Path) -> FileInfo {
  74. let file_path = path.to_path_buf();
  75. let parent = path.parent().unwrap().to_path_buf();
  76. let name = path.file_stem().unwrap().to_string_lossy().to_string();
  77. let components = find_content_components(path);
  78. let relative = if !components.is_empty() {
  79. format!("{}/{}.md", components.join("/"), name)
  80. } else {
  81. format!("{}.md", name)
  82. };
  83. let grand_parent = parent.parent().map(|p| p.to_path_buf());
  84. FileInfo {
  85. filename: file_path.file_name().unwrap().to_string_lossy().to_string(),
  86. path: file_path,
  87. parent,
  88. grand_parent,
  89. name,
  90. components,
  91. relative,
  92. }
  93. }
  94. /// Look for a language in the filename.
  95. /// If a language has been found, update the name of the file in this struct to
  96. /// remove it and return the language code
  97. pub fn find_language(&mut self, config: &Config) -> Result<Option<String>> {
  98. // No languages? Nothing to do
  99. if !config.uses_i18n() {
  100. return Ok(None);
  101. }
  102. if !self.name.contains('.') {
  103. return Ok(None);
  104. }
  105. // Go with the assumption that no one is using `.` in filenames when using i18n
  106. // We can document that
  107. let mut parts: Vec<String> = self.name.splitn(2, '.').map(|s| s.to_string()).collect();
  108. // The language code is not present in the config: typo or the user forgot to add it to the
  109. // config
  110. if !config.languages_codes().contains(&parts[1].as_ref()) {
  111. bail!("File {:?} has a language code of {} which isn't present in the config.toml `languages`", self.path, parts[1]);
  112. }
  113. self.name = parts.swap_remove(0);
  114. let lang = parts.swap_remove(0);
  115. Ok(Some(lang))
  116. }
  117. }
  118. #[doc(hidden)]
  119. impl Default for FileInfo {
  120. fn default() -> FileInfo {
  121. FileInfo {
  122. path: PathBuf::new(),
  123. parent: PathBuf::new(),
  124. grand_parent: None,
  125. filename: String::new(),
  126. name: String::new(),
  127. components: vec![],
  128. relative: String::new(),
  129. }
  130. }
  131. }
  132. #[cfg(test)]
  133. mod tests {
  134. use std::path::Path;
  135. use config::{Config, Language};
  136. use super::{find_content_components, FileInfo};
  137. #[test]
  138. fn can_find_content_components() {
  139. let res =
  140. find_content_components("/home/vincent/code/site/content/posts/tutorials/python.md");
  141. assert_eq!(res, ["posts".to_string(), "tutorials".to_string()]);
  142. }
  143. #[test]
  144. fn can_find_components_in_page_with_assets() {
  145. let file = FileInfo::new_page(&Path::new(
  146. "/home/vincent/code/site/content/posts/tutorials/python/index.md",
  147. ));
  148. assert_eq!(file.components, ["posts".to_string(), "tutorials".to_string()]);
  149. }
  150. #[test]
  151. fn can_find_valid_language_in_page() {
  152. let mut config = Config::default();
  153. config.languages.push(Language { code: String::from("fr"), rss: false });
  154. let mut file = FileInfo::new_page(&Path::new(
  155. "/home/vincent/code/site/content/posts/tutorials/python.fr.md",
  156. ));
  157. let res = file.find_language(&config);
  158. assert!(res.is_ok());
  159. assert_eq!(res.unwrap(), Some(String::from("fr")));
  160. }
  161. #[test]
  162. fn can_find_valid_language_in_page_with_assets() {
  163. let mut config = Config::default();
  164. config.languages.push(Language { code: String::from("fr"), rss: false });
  165. let mut file = FileInfo::new_page(&Path::new(
  166. "/home/vincent/code/site/content/posts/tutorials/python/index.fr.md",
  167. ));
  168. assert_eq!(file.components, ["posts".to_string(), "tutorials".to_string()]);
  169. let res = file.find_language(&config);
  170. assert!(res.is_ok());
  171. assert_eq!(res.unwrap(), Some(String::from("fr")));
  172. }
  173. #[test]
  174. fn do_nothing_on_unknown_language_in_page_with_i18n_off() {
  175. let config = Config::default();
  176. let mut file = FileInfo::new_page(&Path::new(
  177. "/home/vincent/code/site/content/posts/tutorials/python.fr.md",
  178. ));
  179. let res = file.find_language(&config);
  180. assert!(res.is_ok());
  181. assert!(res.unwrap().is_none());
  182. }
  183. #[test]
  184. fn errors_on_unknown_language_in_page_with_i18n_on() {
  185. let mut config = Config::default();
  186. config.languages.push(Language { code: String::from("it"), rss: false });
  187. let mut file = FileInfo::new_page(&Path::new(
  188. "/home/vincent/code/site/content/posts/tutorials/python.fr.md",
  189. ));
  190. let res = file.find_language(&config);
  191. assert!(res.is_err());
  192. }
  193. #[test]
  194. fn can_find_valid_language_in_section() {
  195. let mut config = Config::default();
  196. config.languages.push(Language { code: String::from("fr"), rss: false });
  197. let mut file = FileInfo::new_section(&Path::new(
  198. "/home/vincent/code/site/content/posts/tutorials/_index.fr.md",
  199. ));
  200. let res = file.find_language(&config);
  201. assert!(res.is_ok());
  202. assert_eq!(res.unwrap(), Some(String::from("fr")));
  203. }
  204. }