Skip to content

rendering configuration from environment variables #291

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

Closed
wants to merge 2 commits into from
Closed
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
4 changes: 0 additions & 4 deletions src/book/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -300,10 +300,6 @@ impl MDBook {
let mut highlight_css = File::create(&theme_dir.join("highlight.css"))?;
highlight_css.write_all(theme::HIGHLIGHT_CSS)?;

// highlight.js
let mut highlight_js = File::create(&theme_dir.join("highlight.js"))?;
highlight_js.write_all(theme::HIGHLIGHT_JS)?;

Ok(())
}

Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ mod parse;
pub mod renderer;
pub mod theme;
pub mod utils;
pub mod resources;

pub use book::MDBook;
pub use book::BookItem;
Expand Down
45 changes: 36 additions & 9 deletions src/renderer/html_handlebars/hbs_renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use renderer::Renderer;
use book::MDBook;
use book::bookitem::BookItem;
use {utils, theme};
use resources::{Resources,Resource};
use regex::{Regex, Captures};

use std::ascii::AsciiExt;
Expand Down Expand Up @@ -30,6 +31,7 @@ impl HtmlHandlebars {
impl Renderer for HtmlHandlebars {
fn render(&self, book: &MDBook) -> Result<(), Box<Error>> {
debug!("[fn]: render");
let res = Resources::new();
let mut handlebars = Handlebars::new();

// Load theme
Expand Down Expand Up @@ -95,6 +97,22 @@ impl Renderer for HtmlHandlebars {
data.insert("chapter_title".to_owned(), json!(ch.name));
data.insert("path_to_root".to_owned(), json!(utils::fs::path_to_root(&ch.path)));

{
let mut insert_from_env = |name: &str, conf: &Resource| {
data.insert(name.to_owned() + "_render_url", json!(conf.must_render_url()));
data.insert(name.to_owned() + "_render_embed", json!(conf.must_embed()));
if conf.must_render_url() {
data.insert(name.to_owned() + "_url", json!(conf.url()));
}
};
insert_from_env("highlight", &res.conf.highlight);
insert_from_env("jquery", &res.conf.jquery);
insert_from_env("mathjax", &res.conf.mathjax);
insert_from_env("awesome", &res.conf.awesome);
insert_from_env("source_code_pro", &res.conf.source_code_pro);
insert_from_env("open_sans", &res.conf.open_sans);
}

// Render the handlebars template with the data
debug!("[*]: Render template");
let rendered = handlebars.render("index", &data)?;
Expand Down Expand Up @@ -167,17 +185,26 @@ impl Renderer for HtmlHandlebars {
book.write_file("book.js", &theme.js)?;
book.write_file("book.css", &theme.css)?;
book.write_file("favicon.png", &theme.favicon)?;
book.write_file("jquery.js", &theme.jquery)?;
book.write_file("highlight.css", &theme.highlight_css)?;
book.write_file("tomorrow-night.css", &theme.tomorrow_night_css)?;
book.write_file("highlight.js", &theme.highlight_js)?;
book.write_file("_FontAwesome/css/font-awesome.css", theme::FONT_AWESOME)?;
book.write_file("_FontAwesome/fonts/fontawesome-webfont.eot", theme::FONT_AWESOME_EOT)?;
book.write_file("_FontAwesome/fonts/fontawesome-webfont.svg", theme::FONT_AWESOME_SVG)?;
book.write_file("_FontAwesome/fonts/fontawesome-webfont.ttf", theme::FONT_AWESOME_TTF)?;
book.write_file("_FontAwesome/fonts/fontawesome-webfont.woff", theme::FONT_AWESOME_WOFF)?;
book.write_file("_FontAwesome/fonts/fontawesome-webfont.woff2", theme::FONT_AWESOME_WOFF2)?;
book.write_file("_FontAwesome/fonts/FontAwesome.ttf", theme::FONT_AWESOME_TTF)?;

if res.conf.jquery.must_embed() {
book.write_file("jquery.js", &res.jquery())?;
}

if res.conf.highlight.must_embed() {
book.write_file("highlight.js", &res.highlight_js())?;
}

if res.conf.awesome.must_embed() {
book.write_file("_FontAwesome/css/font-awesome.css", &res.awesome_css())?;
book.write_file("_FontAwesome/fonts/fontawesome-webfont.eot", &res.awesome_eot())?;
book.write_file("_FontAwesome/fonts/fontawesome-webfont.svg", &res.awesome_svg())?;
book.write_file("_FontAwesome/fonts/fontawesome-webfont.ttf", &res.awesome_ttf())?;
book.write_file("_FontAwesome/fonts/fontawesome-webfont.woff", &res.awesome_woff())?;
book.write_file("_FontAwesome/fonts/fontawesome-webfont.woff2", &res.awesome_woff2())?;
book.write_file("_FontAwesome/fonts/FontAwesome.ttf", &res.awesome_ttf())?;
}

// Copy all remaining files
utils::fs::copy_files_except_ext(book.get_src(), book.get_dest(), true, &["md"])?;
Expand Down
233 changes: 233 additions & 0 deletions src/resources/env_config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
//! Configuration options that can be passed through environment variables.
//!
//! By default, mdBook will try to retrieve external resources through a content
//! delivery network. If that fails, it'll load an embedded version of the
//! resource. Using environment variables this behavior can be changed in
//! various ways.
//!
//! The variables are as follows. [name] can be one of JQUERY, MATHJAX,
//! HIGHLIGHT, AWESOME, OPEN_SANS, SOURCE_CODE_PRO:
//!
//! - MDBOOK_GLOBAL_STRATEGY: Strategy for finding external resources. One of
//! UrlWithFallback (the default), UrlOnly and Omit.
//! - MDBOOK_[name]_URL: The URL of the resource.
//! - MDBOOK_[name]_SOURCE: The source file for the embedded resource.
//! - MDBOOK_[name]_STRATEGY: Strategy for this particular resource. This
//! overrides the global strategy.
//!
//! All variables have sane defaults which were determined at compile time.
//! the SOURCE variables, when left empty, ensure that a version is used that
//! was embedded into mdBook itself at compile time.

use std::env;

pub static JQUERY_URL: &'static str = &"https://code.jquery.com/jquery-2.1.4.min.js";
pub static MATHJAX_URL: &'static str = &"https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML";
pub static HIGHLIGHT_URL: &'static str = &"highlight.js";
pub static AWESOME_URL: &'static str = &"https://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css";
pub static OPEN_SANS_URL: &'static str = &"https://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,800italic,400,300,600,700,800";
pub static SOURCE_CODE_PRO_URL: &'static str = &"https://fonts.googleapis.com/css?family=Source+Code+Pro:500";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think any of these leading &'s are necessary. A string literal is usually a &'static str.


/// A third-party resource
pub trait Resource {
/// returns true if this resource has to be rendered as an url
fn must_render_url(&self) -> bool;
/// returns true if this resource has to be embedded in the book
fn must_embed(&self) -> bool;
/// returns the url for this resource. This panics if must_render_url returns false.
fn url(&self) -> String;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just out of curiosity, would there be any benefit to using a strongly typed Url here instead of a plain String?

/// returns the source location for this resource, or None if none was configured.
/// This panics if must_embed returns false.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't both panicking and returning an Option<_> here a bit of an antipattern? I would have thought a function which returns a fallible type should return None instead of panicking.

fn source(&self) -> Option<String>;
}

/// A third-party resource with nothing special to it.
pub enum BasicResource {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any chance you can make this guy derive Debug and Clone? Not having a debug impl makes things annoying when you are debugging.

UrlWithFallback {
url: String,
source: Option<String>
},
UrlOnly {
url: String
},
Omit
}

impl Resource for BasicResource {
fn must_render_url(&self) -> bool {
match self {
&BasicResource::UrlWithFallback{url: _, source: _} => true,
&BasicResource::UrlOnly{url: _} => true,
_ => false
}
}

fn must_embed(&self) -> bool {
match self {
&BasicResource::UrlWithFallback{url: _, source: _} => true,
_ => false
}
}

fn url(&self) -> String {
match self {
&BasicResource::UrlWithFallback{ref url, source: _} => url.clone(),
&BasicResource::UrlOnly{ref url} => url.clone(),
_ => panic!("no url available")
}
}

fn source(&self) -> Option<String> {
match self {
&BasicResource::UrlWithFallback{url: _, ref source} => source.clone(),
_ => panic!("no source available")
}
}
}

/// Special struct for font-awesome, as it consists of multiple embedded files
/// but is configured as a directory. Methods implemented for this struct
/// calculate the paths of all the files in this directory.
pub struct Awesome {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably also have a #[derive(Debug, Clone)] attribute.

resource: BasicResource
}

impl Resource for Awesome {
fn must_render_url(&self) -> bool {
self.resource.must_render_url()
}

fn must_embed(&self) -> bool {
self.resource.must_embed()
}

fn url(&self) -> String {
self.resource.url()
}

fn source(&self) -> Option<String> {
self.resource.source()
}
}

impl Awesome {
pub fn css_source(&self) -> Option<String> {
self.resource.source().map(|p| p + "/css/font-awesome.min.css")
}

pub fn eot_source(&self) -> Option<String> {
self.resource.source().map(|p| p + "/fonts/fontawesome-webfont.eot")
}

pub fn svg_source(&self) -> Option<String> {
self.resource.source().map(|p| p + "/fonts/fontawesome-webfont.svg")
}

pub fn ttf_source(&self) -> Option<String> {
self.resource.source().map(|p| p + "/fonts/fontawesome-webfont.ttf")
}

pub fn woff_source(&self) -> Option<String> {
self.resource.source().map(|p| p + "/fonts/fontawesome-webfont.woff")
}

pub fn woff2_source(&self) -> Option<String> {
self.resource.source().map(|p| p + "/fonts/fontawesome-webfont.woff2")
}

pub fn otf_source(&self) -> Option<String> {
self.resource.source().map(|p| p + "/fonts/FontAwesome.otf")
}
}

/// Configuration information for all the third-party resources. This specifies
/// for every resource whether it should be included at all, and if so, just as
/// an URL or also as an embedded resource.
pub struct Configuration {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to Awesome and BasicResource, Configuration should also implement Debug. Clone is also a useful thing to have.

pub jquery: BasicResource,
pub mathjax: BasicResource,
pub highlight: BasicResource,
pub awesome: Awesome,
pub open_sans: BasicResource,
pub source_code_pro: BasicResource
}

/// Calculate an environment variable name of the format MDBOOK_[name]_[key]
fn varname(resource : &str, key: &str) -> String {
"MDBOOK_".to_string() + &resource.to_uppercase() + "_" + key
}

/// Returns the contents of the environment variable of the given resource with
/// the given key, or None if the environment variable was not set.
fn var(resource: &str, key: &str) -> Option<String> {
env::var(varname(resource, key)).ok()
}

/// Returns a variable as var would. If the variable could not be found, return
/// the given default.
fn var_default(resource: &str, key: &str, default: &str) -> String {
var(resource, key).unwrap_or(String::from(default))
}

/// The rendering strategy for a resource
enum Strategy {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could probably derive Debug, Clone, Copy, and PartialEq (the last one makes testing nicer).

/// Render both an inclusion through an URL, and a fallback if it exists.
UrlWithFallback,
/// Render only the inclusion through an URL
UrlOnly,
/// Omit the resource altogether
Omit
}

/// Return a strategy from an environment variable, or the default
/// `UrlWithFallback` if it can't be found.
fn strategy(resource: &str) -> Strategy {
match var(resource, "STRATEGY")
.unwrap_or(var_default("DEFAULT", "STRATEGY", "UrlWithFallback")).as_ref() {
"UrlOnly" => Strategy::UrlOnly,
"Omit" => Strategy::Omit,
_ => Strategy::UrlWithFallback
}
}

/// return an URL from an environment variable, or the given default if the URL
/// was not set.
fn url(resource: &str, default: &str) -> String {
var_default(resource, "URL", default)
}

/// return a source location from an environment variable, if it can't be found.
fn source(resource : &str) -> Option<String> {
var(resource, "EMBED_SOURCE")
}

/// return a resource from various environment variables.
fn resource(resource: &str, url_default: &str) -> BasicResource {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This smells a lot like a constructor to me... Should it instead be moved to something like BasicResources::new()?

match strategy(resource) {
Strategy::UrlWithFallback => BasicResource::UrlWithFallback {
url: url(resource, url_default),
source: source(resource)
},
Strategy::UrlOnly => BasicResource::UrlOnly {
url: url(resource, url_default)
},
Strategy::Omit => BasicResource::Omit
}
}

/// parse font_awesome configuration from environment variables.
fn awesome() -> Awesome {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't this be better as a function associated with Awesome (e.g. Awesome::new()) instead of just a normal free function?

Awesome { resource: resource("AWESOME", AWESOME_URL) }
}

/// Parse the configuration from the environment variables.
pub fn configuration_from_env() -> Configuration {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also looks a lot like a constructor for Configuration instead of just a free function. What are your thoughts on naming it Configuration::from_env()?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a couple integration tests (most probably in the tests/ directory) which set some environment variables and make sure Configuration_from_env() picks up the appropriate resources? The implementation itself is fairly straightforward, but it's always nice to have a couple sanity tests to make sure it all works and prevent any regressions from being introduced down the track.

Configuration {
jquery: resource("JQUERY", JQUERY_URL),
mathjax: resource("MATHJAX", MATHJAX_URL),
highlight: resource("HIGHLIGHT", HIGHLIGHT_URL),
awesome: awesome(),
open_sans: resource("OPEN_SANS", OPEN_SANS_URL),
source_code_pro: resource("SOURCE_CODE_PRO", SOURCE_CODE_PRO_URL)
}
}
File renamed without changes.
File renamed without changes.
Loading