diff --git a/components/content/src/section.rs b/components/content/src/section.rs index 58567f3..6426eb9 100644 --- a/components/content/src/section.rs +++ b/components/content/src/section.rs @@ -8,7 +8,7 @@ use serde::ser::{SerializeStruct, self}; use config::Config; use front_matter::{SectionFrontMatter, split_section_content}; use errors::{Result, ResultExt}; -use utils::fs::read_file; +use utils::fs::{read_file, find_related_assets}; use utils::templates::render_template; use utils::site::get_reading_analytics; use rendering::{RenderContext, Header, render_content}; @@ -33,6 +33,8 @@ pub struct Section { pub raw_content: String, /// The HTML rendered of the page pub content: String, + /// All the non-md files we found next to the .md file + pub assets: Vec, /// All direct pages of that section pub pages: Vec, /// All pages that cannot be sorted in this section @@ -54,6 +56,7 @@ impl Section { components: vec![], permalink: "".to_string(), raw_content: "".to_string(), + assets: vec![], content: "".to_string(), pages: vec![], ignored_pages: vec![], @@ -79,8 +82,31 @@ impl Section { pub fn from_file>(path: P, config: &Config) -> Result
{ let path = path.as_ref(); let content = read_file(path)?; + let mut section = Section::parse(path, &content, config)?; - Section::parse(path, &content, config) + let parent_dir = path.parent().unwrap(); + let assets = find_related_assets(parent_dir); + + if let Some(ref globset) = config.ignored_content_globset { + // `find_related_assets` only scans the immediate directory (it is not recursive) so our + // filtering only needs to work against the file_name component, not the full suffix. If + // `find_related_assets` was changed to also return files in subdirectories, we could + // use `PathBuf.strip_prefix` to remove the parent directory and then glob-filter + // against the remaining path. Note that the current behaviour effectively means that + // the `ignored_content` setting in the config file is limited to single-file glob + // patterns (no "**" patterns). + section.assets = assets.into_iter() + .filter(|path| + match path.file_name() { + None => true, + Some(file) => !globset.is_match(file) + } + ).collect(); + } else { + section.assets = assets; + } + + Ok(section) } pub fn get_template_name(&self) -> String { @@ -146,6 +172,15 @@ impl Section { pub fn is_child_page(&self, path: &PathBuf) -> bool { self.all_pages_path().contains(path) } + + /// Creates a vectors of asset URLs. + fn serialize_assets(&self) -> Vec { + self.assets.iter() + .filter_map(|asset| asset.file_name()) + .filter_map(|filename| filename.to_str()) + .map(|filename| self.path.clone() + filename) + .collect() + } } impl ser::Serialize for Section { @@ -165,6 +200,8 @@ impl ser::Serialize for Section { state.serialize_field("word_count", &word_count)?; state.serialize_field("reading_time", &reading_time)?; state.serialize_field("toc", &self.toc)?; + let assets = self.serialize_assets(); + state.serialize_field("assets", &assets)?; state.end() } } @@ -179,6 +216,7 @@ impl Default for Section { components: vec![], permalink: "".to_string(), raw_content: "".to_string(), + assets: vec![], content: "".to_string(), pages: vec![], ignored_pages: vec![], @@ -187,3 +225,69 @@ impl Default for Section { } } } + +#[cfg(test)] +mod tests { + use std::io::Write; + use std::fs::{File, create_dir}; + + use tempfile::tempdir; + use globset::{Glob, GlobSetBuilder}; + + use config::Config; + use super::Section; + + #[test] + fn section_with_assets_gets_right_info() { + let tmp_dir = tempdir().expect("create temp dir"); + let path = tmp_dir.path(); + create_dir(&path.join("content")).expect("create content temp dir"); + create_dir(&path.join("content").join("posts")).expect("create posts temp dir"); + let nested_path = path.join("content").join("posts").join("with-assets"); + create_dir(&nested_path).expect("create nested temp dir"); + let mut f = File::create(nested_path.join("_index.md")).unwrap(); + f.write_all(b"+++\n+++\n").unwrap(); + File::create(nested_path.join("example.js")).unwrap(); + File::create(nested_path.join("graph.jpg")).unwrap(); + File::create(nested_path.join("fail.png")).unwrap(); + + let res = Section::from_file( + nested_path.join("_index.md").as_path(), + &Config::default(), + ); + assert!(res.is_ok()); + let section = res.unwrap(); + assert_eq!(section.assets.len(), 3); + assert_eq!(section.permalink, "http://a-website.com/posts/with-assets/"); + } + + #[test] + fn section_with_ignored_assets_filters_out_correct_files() { + let tmp_dir = tempdir().expect("create temp dir"); + let path = tmp_dir.path(); + create_dir(&path.join("content")).expect("create content temp dir"); + create_dir(&path.join("content").join("posts")).expect("create posts temp dir"); + let nested_path = path.join("content").join("posts").join("with-assets"); + create_dir(&nested_path).expect("create nested temp dir"); + let mut f = File::create(nested_path.join("_index.md")).unwrap(); + f.write_all(b"+++\nslug=\"hey\"\n+++\n").unwrap(); + File::create(nested_path.join("example.js")).unwrap(); + File::create(nested_path.join("graph.jpg")).unwrap(); + File::create(nested_path.join("fail.png")).unwrap(); + + let mut gsb = GlobSetBuilder::new(); + gsb.add(Glob::new("*.{js,png}").unwrap()); + let mut config = Config::default(); + config.ignored_content_globset = Some(gsb.build().unwrap()); + + let res = Section::from_file( + nested_path.join("_index.md").as_path(), + &config, + ); + + assert!(res.is_ok()); + let page = res.unwrap(); + assert_eq!(page.assets.len(), 1); + assert_eq!(page.assets[0].file_name().unwrap().to_str(), Some("graph.jpg")); + } +} diff --git a/components/site/src/lib.rs b/components/site/src/lib.rs index dd351ef..d2e198b 100644 --- a/components/site/src/lib.rs +++ b/components/site/src/lib.rs @@ -187,7 +187,6 @@ impl Site { section_entries .into_par_iter() - .filter(|entry| entry.as_path().file_name().unwrap() == "_index.md") .map(|entry| { let path = entry.as_path(); Section::from_file(path, config) @@ -200,7 +199,6 @@ impl Site { page_entries .into_par_iter() - .filter(|entry| entry.as_path().file_name().unwrap() != "_index.md") .map(|entry| { let path = entry.as_path(); Page::from_file(path, config) @@ -216,7 +214,7 @@ impl Site { } // Insert a default index section if necessary so we don't need to create - // a _index.md to render the index page + // a _index.md to render the index page at the root of the site let index_path = self.index_section_path(); if let Some(ref index_section) = self.sections.get(&index_path) { if self.config.build_search_index && !index_section.meta.in_search_index { @@ -837,6 +835,12 @@ 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()))?; + } + if render_pages { section .pages diff --git a/docs/content/documentation/content/overview.md b/docs/content/documentation/content/overview.md index c2e30d1..969e966 100644 --- a/docs/content/documentation/content/overview.md +++ b/docs/content/documentation/content/overview.md @@ -40,18 +40,34 @@ While not shown in the example, sections can be nested indefinitely. ## Assets colocation The `content` directory is not limited to markup files though: it's natural to want to co-locate a page and some related -assets. +assets, for instance images or spreadsheets. Gutenberg supports that pattern out of the box for both sections and pages. + +Any non-markdown file you add in the page/section folder will be copied alongside the generated page when building the site, +which allows us to use a relative path to access them. + +For pages to use assets colocation, they should not be placed directly in their section folder (such as `latest-experiment.md`), but as an `index.md` file +in a dedicated folder (`latest-experiment/index.md`), like so: -Gutenberg supports that pattern out of the box: create a folder, add a `index.md` file and as many non-markdown files as you want. -Those assets will be copied in the same folder when building the site which allows you to use a relative path to access them. ```bash -└── with-assets - ├── index.md - └── yavascript.js +└── research + ├── latest-experiment + │ ├── index.md + │ └── yavascript.js + ├── _index.md + └── research.jpg +``` + +In this setup, you may access `research.jpg` from your 'research' section, +and `yavascript.js` from your 'latest-experiment' directly within the Markdown: + +```markdown +Check out the complete program [here](yavascript.js). It's **really cool free-software**! ``` -By default, this page will get the folder name (`with-assets` in this case) as its slug. +By default, this page will get the folder name as its slug. So its permalink would be in the form of `https://example.com/research/latest-experiment/` + +### Excluding files from assets It is possible to ignore selected asset files using the [ignored_content](./documentation/getting-started/configuration.md) setting in the config file. diff --git a/docs/content/documentation/content/page.md b/docs/content/documentation/content/page.md index 7d61c27..733f412 100644 --- a/docs/content/documentation/content/page.md +++ b/docs/content/documentation/content/page.md @@ -20,7 +20,7 @@ content directory `about.md` would also create a page at `[base_url]/about`. As you can see, creating an `about.md` file is exactly equivalent to creating an `about/index.md` file. The only difference between the two methods is that creating the `about` folder allows you to use asset colocation, as discussed in the -[Overview](./documentation/content/overview.md) section of this documentation. +[Overview](./documentation/content/overview.md#assets-colocation) section of this documentation. ## Front-matter diff --git a/docs/content/documentation/content/section.md b/docs/content/documentation/content/section.md index 7baba79..74c32e0 100644 --- a/docs/content/documentation/content/section.md +++ b/docs/content/documentation/content/section.md @@ -14,6 +14,8 @@ not have any content or metadata. If you would like to add content or metadata, `_index.md` file at the root of the `content` folder and edit it just as you would edit any other `_index.md` file; your `index.html` template will then have access to that content and metadata. +Any non-Markdown file in the section folder is added to the `assets` collection of the section, as explained in the [Content Overview](./documentation/content/overview.md#assets-colocation). These files are then available from the Markdown using relative links. + ## Front-matter The `_index.md` file within a folder defines the content and metadata for that section. To set diff --git a/docs/content/documentation/templates/pages-sections.md b/docs/content/documentation/templates/pages-sections.md index 297c95b..bb4d754 100644 --- a/docs/content/documentation/templates/pages-sections.md +++ b/docs/content/documentation/templates/pages-sections.md @@ -78,6 +78,8 @@ word_count: Number; reading_time: Number; // See the Table of contents section below for more details toc: Array
; +// Paths of colocated assets, relative to the content directory +assets: Array; ``` ## Table of contents