#![allow(missing_docs)]
pub mod playground_editor;
#[cfg(feature = "search")]
pub mod searcher;
use anyhow::anyhow;
use log::warn;
use std::env::current_dir;
use std::fs::File;
use std::io::Read;
use std::path::{Path, PathBuf};
use crate::errors::*;
#[derive(Debug, PartialEq)]
pub struct Theme {
pub index: Vec<u8>,
pub head: Vec<u8>,
pub redirect: Vec<u8>,
pub header: Vec<u8>,
pub chrome_css: Vec<u8>,
pub general_css: Vec<u8>,
pub print_css: Vec<u8>,
pub variables_css: Vec<u8>,
pub favicon_png: Option<Vec<u8>>,
pub favicon_svg: Option<Vec<u8>>,
pub js: Vec<u8>,
pub highlight_css: Vec<u8>,
pub tomorrow_night_css: Vec<u8>,
pub ayu_highlight_css: Vec<u8>,
pub highlight_js: Vec<u8>,
pub clipboard_js: Vec<u8>,
pub font_awesome_css: Vec<u8>,
pub fontawesome_webfont_eot: Vec<u8>,
pub fontawesome_webfont_svg: Vec<u8>,
pub fontawesome_webfont_ttf: Vec<u8>,
pub fontawesome_webfont_woff: Vec<u8>,
pub fontawesome_webfont_woff2: Vec<u8>,
pub font_awesome_ttf: Vec<u8>,
pub fonts_css: Vec<u8>,
pub fonts_licenses: Vec<(String, Vec<u8>)>,
pub fonts_open_sans: Vec<(String, Vec<u8>)>,
pub source_code_pro: (String, Vec<u8>),
}
impl Theme {
pub fn new<P: AsRef<Path>>(theme_dir: P) -> Self {
let theme_dir = theme_dir.as_ref();
let mut theme = Theme::default();
if !theme_dir.exists() || !theme_dir.is_dir() {
return theme;
}
{
let files = vec![
(theme_dir.join("index.hbs"), &mut theme.index),
(theme_dir.join("head.hbs"), &mut theme.head),
(theme_dir.join("redirect.hbs"), &mut theme.redirect),
(theme_dir.join("header.hbs"), &mut theme.header),
(theme_dir.join("book.js"), &mut theme.js),
(theme_dir.join("css/chrome.css"), &mut theme.chrome_css),
(theme_dir.join("css/general.css"), &mut theme.general_css),
(theme_dir.join("css/print.css"), &mut theme.print_css),
(
theme_dir.join("css/variables.css"),
&mut theme.variables_css,
),
(theme_dir.join("highlight.js"), &mut theme.highlight_js),
(theme_dir.join("clipboard.min.js"), &mut theme.clipboard_js),
(theme_dir.join("highlight.css"), &mut theme.highlight_css),
(
theme_dir.join("tomorrow-night.css"),
&mut theme.tomorrow_night_css,
),
(
theme_dir.join("ayu-highlight.css"),
&mut theme.ayu_highlight_css,
),
(
theme_dir.join(FONT_AWESOME_CSS_NAME),
&mut theme.font_awesome_css,
),
(
theme_dir.join(FONT_AWESOME_EOT_NAME),
&mut theme.fontawesome_webfont_eot,
),
(
theme_dir.join(FONT_AWESOME_SVG_NAME),
&mut theme.fontawesome_webfont_svg,
),
(
theme_dir.join(FONT_AWESOME_TTF_NAME),
&mut theme.fontawesome_webfont_ttf,
),
(
theme_dir.join(FONT_AWESOME_WOFF_NAME),
&mut theme.fontawesome_webfont_woff,
),
(
theme_dir.join(FONT_AWESOME_WOFF2_NAME),
&mut theme.fontawesome_webfont_woff2,
),
(
theme_dir.join(FONT_AWESOME_TTF_NAME),
&mut theme.font_awesome_ttf,
),
(theme_dir.join(FONT_CSS_NAME), &mut theme.fonts_css),
];
let load_with_warn = |filename: &Path, dest| {
if !filename.exists() {
return false;
}
if let Err(e) = load_file_contents(filename, dest) {
warn!("Couldn't load custom file, {}: {}", filename.display(), e);
false
} else {
true
}
};
for (filename, dest) in files {
load_with_warn(&filename, dest);
}
for (name, bytes) in &mut theme.fonts_licenses {
let p = theme_dir.join(name);
load_with_warn(&p, bytes);
}
for (name, bytes) in &mut theme.fonts_open_sans {
let p = theme_dir.join(name);
load_with_warn(&p, bytes);
}
{
let p = theme_dir.join(&theme.source_code_pro.0);
load_with_warn(&p, &mut theme.source_code_pro.1);
}
let favicon_png = &mut theme.favicon_png.as_mut().unwrap();
let png = load_with_warn(&theme_dir.join("favicon.png"), favicon_png);
let favicon_svg = &mut theme.favicon_svg.as_mut().unwrap();
let svg = load_with_warn(&theme_dir.join("favicon.svg"), favicon_svg);
match (png, svg) {
(true, true) | (false, false) => {}
(true, false) => {
theme.favicon_svg = None;
}
(false, true) => {
theme.favicon_png = None;
}
}
}
theme
}
}
pub const INDEX_NAME: &'static str = "index.hbs";
pub const HEAD_NAME: &'static str = "head.hbs";
pub const REDIRECT_NAME: &'static str = "redirect.hbs";
pub const HEADER_NAME: &'static str = "header.hbs";
pub const CHROME_CSS_NAME: &'static str = "css/chrome.css";
pub const GENERAL_CSS_NAME: &'static str = "css/general.css";
pub const PRINT_CSS_NAME: &'static str = "css/print.css";
pub const VARIABLES_CSS_NAME: &'static str = "css/variables.css";
pub const FAVICON_PNG_NAME: &'static str = "favicon.png";
pub const FAVICON_SVG_NAME: &'static str = "favicon.svg";
pub const JS_NAME: &'static str = "book.js";
pub const HIGHLIGHT_JS_NAME: &'static str = "highlight.js";
pub const TOMORROW_NIGHT_CSS_NAME: &'static str = "tomorrow-night.css";
pub const HIGHLIGHT_CSS_NAME: &'static str = "highlight.css";
pub const AYU_HIGHLIGHT_CSS_NAME: &'static str = "ayu-highlight.css";
pub const CLIPBOARD_JS_NAME: &'static str = "clipboard.min.js";
pub const FONT_AWESOME_CSS_NAME: &'static str = "FontAwesome/css/font-awesome.min.css";
pub const FONT_AWESOME_EOT_NAME: &'static str = "FontAwesome/fonts/fontawesome-webfont.eot";
pub const FONT_AWESOME_SVG_NAME: &'static str = "FontAwesome/fonts/fontawesome-webfont.svg";
pub const FONT_AWESOME_TTF_NAME: &'static str = "FontAwesome/fonts/fontawesome-webfont.ttf";
pub const FONT_AWESOME_WOFF_NAME: &'static str = "FontAwesome/fonts/fontawesome-webfont.woff";
pub const FONT_AWESOME_WOFF2_NAME: &'static str = "FontAwesome/fonts/fontawesome-webfont.woff2";
pub const FONT_AWESOME_OTF_NAME: &'static str = "FontAwesome/fonts/FontAwesome.otf";
pub const FONT_CSS_NAME: &'static str = "fonts/fonts.css";
pub const OPEN_SANS_LICENSE_NAME: &'static str = "fonts/OPEN-SANS-LICENSE.txt";
pub const OPEN_SANS_SOURCE_CODE_PRO_LICENSE_NAME: &'static str =
"fonts/SOURCE-CODE-PRO-LICENSE.txt";
pub const FONT_OPEN_SANS_17_ALL_CHARSETS_300_NAME: &'static str =
"fonts/open-sans-v17-all-charsets-300.woff2";
pub const FONT_OPEN_SANS_17_ALL_CHARSETS_300ITALIC_NAME: &'static str =
"fonts/open-sans-v17-all-charsets-300italic.woff2";
pub const FONT_OPEN_SANS_17_ALL_CHARSETS_REGULAR_NAME: &'static str =
"fonts/open-sans-v17-all-charsets-regular.woff2";
pub const FONT_OPEN_SANS_17_ALL_CHARSETS_ITALIC_NAME: &'static str =
"fonts/open-sans-v17-all-charsets-italic.woff2";
pub const FONT_OPEN_SANS_17_ALL_CHARSETS_600_NAME: &'static str =
"fonts/open-sans-v17-all-charsets-600.woff2";
pub const FONT_OPEN_SANS_17_ALL_CHARSETS_600ITALIC_NAME: &'static str =
"fonts/open-sans-v17-all-charsets-600italic.woff2";
pub const FONT_OPEN_SANS_17_ALL_CHARSETS_700_NAME: &'static str =
"fonts/open-sans-v17-all-charsets-700.woff2";
pub const FONT_OPEN_SANS_17_ALL_CHARSETS_700ITALIC_NAME: &'static str =
"fonts/open-sans-v17-all-charsets-700italic.woff2";
pub const FONT_OPEN_SANS_17_ALL_CHARSETS_800_NAME: &'static str =
"fonts/open-sans-v17-all-charsets-800.woff2";
pub const FONT_OPEN_SANS_17_ALL_CHARSETS_800ITALIC_NAME: &'static str =
"fonts/open-sans-v17-all-charsets-800italic.woff2";
pub const FONT_SOURCE_CODE_PRO_11_ALL_CHARSETS_500_NAME: &'static str =
"fonts/source-code-pro-v11-all-charsets-500.woff2";
pub const RESOURCES: &'static [&str; 37] = &[
INDEX_NAME,
HEAD_NAME,
REDIRECT_NAME,
HEADER_NAME,
CHROME_CSS_NAME,
GENERAL_CSS_NAME,
PRINT_CSS_NAME,
VARIABLES_CSS_NAME,
FAVICON_PNG_NAME,
FAVICON_SVG_NAME,
JS_NAME,
HIGHLIGHT_JS_NAME,
TOMORROW_NIGHT_CSS_NAME,
HIGHLIGHT_CSS_NAME,
AYU_HIGHLIGHT_CSS_NAME,
CLIPBOARD_JS_NAME,
FONT_AWESOME_CSS_NAME,
FONT_AWESOME_EOT_NAME,
FONT_AWESOME_SVG_NAME,
FONT_AWESOME_TTF_NAME,
FONT_AWESOME_WOFF_NAME,
FONT_AWESOME_WOFF2_NAME,
FONT_AWESOME_OTF_NAME,
FONT_CSS_NAME,
OPEN_SANS_LICENSE_NAME,
OPEN_SANS_SOURCE_CODE_PRO_LICENSE_NAME,
FONT_OPEN_SANS_17_ALL_CHARSETS_300_NAME,
FONT_OPEN_SANS_17_ALL_CHARSETS_300ITALIC_NAME,
FONT_OPEN_SANS_17_ALL_CHARSETS_REGULAR_NAME,
FONT_OPEN_SANS_17_ALL_CHARSETS_ITALIC_NAME,
FONT_OPEN_SANS_17_ALL_CHARSETS_600_NAME,
FONT_OPEN_SANS_17_ALL_CHARSETS_600ITALIC_NAME,
FONT_OPEN_SANS_17_ALL_CHARSETS_700_NAME,
FONT_OPEN_SANS_17_ALL_CHARSETS_700ITALIC_NAME,
FONT_OPEN_SANS_17_ALL_CHARSETS_800_NAME,
FONT_OPEN_SANS_17_ALL_CHARSETS_800ITALIC_NAME,
FONT_SOURCE_CODE_PRO_11_ALL_CHARSETS_500_NAME,
];
pub const OPEN_SANS_LICENSES: &'static [&str; 2] = &[
OPEN_SANS_LICENSE_NAME,
OPEN_SANS_SOURCE_CODE_PRO_LICENSE_NAME,
];
pub const FONTS_OPEN_SANS: &'static [&str; 10] = &[
FONT_OPEN_SANS_17_ALL_CHARSETS_300_NAME,
FONT_OPEN_SANS_17_ALL_CHARSETS_300ITALIC_NAME,
FONT_OPEN_SANS_17_ALL_CHARSETS_REGULAR_NAME,
FONT_OPEN_SANS_17_ALL_CHARSETS_ITALIC_NAME,
FONT_OPEN_SANS_17_ALL_CHARSETS_600_NAME,
FONT_OPEN_SANS_17_ALL_CHARSETS_600ITALIC_NAME,
FONT_OPEN_SANS_17_ALL_CHARSETS_700_NAME,
FONT_OPEN_SANS_17_ALL_CHARSETS_700ITALIC_NAME,
FONT_OPEN_SANS_17_ALL_CHARSETS_800_NAME,
FONT_OPEN_SANS_17_ALL_CHARSETS_800ITALIC_NAME,
];
impl Default for Theme {
fn default() -> Theme {
Theme {
index: load_file_contents_from_know_dir_to_vec(INDEX_NAME),
head: load_file_contents_from_know_dir_to_vec(HEAD_NAME),
redirect: load_file_contents_from_know_dir_to_vec(REDIRECT_NAME),
header: load_file_contents_from_know_dir_to_vec(HEADER_NAME),
chrome_css: load_file_contents_from_know_dir_to_vec(CHROME_CSS_NAME),
general_css: load_file_contents_from_know_dir_to_vec(GENERAL_CSS_NAME),
print_css: load_file_contents_from_know_dir_to_vec(PRINT_CSS_NAME),
variables_css: load_file_contents_from_know_dir_to_vec(VARIABLES_CSS_NAME),
favicon_png: Some(load_file_contents_from_know_dir_to_vec(FAVICON_PNG_NAME)),
favicon_svg: Some(load_file_contents_from_know_dir_to_vec(FAVICON_SVG_NAME)),
js: load_file_contents_from_know_dir_to_vec(JS_NAME),
highlight_css: load_file_contents_from_know_dir_to_vec(HIGHLIGHT_CSS_NAME),
tomorrow_night_css: load_file_contents_from_know_dir_to_vec(TOMORROW_NIGHT_CSS_NAME),
ayu_highlight_css: load_file_contents_from_know_dir_to_vec(AYU_HIGHLIGHT_CSS_NAME),
highlight_js: load_file_contents_from_know_dir_to_vec(HIGHLIGHT_JS_NAME),
clipboard_js: load_file_contents_from_know_dir_to_vec(CLIPBOARD_JS_NAME),
font_awesome_css: load_file_contents_from_know_dir_to_vec(FONT_AWESOME_CSS_NAME),
fontawesome_webfont_eot: load_file_contents_from_know_dir_to_vec(FONT_AWESOME_EOT_NAME),
fontawesome_webfont_svg: load_file_contents_from_know_dir_to_vec(FONT_AWESOME_SVG_NAME),
fontawesome_webfont_ttf: load_file_contents_from_know_dir_to_vec(FONT_AWESOME_TTF_NAME),
fontawesome_webfont_woff: load_file_contents_from_know_dir_to_vec(
FONT_AWESOME_WOFF_NAME,
),
fontawesome_webfont_woff2: load_file_contents_from_know_dir_to_vec(
FONT_AWESOME_WOFF2_NAME,
),
font_awesome_ttf: load_file_contents_from_know_dir_to_vec(FONT_AWESOME_TTF_NAME),
fonts_css: load_file_contents_from_know_dir_to_vec(FONT_CSS_NAME),
fonts_licenses: vec![
load_file_contents_from_know_dir_to_name_with_vec(OPEN_SANS_LICENSE_NAME),
load_file_contents_from_know_dir_to_name_with_vec(
OPEN_SANS_SOURCE_CODE_PRO_LICENSE_NAME,
),
],
fonts_open_sans: vec![
load_file_contents_from_know_dir_to_name_with_vec(
FONT_OPEN_SANS_17_ALL_CHARSETS_300_NAME,
),
load_file_contents_from_know_dir_to_name_with_vec(
FONT_OPEN_SANS_17_ALL_CHARSETS_300ITALIC_NAME,
),
load_file_contents_from_know_dir_to_name_with_vec(
FONT_OPEN_SANS_17_ALL_CHARSETS_REGULAR_NAME,
),
load_file_contents_from_know_dir_to_name_with_vec(
FONT_OPEN_SANS_17_ALL_CHARSETS_ITALIC_NAME,
),
load_file_contents_from_know_dir_to_name_with_vec(
FONT_OPEN_SANS_17_ALL_CHARSETS_600_NAME,
),
load_file_contents_from_know_dir_to_name_with_vec(
FONT_OPEN_SANS_17_ALL_CHARSETS_600ITALIC_NAME,
),
load_file_contents_from_know_dir_to_name_with_vec(
FONT_OPEN_SANS_17_ALL_CHARSETS_700_NAME,
),
load_file_contents_from_know_dir_to_name_with_vec(
FONT_OPEN_SANS_17_ALL_CHARSETS_700ITALIC_NAME,
),
load_file_contents_from_know_dir_to_name_with_vec(
FONT_OPEN_SANS_17_ALL_CHARSETS_800_NAME,
),
load_file_contents_from_know_dir_to_name_with_vec(
FONT_OPEN_SANS_17_ALL_CHARSETS_800ITALIC_NAME,
),
],
source_code_pro: load_file_contents_from_know_dir_to_name_with_vec(
FONT_SOURCE_CODE_PRO_11_ALL_CHARSETS_500_NAME,
),
}
}
}
fn load_file_contents<P: AsRef<Path>>(filename: P, dest: &mut Vec<u8>) -> Result<()> {
let filename = filename.as_ref();
let mut buffer = Vec::new();
File::open(filename)?.read_to_end(&mut buffer)?;
dest.clear();
dest.append(&mut buffer);
Ok(())
}
fn load_file_contents_any_p_joined_to_vec<P: AsRef<Path>, Y: AsRef<Path>>(
filenames: &[Option<P>],
join: Y,
) -> Result<Vec<u8>> {
let filename = filenames
.iter()
.flat_map(|p| (*p).as_ref())
.map(|p| p.as_ref().join(join.as_ref()))
.find(|p| p.exists())
.ok_or_else(|| anyhow!("no files found"))?;
let mut buffer = Vec::new();
File::open(filename)?.read_to_end(&mut buffer)?;
Ok(buffer)
}
pub fn load_file_contents_from_know_dir_to_vec<P: AsRef<Path>>(name: P) -> Vec<u8> {
let theme_path = std::env::var("DEFAULT_THEME_PATH")
.map(|p| PathBuf::from(p))
.ok();
let regular_theme_path = PathBuf::from("src/theme/");
load_file_contents_any_p_joined_to_vec(
&[theme_path.clone(), Some(regular_theme_path.clone())],
name.as_ref(),
)
.expect(&format!(
"{:?} expected to exists under either: {:?} or {:?}",
name.as_ref(),
full_path(theme_path.map(|p| p.join(name.as_ref()))),
full_path(Some(regular_theme_path.join(name.as_ref()))),
))
}
fn full_path<P: AsRef<Path>>(opt: Option<P>) -> Option<String> {
opt.map(|p| {
p.as_ref()
.canonicalize()
.ok()
.or(current_dir().ok().map(|d| d.join(p.as_ref())))
})
.flatten()
.map(|p| p.to_string_lossy().to_string())
}
fn load_file_contents_from_know_dir_to_name_with_vec<P: AsRef<Path>>(name: P) -> (String, Vec<u8>) {
(
name.as_ref().to_string_lossy().to_string(),
load_file_contents_from_know_dir_to_vec(name),
)
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::path::PathBuf;
use tempfile::Builder as TempFileBuilder;
#[test]
fn theme_uses_defaults_with_nonexistent_src_dir() {
let non_existent = PathBuf::from("/non/existent/directory/");
assert!(!non_existent.exists());
let should_be = Theme::default();
let got = Theme::new(&non_existent);
assert_eq!(got, should_be);
}
#[test]
fn theme_dir_overrides_defaults() {
let files = *RESOURCES;
let temp = TempFileBuilder::new().prefix("mdbook-").tempdir().unwrap();
fs::create_dir(temp.path().join("css")).unwrap();
for file in &files {
let p = temp.path().join(file);
if !p.parent().map(|p| p.exists()).unwrap_or(true) {
fs::create_dir_all(p.parent().unwrap()).unwrap();
}
File::create(&p).expect(&format!("{:?} expected to exists", p));
}
let got = Theme::new(temp.path());
let empty = Theme {
index: Vec::new(),
head: Vec::new(),
redirect: Vec::new(),
header: Vec::new(),
chrome_css: Vec::new(),
general_css: Vec::new(),
print_css: Vec::new(),
variables_css: Vec::new(),
favicon_png: Some(Vec::new()),
favicon_svg: Some(Vec::new()),
js: Vec::new(),
highlight_css: Vec::new(),
tomorrow_night_css: Vec::new(),
ayu_highlight_css: Vec::new(),
highlight_js: Vec::new(),
clipboard_js: Vec::new(),
font_awesome_css: vec![],
fontawesome_webfont_eot: vec![],
fontawesome_webfont_svg: vec![],
fontawesome_webfont_ttf: vec![],
fontawesome_webfont_woff: vec![],
fontawesome_webfont_woff2: vec![],
font_awesome_ttf: vec![],
fonts_css: vec![],
fonts_licenses: OPEN_SANS_LICENSES
.iter()
.map(|s| (s.to_string(), vec![]))
.collect(),
fonts_open_sans: FONTS_OPEN_SANS
.iter()
.map(|s| (s.to_string(), vec![]))
.collect(),
source_code_pro: (
FONT_SOURCE_CODE_PRO_11_ALL_CHARSETS_500_NAME.to_string(),
vec![],
),
};
assert_eq!(got, empty);
}
#[test]
fn favicon_override() {
let temp = TempFileBuilder::new().prefix("mdbook-").tempdir().unwrap();
fs::write(temp.path().join("favicon.png"), "1234").unwrap();
let got = Theme::new(temp.path());
assert_eq!(got.favicon_png.as_ref().unwrap(), b"1234");
assert_eq!(got.favicon_svg, None);
let temp = TempFileBuilder::new().prefix("mdbook-").tempdir().unwrap();
fs::write(temp.path().join("favicon.svg"), "4567").unwrap();
let got = Theme::new(temp.path());
assert_eq!(got.favicon_png, None);
assert_eq!(got.favicon_svg.as_ref().unwrap(), b"4567");
}
}