From 6127bb050c2f65b6bca5b6f85bad29c4fc6581e4 Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Wed, 10 Jan 2018 18:00:31 +0800 Subject: [PATCH 1/8] Added handlebars templating --- Cargo.toml | 1 + src/config.rs | 37 ++++++++++++++++++++++++++++++-- src/generator.rs | 49 ++++++++++++++++++++++++++++++++++++------- src/index.hbs | 13 ++++++++++++ src/lib.rs | 2 ++ tests/dummy/book.toml | 1 + 6 files changed, 93 insertions(+), 10 deletions(-) create mode 100644 src/index.hbs diff --git a/Cargo.toml b/Cargo.toml index 2d27bc8b7..4a51de583 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ structopt-derive = "0.1.6" mime_guess = "1.8.3" env_logger = "0.5.0-rc.2" log = "0.4.1" +handlebars = "0.29.1" [dependencies.mdbook] git = "https://github.com/rust-lang-nursery/mdbook" diff --git a/src/config.rs b/src/config.rs index f5ccd93f4..7d15e61fb 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,7 +1,11 @@ use std::path::PathBuf; -use failure::Error; +use std::io::Read; +use std::fs::File; +use failure::{Error, ResultExt}; use mdbook::renderer::RenderContext; +pub const DEFAULT_TEMPLATE: &'static str = include_str!("index.hbs"); + /// The configuration struct used to tweak how an EPUB document is generated. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(default, rename_all = "kebab-case")] @@ -10,6 +14,9 @@ pub struct Config { pub additional_css: Vec, /// Should we use the default stylesheet (default: true)? pub use_default_css: bool, + /// The template file to use when rendering individual chapters (relative + /// to the book root). + pub index_template: Option, } impl Config { @@ -17,10 +24,35 @@ impl Config { /// falling back to the default if pub fn from_render_context(ctx: &RenderContext) -> Result { match ctx.config.get("output.epub") { - Some(table) => table.clone().try_into().map_err(|e| Error::from(e)), + Some(table) => { + let mut cfg: Config = table.clone().try_into()?; + + // make sure we update the `index_template` to make it relative + // to the book root + if let Some(template_file) = cfg.index_template.take() { + cfg.index_template = Some(ctx.root.join(template_file)); + } + + Ok(cfg) + } None => Ok(Config::default()), } } + + pub fn template(&self) -> Result { + match self.index_template { + Some(ref filename) => { + let mut buffer = String::new(); + File::open(filename) + .with_context(|_| format!("Unable to open template ({})", filename.display()))? + .read_to_string(&mut buffer) + .context("Unable to read the template file")?; + + Ok(buffer) + } + None => Ok(DEFAULT_TEMPLATE.to_string()), + } + } } impl Default for Config { @@ -28,6 +60,7 @@ impl Default for Config { Config { use_default_css: true, additional_css: Vec::new(), + index_template: None, } } } diff --git a/src/generator.rs b/src/generator.rs index 8871b0238..e72ab2044 100644 --- a/src/generator.rs +++ b/src/generator.rs @@ -1,4 +1,5 @@ -use std::io::{Cursor, Read, Write}; +use std::io::{Read, Write}; +use std::fmt::{self, Debug, Formatter}; use std::fs::File; use mdbook::renderer::RenderContext; @@ -6,6 +7,7 @@ use mdbook::book::{BookItem, Chapter}; use epub_builder::{EpubBuilder, EpubContent, TocElement, ZipLibrary}; use failure::{Error, ResultExt}; use pulldown_cmark::{html, Parser}; +use handlebars::{Handlebars, RenderError}; use config::Config; use resources::{self, Asset}; @@ -13,23 +15,27 @@ use DEFAULT_CSS; use utils::ResultExt as SyncResultExt; /// The actual EPUB book renderer. -#[derive(Debug)] pub struct Generator<'a> { ctx: &'a RenderContext, builder: EpubBuilder, config: Config, + hbs: Handlebars, } impl<'a> Generator<'a> { pub fn new(ctx: &'a RenderContext) -> Result, Error> { let builder = EpubBuilder::new(ZipLibrary::new().sync()?).sync()?; - let config = Config::from_render_context(ctx)?; + let mut hbs = Handlebars::new(); + hbs.register_template_string("index", config.template()?) + .context("Couldn't parse the template")?; + Ok(Generator { builder, ctx, config, + hbs, }) } @@ -38,7 +44,10 @@ impl<'a> Generator<'a> { if let Some(title) = self.ctx.config.book.title.clone() { self.builder.metadata("title", title).sync()?; + } else { + warn!("No `title` attribute found yet all EPUB documents should have a title"); } + if let Some(desc) = self.ctx.config.book.description.clone() { self.builder.metadata("description", desc).sync()?; } @@ -49,6 +58,11 @@ impl<'a> Generator<'a> { .sync()?; } + self.builder + .metadata("generator", env!("CARGO_PKG_NAME")) + .sync()?; + self.builder .metadata("lang", "en") .sync()?; + Ok(()) } @@ -79,13 +93,12 @@ impl<'a> Generator<'a> { } fn add_chapter(&mut self, ch: &Chapter) -> Result<(), Error> { - let mut buffer = String::new(); - html::push_html(&mut buffer, Parser::new(&ch.content)); - - let data = Cursor::new(Vec::from(buffer)); + let rendered = self.render_chapter(ch) + .sync() + .context("Unable to render template")?; let path = ch.path.with_extension("html").display().to_string(); - let mut content = EpubContent::new(path, data).title(format!("{}", ch)); + let mut content = EpubContent::new(path, rendered.as_bytes()).title(format!("{}", ch)); let level = ch.number.as_ref().map(|n| n.len() as i32 - 1).unwrap_or(0); content = content.level(level); @@ -112,6 +125,16 @@ impl<'a> Generator<'a> { Ok(()) } + /// Render the chapter into its fully formed HTML representation. + fn render_chapter(&self, ch: &Chapter) -> Result { + let mut body = String::new(); + html::push_html(&mut body, Parser::new(&ch.content)); + + let ctx = json!({ "body": body, "title": ch.name }); + + self.hbs.render("index", &ctx) + } + /// Generate the stylesheet and add it to the document. fn embed_stylesheets(&mut self) -> Result<(), Error> { debug!("Embedding stylesheets"); @@ -168,3 +191,13 @@ impl<'a> Generator<'a> { Ok(stylesheet) } } + +impl<'a> Debug for Generator<'a> { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.debug_struct("Generator") + .field("ctx", &self.ctx) + .field("builder", &self.builder) + .field("config", &self.config) + .finish() + } +} diff --git a/src/index.hbs b/src/index.hbs new file mode 100644 index 000000000..b965f7374 --- /dev/null +++ b/src/index.hbs @@ -0,0 +1,13 @@ + + + + + {{ title }} + + + + + {{{ body }}} + + + \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 8b78221b5..c9aada075 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,7 @@ extern crate epub_builder; extern crate failure; #[macro_use] extern crate failure_derive; +extern crate handlebars; #[macro_use] extern crate log; extern crate mdbook; @@ -13,6 +14,7 @@ extern crate semver; extern crate serde; #[macro_use] extern crate serde_derive; +#[macro_use] extern crate serde_json; use std::fs::{create_dir_all, File}; diff --git a/tests/dummy/book.toml b/tests/dummy/book.toml index e75cb6f3c..c8aa243fd 100644 --- a/tests/dummy/book.toml +++ b/tests/dummy/book.toml @@ -1,4 +1,5 @@ [book] +title = "Dummy Book" authors = [] multilingual = false src = "src" From bed53fecf31c98bf0e4525e60e1633fe190a5591 Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Wed, 10 Jan 2018 18:59:38 +0800 Subject: [PATCH 2/8] The stylesheet location is now passed into the template --- src/generator.rs | 26 +++++++++++++------------- src/index.hbs | 2 +- tests/integration_tests.rs | 23 +++++++++++++++++++++++ 3 files changed, 37 insertions(+), 14 deletions(-) diff --git a/src/generator.rs b/src/generator.rs index e72ab2044..aaa379941 100644 --- a/src/generator.rs +++ b/src/generator.rs @@ -1,10 +1,11 @@ +use std::iter; use std::io::{Read, Write}; use std::fmt::{self, Debug, Formatter}; use std::fs::File; use mdbook::renderer::RenderContext; use mdbook::book::{BookItem, Chapter}; -use epub_builder::{EpubBuilder, EpubContent, TocElement, ZipLibrary}; +use epub_builder::{EpubBuilder, EpubContent, ZipLibrary}; use failure::{Error, ResultExt}; use pulldown_cmark::{html, Parser}; use handlebars::{Handlebars, RenderError}; @@ -61,7 +62,7 @@ impl<'a> Generator<'a> { self.builder .metadata("generator", env!("CARGO_PKG_NAME")) .sync()?; - self.builder .metadata("lang", "en") .sync()?; + self.builder.metadata("lang", "en").sync()?; Ok(()) } @@ -103,16 +104,6 @@ impl<'a> Generator<'a> { let level = ch.number.as_ref().map(|n| n.len() as i32 - 1).unwrap_or(0); content = content.level(level); - // unfortunately we need to do two passes through `ch.sub_items` here. - // The first pass will add each sub-item to the current chapter's toc - // and the second pass actually adds the sub-items to the book. - for sub_item in &ch.sub_items { - if let BookItem::Chapter(ref sub_ch) = *sub_item { - let child_path = sub_ch.path.with_extension("html").display().to_string(); - content = content.child(TocElement::new(child_path, format!("{}", sub_ch))); - } - } - self.builder.add_content(content).sync()?; // second pass to actually add the sub-chapters @@ -130,7 +121,16 @@ impl<'a> Generator<'a> { let mut body = String::new(); html::push_html(&mut body, Parser::new(&ch.content)); - let ctx = json!({ "body": body, "title": ch.name }); + let stylesheet_path = ch.path + .parent() + .expect("All chapters have a parent") + .components() + .map(|_| "..") + .chain(iter::once("stylesheet.css")) + .collect::>() + .join("/"); + + let ctx = json!({ "title": ch.name, "body": body, "stylesheet": stylesheet_path }); self.hbs.render("index", &ctx) } diff --git a/src/index.hbs b/src/index.hbs index b965f7374..1b28bfe6a 100644 --- a/src/index.hbs +++ b/src/index.hbs @@ -3,7 +3,7 @@ {{ title }} - + diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index b7ed46e45..a06c311f8 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -5,6 +5,7 @@ extern crate mdbook_epub; extern crate tempdir; use std::path::Path; +use std::process::Command; use failure::{Error, SyncFailure}; use tempdir::TempDir; use epub::doc::EpubDoc; @@ -41,6 +42,28 @@ fn output_epub_is_valid() { let got = EpubDoc::new(&output_file); assert!(got.is_ok()); + + // also try to run epubcheck, if it's available + epub_check(&output_file).unwrap(); +} + +fn epub_check(path: &Path) -> Result<(), Error> { + let cmd = Command::new("epubcheck").arg(path).output(); + + match cmd { + Ok(output) => { + if output.status.success() { + Ok(()) + } else { + let msg = failure::err_msg(format!("epubcheck failed\n{:?}", output)); + Err(msg) + } + } + Err(_) => { + // failed to launch epubcheck, it's probably not installed + Ok(()) + } + } } #[test] From d8f611f6ac62531124b544f5708ad5c163a5b4c3 Mon Sep 17 00:00:00 2001 From: Gambhiro Date: Sat, 15 Feb 2020 17:55:35 +0000 Subject: [PATCH 3/8] compatibility fixes --- Cargo.toml | 4 ++-- build.rs | 2 +- src/bin/mdbook-epub.rs | 20 +++++++++++--------- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4a51de583..19c2a080c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,10 +10,10 @@ name = "mdbook-epub" doc = false [build-dependencies] -cargo = "0.24.0" +cargo = "0.42.0" [dependencies] -epub-builder = "0.3.0" +epub-builder = "0.4" failure = "0.1.1" failure_derive = "0.1.1" pulldown-cmark = "0.1.0" diff --git a/build.rs b/build.rs index e05a77404..43704104c 100644 --- a/build.rs +++ b/build.rs @@ -26,7 +26,7 @@ fn main() { fn find_dependency_version>( manifest_dir: P, dep: &str, -) -> Result> { +) -> Result> { let config = Config::default()?; let manifest = manifest_dir.as_ref().join("Cargo.toml"); diff --git a/src/bin/mdbook-epub.rs b/src/bin/mdbook-epub.rs index d885338c3..1f15b927a 100644 --- a/src/bin/mdbook-epub.rs +++ b/src/bin/mdbook-epub.rs @@ -24,11 +24,11 @@ fn main() { if let Err(e) = run(&args) { eprintln!("Error: {}", e); - for cause in e.causes().skip(1) { + for cause in e.iter_chain().skip(1) { eprintln!("\tCaused By: {}", cause); } - if let Ok(_) = env::var("RUST_BACKTRACE") { + if env::var("RUST_BACKTRACE").is_ok() { eprintln!(); eprintln!("{}", e.backtrace()); } @@ -44,13 +44,15 @@ fn run(args: &Args) -> Result<(), Error> { let md = MDBook::load(&args.root).map_err(SyncFailure::new)?; let destination = md.build_dir_for("epub"); - RenderContext { - version: mdbook_epub::MDBOOK_VERSION.to_string(), - root: md.root, - book: md.book, - config: md.config, - destination: destination, - } + // NOTE not checking the version for now. + // mdbook_epub::MDBOOK_VERSION + + RenderContext::new( + md.root, + md.book, + md.config, + destination, + ) } else { serde_json::from_reader(io::stdin()).context("Unable to parse RenderContext")? }; From c55cfe3f169ff3fd4e950ff4f96caf2a7e6746d6 Mon Sep 17 00:00:00 2001 From: Gambhiro Date: Sat, 15 Feb 2020 18:02:00 +0000 Subject: [PATCH 4/8] content-type --- src/index.hbs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/index.hbs b/src/index.hbs index 1b28bfe6a..63f7cfdb1 100644 --- a/src/index.hbs +++ b/src/index.hbs @@ -1,7 +1,9 @@ - + + + - + {{ title }} @@ -10,4 +12,4 @@ {{{ body }}} - \ No newline at end of file + From 37fce92419aeb87851d06506c3985eaf3f23206c Mon Sep 17 00:00:00 2001 From: Gambhiro Date: Sat, 15 Feb 2020 18:17:52 +0000 Subject: [PATCH 5/8] doctype --- src/index.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.hbs b/src/index.hbs index 63f7cfdb1..c5a93477e 100644 --- a/src/index.hbs +++ b/src/index.hbs @@ -1,5 +1,5 @@ - + From 220ed8183997de74cf47531ecaf4d69b13e2a0aa Mon Sep 17 00:00:00 2001 From: Gambhiro Date: Wed, 19 Feb 2020 14:53:50 +0000 Subject: [PATCH 6/8] cover-image option --- README.md | 11 +++++++++-- src/config.rs | 5 ++++- src/generator.rs | 17 +++++++++++++++++ 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e79d8580f..8456cc69f 100644 --- a/README.md +++ b/README.md @@ -55,14 +55,21 @@ $ mdbook-epub --standalone ./path/to/book/dir ## Configuration -Configuration is fairly bare bones at the moment. All you can do is add -additional CSS files and disable the default stylesheet. +Configuration is fairly bare bones at the moment. +Recognized options: + +`additional-css`: A list of paths to CSS stylesheets to include. + +`use-default-css`: Controls whether to include the default stylesheet. + +`cover-image`: A path to a cover image file for the ebook. ```toml [output.epub] additional-css = ["./path/to/main.css"] use-default-css = false +cover-image = "ebook-cover.png" ``` diff --git a/src/config.rs b/src/config.rs index 7d15e61fb..d89aef00c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -4,7 +4,7 @@ use std::fs::File; use failure::{Error, ResultExt}; use mdbook::renderer::RenderContext; -pub const DEFAULT_TEMPLATE: &'static str = include_str!("index.hbs"); +pub const DEFAULT_TEMPLATE: &str = include_str!("index.hbs"); /// The configuration struct used to tweak how an EPUB document is generated. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -17,6 +17,8 @@ pub struct Config { /// The template file to use when rendering individual chapters (relative /// to the book root). pub index_template: Option, + /// A cover image to use for the epub. + pub cover_image: Option, } impl Config { @@ -61,6 +63,7 @@ impl Default for Config { use_default_css: true, additional_css: Vec::new(), index_template: None, + cover_image: None, } } } diff --git a/src/generator.rs b/src/generator.rs index 9c02ee4e8..b0605ea4f 100644 --- a/src/generator.rs +++ b/src/generator.rs @@ -73,6 +73,7 @@ impl<'a> Generator<'a> { self.populate_metadata()?; self.generate_chapters()?; + self.add_cover_image()?; self.embed_stylesheets()?; self.additional_assets()?; self.builder.generate(writer).sync()?; @@ -175,6 +176,22 @@ impl<'a> Generator<'a> { Ok(()) } + fn add_cover_image(&mut self) -> Result<(), Error> { + debug!("Adding cover image"); + + if let Some(ref path) = self.config.cover_image { + let name = path.file_name().expect("Can't provide file name."); + let full_path = path.canonicalize()?; + let mt = mime_guess::from_path(&full_path).first_or_octet_stream(); + + let content = File::open(&full_path).context("Unable to open asset")?; + + self.builder.add_cover_image(&name, content, mt.to_string()).sync()?; + } + + Ok(()) + } + fn load_asset(&mut self, asset: &Asset) -> Result<(), Error> { let content = File::open(&asset.location_on_disk).context("Unable to open asset")?; From 7cfebca0b00ee20660abe4192c62d9da76c326e9 Mon Sep 17 00:00:00 2001 From: Gambhiro Date: Wed, 19 Feb 2020 18:17:04 +0000 Subject: [PATCH 7/8] additional-resources option --- README.md | 4 ++++ src/config.rs | 3 +++ src/generator.rs | 19 +++++++++++++++++++ 3 files changed, 26 insertions(+) diff --git a/README.md b/README.md index 8456cc69f..674425690 100644 --- a/README.md +++ b/README.md @@ -65,11 +65,15 @@ Recognized options: `cover-image`: A path to a cover image file for the ebook. +`additional-resources`: A list of path to files which should be added to the +EPUB, such as typefaces. They will be added with path `OEBPS/`. + ```toml [output.epub] additional-css = ["./path/to/main.css"] use-default-css = false cover-image = "ebook-cover.png" +additional-resources = ["./assets/Open-Sans-Regular.ttf"] ``` diff --git a/src/config.rs b/src/config.rs index d89aef00c..953ab0764 100644 --- a/src/config.rs +++ b/src/config.rs @@ -19,6 +19,8 @@ pub struct Config { pub index_template: Option, /// A cover image to use for the epub. pub cover_image: Option, + /// Additional assets to include in the ebook, such as typefaces. + pub additional_resources: Vec, } impl Config { @@ -64,6 +66,7 @@ impl Default for Config { additional_css: Vec::new(), index_template: None, cover_image: None, + additional_resources: Vec::new(), } } } diff --git a/src/generator.rs b/src/generator.rs index b0605ea4f..ee719621e 100644 --- a/src/generator.rs +++ b/src/generator.rs @@ -76,6 +76,7 @@ impl<'a> Generator<'a> { self.add_cover_image()?; self.embed_stylesheets()?; self.additional_assets()?; + self.additional_resources()?; self.builder.generate(writer).sync()?; Ok(()) @@ -176,6 +177,24 @@ impl<'a> Generator<'a> { Ok(()) } + fn additional_resources(&mut self) -> Result<(), Error> { + debug!("Embedding additional resources"); + + for path in self.config.additional_resources.iter() { + debug!("Embedding {:?}", path); + + let name = path.file_name().unwrap_or_else(|| panic!("Can't determine file name of: {:?}", &path)); + let full_path = path.canonicalize()?; + let mt = mime_guess::from_path(&full_path).first_or_octet_stream(); + + let content = File::open(&full_path).context("Unable to open asset").unwrap(); + + self.builder.add_resource(&name, content, mt.to_string()).sync()?; + } + + Ok(()) + } + fn add_cover_image(&mut self) -> Result<(), Error> { debug!("Adding cover image"); From 953147ce45fc81d3f878a30b89f1f1a7f32199c0 Mon Sep 17 00:00:00 2001 From: Gambhiro Date: Thu, 20 Feb 2020 15:15:46 +0000 Subject: [PATCH 8/8] bump Rust version, remove ch.sub_items loop --- .travis.yml | 5 ++++- src/generator.rs | 13 ------------- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/.travis.yml b/.travis.yml index 14823fa59..0fa0f9bbd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,9 +8,12 @@ addons: - python3 - python3-pip +# Rust 1.38 and above supports the new Cargo.lock format +# https://github.com/rust-lang/cargo/pull/7579 + rust: - stable -- "1.32.0" +- "1.40.0" - nightly os: diff --git a/src/generator.rs b/src/generator.rs index ee719621e..89339c46d 100644 --- a/src/generator.rs +++ b/src/generator.rs @@ -106,19 +106,6 @@ impl<'a> Generator<'a> { let level = ch.number.as_ref().map(|n| n.len() as i32 - 1).unwrap_or(0); content = content.level(level); - /* FIXME: is this logic still necessary? - - // unfortunately we need to do two passes through `ch.sub_items` here. - // The first pass will add each sub-item to the current chapter's toc - // and the second pass actually adds the sub-items to the book. - for sub_item in &ch.sub_items { - if let BookItem::Chapter(ref sub_ch) = *sub_item { - let child_path = sub_ch.path.with_extension("html").display().to_string(); - content = content.child(TocElement::new(child_path, format!("{}", sub_ch))); - } - } - */ - self.builder.add_content(content).sync()?; // second pass to actually add the sub-chapters