Skip to content

Add minification for CSS and JS files #2728

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

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ handlebars = "6.0"
hex = "0.4.3"
log = "0.4.17"
memchr = "2.5.0"
minifier = "0.3.6"
opener = "0.8.1"
pulldown-cmark = { version = "0.10.0", default-features = false, features = ["html"] } # Do not update, part of the public api.
regex = "1.8.1"
Expand Down
12 changes: 6 additions & 6 deletions src/book/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,18 +131,18 @@ impl BookBuilder {
}

let mut general_css = File::create(cssdir.join("general.css"))?;
general_css.write_all(theme::GENERAL_CSS)?;
theme::GENERAL_CSS.write_into(&mut general_css)?;

let mut chrome_css = File::create(cssdir.join("chrome.css"))?;
chrome_css.write_all(theme::CHROME_CSS)?;
theme::CHROME_CSS.write_into(&mut chrome_css)?;

if html_config.print.enable {
let mut print_css = File::create(cssdir.join("print.css"))?;
print_css.write_all(theme::PRINT_CSS)?;
theme::PRINT_CSS.write_into(&mut print_css)?;
}

let mut variables_css = File::create(cssdir.join("variables.css"))?;
variables_css.write_all(theme::VARIABLES_CSS)?;
theme::VARIABLES_CSS.write_into(&mut variables_css)?;

