commit
9675ab190d
|
@ -8,7 +8,7 @@ use serde::ser::{SerializeStruct, self};
|
||||||
use config::Config;
|
use config::Config;
|
||||||
use front_matter::{SectionFrontMatter, split_section_content};
|
use front_matter::{SectionFrontMatter, split_section_content};
|
||||||
use errors::{Result, ResultExt};
|
use errors::{Result, ResultExt};
|
||||||
use utils::fs::read_file;
|
use utils::fs::{read_file, find_related_assets};
|
||||||
use utils::templates::render_template;
|
use utils::templates::render_template;
|
||||||
use utils::site::get_reading_analytics;
|
use utils::site::get_reading_analytics;
|
||||||
use rendering::{RenderContext, Header, render_content};
|
use rendering::{RenderContext, Header, render_content};
|
||||||
|
@ -33,6 +33,8 @@ pub struct Section {
|
||||||
pub raw_content: String,
|
pub raw_content: String,
|
||||||
/// The HTML rendered of the page
|
/// The HTML rendered of the page
|
||||||
pub content: String,
|
pub content: String,
|
||||||
|
/// All the non-md files we found next to the .md file
|
||||||
|
pub assets: Vec<PathBuf>,
|
||||||
/// All direct pages of that section
|
/// All direct pages of that section
|
||||||
pub pages: Vec<Page>,
|
pub pages: Vec<Page>,
|
||||||
/// All pages that cannot be sorted in this section
|
/// All pages that cannot be sorted in this section
|
||||||
|
@ -54,6 +56,7 @@ impl Section {
|
||||||
components: vec![],
|
components: vec![],
|
||||||
permalink: "".to_string(),
|
permalink: "".to_string(),
|
||||||
raw_content: "".to_string(),
|
raw_content: "".to_string(),
|
||||||
|
assets: vec![],
|
||||||
content: "".to_string(),
|
content: "".to_string(),
|
||||||
pages: vec![],
|
pages: vec![],
|
||||||
ignored_pages: vec![],
|
ignored_pages: vec![],
|
||||||
|
@ -79,8 +82,31 @@ impl Section {
|
||||||
pub fn from_file<P: AsRef<Path>>(path: P, config: &Config) -> Result<Section> {
|
pub fn from_file<P: AsRef<Path>>(path: P, config: &Config) -> Result<Section> {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
let content = read_file(path)?;
|
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 {
|
pub fn get_template_name(&self) -> String {
|
||||||
|
@ -146,6 +172,15 @@ impl Section {
|
||||||
pub fn is_child_page(&self, path: &PathBuf) -> bool {
|
pub fn is_child_page(&self, path: &PathBuf) -> bool {
|
||||||
self.all_pages_path().contains(path)
|
self.all_pages_path().contains(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates a vectors of asset URLs.
|
||||||
|
fn serialize_assets(&self) -> Vec<String> {
|
||||||
|
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 {
|
impl ser::Serialize for Section {
|
||||||
|
@ -165,6 +200,8 @@ impl ser::Serialize for Section {
|
||||||
state.serialize_field("word_count", &word_count)?;
|
state.serialize_field("word_count", &word_count)?;
|
||||||
state.serialize_field("reading_time", &reading_time)?;
|
state.serialize_field("reading_time", &reading_time)?;
|
||||||
state.serialize_field("toc", &self.toc)?;
|
state.serialize_field("toc", &self.toc)?;
|
||||||
|
let assets = self.serialize_assets();
|
||||||
|
state.serialize_field("assets", &assets)?;
|
||||||
state.end()
|
state.end()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -179,6 +216,7 @@ impl Default for Section {
|
||||||
components: vec![],
|
components: vec![],
|
||||||
permalink: "".to_string(),
|
permalink: "".to_string(),
|
||||||
raw_content: "".to_string(),
|
raw_content: "".to_string(),
|
||||||
|
assets: vec![],
|
||||||
content: "".to_string(),
|
content: "".to_string(),
|
||||||
pages: vec![],
|
pages: vec![],
|
||||||
ignored_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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -187,7 +187,6 @@ impl Site {
|
||||||
|
|
||||||
section_entries
|
section_entries
|
||||||
.into_par_iter()
|
.into_par_iter()
|
||||||
.filter(|entry| entry.as_path().file_name().unwrap() == "_index.md")
|
|
||||||
.map(|entry| {
|
.map(|entry| {
|
||||||
let path = entry.as_path();
|
let path = entry.as_path();
|
||||||
Section::from_file(path, config)
|
Section::from_file(path, config)
|
||||||
|
@ -200,7 +199,6 @@ impl Site {
|
||||||
|
|
||||||
page_entries
|
page_entries
|
||||||
.into_par_iter()
|
.into_par_iter()
|
||||||
.filter(|entry| entry.as_path().file_name().unwrap() != "_index.md")
|
|
||||||
.map(|entry| {
|
.map(|entry| {
|
||||||
let path = entry.as_path();
|
let path = entry.as_path();
|
||||||
Page::from_file(path, config)
|
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
|
// 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();
|
let index_path = self.index_section_path();
|
||||||
if let Some(ref index_section) = self.sections.get(&index_path) {
|
if let Some(ref index_section) = self.sections.get(&index_path) {
|
||||||
if self.config.build_search_index && !index_section.meta.in_search_index {
|
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 {
|
if render_pages {
|
||||||
section
|
section
|
||||||
.pages
|
.pages
|
||||||
|
|
|
@ -40,18 +40,34 @@ While not shown in the example, sections can be nested indefinitely.
|
||||||
## Assets colocation
|
## 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
|
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
|
```bash
|
||||||
└── with-assets
|
└── research
|
||||||
├── index.md
|
├── latest-experiment
|
||||||
└── yavascript.js
|
│ ├── index.md
|
||||||
|
│ └── yavascript.js
|
||||||
|
├── _index.md
|
||||||
|
└── research.jpg
|
||||||
```
|
```
|
||||||
|
|
||||||
By default, this page will get the folder name (`with-assets` in this case) as its slug.
|
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 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
|
It is possible to ignore selected asset files using the
|
||||||
[ignored_content](./documentation/getting-started/configuration.md) setting in the config file.
|
[ignored_content](./documentation/getting-started/configuration.md) setting in the config file.
|
||||||
|
|
|
@ -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
|
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
|
`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
|
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
|
## Front-matter
|
||||||
|
|
||||||
|
|
|
@ -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 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.
|
`_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
|
## Front-matter
|
||||||
|
|
||||||
The `_index.md` file within a folder defines the content and metadata for that section. To set
|
The `_index.md` file within a folder defines the content and metadata for that section. To set
|
||||||
|
|
|
@ -78,6 +78,8 @@ word_count: Number;
|
||||||
reading_time: Number;
|
reading_time: Number;
|
||||||
// See the Table of contents section below for more details
|
// See the Table of contents section below for more details
|
||||||
toc: Array<Header>;
|
toc: Array<Header>;
|
||||||
|
// Paths of colocated assets, relative to the content directory
|
||||||
|
assets: Array<String>;
|
||||||
```
|
```
|
||||||
|
|
||||||
## Table of contents
|
## Table of contents
|
||||||
|
|
Loading…
Reference in a new issue