Merge pull request #1987 from ehuss/theme-fonts

Make fonts part of the theme.
This commit is contained in:
Eric Huss 2023-02-08 15:56:40 -08:00 committed by GitHub
commit 2c710d3b7d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 204 additions and 8 deletions

View File

@ -126,7 +126,10 @@ The following configuration options are available:
that occur in code blocks and code spans. Defaults to `false`. that occur in code blocks and code spans. Defaults to `false`.
- **mathjax-support:** Adds support for [MathJax](../mathjax.md). Defaults to - **mathjax-support:** Adds support for [MathJax](../mathjax.md). Defaults to
`false`. `false`.
- **copy-fonts:** Copies fonts.css and respective font files to the output directory and use them in the default theme. Defaults to `true`. - **copy-fonts:** (**Deprecated**) If `true` (the default), mdBook uses its built-in fonts which are copied to the output directory.
If `false`, the built-in fonts will not be used.
This option is deprecated. If you want to define your own custom fonts,
create a `theme/fonts/fonts.css` file and store the fonts in the `theme/fonts/` directory.
- **google-analytics:** This field has been deprecated and will be removed in a future release. - **google-analytics:** This field has been deprecated and will be removed in a future release.
Use the `theme/head.hbs` file to add the appropriate Google Analytics code instead. Use the `theme/head.hbs` file to add the appropriate Google Analytics code instead.
- **additional-css:** If you need to slightly change the appearance of your book - **additional-css:** If you need to slightly change the appearance of your book

View File

@ -26,6 +26,8 @@ Here are the files you can override:
- **_highlight.css_** is the theme used for the code highlighting. - **_highlight.css_** is the theme used for the code highlighting.
- **_favicon.svg_** and **_favicon.png_** the favicon that will be used. The SVG - **_favicon.svg_** and **_favicon.png_** the favicon that will be used. The SVG
version is used by [newer browsers]. version is used by [newer browsers].
- **fonts/fonts.css** contains the definition of which fonts to load.
Custom fonts can be included in the `fonts` directory.
Generally, when you want to tweak the theme, you don't need to override all the Generally, when you want to tweak the theme, you don't need to override all the
files. If you only need changes in the stylesheet, there is no point in files. If you only need changes in the stylesheet, there is no point in

View File

@ -6,6 +6,7 @@ use super::MDBook;
use crate::config::Config; use crate::config::Config;
use crate::errors::*; use crate::errors::*;
use crate::theme; use crate::theme;
use crate::utils::fs::write_file;
use log::{debug, error, info, trace}; use log::{debug, error, info, trace};
/// A helper for setting up a new book and its directory structure. /// A helper for setting up a new book and its directory structure.
@ -160,6 +161,19 @@ impl BookBuilder {
let mut highlight_js = File::create(themedir.join("highlight.js"))?; let mut highlight_js = File::create(themedir.join("highlight.js"))?;
highlight_js.write_all(theme::HIGHLIGHT_JS)?; highlight_js.write_all(theme::HIGHLIGHT_JS)?;
write_file(&themedir.join("fonts"), "fonts.css", theme::fonts::CSS)?;
for (file_name, contents) in theme::fonts::LICENSES {
write_file(&themedir, file_name, contents)?;
}
for (file_name, contents) in theme::fonts::OPEN_SANS.iter() {
write_file(&themedir, file_name, contents)?;
}
write_file(
&themedir,
theme::fonts::SOURCE_CODE_PRO.0,
theme::fonts::SOURCE_CODE_PRO.1,
)?;
Ok(()) Ok(())
} }

View File