let mut favicon = File::create(themedir.join("favicon.png"))?;
favicon.write_all(theme::FAVICON_PNG)?;
Expand All @@ -151,10 +151,10 @@ impl BookBuilder {
favicon.write_all(theme::FAVICON_SVG)?;

let mut js = File::create(themedir.join("book.js"))?;
js.write_all(theme::JS)?;
theme::JS.write_into(&mut js)?;

let mut highlight_css = File::create(themedir.join("highlight.css"))?;
highlight_css.write_all(theme::HIGHLIGHT_CSS)?;
theme::HIGHLIGHT_CSS.write_into(&mut highlight_css)?;

let mut highlight_js = File::create(themedir.join("highlight.js"))?;
highlight_js.write_all(theme::HIGHLIGHT_JS)?;
Expand Down
6 changes: 6 additions & 0 deletions src/book/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,12 @@ impl MDBook {
Ok(())
}

/// This method is only used by mdbook's tests.
#[doc(hidden)]
pub fn disable_minification(&mut self) {
self.config.build.minification = false;
}

/// Run preprocessors and return the final book.
pub fn preprocess_book(&self, renderer: &dyn Renderer) -> Result<(Book, PreprocessorContext)> {
let preprocess_ctx = PreprocessorContext::new(
Expand Down
5 changes: 5 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,8 @@ pub struct BuildConfig {
pub use_default_preprocessors: bool,
/// Extra directories to trigger rebuild when watching/serving
pub extra_watch_dirs: Vec<PathBuf>,
/// Whether or not JS and CSS files are minified.
pub minification: bool,
}

impl Default for BuildConfig {
Expand All @@ -505,6 +507,7 @@ impl Default for BuildConfig {
create_missing: true,
use_default_preprocessors: true,
extra_watch_dirs: Vec::new(),
minification: true,
}
}
}
Expand Down Expand Up @@ -872,6 +875,7 @@ mod tests {
create_missing: false,
use_default_preprocessors: true,
extra_watch_dirs: Vec::new(),
minification: true,
};
let rust_should_be = RustConfig { edition: None };
let playground_should_be = Playground {
Expand Down Expand Up @@ -1083,6 +1087,7 @@ mod tests {
create_missing: true,
use_default_preprocessors: true,
extra_watch_dirs: Vec::new(),
minification: true,
};

let html_should_be = HtmlConfig {
Expand Down
88 changes: 62 additions & 26 deletions src/front-end/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,24 @@ pub static REDIRECT: &[u8] = include_bytes!("templates/redirect.hbs");
pub static HEADER: &[u8] = include_bytes!("templates/header.hbs");
pub static TOC_JS: &[u8] = include_bytes!("templates/toc.js.hbs");
pub static TOC_HTML: &[u8] = include_bytes!("templates/toc.html.hbs");
pub static CHROME_CSS: &[u8] = include_bytes!("css/chrome.css");
pub static GENERAL_CSS: &[u8] = include_bytes!("css/general.css");
pub static PRINT_CSS: &[u8] = include_bytes!("css/print.css");
pub static VARIABLES_CSS: &[u8] = include_bytes!("css/variables.css");
pub static CHROME_CSS: ContentToMinify<'static> =
ContentToMinify::CSS(include_str!("css/chrome.css"));
pub static GENERAL_CSS: ContentToMinify<'static> =
ContentToMinify::CSS(include_str!("css/general.css"));
pub static PRINT_CSS: ContentToMinify<'static> =
ContentToMinify::CSS(include_str!("css/print.css"));
pub static VARIABLES_CSS: ContentToMinify<'static> =
ContentToMinify::CSS(include_str!("css/variables.css"));
pub static FAVICON_PNG: &[u8] = include_bytes!("images/favicon.png");
pub static FAVICON_SVG: &[u8] = include_bytes!("images/favicon.svg");
pub static JS: &[u8] = include_bytes!("js/book.js");
pub static JS: ContentToMinify<'static> = ContentToMinify::JS(include_str!("js/book.js"));
pub static HIGHLIGHT_JS: &[u8] = include_bytes!("js/highlight.js");
pub static TOMORROW_NIGHT_CSS: &[u8] = include_bytes!("css/tomorrow-night.css");
pub static HIGHLIGHT_CSS: &[u8] = include_bytes!("css/highlight.css");
pub static AYU_HIGHLIGHT_CSS: &[u8] = include_bytes!("css/ayu-highlight.css");
pub static TOMORROW_NIGHT_CSS: ContentToMinify<'static> =
ContentToMinify::CSS(include_str!("css/tomorrow-night.css"));
pub static HIGHLIGHT_CSS: ContentToMinify<'static> =
ContentToMinify::CSS(include_str!("css/highlight.css"));
pub static AYU_HIGHLIGHT_CSS: ContentToMinify<'static> =
ContentToMinify::CSS(include_str!("css/ayu-highlight.css"));
pub static CLIPBOARD_JS: &[u8] = include_bytes!("js/clipboard.min.js");
pub static FONT_AWESOME: &[u8] = include_bytes!("css/font-awesome.min.css");
pub static FONT_AWESOME_EOT: &[u8] = include_bytes!("fonts/fontawesome-webfont.eot");
Expand All @@ -39,6 +46,36 @@ pub static FONT_AWESOME_WOFF: &[u8] = include_bytes!("fonts/fontawesome-webfont.
pub static FONT_AWESOME_WOFF2: &[u8] = include_bytes!("fonts/fontawesome-webfont.woff2");
pub static FONT_AWESOME_OTF: &[u8] = include_bytes!("fonts/FontAwesome.otf");

#[derive(Clone, Copy)]
pub enum ContentToMinify<'a> {
CSS(&'a str),
JS(&'a str),
}

impl<'a> ContentToMinify<'a> {
/// If `minification` is false, it simply returns the inner data converted into a `Vec`.
pub fn minified(self, minification: bool) -> Vec<u8> {
if !minification {
let (Self::CSS(data) | Self::JS(data)) = self;
return data.as_bytes().to_owned();
}
let mut out = Vec::new();
self.write_into(&mut out).unwrap();
out
}

pub fn write_into<W: std::io::Write>(self, out: &mut W) -> std::io::Result<()> {
match self {
Self::CSS(data) => match minifier::css::minify(data) {
Ok(data) => return data.write(out),
Err(_) => out.write(data.as_bytes())?,
},
Self::JS(data) => return minifier::js::minify(data).write(out),
};
Ok(())
}
}

/// The `Theme` struct should be used instead of the static variables because
/// the `new()` method will look if the user has a theme directory in their
/// source folder and use the users theme instead of the default.
Expand Down Expand Up @@ -72,9 +109,9 @@ pub struct Theme {
impl Theme {
/// Creates a `Theme` from the given `theme_dir`.
/// If a file is found in the theme dir, it will override the default version.
pub fn new<P: AsRef<Path>>(theme_dir: P) -> Self {
pub fn new<P: AsRef<Path>>(theme_dir: P, minification: bool) -> Self {
let theme_dir = theme_dir.as_ref();
let mut theme = Theme::default();
let mut theme = Self::new_with_set_fields(minification);

// If the theme directory doesn't exist there's no point continuing...
if !theme_dir.exists() || !theme_dir.is_dir() {
Expand Down Expand Up @@ -170,29 +207,27 @@ impl Theme {

theme
}
}

impl Default for Theme {
fn default() -> Theme {
fn new_with_set_fields(minification: bool) -> Self {
Theme {
index: INDEX.to_owned(),
head: HEAD.to_owned(),
redirect: REDIRECT.to_owned(),
header: HEADER.to_owned(),
toc_js: TOC_JS.to_owned(),
toc_html: TOC_HTML.to_owned(),
chrome_css: CHROME_CSS.to_owned(),
general_css: GENERAL_CSS.to_owned(),
print_css: PRINT_CSS.to_owned(),
variables_css: VARIABLES_CSS.to_owned(),
chrome_css: CHROME_CSS.minified(minification),
general_css: GENERAL_CSS.minified(minification),
print_css: PRINT_CSS.minified(minification),
variables_css: VARIABLES_CSS.minified(minification),
fonts_css: None,
font_files: Vec::new(),
favicon_png: Some(FAVICON_PNG.to_owned()),
favicon_svg: Some(FAVICON_SVG.to_owned()),
js: JS.to_owned(),
highlight_css: HIGHLIGHT_CSS.to_owned(),
tomorrow_night_css: TOMORROW_NIGHT_CSS.to_owned(),
ayu_highlight_css: AYU_HIGHLIGHT_CSS.to_owned(),
js: JS.minified(minification),
highlight_css: HIGHLIGHT_CSS.minified(minification),
tomorrow_night_css: TOMORROW_NIGHT_CSS.minified(minification),
ayu_highlight_css: AYU_HIGHLIGHT_CSS.minified(minification),
highlight_js: HIGHLIGHT_JS.to_owned(),
clipboard_js: CLIPBOARD_JS.to_owned(),
}
Expand Down Expand Up @@ -226,8 +261,9 @@ mod tests {
let non_existent = PathBuf::from("/non/existent/directory/");
assert!(!non_existent.exists());

let should_be = Theme::default();
let got = Theme::new(&non_existent);
let minification = false;
let should_be = Theme::new_with_set_fields(minification);
let got = Theme::new(&non_existent, minification);

assert_eq!(got, should_be);
}
Expand Down Expand Up @@ -265,7 +301,7 @@ mod tests {
File::create(&temp.path().join(file)).unwrap();
}

let got = Theme::new(temp.path());
let got = Theme::new(temp.path(), false);

let empty = Theme {
index: Vec::new(),
Expand Down Expand Up @@ -297,13 +333,13 @@ mod tests {
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());
let got = Theme::new(temp.path(), false);
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());
let got = Theme::new(temp.path(), false);
assert_eq!(got.favicon_png, None);
assert_eq!(got.favicon_svg.as_ref().unwrap(), b"4567");
}
Expand Down
4 changes: 3 additions & 1 deletion src/front-end/searcher/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
//! Theme dependencies for in-browser search. Not included in mdbook when
//! the "search" cargo feature is disabled.

pub static JS: &[u8] = include_bytes!("searcher.js");
use crate::theme::ContentToMinify;

pub static JS: ContentToMinify<'static> = ContentToMinify::JS(include_str!("searcher.js"));
pub static MARK_JS: &[u8] = include_bytes!("mark.min.js");
pub static ELASTICLUNR_JS: &[u8] = include_bytes!("elasticlunr.min.js");
10 changes: 6 additions & 4 deletions src/renderer/html_handlebars/hbs_renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crate::errors::*;
use crate::renderer::html_handlebars::helpers;
use crate::renderer::html_handlebars::StaticFiles;
use crate::renderer::{RenderContext, Renderer};
use crate::theme::{self, Theme};
use crate::theme::{self, ContentToMinify, Theme};
use crate::utils;

use std::borrow::Cow;
Expand Down Expand Up @@ -330,6 +330,7 @@ impl Renderer for HtmlHandlebars {
let destination = &ctx.destination;
let book = &ctx.book;
let build_dir = ctx.root.join(&ctx.config.build.build_dir);
let minification = ctx.config.build.minification;

if destination.exists() {
utils::fs::remove_dir_content(destination)
Expand All @@ -350,7 +351,7 @@ impl Renderer for HtmlHandlebars {
None => ctx.root.join("theme"),
};

let theme = theme::Theme::new(theme_dir);
let theme = theme::Theme::new(theme_dir, minification);

debug!("Register the index handlebars template");
handlebars.register_template_string("index", String::from_utf8(theme.index.clone())?)?;
Expand Down Expand Up @@ -389,14 +390,15 @@ impl Renderer for HtmlHandlebars {
let default = crate::config::Search::default();
let search = html_config.search.as_ref().unwrap_or(&default);
if search.enable {
super::search::create_files(&search, &mut static_files, &book)?;
super::search::create_files(&search, &mut static_files, &book, minification)?;
}
}

debug!("Render toc js");
{
let rendered_toc = handlebars.render("toc_js", &data)?;
static_files.add_builtin("toc.js", rendered_toc.as_bytes());
let rendered_toc = ContentToMinify::JS(&rendered_toc);
static_files.add_owned_builtin("toc.js", rendered_toc.minified(minification));
debug!("Creating toc.js ✓");
}

Expand Down
3 changes: 2 additions & 1 deletion src/renderer/html_handlebars/search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ pub fn create_files(
search_config: &Search,
static_files: &mut StaticFiles,
book: &Book,
minification: bool,
) -> Result<()> {
let mut index = IndexBuilder::new()
.add_field_with_tokenizer("title", Box::new(&tokenize))
Expand Down Expand Up @@ -74,7 +75,7 @@ pub fn create_files(
)
.as_bytes(),
);
static_files.add_builtin("searcher.js", searcher::JS);
static_files.add_owned_builtin("searcher.js", searcher::JS.minified(minification));
static_files.add_builtin("mark.min.js", searcher::MARK_JS);
static_files.add_builtin("elasticlunr.min.js", searcher::ELASTICLUNR_JS);
debug!("Copying search files ✓");
Expand Down
4 changes: 4 additions & 0 deletions src/renderer/html_handlebars/static_files.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,10 @@ impl StaticFiles {
}

pub fn add_builtin(&mut self, filename: &str, data: &[u8]) {
self.add_owned_builtin(filename, data.to_owned());
}

pub fn add_owned_builtin(&mut self, filename: &str, data: Vec<u8>) {
self.static_files.push(StaticFile::Builtin {
filename: filename.to_owned(),
data: data.to_owned(),
Expand Down
3 changes: 2 additions & 1 deletion tests/testsuite/book_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,8 @@ impl BookTest {

/// Builds the book in the temp directory.
pub fn build(&mut self) -> &mut Self {
let book = self.load_book();
let mut book = self.load_book();
book.disable_minification();
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is this necessary? I'm worried about disabling this in tests, since this isn't testing what is actually generated.

Copy link
Member Author

Choose a reason for hiding this comment

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

Because we do some string search in the tests. So for test code readability, I think it's better to disable it here.

We already check most things are working as expected in GUI tests (both CSS and JS) when minified. So I'm not sure it makes a big difference here.

book.build()
.unwrap_or_else(|e| panic!("book failed to build: {e:?}"));
self.built = true;
Expand Down
1 change: 1 addition & 0 deletions tests/testsuite/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ src = "in"
build-dir = "out"
create-missing = true
extra-watch-dirs = []
minification = true
use-default-preprocessors = true

"#]],
Expand Down
7 changes: 7 additions & 0 deletions tests/testsuite/renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,13 @@ fn backends_receive_render_context_via_stdin() {
"language": "en",
"src": "src"
},
"build": {
"build-dir": "book",
"create-missing": true,
"extra-watch-dirs": [],
"minification": false,
"use-default-preprocessors": true
},
"output": {
"cat-to-file": {
"command": "./cat-to-file"
Expand Down
Loading