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 2 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
4 changes: 4 additions & 0 deletions src/cmd/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ pub fn make_subcommand() -> Command {
.about("Builds a book from its markdown files")
.arg_dest_dir()
.arg_root_dir()
.arg_disable_minification()
.arg_open()
}

Expand All @@ -21,6 +22,9 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
if let Some(dest_dir) = args.get_one::<PathBuf>("dest-dir") {
book.config.build.build_dir = dest_dir.into();
}
if let Some(true) = args.get_one::<bool>("disable-minification") {
book.config.build.minification = false;
}

book.build()?;

Expand Down
9 changes: 9 additions & 0 deletions src/cmd/command_prelude.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ pub trait CommandExt: Sized {
self._arg(arg!(-o --open "Opens the compiled book in a web browser"))
}

fn arg_disable_minification(self) -> Self {
self._arg(
Arg::new("disable-minification")
.long("disable-minification")
.action(clap::ArgAction::SetTrue)
.help("Disable CSS and JS minification"),
)
}

#[cfg(any(feature = "watch", feature = "serve"))]
fn arg_watcher(self) -> Self {
#[cfg(feature = "watch")]
Expand Down
4 changes: 4 additions & 0 deletions src/cmd/serve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ pub fn make_subcommand() -> Command {
.help("Port to use for HTTP connections"),
)
.arg_open()
.arg_disable_minification()
.arg_watcher()
}

Expand All @@ -63,6 +64,9 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
if let Some(dest_dir) = args.get_one::<PathBuf>("dest-dir") {
book.config.build.build_dir = dest_dir.into();
}
if let Some(true) = args.get_one::<bool>("disable-minification") {
book.config.build.minification = false;
}
// Override site-url for local serving of the 404 file
book.config.set("output.html.site-url", "/").unwrap();
};
Expand Down
4 changes: 4 additions & 0 deletions src/cmd/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ pub fn make_subcommand() -> Command {
search path when building tests",
),
)
.arg_disable_minification()
}

// test command implementation
Expand All @@ -49,6 +50,9 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
if let Some(dest_dir) = args.get_one::<PathBuf>("dest-dir") {
book.config.build.build_dir = dest_dir.to_path_buf();
}
if let Some(true) = args.get_one::<bool>("disable-minification") {
book.config.build.minification = false;
}
match chapter {
Some(_) => book.test_chapter(library_paths, chapter),
None => book.test(library_paths),
Expand Down
4 changes: 4 additions & 0 deletions src/cmd/watch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub fn make_subcommand() -> Command {
.arg_dest_dir()
.arg_root_dir()
.arg_open()
.arg_disable_minification()
.arg_watcher()
}

Expand Down Expand Up @@ -41,6 +42,9 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
if let Some(dest_dir) = args.get_one::<PathBuf>("dest-dir") {
book.config.build.build_dir = dest_dir.into();
}
if let Some(true) = args.get_one::<bool>("disable-minification") {
book.config.build.minification = false;
}
};
update_config(&mut book);

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");
Loading