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.

434 lines
16KB

  1. extern crate site;
  2. #[macro_use]
  3. extern crate errors;
  4. extern crate front_matter;
  5. extern crate library;
  6. use std::path::{Component, Path};
  7. use errors::Result;
  8. use front_matter::{PageFrontMatter, SectionFrontMatter};
  9. use library::{Page, Section};
  10. use site::Site;
  11. #[derive(Debug, Clone, Copy, PartialEq)]
  12. pub enum PageChangesNeeded {
  13. /// Editing `taxonomies`
  14. Taxonomies,
  15. /// Editing `date`, `order` or `weight`
  16. Sort,
  17. /// Editing anything causes a re-render of the page
  18. Render,
  19. }
  20. #[derive(Debug, Clone, Copy, PartialEq)]
  21. pub enum SectionChangesNeeded {
  22. /// Editing `sort_by`
  23. Sort,
  24. /// Editing `title`, `description`, `extra`, `template` or setting `render` to true
  25. Render,
  26. /// Editing `paginate_by`, `paginate_path` or `insert_anchor_links`
  27. RenderWithPages,
  28. /// Setting `render` to false
  29. Delete,
  30. /// Changing `transparent`
  31. Transparent,
  32. }
  33. /// Evaluates all the params in the front matter that changed so we can do the smallest
  34. /// delta in the serve command
  35. /// Order matters as the actions will be done in insertion order
  36. fn find_section_front_matter_changes(
  37. current: &SectionFrontMatter,
  38. new: &SectionFrontMatter,
  39. ) -> Vec<SectionChangesNeeded> {
  40. let mut changes_needed = vec![];
  41. if current.sort_by != new.sort_by {
  42. changes_needed.push(SectionChangesNeeded::Sort);
  43. }
  44. if current.transparent != new.transparent {
  45. changes_needed.push(SectionChangesNeeded::Transparent);
  46. }
  47. // We want to hide the section
  48. // TODO: what to do on redirect_path change?
  49. if current.render && !new.render {
  50. changes_needed.push(SectionChangesNeeded::Delete);
  51. // Nothing else we can do
  52. return changes_needed;
  53. }
  54. if current.paginate_by != new.paginate_by
  55. || current.paginate_path != new.paginate_path
  56. || current.insert_anchor_links != new.insert_anchor_links
  57. {
  58. changes_needed.push(SectionChangesNeeded::RenderWithPages);
  59. // Nothing else we can do
  60. return changes_needed;
  61. }
  62. // Any new change will trigger a re-rendering of the section page only
  63. changes_needed.push(SectionChangesNeeded::Render);
  64. changes_needed
  65. }
  66. /// Evaluates all the params in the front matter that changed so we can do the smallest
  67. /// delta in the serve command
  68. /// Order matters as the actions will be done in insertion order
  69. fn find_page_front_matter_changes(
  70. current: &PageFrontMatter,
  71. other: &PageFrontMatter,
  72. ) -> Vec<PageChangesNeeded> {
  73. let mut changes_needed = vec![];
  74. if current.taxonomies != other.taxonomies {
  75. changes_needed.push(PageChangesNeeded::Taxonomies);
  76. }
  77. if current.date != other.date || current.order != other.order || current.weight != other.weight
  78. {
  79. changes_needed.push(PageChangesNeeded::Sort);
  80. }
  81. changes_needed.push(PageChangesNeeded::Render);
  82. changes_needed
  83. }
  84. /// Handles a path deletion: could be a page, a section, a folder
  85. fn delete_element(site: &mut Site, path: &Path, is_section: bool) -> Result<()> {
  86. // Ignore the event if this path was not known
  87. if !site.library.contains_section(&path.to_path_buf())
  88. && !site.library.contains_page(&path.to_path_buf())
  89. {
  90. return Ok(());
  91. }
  92. if is_section {
  93. if let Some(s) = site.library.remove_section(&path.to_path_buf()) {
  94. site.permalinks.remove(&s.file.relative);
  95. }
  96. } else if let Some(p) = site.library.remove_page(&path.to_path_buf()) {
  97. site.permalinks.remove(&p.file.relative);
  98. if !p.meta.taxonomies.is_empty() {
  99. site.populate_taxonomies()?;
  100. }
  101. }
  102. site.populate_sections();
  103. site.populate_taxonomies()?;
  104. // Ensure we have our fn updated so it doesn't contain the permalink(s)/section/page deleted
  105. site.register_early_global_fns();
  106. site.register_tera_global_fns();
  107. // Deletion is something that doesn't happen all the time so we
  108. // don't need to optimise it too much
  109. site.build()
  110. }
  111. /// Handles a `_index.md` (a section) being edited in some ways
  112. fn handle_section_editing(site: &mut Site, path: &Path) -> Result<()> {
  113. let section = Section::from_file(path, &site.config)?;
  114. let pathbuf = path.to_path_buf();
  115. match site.add_section(section, true)? {
  116. // Updating a section
  117. Some(prev) => {
  118. site.populate_sections();
  119. if site.library.get_section(&pathbuf).unwrap().meta == prev.meta {
  120. // Front matter didn't change, only content did
  121. // so we render only the section page, not its pages
  122. return site.render_section(&site.library.get_section(&pathbuf).unwrap(), false);
  123. }
  124. // Front matter changed
  125. for changes in find_section_front_matter_changes(
  126. &site.library.get_section(&pathbuf).unwrap().meta,
  127. &prev.meta,
  128. ) {
  129. // Sort always comes first if present so the rendering will be fine
  130. match changes {
  131. SectionChangesNeeded::Sort => {
  132. site.register_tera_global_fns();
  133. }
  134. SectionChangesNeeded::Render => {
  135. site.render_section(&site.library.get_section(&pathbuf).unwrap(), false)?
  136. }
  137. SectionChangesNeeded::RenderWithPages => {
  138. site.render_section(&site.library.get_section(&pathbuf).unwrap(), true)?
  139. }
  140. // not a common enough operation to make it worth optimizing
  141. SectionChangesNeeded::Delete | SectionChangesNeeded::Transparent => {
  142. site.build()?;
  143. }
  144. };
  145. }
  146. Ok(())
  147. }
  148. // New section, only render that one
  149. None => {
  150. site.populate_sections();
  151. site.register_tera_global_fns();
  152. site.render_section(&site.library.get_section(&pathbuf).unwrap(), true)
  153. }
  154. }
  155. }
  156. macro_rules! render_parent_section {
  157. ($site: expr, $path: expr) => {
  158. if let Some(s) = $site.library.find_parent_section($path) {
  159. $site.render_section(s, false)?;
  160. };
  161. };
  162. }
  163. /// Handles a page being edited in some ways
  164. fn handle_page_editing(site: &mut Site, path: &Path) -> Result<()> {
  165. let page = Page::from_file(path, &site.config)?;
  166. let pathbuf = path.to_path_buf();
  167. match site.add_page(page, true)? {
  168. // Updating a page
  169. Some(prev) => {
  170. site.populate_sections();
  171. site.populate_taxonomies()?;
  172. // Front matter didn't change, only content did
  173. if site.library.get_page(&pathbuf).unwrap().meta == prev.meta {
  174. // Other than the page itself, the summary might be seen
  175. // on a paginated list for a blog for example
  176. if site.library.get_page(&pathbuf).unwrap().summary.is_some() {
  177. render_parent_section!(site, path);
  178. }
  179. site.register_tera_global_fns();
  180. return site.render_page(&site.library.get_page(&pathbuf).unwrap());
  181. }
  182. // Front matter changed
  183. for changes in find_page_front_matter_changes(
  184. &site.library.get_page(&pathbuf).unwrap().meta,
  185. &prev.meta,
  186. ) {
  187. site.register_tera_global_fns();
  188. // Sort always comes first if present so the rendering will be fine
  189. match changes {
  190. PageChangesNeeded::Taxonomies => {
  191. site.populate_taxonomies()?;
  192. site.render_taxonomies()?;
  193. }
  194. PageChangesNeeded::Sort => {
  195. site.render_index()?;
  196. }
  197. PageChangesNeeded::Render => {
  198. render_parent_section!(site, path);
  199. site.render_page(&site.library.get_page(&path.to_path_buf()).unwrap())?;
  200. }
  201. };
  202. }
  203. Ok(())
  204. }
  205. // It's a new page!
  206. None => {
  207. site.populate_sections();
  208. site.populate_taxonomies()?;
  209. site.register_early_global_fns();
  210. site.register_tera_global_fns();
  211. // No need to optimise that yet, we can revisit if it becomes an issue
  212. site.build()
  213. }
  214. }
  215. }
  216. /// What happens when we rename a file/folder in the content directory.
  217. /// Note that this is only called for folders when it isn't empty
  218. pub fn after_content_rename(site: &mut Site, old: &Path, new: &Path) -> Result<()> {
  219. let new_path = if new.is_dir() {
  220. if new.join("_index.md").exists() {
  221. // This is a section keep the dir folder to differentiate from renaming _index.md
  222. // which doesn't do the same thing
  223. new.to_path_buf()
  224. } else if new.join("index.md").exists() {
  225. new.join("index.md")
  226. } else {
  227. bail!("Got unexpected folder {:?} while handling renaming that was not expected", new);
  228. }
  229. } else {
  230. new.to_path_buf()
  231. };
  232. // A section folder has been renamed: just reload the whole site and rebuild it as we
  233. // do not really know what needs to be rendered
  234. if new_path.is_dir() {
  235. site.load()?;
  236. return site.build();
  237. }
  238. // We ignore renames on non-markdown files for now
  239. if let Some(ext) = new_path.extension() {
  240. if ext != "md" {
  241. return Ok(());
  242. }
  243. }
  244. // Renaming a file to _index.md, let the section editing do something and hope for the best
  245. if new_path.file_name().unwrap() == "_index.md" {
  246. // We aren't entirely sure where the original thing was so just try to delete whatever was
  247. // at the old path
  248. site.library.remove_page(&old.to_path_buf());
  249. site.library.remove_section(&old.to_path_buf());
  250. return handle_section_editing(site, &new_path);
  251. }
  252. // If it is a page, just delete what was there before and
  253. // fake it's a new page
  254. let old_path = if new_path.file_name().unwrap() == "index.md" {
  255. old.join("index.md")
  256. } else {
  257. old.to_path_buf()
  258. };
  259. site.library.remove_page(&old_path);
  260. handle_page_editing(site, &new_path)
  261. }
  262. /// What happens when a section or a page is created/edited
  263. pub fn after_content_change(site: &mut Site, path: &Path) -> Result<()> {
  264. let is_section = path.file_name().unwrap() == "_index.md";
  265. let is_md = path.extension().unwrap() == "md";
  266. let index = path.parent().unwrap().join("index.md");
  267. let mut potential_indices = vec![path.parent().unwrap().join("index.md")];
  268. for language in &site.config.languages {
  269. potential_indices.push(path.parent().unwrap().join(format!("index.{}.md", language.code)));
  270. }
  271. let colocated_index = potential_indices.contains(&path.to_path_buf());
  272. // A few situations can happen:
  273. // 1. Change on .md files
  274. // a. Is there already an `index.md`? Return an error if it's something other than delete
  275. // b. Deleted? remove the element
  276. // c. Edited?
  277. // 1. filename is `_index.md`, this is a section
  278. // 1. it's a page otherwise
  279. // 2. Change on non .md files
  280. // a. Try to find a corresponding `_index.md`
  281. // 1. Nothing? Return Ok
  282. // 2. Something? Update the page
  283. if is_md {
  284. // only delete if it was able to be added in the first place
  285. if !index.exists() && !path.exists() {
  286. return delete_element(site, path, is_section);
  287. }
  288. // Added another .md in a assets directory
  289. if index.exists() && path.exists() && !colocated_index {
  290. bail!(
  291. "Change on {:?} detected but only files named `index.md` with an optional language code are allowed",
  292. path.display()
  293. );
  294. } else if index.exists() && !path.exists() {
  295. // deleted the wrong .md, do nothing
  296. return Ok(());
  297. }
  298. if is_section {
  299. handle_section_editing(site, path)
  300. } else {
  301. handle_page_editing(site, path)
  302. }
  303. } else if index.exists() {
  304. handle_page_editing(site, &index)
  305. } else {
  306. Ok(())
  307. }
  308. }
  309. /// What happens when a template is changed
  310. pub fn after_template_change(site: &mut Site, path: &Path) -> Result<()> {
  311. site.tera.full_reload()?;
  312. let filename = path.file_name().unwrap().to_str().unwrap();
  313. match filename {
  314. "sitemap.xml" => site.render_sitemap(),
  315. "rss.xml" => site.render_rss_feed(site.library.pages_values(), None),
  316. "robots.txt" => site.render_robots(),
  317. "single.html" | "list.html" => site.render_taxonomies(),
  318. "page.html" => {
  319. site.render_sections()?;
  320. site.render_orphan_pages()
  321. }
  322. "section.html" => site.render_sections(),
  323. "404.html" => site.render_404(),
  324. // Either the index or some unknown template changed
  325. // We can't really know what this change affects so rebuild all
  326. // the things
  327. _ => {
  328. // If we are updating a shortcode, re-render the markdown of all pages/site
  329. // because we have no clue which one needs rebuilding
  330. // TODO: look if there the shortcode is used in the markdown instead of re-rendering
  331. // everything
  332. if path.components().any(|x| x == Component::Normal("shortcodes".as_ref())) {
  333. site.render_markdown()?;
  334. }
  335. site.populate_sections();
  336. site.populate_taxonomies()?;
  337. site.render_sections()?;
  338. site.render_orphan_pages()?;
  339. site.render_taxonomies()
  340. }
  341. }
  342. }
  343. #[cfg(test)]
  344. mod tests {
  345. use std::collections::HashMap;
  346. use super::{
  347. find_page_front_matter_changes, find_section_front_matter_changes, PageChangesNeeded,
  348. SectionChangesNeeded,
  349. };
  350. use front_matter::{PageFrontMatter, SectionFrontMatter, SortBy};
  351. #[test]
  352. fn can_find_taxonomy_changes_in_page_frontmatter() {
  353. let mut taxonomies = HashMap::new();
  354. taxonomies.insert("tags".to_string(), vec!["a tag".to_string()]);
  355. let new = PageFrontMatter { taxonomies, ..PageFrontMatter::default() };
  356. let changes = find_page_front_matter_changes(&PageFrontMatter::default(), &new);
  357. assert_eq!(changes, vec![PageChangesNeeded::Taxonomies, PageChangesNeeded::Render]);
  358. }
  359. #[test]
  360. fn can_find_multiple_changes_in_page_frontmatter() {
  361. let mut taxonomies = HashMap::new();
  362. taxonomies.insert("categories".to_string(), vec!["a category".to_string()]);
  363. let current = PageFrontMatter { taxonomies, order: Some(1), ..PageFrontMatter::default() };
  364. let changes = find_page_front_matter_changes(&current, &PageFrontMatter::default());
  365. assert_eq!(
  366. changes,
  367. vec![PageChangesNeeded::Taxonomies, PageChangesNeeded::Sort, PageChangesNeeded::Render]
  368. );
  369. }
  370. #[test]
  371. fn can_find_sort_changes_in_section_frontmatter() {
  372. let new = SectionFrontMatter { sort_by: SortBy::Date, ..SectionFrontMatter::default() };
  373. let changes = find_section_front_matter_changes(&SectionFrontMatter::default(), &new);
  374. assert_eq!(changes, vec![SectionChangesNeeded::Sort, SectionChangesNeeded::Render]);
  375. }
  376. #[test]
  377. fn can_find_render_changes_in_section_frontmatter() {
  378. let new = SectionFrontMatter { render: false, ..SectionFrontMatter::default() };
  379. let changes = find_section_front_matter_changes(&SectionFrontMatter::default(), &new);
  380. assert_eq!(changes, vec![SectionChangesNeeded::Delete]);
  381. }
  382. #[test]
  383. fn can_find_paginate_by_changes_in_section_frontmatter() {
  384. let new = SectionFrontMatter { paginate_by: Some(10), ..SectionFrontMatter::default() };
  385. let changes = find_section_front_matter_changes(&SectionFrontMatter::default(), &new);
  386. assert_eq!(changes, vec![SectionChangesNeeded::RenderWithPages]);
  387. }
  388. }