Skip to content

Allow preprocessors to pass generated resources (fix issue #1087) #1341

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 5 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: 4 additions & 3 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 @@ -33,6 +33,7 @@ serde_json = "1.0"
shlex = "0.1"
tempfile = "3.0"
toml = "0.5.1"
base64 = "0.12.3"

# Watch feature
notify = { version = "4.0", optional = true }
Expand Down
80 changes: 80 additions & 0 deletions src/book/book.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,9 @@ pub struct Chapter {
pub path: Option<PathBuf>,
/// An ordered list of the names of each chapter above this one, in the hierarchy.
pub parent_names: Vec<String>,
/// The list of resources associated with this chapter. Typically populated
/// by a preprocessor that generates additional content.
pub resources: Vec<Resource>,
}

impl Chapter {
Expand Down Expand Up @@ -199,6 +202,53 @@ impl Chapter {
}
}

/// The representation of a "Resource", typically an image.
/// An example of a resource could be:
/// ```rust,no_run,noplayground
/// use mdbook::book::Resource;
/// Resource {
/// relative_url: String::from("./circle.svg"),
/// data: "<svg height='100' width='100'><circle cx='50' cy='50' r='40' /></svg>".as_bytes().to_vec()
/// };
/// ```
/// This resource should be saved by the renderer in such a way that any reference
/// to ./circle.svg in the chapter somehow renders/links the resource data.
/// The HBS renderer for example will save a file called circle.svg next to the
/// chapter's html file.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct Resource {
/// The resource's relative url (as used in a chapter, e.g. for ![](./foo.png)
/// it would be './foo.png'. The URL is relative to the chapter (for the HBS
/// renderer this would mean relative to the chapter's output directory).
pub relative_url: String,
/// The resource data (binary data, base64 encoded in serialization).
#[serde(with = "resource_data_base64")]
pub data: Vec<u8>,
}

/// The resource data is a vec of u8, which would have a huge overhead in JSON
/// using the default format.
/// So convert to base64 as a more optimal (and more logical) format
mod resource_data_base64 {
use base64;
use serde::{de, Deserialize, Deserializer, Serializer};

pub fn serialize<S>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&base64::encode(bytes))
}

pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
where
D: Deserializer<'de>,
{
let s = <&str>::deserialize(deserializer)?;
base64::decode(s).map_err(de::Error::custom)
}
}

/// Use the provided `Summary` to load a `Book` from disk.
///
/// You need to pass in the book's source directory because all the links in
Expand Down Expand Up @@ -331,6 +381,7 @@ impl Display for Chapter {
#[cfg(test)]
mod tests {
use super::*;
use serde_json::Value;
use std::io::Write;
use tempfile::{Builder as TempFileBuilder, TempDir};

Expand Down Expand Up @@ -412,6 +463,7 @@ And here is some \
path: Some(PathBuf::from("second.md")),
parent_names: vec![String::from("Chapter 1")],
sub_items: Vec::new(),
resources: Vec::new(),
};
let should_be = BookItem::Chapter(Chapter {
name: String::from("Chapter 1"),
Expand All @@ -424,6 +476,7 @@ And here is some \
BookItem::Separator,
BookItem::Chapter(nested.clone()),
],
resources: Vec::new(),
});

let got = load_summary_item(&SummaryItem::Link(root), temp.path(), Vec::new()).unwrap();
Expand Down Expand Up @@ -498,6 +551,7 @@ And here is some \
Vec::new(),
)),
],
resources: Vec::new(),
}),
BookItem::Separator,
],
Expand Down Expand Up @@ -550,6 +604,7 @@ And here is some \
Vec::new(),
)),
],
resources: Vec::new(),
}),
BookItem::Separator,
],
Expand Down Expand Up @@ -599,4 +654,29 @@ And here is some \
let got = load_book_from_disk(&summary, temp.path());
assert!(got.is_err());
}

#[test]
fn resource_serialization() {
let resource = Resource {
relative_url: String::from("foo/bar"),
data: "File contents\nwith newline".as_bytes().to_vec(),
};

let json = serde_json::to_string(&resource).unwrap();
let v: Value = serde_json::from_str(&json).unwrap();
let expected_data = base64::encode(&resource.data);
assert_eq!(expected_data, v["data"]);
}

#[test]
fn resource_deserialization() {
let json_resource = json!({
"relative_url": "my/relative/url",
"data": base64::encode("froboz electric"),
});

let resource: Resource = serde_json::from_str(&json_resource.to_string()).unwrap();
assert_eq!("my/relative/url", resource.relative_url);
assert_eq!("froboz electric".as_bytes().to_vec(), resource.data);
}
}
2 changes: 1 addition & 1 deletion src/book/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ mod book;
mod init;
mod summary;

pub use self::book::{load_book, Book, BookItem, BookItems, Chapter};
pub use self::book::{load_book, Book, BookItem, BookItems, Chapter, Resource};
pub use self::init::BookBuilder;
pub use self::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem};

Expand Down
Loading