@ -289,6 +289,31 @@ impl HtmlHandlebars {
theme::fonts::SOURCE_CODE_PRO.1, theme::fonts::SOURCE_CODE_PRO.1,
)?; )?;
} }
if let Some(fonts_css) = &theme.fonts_css {
if !fonts_css.is_empty() {
if html_config.copy_fonts {
warn!(
"output.html.copy_fonts is deprecated.\n\
Set copy_fonts=false and ensure the fonts you want are in \
the `theme/fonts/` directory."
);
}
write_file(destination, "fonts/fonts.css", &fonts_css)?;
}
}
if !html_config.copy_fonts && theme.fonts_css.is_none() {
warn!(
"output.html.copy_fonts is deprecated.\n\
This book appears to have copy_fonts=false without a fonts.css file.\n\
Add an empty `theme/fonts/fonts.css` file to squelch this warning."
);
}
for font_file in &theme.font_files {
let contents = fs::read(font_file)?;
let filename = font_file.file_name().unwrap();
let filename = Path::new("fonts").join(filename);
write_file(destination, filename, &contents)?;
}
let playground_config = &html_config.playground; let playground_config = &html_config.playground;
@ -656,7 +681,8 @@ fn make_data(
data.insert("mathjax_support".to_owned(), json!(true)); data.insert("mathjax_support".to_owned(), json!(true));
} }
if html_config.copy_fonts { // This `matches!` checks for a non-empty file.
if html_config.copy_fonts || matches!(theme.fonts_css.as_deref(), Some([_, ..])) {
data.insert("copy_fonts".to_owned(), json!(true)); data.insert("copy_fonts".to_owned(), json!(true));
} }

View File

