Skip to content

Load additional (external) resources #940

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ shlex = "0.1"
tempfile = "3.0"
toml = "0.5.1"
toml-query = "0.9"
glob = "0.3.0"

# Watch feature
notify = { version = "4.0", optional = true }
Expand Down
22 changes: 22 additions & 0 deletions book-example/src/format/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,10 @@ The following configuration options are available:
- **additional-js:** If you need to add some behaviour to your book without
removing the current behaviour, you can specify a set of JavaScript files that
will be loaded alongside the default one.
- **additional-resources:** If you need to include some additional resources,
like for instance image files, that are located outside of the book
directory, you can specify one or more additional-resources sections to
include these.
- **no-section-label:** mdBook by defaults adds section label in table of
contents column. For example, "1.", "2.1". Set this option to true to disable
those labels. Defaults to `false`.
Expand All @@ -172,6 +176,16 @@ The following configuration options are available:
- **git-repository-icon:** The FontAwesome icon class to use for the git
repository link. Defaults to `fa-github`.

Available configuration options for the `[[output.html.additional-resources]]`
table array:

- **output-dir:** The directory to copy the files to (relative to book output
directory). The directory is created recursively.
- **src:** The glob pattern to use when locating the additional resource files.

Note that the found files are flattened in the output dircetory, meaning the
original directory structure is lost.

Available configuration options for the `[output.html.playpen]` table:

- **editable:** Allow editing the source code. Defaults to `false`.
Expand Down Expand Up @@ -238,6 +252,14 @@ boost-paragraph = 1
expand = true
heading-split-level = 3
copy-js = true

[[output.html.additional-resources]]
output-dir="img"
src="../img/*.png"

[[output.html.additional-resources]]
output-dir="img/project"
src="../project/uml/*.png"
```

### Custom Renderers
Expand Down
24 changes: 23 additions & 1 deletion src/cmd/watch.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
extern crate notify;
use self::notify::Watcher;
use crate::{get_book_dir, open};
use clap::{App, ArgMatches, SubCommand};
use glob::glob;
use mdbook::errors::Result;
use mdbook::utils;
use mdbook::MDBook;
use notify::Watcher;
use std::path::{Path, PathBuf};
use std::sync::mpsc::channel;
use std::thread::sleep;
Expand Down Expand Up @@ -48,6 +50,24 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
Ok(())
}

fn watch_additional_resources(book: &MDBook, watcher: &mut impl notify::Watcher) {
use notify::RecursiveMode::NonRecursive;

book.config
.html_config()
.and_then(|html_config| html_config.additional_resources)
.map(|additional_resources| {
for res in additional_resources {
let found_files = glob(res.src.as_str())
.expect("Failed to read glob pattern for additional resource");
for path in found_files.filter_map(std::result::Result::ok) {
let _ = watcher.watch(&path, NonRecursive);
debug!("Watching {:?}", path);
}
}
});
}

/// Calls the closure when a book source file is changed, blocking indefinitely.
pub fn trigger_on_change<F>(book: &MDBook, closure: F)
where
Expand Down Expand Up @@ -78,6 +98,8 @@ where
// Add the book.toml file to the watcher if it exists
let _ = watcher.watch(book.root.join("book.toml"), NonRecursive);

watch_additional_resources(book, &mut watcher);

info!("Listening for changes...");

loop {
Expand Down
54 changes: 54 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,39 @@ impl Default for BookConfig {
}
}

/// Additional files not normaly copied (like external images etc), which
/// you want to add to the book output.
/// In book.toml:
/// ```toml
/// [[output.html.additional-resources]]
/// output-dir="img"
/// src="../plantuml/*.png"
///
/// [[output.html.additional-resources]]
/// output-dir="foo"
/// src="bar/*.xml"
/// ```
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case")]
pub struct AdditionalResource {
/// Glob string for the files to add (e.g. "../foo/*.png"), path is
/// relative to book root dir. The files found by the pattern
/// are flattened in the output dir (i.e. the directory structure
/// is not copied, only the files)
pub src: String,
/// Path relative to the book output dir
pub output_dir: PathBuf,
}

impl Default for AdditionalResource {
fn default() -> AdditionalResource {
AdditionalResource {
src: String::from(""),
output_dir: PathBuf::from("src"),
}
}
}

/// Configuration for the build procedure.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case")]
Expand Down Expand Up @@ -452,6 +485,9 @@ pub struct HtmlConfig {
/// FontAwesome icon class to use for the Git repository link.
/// Defaults to `fa-github` if `None`.
pub git_repository_icon: Option<String>,

/// The additional resources to copy
pub additional_resources: Option<Vec<AdditionalResource>>,
}

