Allow an additional custom stylesheets, closes #178

This commit is contained in:
Mathieu David 2017-05-20 13:56:01 +02:00
parent c6bfe0b1d7
commit bb4ceb481f
7 changed files with 87 additions and 230 deletions

View File

@ -1,230 +0,0 @@
extern crate toml;
use std::process::exit;
use std::fs::File;
use std::io::Read;
use std::path::{Path, PathBuf};
use std::collections::BTreeMap;
use std::str::FromStr;
use serde_json;
#[derive(Debug, Clone)]
pub struct BookConfig {
root: PathBuf,
pub dest: PathBuf,
pub src: PathBuf,
pub theme_path: PathBuf,
pub title: String,
pub author: String,
pub description: String,
pub indent_spaces: i32,
multilingual: bool,
pub google_analytics: Option<String>,
}
impl BookConfig {
pub fn new(root: &Path) -> Self {
BookConfig {
root: root.to_owned(),
dest: root.join("book"),
src: root.join("src"),
theme_path: root.join("theme"),
title: String::new(),
author: String::new(),
description: String::new(),
indent_spaces: 4, // indentation used for SUMMARY.md
multilingual: false,
google_analytics: None,
}
}
pub fn read_config(&mut self, root: &Path) -> &mut Self {
debug!("[fn]: read_config");
let read_file = |path: PathBuf| -> String {
let mut data = String::new();
let mut f: File = match File::open(&path) {
Ok(x) => x,
Err(_) => {
error!("[*]: Failed to open {:?}", &path);
exit(2);
},
};
if f.read_to_string(&mut data).is_err() {
error!("[*]: Failed to read {:?}", &path);
exit(2);
}
data
};
// Read book.toml or book.json if exists
if root.join("book.toml").exists() {
debug!("[*]: Reading config");
let data = read_file(root.join("book.toml"));
self.parse_from_toml_string(&data);
} else if root.join("book.json").exists() {
debug!("[*]: Reading config");
let data = read_file(root.join("book.json"));
self.parse_from_json_string(&data);
} else {
debug!("[*]: No book.toml or book.json was found, using defaults.");
}
self
}
pub fn parse_from_toml_string(&mut self, data: &str) -> &mut Self {
let config = match toml::from_str(data) {
Ok(x) => x,
Err(e) => {
error!("[*]: Toml parse errors in book.toml: {:?}", e);
exit(2);
},
};
self.parse_from_btreemap(&config);
self
}
/// Parses the string to JSON and converts it
/// to BTreeMap<String, toml::Value>.
pub fn parse_from_json_string(&mut self, data: &str) -> &mut Self {
let c: serde_json::Value = match serde_json::from_str(data) {
Ok(x) => x,
Err(e) => {
error!("[*]: JSON parse errors in book.json: {:?}", e);
exit(2);
},
};
let config = json_object_to_btreemap(c.as_object().unwrap());
self.parse_from_btreemap(&config);
self
}
pub fn parse_from_btreemap(&mut self, config: &BTreeMap<String, toml::Value>) -> &mut Self {
// Title, author, description
if let Some(a) = config.get("title") {
self.title = a.to_string().replace("\"", "");
}
if let Some(a) = config.get("author") {
self.author = a.to_string().replace("\"", "");
}
if let Some(a) = config.get("description") {
self.description = a.to_string().replace("\"", "");
}
// Destination folder
if let Some(a) = config.get("dest") {
let mut dest = PathBuf::from(&a.to_string().replace("\"", ""));
// If path is relative make it absolute from the parent directory of src
if dest.is_relative() {
dest = self.get_root().join(&dest);
}
self.set_dest(&dest);
}
// Source folder
if let Some(a) = config.get("src") {
let mut src = PathBuf::from(&a.to_string().replace("\"", ""));
if src.is_relative() {
src = self.get_root().join(&src);
}
self.set_src(&src);
}
// Theme path folder
if let Some(a) = config.get("theme_path") {
let mut theme_path = PathBuf::from(&a.to_string().replace("\"", ""));
if theme_path.is_relative() {
theme_path = self.get_root().join(&theme_path);
}
self.set_theme_path(&theme_path);
}
// Google analytics code
if let Some(id) = config.get("google_analytics_id") {
self.google_analytics = Some(id.to_string().replace("\"", ""));
}
self
}
pub fn get_root(&self) -> &Path {
&self.root
}
pub fn set_root(&mut self, root: &Path) -> &mut Self {
self.root = root.to_owned();
self
}
pub fn get_dest(&self) -> &Path {
&self.dest
}
pub fn set_dest(&mut self, dest: &Path) -> &mut Self {
self.dest = dest.to_owned();
self
}
pub fn get_src(&self) -> &Path {
&self.src
}
pub fn set_src(&mut self, src: &Path) -> &mut Self {
self.src = src.to_owned();
self
}
pub fn get_theme_path(&self) -> &Path {
&self.theme_path
}
pub fn set_theme_path(&mut self, theme_path: &Path) -> &mut Self {
self.theme_path = theme_path.to_owned();
self
}
}
pub fn json_object_to_btreemap(json: &serde_json::Map<String, serde_json::Value>) -> BTreeMap<String, toml::Value> {
let mut config: BTreeMap<String, toml::Value> = BTreeMap::new();
for (key, value) in json.iter() {
config.insert(String::from_str(key).unwrap(), json_value_to_toml_value(value.to_owned()));
}
config
}
pub fn json_value_to_toml_value(json: serde_json::Value) -> toml::Value {
match json {
serde_json::Value::Null => toml::Value::String("".to_string()),
serde_json::Value::Bool(x) => toml::Value::Boolean(x),
serde_json::Value::Number(ref x) if x.is_i64() => toml::Value::Integer(x.as_i64().unwrap()),
serde_json::Value::Number(ref x) if x.is_u64() => toml::Value::Integer(x.as_i64().unwrap()),
serde_json::Value::Number(x) => toml::Value::Float(x.as_f64().unwrap()),
serde_json::Value::String(x) => toml::Value::String(x),
serde_json::Value::Array(x) => {
toml::Value::Array(x.iter()
.map(|v| json_value_to_toml_value(v.to_owned()))
.collect())
},
serde_json::Value::Object(x) => toml::Value::Table(json_object_to_btreemap(&x)),
}
}