@ -9,7 +9,7 @@ pub mod searcher;
use std::fs::File; use std::fs::File;
use std::io::Read; use std::io::Read;
use std::path::Path; use std::path::{Path, PathBuf};
use crate::errors::*; use crate::errors::*;
use log::warn; use log::warn;
@ -54,6 +54,8 @@ pub struct Theme {
pub general_css: Vec<u8>, pub general_css: Vec<u8>,
pub print_css: Vec<u8>, pub print_css: Vec<u8>,
pub variables_css: Vec<u8>, pub variables_css: Vec<u8>,
pub fonts_css: Option<Vec<u8>>,
pub font_files: Vec<PathBuf>,
pub favicon_png: Option<Vec<u8>>, pub favicon_png: Option<Vec<u8>>,
pub favicon_svg: Option<Vec<u8>>, pub favicon_svg: Option<Vec<u8>>,
pub js: Vec<u8>, pub js: Vec<u8>,
@ -104,7 +106,7 @@ impl Theme {
), ),
]; ];
let load_with_warn = |filename: &Path, dest| { let load_with_warn = |filename: &Path, dest: &mut Vec<u8>| {
if !filename.exists() { if !filename.exists() {
// Don't warn if the file doesn't exist. // Don't warn if the file doesn't exist.
return false; return false;
@ -121,6 +123,29 @@ impl Theme {
load_with_warn(&filename, dest); load_with_warn(&filename, dest);
} }
let fonts_dir = theme_dir.join("fonts");
if fonts_dir.exists() {
let mut fonts_css = Vec::new();
if load_with_warn(&fonts_dir.join("fonts.css"), &mut fonts_css) {
theme.fonts_css.replace(fonts_css);
}
if let Ok(entries) = fonts_dir.read_dir() {
theme.font_files = entries
.filter_map(|entry| {
let entry = entry.ok()?;
if entry.file_name() == "fonts.css" {
None
} else if entry.file_type().ok()?.is_dir() {
log::info!("skipping font directory {:?}", entry.path());
None
} else {
Some(entry.path())
}
})
.collect();
}
}
// If the user overrides one favicon, but not the other, do not // If the user overrides one favicon, but not the other, do not
// copy the default for the other. // copy the default for the other.
let favicon_png = &mut theme.favicon_png.as_mut().unwrap(); let favicon_png = &mut theme.favicon_png.as_mut().unwrap();
@ -153,6 +178,8 @@ impl Default for Theme {
general_css: GENERAL_CSS.to_owned(), general_css: GENERAL_CSS.to_owned(),
print_css: PRINT_CSS.to_owned(), print_css: PRINT_CSS.to_owned(),
variables_css: VARIABLES_CSS.to_owned(), variables_css: VARIABLES_CSS.to_owned(),
fonts_css: None,
font_files: Vec::new(),
favicon_png: Some(FAVICON_PNG.to_owned()), favicon_png: Some(FAVICON_PNG.to_owned()),
favicon_svg: Some(FAVICON_SVG.to_owned()), favicon_svg: Some(FAVICON_SVG.to_owned()),
js: JS.to_owned(), js: JS.to_owned(),
@ -209,10 +236,10 @@ mod tests {
"favicon.png", "favicon.png",
"favicon.svg", "favicon.svg",
"css/chrome.css", "css/chrome.css",
"css/fonts.css",
"css/general.css", "css/general.css",
"css/print.css", "css/print.css",
"css/variables.css", "css/variables.css",
"fonts/fonts.css",
"book.js", "book.js",
"highlight.js", "highlight.js",
"tomorrow-night.css", "tomorrow-night.css",
@ -223,6 +250,7 @@ mod tests {
let temp = TempFileBuilder::new().prefix("mdbook-").tempdir().unwrap(); let temp = TempFileBuilder::new().prefix("mdbook-").tempdir().unwrap();
fs::create_dir(temp.path().join("css")).unwrap(); fs::create_dir(temp.path().join("css")).unwrap();
fs::create_dir(temp.path().join("fonts")).unwrap();
// "touch" all of the special files so we have empty copies // "touch" all of the special files so we have empty copies
for file in &files { for file in &files {
@ -240,6 +268,8 @@ mod tests {
general_css: Vec::new(), general_css: Vec::new(),
print_css: Vec::new(), print_css: Vec::new(),
variables_css: Vec::new(), variables_css: Vec::new(),
fonts_css: Some(Vec::new()),
font_files: Vec::new(),
favicon_png: Some(Vec::new()), favicon_png: Some(Vec::new()),
favicon_svg: Some(Vec::new()), favicon_svg: Some(Vec::new()),
js: Vec::new(), js: Vec::new(),

View File

@ -1,5 +1,6 @@
use mdbook::config::Config; use mdbook::config::Config;
use mdbook::MDBook; use mdbook::MDBook;
use pretty_assertions::assert_eq;
use std::fs; use std::fs;
use std::fs::File; use std::fs::File;
use std::io::prelude::*; use std::io::prelude::*;
@ -121,6 +122,20 @@ fn copy_theme() {
"css/variables.css", "css/variables.css",
"favicon.png", "favicon.png",
"favicon.svg", "favicon.svg",
"fonts/OPEN-SANS-LICENSE.txt",
"fonts/SOURCE-CODE-PRO-LICENSE.txt",
"fonts/fonts.css",
"fonts/open-sans-v17-all-charsets-300.woff2",
"fonts/open-sans-v17-all-charsets-300italic.woff2",
"fonts/open-sans-v17-all-charsets-600.woff2",
"fonts/open-sans-v17-all-charsets-600italic.woff2",
"fonts/open-sans-v17-all-charsets-700.woff2",
"fonts/open-sans-v17-all-charsets-700italic.woff2",
"fonts/open-sans-v17-all-charsets-800.woff2",
"fonts/open-sans-v17-all-charsets-800italic.woff2",
"fonts/open-sans-v17-all-charsets-italic.woff2",
"fonts/open-sans-v17-all-charsets-regular.woff2",
"fonts/source-code-pro-v11-all-charsets-500.woff2",
"highlight.css", "highlight.css",
"highlight.js", "highlight.js",
"index.hbs", "index.hbs",

View File

@ -1,6 +1,3 @@
#[macro_use]
extern crate pretty_assertions;
mod dummy_book; mod dummy_book;
use crate::dummy_book::{assert_contains_strings, assert_doesnt_contain_strings, DummyBook}; use crate::dummy_book::{assert_contains_strings, assert_doesnt_contain_strings, DummyBook};
@ -10,6 +7,7 @@ use mdbook::config::Config;
use mdbook::errors::*; use mdbook::errors::*;
use mdbook::utils::fs::write_file; use mdbook::utils::fs::write_file;
use mdbook::MDBook; use mdbook::MDBook;
use pretty_assertions::assert_eq;
use select::document::Document; use select::document::Document;
use select::predicate::{Class, Name, Predicate}; use select::predicate::{Class, Name, Predicate};
use std::collections::HashMap; use std::collections::HashMap;
@ -842,3 +840,111 @@ mod search {
} }
} }
} }
#[test]
fn custom_fonts() {
// Tests to ensure custom fonts are copied as expected.
let builtin_fonts = [
"OPEN-SANS-LICENSE.txt",
"SOURCE-CODE-PRO-LICENSE.txt",
"fonts.css",
"open-sans-v17-all-charsets-300.woff2",
"open-sans-v17-all-charsets-300italic.woff2",
"open-sans-v17-all-charsets-600.woff2",
"open-sans-v17-all-charsets-600italic.woff2",
"open-sans-v17-all-charsets-700.woff2",
"open-sans-v17-all-charsets-700italic.woff2",
"open-sans-v17-all-charsets-800.woff2",
"open-sans-v17-all-charsets-800italic.woff2",
"open-sans-v17-all-charsets-italic.woff2",
"open-sans-v17-all-charsets-regular.woff2",
"source-code-pro-v11-all-charsets-500.woff2",
];
let actual_files = |path: &Path| -> Vec<String> {
let mut actual: Vec<_> = path
.read_dir()
.unwrap()
.map(|entry| entry.unwrap().file_name().into_string().unwrap())
.collect();
actual.sort();
actual
};
let has_fonts_css = |path: &Path| -> bool {
let contents = fs::read_to_string(path.join("book/index.html")).unwrap();
contents.contains("fonts/fonts.css")
};
// No theme:
let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap();
let p = temp.path();
MDBook::init(p).build().unwrap();
MDBook::load(p).unwrap().build().unwrap();
assert_eq!(actual_files(&p.join("book/fonts")), &builtin_fonts);
assert!(has_fonts_css(p));
// Full theme.
let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap();
let p = temp.path();
MDBook::init(p).copy_theme(true).build().unwrap();
assert_eq!(actual_files(&p.join("theme/fonts")), &builtin_fonts);
MDBook::load(p).unwrap().build().unwrap();
assert_eq!(actual_files(&p.join("book/fonts")), &builtin_fonts);
assert!(has_fonts_css(p));
// Mixed with copy_fonts=true
// This should generate a deprecation warning.
let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap();
let p = temp.path();
MDBook::init(p).build().unwrap();
write_file(&p.join("theme/fonts"), "fonts.css", b"/*custom*/").unwrap();
write_file(&p.join("theme/fonts"), "myfont.woff", b"").unwrap();
MDBook::load(p).unwrap().build().unwrap();
assert!(has_fonts_css(p));
let mut expected = Vec::from(builtin_fonts);
expected.push("myfont.woff");
expected.sort();
assert_eq!(actual_files(&p.join("book/fonts")), expected.as_slice());
// copy-fonts=false, no theme
// This should generate a deprecation warning.
let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap();
let p = temp.path();
MDBook::init(p).build().unwrap();
let config = Config::from_str("output.html.copy-fonts = false").unwrap();
MDBook::load_with_config(p, config)
.unwrap()
.build()
.unwrap();
assert!(!has_fonts_css(p));
assert!(!p.join("book/fonts").exists());
// copy-fonts=false with empty fonts.css
let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap();
let p = temp.path();
MDBook::init(p).build().unwrap();
write_file(&p.join("theme/fonts"), "fonts.css", b"").unwrap();
let config = Config::from_str("output.html.copy-fonts = false").unwrap();
MDBook::load_with_config(p, config)
.unwrap()
.build()
.unwrap();
assert!(!has_fonts_css(p));
assert!(!p.join("book/fonts").exists());
// copy-fonts=false with fonts theme
let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap();
let p = temp.path();
MDBook::init(p).build().unwrap();
write_file(&p.join("theme/fonts"), "fonts.css", b"/*custom*/").unwrap();
write_file(&p.join("theme/fonts"), "myfont.woff", b"").unwrap();
let config = Config::from_str("output.html.copy-fonts = false").unwrap();
MDBook::load_with_config(p, config)
.unwrap()
.build()
.unwrap();
assert!(has_fonts_css(p));
assert_eq!(
actual_files(&p.join("book/fonts")),
&["fonts.css", "myfont.woff"]
);
}