impl HtmlConfig {
Expand Down Expand Up @@ -587,6 +623,14 @@ mod tests {
git-repository-url = "https://foo.com/"
git-repository-icon = "fa-code-fork"

[[output.html.additional-resources]]
src="foo*.*"
output-dir="bar"

[[output.html.additional-resources]]
src="chuck*.*"
output-dir="norris"

[output.html.playpen]
editable = true
editor = "ace"
Expand Down Expand Up @@ -625,6 +669,16 @@ mod tests {
playpen: playpen_should_be,
git_repository_url: Some(String::from("https://foo.com/")),
git_repository_icon: Some(String::from("fa-code-fork")),
additional_resources: Some(vec![
AdditionalResource {
src: String::from("foo*.*"),
output_dir: PathBuf::from("bar"),
},
AdditionalResource {
src: String::from("chuck*.*"),
output_dir: PathBuf::from("norris"),
},
]),
..Default::default()
};

Expand Down
122 changes: 121 additions & 1 deletion src/renderer/html_handlebars/hbs_renderer.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::book::{Book, BookItem};
use crate::config::{Config, HtmlConfig, Playpen};
use crate::config::{AdditionalResource, Config, HtmlConfig, Playpen};
use crate::errors::*;
use crate::renderer::html_handlebars::helpers;
use crate::renderer::{RenderContext, Renderer};
Expand All @@ -11,6 +11,7 @@ use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};

use glob::glob;
use handlebars::Handlebars;
use regex::{Captures, Regex};

Expand Down Expand Up @@ -366,11 +367,66 @@ impl Renderer for HtmlHandlebars {

// Copy all remaining files
utils::fs::copy_files_except_ext(&src_dir, &destination, true, &["md"])?;
copy_additional_resources(&html_config.additional_resources, &destination)?;

Ok(())
}
}

/// Copy the additional resources specified in the config
fn copy_additional_resources(
resources: &Option<Vec<AdditionalResource>>,
destination: &PathBuf,
) -> Result<()> {
match resources {
Some(additional_resources) => {
for res in additional_resources {
copy_additional_resource(&res, &destination)?;
}
}
None => (), //Optional, so no issue here
}

Ok(())
}

/// Copy the files from a single AdditionalResource entry in config to the book dir
/// The found files are copied by name only, the original directory structure is
/// flattened
fn copy_additional_resource(res: &AdditionalResource, destination: &PathBuf) -> Result<()> {
let dest_dir = destination.join(&res.output_dir);

match fs::create_dir_all(&dest_dir) {
Ok(_) => debug!("Created: {}", dest_dir.display()),
Err(e) => {
error!(
"Failed to create output directory {} for additional resources.",
dest_dir.display()
);
return Err(e.into());
}
}

let found_files =
glob(res.src.as_str()).expect("Failed to read glob pattern for additional resource");
for path in found_files.filter_map(std::result::Result::ok) {
let file_name = path.file_name().unwrap();
let dest_file = dest_dir.join(file_name);

match fs::copy(&path, &dest_file) {
Ok(_) => debug!("Copied {} to {}", path.display(), dest_file.display()),
Err(e) => warn!(
"Failed to copy {} to {} ({})",
path.display(),
dest_file.display(),
e
),
}
}

Ok(())
}

fn make_data(
root: &Path,
book: &Book,
Expand Down Expand Up @@ -669,4 +725,68 @@ mod tests {
assert_eq!(got, should_be);
}
}
#[test]
fn test_copy_additional_resource_fails_to_create_output_dir() {
use tempfile::NamedTempFile;

// Create a file with the same name as the directory we're
// about to create. This will prevent the system from creating
// the directory
let mask_file = NamedTempFile::new().expect("Failed to create dir mask file");

let destination = PathBuf::from(&mask_file.path());
let resource = AdditionalResource {
src: String::from("*.foo"),
output_dir: PathBuf::from(&destination),
};

match copy_additional_resource(&resource, &destination) {
Ok(_) => assert!(false, "Expected a failure when creating the output dir"),
//Error is OS dependant, so not much use in checking the error text
Err(_e) => (),
}
}

#[test]
fn test_copy_additional_resource() {
use tempfile::tempdir;

let src_dir = tempdir().unwrap();
let output_dir = tempdir().unwrap();

let destination = PathBuf::from(&output_dir.path());
let resource = AdditionalResource {
src: String::from(src_dir.path().join("*.txt").to_str().unwrap()),
output_dir: PathBuf::from(&destination),
};

//Create some files
fs::File::create(src_dir.path().join("i_will_not_be_copied.doc")).unwrap();
fs::File::create(src_dir.path().join("i_will_be_copied.txt")).unwrap();
fs::File::create(src_dir.path().join("i_will_be_copied_too.txt")).unwrap();

match copy_additional_resource(&resource, &destination) {
Ok(_) => {
//Just test the most likely candidates have been copied
let test_file = destination.join("i_will_be_copied.txt");
assert!(
test_file.is_file(),
"Expected 'i_will_be_copied.txt' to be copied"
);

let test_file = destination.join("i_will_be_copied_too.txt");
assert!(
test_file.is_file(),
"Expected 'i_will_be_copied_too.txt' to be copied"
);

let test_file = destination.join("i_will_not_be_copied.doc");
assert!(
!test_file.is_file(),
"Did not expect 'i_will_not_be_copied.doc' to be copied"
);
}
Err(e) => assert!(false, "Failed to copy additional resources ({})", e),
}
}
}