View File

@ -497,6 +497,22 @@ impl MDBook {
None
}
pub fn has_additional_css(&self) -> bool {
if let Some(htmlconfig) = self.config.get_html_config() {
return htmlconfig.has_additional_css();
}
false
}
pub fn get_additional_css(&self) -> &[PathBuf] {
if let Some(htmlconfig) = self.config.get_html_config() {
return htmlconfig.get_additional_css();
}
&[]
}
// Construct book
fn parse_summary(&mut self) -> Result<(), Box<Error>> {
// When append becomes stable, use self.content.append() ...

View File

@ -7,6 +7,7 @@ pub struct HtmlConfig {
destination: PathBuf,
theme: Option<PathBuf>,
google_analytics: Option<String>,
additional_css: Vec<PathBuf>,
}
impl HtmlConfig {
@ -26,6 +27,7 @@ impl HtmlConfig {
destination: root.into().join("book"),
theme: None,
google_analytics: None,
additional_css: Vec::new(),
}
}
@ -52,6 +54,16 @@ impl HtmlConfig {
self.google_analytics = tomlconfig.google_analytics;
}
if let Some(stylepaths) = tomlconfig.additional_css {
for path in stylepaths {
if path.is_relative() {
self.additional_css.push(root.join(path));
} else {
self.additional_css.push(path);
}
}
}
self
}
@ -89,4 +101,17 @@ impl HtmlConfig {
pub fn get_google_analytics_id(&self) -> Option<String> {
self.google_analytics.clone()
}
pub fn set_google_analytics_id(&mut self, id: Option<String>) -> &mut Self {
self.google_analytics = id;
self
}
pub fn has_additional_css(&self) -> bool {
!self.additional_css.is_empty()
}
pub fn get_additional_css(&self) -> &[PathBuf] {
&self.additional_css
}
}

View File

@ -24,6 +24,7 @@ pub struct TomlHtmlConfig {
pub destination: Option<PathBuf>,
pub theme: Option<PathBuf>,
pub google_analytics: Option<String>,
pub additional_css: Option<Vec<PathBuf>>,
}
/// Returns a TomlConfig from a TOML string

View File

@ -187,6 +187,19 @@ impl Renderer for HtmlHandlebars {
book.write_file("_FontAwesome/fonts/fontawesome-webfont.woff2", theme::FONT_AWESOME_WOFF2)?;
book.write_file("_FontAwesome/fonts/FontAwesome.ttf", theme::FONT_AWESOME_TTF)?;
for style in book.get_additional_css() {
let mut data = Vec::new();
let mut f = File::open(style)?;
f.read_to_end(&mut data)?;
let name = match style.strip_prefix(book.get_root()) {
Ok(p) => p.to_str().expect("Could not convert to str"),
Err(_) => style.file_name().expect("File has a file name").to_str().expect("Could not convert to str"),
};
book.write_file(name, &data)?;
}
// Copy all remaining files
utils::fs::copy_files_except_ext(
book.get_source(),
@ -215,6 +228,18 @@ fn make_data(book: &MDBook) -> Result<serde_json::Map<String, serde_json::Value>
data.insert("google_analytics".to_owned(), json!(ga));
}
// Add check to see if there is an additional style
if book.has_additional_css() {
let mut css = Vec::new();
for style in book.get_additional_css() {
match style.strip_prefix(book.get_root()) {
Ok(p) => css.push(p.to_str().expect("Could not convert to str")),
Err(_) => css.push(style.file_name().expect("File has a file name").to_str().expect("Could not convert to str")),
}
}
data.insert("additional_css".to_owned(), json!(css));
}
let mut chapters = vec![];
for item in book.iter() {

View File

@ -21,6 +21,11 @@
<link rel="stylesheet" href="highlight.css">
<link rel="stylesheet" href="tomorrow-night.css">
<!-- Custom theme -->
{{#each additional_css}}
<link rel="stylesheet" href="{{this}}">
{{/each}}
<!-- MathJax -->
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>

View File

@ -100,3 +100,18 @@ fn from_toml_output_html_google_analytics() {
assert_eq!(htmlconfig.get_google_analytics_id().expect("the google-analytics key was provided"), String::from("123456"));
}
// Tests that the `output.html.additional-css` key is correcly parsed in the TOML config
#[test]
fn from_toml_output_html_additional_stylesheet() {
let toml = r#"[output.html]
additional-css = ["custom.css", "two/custom.css"]"#;
let parsed = TomlConfig::from_toml(&toml).expect("This should parse");
let config = BookConfig::from_tomlconfig("root", parsed);
let htmlconfig = config.get_html_config().expect("There should be an HtmlConfig");
assert_eq!(htmlconfig.get_additional_css(), &[PathBuf::from("root/custom.css"), PathBuf::from("root/two/custom.css")]);
}