mirror of
https://github.com/rust-lang/mdBook.git
synced 2025-12-27 09:05:40 -05:00
Merge pull request #1368 from notriddle/hash-files
feat(html): cache bust static files by adding hashes to file names
This commit is contained in:
8
Cargo.lock
generated
8
Cargo.lock
generated
@@ -731,6 +731,12 @@ dependencies = [
|
||||
"http 0.2.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "html5ever"
|
||||
version = "0.26.0"
|
||||
@@ -1211,6 +1217,7 @@ dependencies = [
|
||||
"env_logger",
|
||||
"futures-util",
|
||||
"handlebars",
|
||||
"hex",
|
||||
"ignore",
|
||||
"log",
|
||||
"memchr",
|
||||
@@ -1227,6 +1234,7 @@ dependencies = [
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"shlex",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
|
||||
@@ -27,6 +27,7 @@ clap_complete = "4.3.2"
|
||||
once_cell = "1.17.1"
|
||||
env_logger = "0.11.1"
|
||||
handlebars = "6.0"
|
||||
hex = "0.4.3"
|
||||
log = "0.4.17"
|
||||
memchr = "2.5.0"
|
||||
opener = "0.7.0"
|
||||
@@ -34,6 +35,7 @@ pulldown-cmark = { version = "0.10.0", default-features = false, features = ["ht
|
||||
regex = "1.8.1"
|
||||
serde = { version = "1.0.163", features = ["derive"] }
|
||||
serde_json = "1.0.96"
|
||||
sha2 = "0.10.8"
|
||||
shlex = "1.3.0"
|
||||
tempfile = "3.4.0"
|
||||
toml = "0.5.11" # Do not update, see https://github.com/rust-lang/mdBook/issues/2037
|
||||
|
||||
@@ -13,6 +13,7 @@ mathjax-support = true
|
||||
site-url = "/mdBook/"
|
||||
git-repository-url = "https://github.com/rust-lang/mdBook/tree/master/guide"
|
||||
edit-url-template = "https://github.com/rust-lang/mdBook/edit/master/guide/{path}"
|
||||
hash-files = true
|
||||
|
||||
[output.html.playground]
|
||||
editable = true
|
||||
|
||||
@@ -168,6 +168,12 @@ The following configuration options are available:
|
||||
This string will be written to a file named CNAME in the root of your site, as
|
||||
required by GitHub Pages (see [*Managing a custom domain for your GitHub Pages
|
||||
site*][custom domain]).
|
||||
- **hash-files:** Include a cryptographic "fingerprint" of the files' contents in static asset filenames,
|
||||
so that if the contents of the file are changed, the name of the file will also change.
|
||||
For example, `css/chrome.css` may become `css/chrome-9b8f428e.css`.
|
||||
Chapter HTML files are not renamed.
|
||||
Static CSS and JS files can reference each other using `{{ resource "filename" }}` directives.
|
||||
Defaults to `false` (in a future release, this may change to `true`).
|
||||
|
||||
[custom domain]: https://docs.github.com/en/github/working-with-github-pages/managing-a-custom-domain-for-your-github-pages-site
|
||||
|
||||
|
||||
@@ -99,3 +99,13 @@ Of course the inner html can be changed to your liking.
|
||||
|
||||
*If you would like other properties or helpers exposed, please [create a new
|
||||
issue](https://github.com/rust-lang/mdBook/issues)*
|
||||
|
||||
### 3. resource
|
||||
|
||||
The path to a static file.
|
||||
It implicitly includes `path_to_root`,
|
||||
and accounts for files that are renamed with a hash in their filename.
|
||||
|
||||
```handlebars
|
||||
<link rel="stylesheet" href="{{ resource "css/chrome.css" }}">
|
||||
```
|
||||
|
||||
@@ -587,6 +587,9 @@ pub struct HtmlConfig {
|
||||
/// The mapping from old pages to new pages/URLs to use when generating
|
||||
/// redirects.
|
||||
pub redirect: HashMap<String, String>,
|
||||
/// If this option is turned on, "cache bust" static files by adding
|
||||
/// hashes to their file names.
|
||||
pub hash_files: bool,
|
||||
}
|
||||
|
||||
impl Default for HtmlConfig {
|
||||
@@ -616,6 +619,7 @@ impl Default for HtmlConfig {
|
||||
cname: None,
|
||||
live_reload_endpoint: None,
|
||||
redirect: HashMap::new(),
|
||||
hash_files: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,9 @@ use crate::book::{Book, BookItem};
|
||||
use crate::config::{BookConfig, Code, Config, HtmlConfig, Playground, RustEdition};
|
||||
use crate::errors::*;
|
||||
use crate::renderer::html_handlebars::helpers;
|
||||
use crate::renderer::html_handlebars::StaticFiles;
|
||||
use crate::renderer::{RenderContext, Renderer};
|
||||
use crate::theme::{self, playground_editor, Theme};
|
||||
use crate::theme::{self, Theme};
|
||||
use crate::utils;
|
||||
|
||||
use std::borrow::Cow;
|
||||
@@ -222,134 +223,6 @@ impl HtmlHandlebars {
|
||||
rendered
|
||||
}
|
||||
|
||||
fn copy_static_files(
|
||||
&self,
|
||||
destination: &Path,
|
||||
theme: &Theme,
|
||||
html_config: &HtmlConfig,
|
||||
) -> Result<()> {
|
||||
use crate::utils::fs::write_file;
|
||||
|
||||
write_file(
|
||||
destination,
|
||||
".nojekyll",
|
||||
b"This file makes sure that Github Pages doesn't process mdBook's output.\n",
|
||||
)?;
|
||||
|
||||
if let Some(cname) = &html_config.cname {
|
||||
write_file(destination, "CNAME", format!("{cname}\n").as_bytes())?;
|
||||
}
|
||||
|
||||
write_file(destination, "book.js", &theme.js)?;
|
||||
write_file(destination, "css/general.css", &theme.general_css)?;
|
||||
write_file(destination, "css/chrome.css", &theme.chrome_css)?;
|
||||
if html_config.print.enable {
|
||||
write_file(destination, "css/print.css", &theme.print_css)?;
|
||||
}
|
||||
write_file(destination, "css/variables.css", &theme.variables_css)?;
|
||||
if let Some(contents) = &theme.favicon_png {
|
||||
write_file(destination, "favicon.png", contents)?;
|
||||
}
|
||||
if let Some(contents) = &theme.favicon_svg {
|
||||
write_file(destination, "favicon.svg", contents)?;
|
||||
}
|
||||
write_file(destination, "highlight.css", &theme.highlight_css)?;
|
||||
write_file(destination, "tomorrow-night.css", &theme.tomorrow_night_css)?;
|
||||
write_file(destination, "ayu-highlight.css", &theme.ayu_highlight_css)?;
|
||||
write_file(destination, "highlight.js", &theme.highlight_js)?;
|
||||
write_file(destination, "clipboard.min.js", &theme.clipboard_js)?;
|
||||
write_file(
|
||||
destination,
|
||||
"FontAwesome/css/font-awesome.css",
|
||||
theme::FONT_AWESOME,
|
||||
)?;
|
||||
write_file(
|
||||
destination,
|
||||
"FontAwesome/fonts/fontawesome-webfont.eot",
|
||||
theme::FONT_AWESOME_EOT,
|
||||
)?;
|
||||
write_file(
|
||||
destination,
|
||||
"FontAwesome/fonts/fontawesome-webfont.svg",
|
||||
theme::FONT_AWESOME_SVG,
|
||||
)?;
|
||||
write_file(
|
||||
destination,
|
||||
"FontAwesome/fonts/fontawesome-webfont.ttf",
|
||||
theme::FONT_AWESOME_TTF,
|
||||
)?;
|
||||
write_file(
|
||||
destination,
|
||||
"FontAwesome/fonts/fontawesome-webfont.woff",
|
||||
theme::FONT_AWESOME_WOFF,
|
||||
)?;
|
||||
write_file(
|
||||
destination,
|
||||
"FontAwesome/fonts/fontawesome-webfont.woff2",
|
||||
theme::FONT_AWESOME_WOFF2,
|
||||
)?;
|
||||
write_file(
|
||||
destination,
|
||||
"FontAwesome/fonts/FontAwesome.ttf",
|
||||
theme::FONT_AWESOME_TTF,
|
||||
)?;
|
||||
// Don't copy the stock fonts if the user has specified their own fonts to use.
|
||||
if html_config.copy_fonts && theme.fonts_css.is_none() {
|
||||
write_file(destination, "fonts/fonts.css", theme::fonts::CSS)?;
|
||||
for (file_name, contents) in theme::fonts::LICENSES.iter() {
|
||||
write_file(destination, file_name, contents)?;
|
||||
}
|
||||
for (file_name, contents) in theme::fonts::OPEN_SANS.iter() {
|
||||
write_file(destination, file_name, contents)?;
|
||||
}
|
||||
write_file(
|
||||
destination,
|
||||
theme::fonts::SOURCE_CODE_PRO.0,
|
||||
theme::fonts::SOURCE_CODE_PRO.1,
|
||||
)?;
|
||||
}
|
||||
if let Some(fonts_css) = &theme.fonts_css {
|
||||
if !fonts_css.is_empty() {
|
||||
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 in book.toml 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;
|
||||
|
||||
// Ace is a very large dependency, so only load it when requested
|
||||
if playground_config.editable && playground_config.copy_js {
|
||||
// Load the editor
|
||||
write_file(destination, "editor.js", playground_editor::JS)?;
|
||||
write_file(destination, "ace.js", playground_editor::ACE_JS)?;
|
||||
write_file(destination, "mode-rust.js", playground_editor::MODE_RUST_JS)?;
|
||||
write_file(
|
||||
destination,
|
||||
"theme-dawn.js",
|
||||
playground_editor::THEME_DAWN_JS,
|
||||
)?;
|
||||
write_file(
|
||||
destination,
|
||||
"theme-tomorrow_night.js",
|
||||
playground_editor::THEME_TOMORROW_NIGHT_JS,
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update the context with data for this file
|
||||
fn configure_print_version(
|
||||
&self,
|
||||
@@ -381,43 +254,6 @@ impl HtmlHandlebars {
|
||||
handlebars.register_helper("theme_option", Box::new(helpers::theme::theme_option));
|
||||
}
|
||||
|
||||
/// Copy across any additional CSS and JavaScript files which the book
|
||||
/// has been configured to use.
|
||||
fn copy_additional_css_and_js(
|
||||
&self,
|
||||
html: &HtmlConfig,
|
||||
root: &Path,
|
||||
destination: &Path,
|
||||
) -> Result<()> {
|
||||
let custom_files = html.additional_css.iter().chain(html.additional_js.iter());
|
||||
|
||||
debug!("Copying additional CSS and JS");
|
||||
|
||||
for custom_file in custom_files {
|
||||
let input_location = root.join(custom_file);
|
||||
let output_location = destination.join(custom_file);
|
||||
if let Some(parent) = output_location.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.with_context(|| format!("Unable to create {}", parent.display()))?;
|
||||
}
|
||||
debug!(
|
||||
"Copying {} -> {}",
|
||||
input_location.display(),
|
||||
output_location.display()
|
||||
);
|
||||
|
||||
fs::copy(&input_location, &output_location).with_context(|| {
|
||||
format!(
|
||||
"Unable to copy {} to {}",
|
||||
input_location.display(),
|
||||
output_location.display()
|
||||
)
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn emit_redirects(
|
||||
&self,
|
||||
root: &Path,
|
||||
@@ -544,6 +380,57 @@ impl Renderer for HtmlHandlebars {
|
||||
fs::create_dir_all(destination)
|
||||
.with_context(|| "Unexpected error when constructing destination path")?;
|
||||
|
||||
let mut static_files = StaticFiles::new(&theme, &html_config, &ctx.root)?;
|
||||
|
||||
// Render search index
|
||||
#[cfg(feature = "search")]
|
||||
{
|
||||
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)?;
|
||||
}
|
||||
}
|
||||
|
||||
debug!("Render toc js");
|
||||
{
|
||||
let rendered_toc = handlebars.render("toc_js", &data)?;
|
||||
static_files.add_builtin("toc.js", rendered_toc.as_bytes());
|
||||
debug!("Creating toc.js ✓");
|
||||
}
|
||||
|
||||
if html_config.hash_files {
|
||||
static_files.hash_files()?;
|
||||
}
|
||||
|
||||
debug!("Copy static files");
|
||||
let resource_helper = static_files
|
||||
.write_files(&destination)
|
||||
.with_context(|| "Unable to copy across static files")?;
|
||||
|
||||
handlebars.register_helper("resource", Box::new(resource_helper));
|
||||
|
||||
debug!("Render toc html");
|
||||
{
|
||||
data.insert("is_toc_html".to_owned(), json!(true));
|
||||
data.insert("path".to_owned(), json!("toc.html"));
|
||||
let rendered_toc = handlebars.render("toc_html", &data)?;
|
||||
utils::fs::write_file(destination, "toc.html", rendered_toc.as_bytes())?;
|
||||
debug!("Creating toc.html ✓");
|
||||
data.remove("path");
|
||||
data.remove("is_toc_html");
|
||||
}
|
||||
|
||||
utils::fs::write_file(
|
||||
destination,
|
||||
".nojekyll",
|
||||
b"This file makes sure that Github Pages doesn't process mdBook's output.\n",
|
||||
)?;
|
||||
|
||||
if let Some(cname) = &html_config.cname {
|
||||
utils::fs::write_file(destination, "CNAME", format!("{cname}\n").as_bytes())?;
|
||||
}
|
||||
|
||||
let mut is_index = true;
|
||||
for item in book.iter() {
|
||||
let ctx = RenderItemContext {
|
||||
@@ -588,33 +475,6 @@ impl Renderer for HtmlHandlebars {
|
||||
debug!("Creating print.html ✓");
|
||||
}
|
||||
|
||||
debug!("Render toc");
|
||||
{
|
||||
let rendered_toc = handlebars.render("toc_js", &data)?;
|
||||
utils::fs::write_file(destination, "toc.js", rendered_toc.as_bytes())?;
|
||||
debug!("Creating toc.js ✓");
|
||||
data.insert("is_toc_html".to_owned(), json!(true));
|
||||
let rendered_toc = handlebars.render("toc_html", &data)?;
|
||||
utils::fs::write_file(destination, "toc.html", rendered_toc.as_bytes())?;
|
||||
debug!("Creating toc.html ✓");
|
||||
data.remove("is_toc_html");
|
||||
}
|
||||
|
||||
debug!("Copy static files");
|
||||
self.copy_static_files(destination, &theme, &html_config)
|
||||
.with_context(|| "Unable to copy across static files")?;
|
||||
self.copy_additional_css_and_js(&html_config, &ctx.root, destination)
|
||||
.with_context(|| "Unable to copy across additional CSS and JS")?;
|
||||
|
||||
// Render search index
|
||||
#[cfg(feature = "search")]
|
||||
{
|
||||
let search = html_config.search.unwrap_or_default();
|
||||
if search.enable {
|
||||
super::search::create_files(&search, destination, book)?;
|
||||
}
|
||||
}
|
||||
|
||||
self.emit_redirects(&ctx.destination, &handlebars, &html_config.redirect)
|
||||
.context("Unable to emit redirects")?;
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod navigation;
|
||||
pub mod resources;
|
||||
pub mod theme;
|
||||
pub mod toc;
|
||||
|
||||
50
src/renderer/html_handlebars/helpers/resources.rs
Normal file
50
src/renderer/html_handlebars/helpers/resources.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::utils;
|
||||
|
||||
use handlebars::{
|
||||
Context, Handlebars, Helper, HelperDef, Output, RenderContext, RenderError, RenderErrorReason,
|
||||
};
|
||||
|
||||
// Handlebars helper to find filenames with hashes in them
|
||||
#[derive(Clone)]
|
||||
pub struct ResourceHelper {
|
||||
pub hash_map: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl HelperDef for ResourceHelper {
|
||||
fn call<'reg: 'rc, 'rc>(
|
||||
&self,
|
||||
h: &Helper<'rc>,
|
||||
_r: &'reg Handlebars<'_>,
|
||||
ctx: &'rc Context,
|
||||
rc: &mut RenderContext<'reg, 'rc>,
|
||||
out: &mut dyn Output,
|
||||
) -> Result<(), RenderError> {
|
||||
let param = h.param(0).and_then(|v| v.value().as_str()).ok_or_else(|| {
|
||||
RenderErrorReason::Other(
|
||||
"Param 0 with String type is required for theme_option helper.".to_owned(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let base_path = rc
|
||||
.evaluate(ctx, "@root/path")?
|
||||
.as_json()
|
||||
.as_str()
|
||||
.ok_or_else(|| {
|
||||
RenderErrorReason::Other("Type error for `path`, string expected".to_owned())
|
||||
})?
|
||||
.replace("\"", "");
|
||||
|
||||
let path_to_root = utils::fs::path_to_root(&base_path);
|
||||
|
||||
out.write(&path_to_root)?;
|
||||
out.write(
|
||||
self.hash_map
|
||||
.get(¶m[..])
|
||||
.map(|p| &p[..])
|
||||
.unwrap_or(¶m),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
#![allow(missing_docs)] // FIXME: Document this
|
||||
|
||||
pub use self::hbs_renderer::HtmlHandlebars;
|
||||
pub use self::static_files::StaticFiles;
|
||||
|
||||
mod hbs_renderer;
|
||||
mod helpers;
|
||||
mod static_files;
|
||||
|
||||
#[cfg(feature = "search")]
|
||||
mod search;
|
||||
|
||||
@@ -9,6 +9,7 @@ use pulldown_cmark::*;
|
||||
use crate::book::{Book, BookItem, Chapter};
|
||||
use crate::config::{Search, SearchChapterSettings};
|
||||
use crate::errors::*;
|
||||
use crate::renderer::html_handlebars::StaticFiles;
|
||||
use crate::theme::searcher;
|
||||
use crate::utils;
|
||||
use log::{debug, warn};
|
||||
@@ -26,7 +27,11 @@ fn tokenize(text: &str) -> Vec<String> {
|
||||
}
|
||||
|
||||
/// Creates all files required for search.
|
||||
pub fn create_files(search_config: &Search, destination: &Path, book: &Book) -> Result<()> {
|
||||
pub fn create_files(
|
||||
search_config: &Search,
|
||||
static_files: &mut StaticFiles,
|
||||
book: &Book,
|
||||
) -> Result<()> {
|
||||
let mut index = IndexBuilder::new()
|
||||
.add_field_with_tokenizer("title", Box::new(&tokenize))
|
||||
.add_field_with_tokenizer("body", Box::new(&tokenize))
|
||||
@@ -59,15 +64,14 @@ pub fn create_files(search_config: &Search, destination: &Path, book: &Book) ->
|
||||
}
|
||||
|
||||
if search_config.copy_js {
|
||||
utils::fs::write_file(destination, "searchindex.json", index.as_bytes())?;
|
||||
utils::fs::write_file(
|
||||
destination,
|
||||
static_files.add_builtin("searchindex.json", index.as_bytes());
|
||||
static_files.add_builtin(
|
||||
"searchindex.js",
|
||||
format!("Object.assign(window.search, {index});").as_bytes(),
|
||||
)?;
|
||||
utils::fs::write_file(destination, "searcher.js", searcher::JS)?;
|
||||
utils::fs::write_file(destination, "mark.min.js", searcher::MARK_JS)?;
|
||||
utils::fs::write_file(destination, "elasticlunr.min.js", searcher::ELASTICLUNR_JS)?;
|
||||
format!("Object.assign(window.search, {});", index).as_bytes(),
|
||||
);
|
||||
static_files.add_builtin("searcher.js", searcher::JS);
|
||||
static_files.add_builtin("mark.min.js", searcher::MARK_JS);
|
||||
static_files.add_builtin("elasticlunr.min.js", searcher::ELASTICLUNR_JS);
|
||||
debug!("Copying search files ✓");
|
||||
}
|
||||
|
||||
|
||||
358
src/renderer/html_handlebars/static_files.rs
Normal file
358
src/renderer/html_handlebars/static_files.rs
Normal file
@@ -0,0 +1,358 @@
|
||||
//! Support for writing static files.
|
||||
|
||||
use log::{debug, warn};
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use crate::config::HtmlConfig;
|
||||
use crate::errors::*;
|
||||
use crate::renderer::html_handlebars::helpers::resources::ResourceHelper;
|
||||
use crate::theme::{self, playground_editor, Theme};
|
||||
use crate::utils;
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use std::fs::{self, File};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// Map static files to their final names and contents.
|
||||
///
|
||||
/// It performs [fingerprinting], if you call the `hash_files` method.
|
||||
/// If hash-files is turned off, then the files will not be renamed.
|
||||
/// It also writes files to their final destination, when `write_files` is called,
|
||||
/// and interprets the `{{ resource }}` directives to allow assets to name each other.
|
||||
///
|
||||
/// [fingerprinting]: https://guides.rubyonrails.org/asset_pipeline.html#fingerprinting-versioning-with-digest-based-urls
|
||||
pub struct StaticFiles {
|
||||
static_files: Vec<StaticFile>,
|
||||
hash_map: HashMap<String, String>,
|
||||
}
|
||||
|
||||
enum StaticFile {
|
||||
Builtin {
|
||||
data: Vec<u8>,
|
||||
filename: String,
|
||||
},
|
||||
Additional {
|
||||
input_location: PathBuf,
|
||||
filename: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl StaticFiles {
|
||||
pub fn new(theme: &Theme, html_config: &HtmlConfig, root: &Path) -> Result<StaticFiles> {
|
||||
let static_files = Vec::new();
|
||||
let mut this = StaticFiles {
|
||||
hash_map: HashMap::new(),
|
||||
static_files,
|
||||
};
|
||||
|
||||
this.add_builtin("book.js", &theme.js);
|
||||
this.add_builtin("css/general.css", &theme.general_css);
|
||||
this.add_builtin("css/chrome.css", &theme.chrome_css);
|
||||
if html_config.print.enable {
|
||||
this.add_builtin("css/print.css", &theme.print_css);
|
||||
}
|
||||
this.add_builtin("css/variables.css", &theme.variables_css);
|
||||
if let Some(contents) = &theme.favicon_png {
|
||||
this.add_builtin("favicon.png", contents);
|
||||
}
|
||||
if let Some(contents) = &theme.favicon_svg {
|
||||
this.add_builtin("favicon.svg", contents);
|
||||
}
|
||||
this.add_builtin("highlight.css", &theme.highlight_css);
|
||||
this.add_builtin("tomorrow-night.css", &theme.tomorrow_night_css);
|
||||
this.add_builtin("ayu-highlight.css", &theme.ayu_highlight_css);
|
||||
this.add_builtin("highlight.js", &theme.highlight_js);
|
||||
this.add_builtin("clipboard.min.js", &theme.clipboard_js);
|
||||
this.add_builtin("FontAwesome/css/font-awesome.css", theme::FONT_AWESOME);
|
||||
this.add_builtin(
|
||||
"FontAwesome/fonts/fontawesome-webfont.eot",
|
||||
theme::FONT_AWESOME_EOT,
|
||||
);
|
||||
this.add_builtin(
|
||||
"FontAwesome/fonts/fontawesome-webfont.svg",
|
||||
theme::FONT_AWESOME_SVG,
|
||||
);
|
||||
this.add_builtin(
|
||||
"FontAwesome/fonts/fontawesome-webfont.ttf",
|
||||
theme::FONT_AWESOME_TTF,
|
||||
);
|
||||
this.add_builtin(
|
||||
"FontAwesome/fonts/fontawesome-webfont.woff",
|
||||
theme::FONT_AWESOME_WOFF,
|
||||
);
|
||||
this.add_builtin(
|
||||
"FontAwesome/fonts/fontawesome-webfont.woff2",
|
||||
theme::FONT_AWESOME_WOFF2,
|
||||
);
|
||||
this.add_builtin("FontAwesome/fonts/FontAwesome.ttf", theme::FONT_AWESOME_TTF);
|
||||
if html_config.copy_fonts && theme.fonts_css.is_none() {
|
||||
this.add_builtin("fonts/fonts.css", theme::fonts::CSS);
|
||||
for (file_name, contents) in theme::fonts::LICENSES.iter() {
|
||||
this.add_builtin(file_name, contents);
|
||||
}
|
||||
for (file_name, contents) in theme::fonts::OPEN_SANS.iter() {
|
||||
this.add_builtin(file_name, contents);
|
||||
}
|
||||
this.add_builtin(
|
||||
theme::fonts::SOURCE_CODE_PRO.0,
|
||||
theme::fonts::SOURCE_CODE_PRO.1,
|
||||
);
|
||||
} else if let Some(fonts_css) = &theme.fonts_css {
|
||||
if !fonts_css.is_empty() {
|
||||
this.add_builtin("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 in book.toml without a fonts.css file.\n\
|
||||
Add an empty `theme/fonts/fonts.css` file to squelch this warning."
|
||||
);
|
||||
}
|
||||
|
||||
let playground_config = &html_config.playground;
|
||||
|
||||
// Ace is a very large dependency, so only load it when requested
|
||||
if playground_config.editable && playground_config.copy_js {
|
||||
// Load the editor
|
||||
this.add_builtin("editor.js", playground_editor::JS);
|
||||
this.add_builtin("ace.js", playground_editor::ACE_JS);
|
||||
this.add_builtin("mode-rust.js", playground_editor::MODE_RUST_JS);
|
||||
this.add_builtin("theme-dawn.js", playground_editor::THEME_DAWN_JS);
|
||||
this.add_builtin(
|
||||
"theme-tomorrow_night.js",
|
||||
playground_editor::THEME_TOMORROW_NIGHT_JS,
|
||||
);
|
||||
}
|
||||
|
||||
let custom_files = html_config
|
||||
.additional_css
|
||||
.iter()
|
||||
.chain(html_config.additional_js.iter());
|
||||
|
||||
for custom_file in custom_files {
|
||||
let input_location = root.join(custom_file);
|
||||
|
||||
this.static_files.push(StaticFile::Additional {
|
||||
input_location,
|
||||
filename: custom_file
|
||||
.to_str()
|
||||
.with_context(|| "resource file names must be valid utf8")?
|
||||
.to_owned(),
|
||||
});
|
||||
}
|
||||
|
||||
for input_location in theme.font_files.iter().cloned() {
|
||||
let filename = Path::new("fonts")
|
||||
.join(input_location.file_name().unwrap())
|
||||
.to_str()
|
||||
.with_context(|| "resource file names must be valid utf8")?
|
||||
.to_owned();
|
||||
this.static_files.push(StaticFile::Additional {
|
||||
input_location,
|
||||
filename,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(this)
|
||||
}
|
||||
|
||||
pub fn add_builtin(&mut self, filename: &str, data: &[u8]) {
|
||||
self.static_files.push(StaticFile::Builtin {
|
||||
filename: filename.to_owned(),
|
||||
data: data.to_owned(),
|
||||
});
|
||||
}
|
||||
|
||||
/// Updates this [`StaticFiles`] to hash the contents for determining the
|
||||
/// filename for each resource.
|
||||
pub fn hash_files(&mut self) -> Result<()> {
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::io::Read;
|
||||
for static_file in &mut self.static_files {
|
||||
match static_file {
|
||||
StaticFile::Builtin {
|
||||
ref mut filename,
|
||||
ref data,
|
||||
} => {
|
||||
let mut parts = filename.splitn(2, '.');
|
||||
let parts = parts.next().and_then(|p| Some((p, parts.next()?)));
|
||||
if let Some((name, suffix)) = parts {
|
||||
// FontAwesome already does its own cache busting with the ?v=4.7.0 thing,
|
||||
// and I don't want to have to patch its CSS file to use `{{ resource }}`
|
||||
if name != ""
|
||||
&& suffix != ""
|
||||
&& suffix != "txt"
|
||||
&& !name.starts_with("FontAwesome/fonts/")
|
||||
{
|
||||
let hex = hex::encode(&Sha256::digest(data)[..4]);
|
||||
let new_filename = format!("{}-{}.{}", name, hex, suffix);
|
||||
self.hash_map.insert(filename.clone(), new_filename.clone());
|
||||
*filename = new_filename;
|
||||
}
|
||||
}
|
||||
}
|
||||
StaticFile::Additional {
|
||||
ref mut filename,
|
||||
ref input_location,
|
||||
} => {
|
||||
let mut parts = filename.splitn(2, '.');
|
||||
let parts = parts.next().and_then(|p| Some((p, parts.next()?)));
|
||||
if let Some((name, suffix)) = parts {
|
||||
if name != "" && suffix != "" {
|
||||
let mut digest = Sha256::new();
|
||||
let mut input_file = File::open(input_location)
|
||||
.with_context(|| "open static file for hashing")?;
|
||||
let mut buf = vec![0; 1024];
|
||||
loop {
|
||||
let amt = input_file
|
||||
.read(&mut buf)
|
||||
.with_context(|| "read static file for hashing")?;
|
||||
if amt == 0 {
|
||||
break;
|
||||
};
|
||||
digest.update(&buf[..amt]);
|
||||
}
|
||||
let hex = hex::encode(&digest.finalize()[..4]);
|
||||
let new_filename = format!("{}-{}.{}", name, hex, suffix);
|
||||
self.hash_map.insert(filename.clone(), new_filename.clone());
|
||||
*filename = new_filename;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn write_files(self, destination: &Path) -> Result<ResourceHelper> {
|
||||
use crate::utils::fs::write_file;
|
||||
use regex::bytes::{Captures, Regex};
|
||||
// The `{{ resource "name" }}` directive in static resources look like
|
||||
// handlebars syntax, even if they technically aren't.
|
||||
static RESOURCE: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r#"\{\{ resource "([^"]+)" \}\}"#).unwrap());
|
||||
fn replace_all<'a>(
|
||||
hash_map: &HashMap<String, String>,
|
||||
data: &'a [u8],
|
||||
filename: &str,
|
||||
) -> Cow<'a, [u8]> {
|
||||
RESOURCE.replace_all(data, move |captures: &Captures<'_>| {
|
||||
let name = captures
|
||||
.get(1)
|
||||
.expect("capture 1 in resource regex")
|
||||
.as_bytes();
|
||||
let name = std::str::from_utf8(name).expect("resource name with invalid utf8");
|
||||
let resource_filename = hash_map.get(name).map(|s| &s[..]).unwrap_or(name);
|
||||
let path_to_root = utils::fs::path_to_root(filename);
|
||||
format!("{}{}", path_to_root, resource_filename)
|
||||
.as_bytes()
|
||||
.to_owned()
|
||||
})
|
||||
}
|
||||
for static_file in &self.static_files {
|
||||
match static_file {
|
||||
StaticFile::Builtin { filename, data } => {
|
||||
debug!("Writing builtin -> {}", filename);
|
||||
let data = if filename.ends_with(".css") || filename.ends_with(".js") {
|
||||
replace_all(&self.hash_map, data, filename)
|
||||
} else {
|
||||
Cow::Borrowed(&data[..])
|
||||
};
|
||||
write_file(destination, filename, &data)?;
|
||||
}
|
||||
StaticFile::Additional {
|
||||
ref input_location,
|
||||
ref filename,
|
||||
} => {
|
||||
let output_location = destination.join(filename);
|
||||
debug!(
|
||||
"Copying {} -> {}",
|
||||
input_location.display(),
|
||||
output_location.display()
|
||||
);
|
||||
if let Some(parent) = output_location.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.with_context(|| format!("Unable to create {}", parent.display()))?;
|
||||
}
|
||||
if filename.ends_with(".css") || filename.ends_with(".js") {
|
||||
let data = fs::read(input_location)?;
|
||||
let data = replace_all(&self.hash_map, &data, filename);
|
||||
write_file(destination, filename, &data)?;
|
||||
} else {
|
||||
fs::copy(input_location, &output_location).with_context(|| {
|
||||
format!(
|
||||
"Unable to copy {} to {}",
|
||||
input_location.display(),
|
||||
output_location.display()
|
||||
)
|
||||
})?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let hash_map = self.hash_map;
|
||||
Ok(ResourceHelper { hash_map })
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::config::HtmlConfig;
|
||||
use crate::theme::Theme;
|
||||
use crate::utils::fs::write_file;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn test_write_directive() {
|
||||
let theme = 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(),
|
||||
toc_js: Vec::new(),
|
||||
toc_html: Vec::new(),
|
||||
fonts_css: None,
|
||||
font_files: Vec::new(),
|
||||
};
|
||||
let temp_dir = TempDir::with_prefix("mdbook-").unwrap();
|
||||
let reference_js = Path::new("static-files-test-case-reference.js");
|
||||
let mut html_config = HtmlConfig::default();
|
||||
html_config.additional_js.push(reference_js.to_owned());
|
||||
write_file(
|
||||
temp_dir.path(),
|
||||
reference_js,
|
||||
br#"{{ resource "book.js" }}"#,
|
||||
)
|
||||
.unwrap();
|
||||
let mut static_files = StaticFiles::new(&theme, &html_config, temp_dir.path()).unwrap();
|
||||
static_files.hash_files().unwrap();
|
||||
static_files.write_files(temp_dir.path()).unwrap();
|
||||
// custom JS winds up referencing book.js
|
||||
let reference_js_content = std::fs::read_to_string(
|
||||
temp_dir
|
||||
.path()
|
||||
.join("static-files-test-case-reference-635c9cdc.js"),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!("book-e3b0c442.js", reference_js_content);
|
||||
// book.js winds up empty
|
||||
let book_js_content =
|
||||
std::fs::read_to_string(temp_dir.path().join("book-e3b0c442.js")).unwrap();
|
||||
assert_eq!("", book_js_content);
|
||||
}
|
||||
}
|
||||
@@ -294,9 +294,9 @@ function playground_text(playground, hidden = true) {
|
||||
themeIds.push(el.id);
|
||||
});
|
||||
var stylesheets = {
|
||||
ayuHighlight: document.querySelector("[href$='ayu-highlight.css']"),
|
||||
tomorrowNight: document.querySelector("[href$='tomorrow-night.css']"),
|
||||
highlight: document.querySelector("[href$='highlight.css']"),
|
||||
ayuHighlight: document.querySelector("#ayu-highlight-css"),
|
||||
tomorrowNight: document.querySelector("#tomorrow-night-css"),
|
||||
highlight: document.querySelector("#highlight-css"),
|
||||
};
|
||||
|
||||
function showThemes() {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: local('Open Sans Light'), local('OpenSans-Light'),
|
||||
url('open-sans-v17-all-charsets-300.woff2') format('woff2');
|
||||
url('{{ resource "fonts/open-sans-v17-all-charsets-300.woff2" }}') format('woff2');
|
||||
}
|
||||
|
||||
/* open-sans-300italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@@ -16,7 +16,7 @@
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
src: local('Open Sans Light Italic'), local('OpenSans-LightItalic'),
|
||||
url('open-sans-v17-all-charsets-300italic.woff2') format('woff2');
|
||||
url('{{ resource "fonts/open-sans-v17-all-charsets-300italic.woff2" }}') format('woff2');
|
||||
}
|
||||
|
||||
/* open-sans-regular - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@@ -25,7 +25,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Open Sans Regular'), local('OpenSans-Regular'),
|
||||
url('open-sans-v17-all-charsets-regular.woff2') format('woff2');
|
||||
url('{{ resource "fonts/open-sans-v17-all-charsets-regular.woff2" }}') format('woff2');
|
||||
}
|
||||
|
||||
/* open-sans-italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@@ -34,7 +34,7 @@
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: local('Open Sans Italic'), local('OpenSans-Italic'),
|
||||
url('open-sans-v17-all-charsets-italic.woff2') format('woff2');
|
||||
url('{{ resource "fonts/open-sans-v17-all-charsets-italic.woff2" }}') format('woff2');
|
||||
}
|
||||
|
||||
/* open-sans-600 - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@@ -43,7 +43,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: local('Open Sans SemiBold'), local('OpenSans-SemiBold'),
|
||||
url('open-sans-v17-all-charsets-600.woff2') format('woff2');
|
||||
url('{{ resource "fonts/open-sans-v17-all-charsets-600.woff2" }}') format('woff2');
|
||||
}
|
||||
|
||||
/* open-sans-600italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@@ -52,7 +52,7 @@
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
src: local('Open Sans SemiBold Italic'), local('OpenSans-SemiBoldItalic'),
|
||||
url('open-sans-v17-all-charsets-600italic.woff2') format('woff2');
|
||||
url('{{ resource "fonts/open-sans-v17-all-charsets-600italic.woff2" }}') format('woff2');
|
||||
}
|
||||
|
||||
/* open-sans-700 - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@@ -61,7 +61,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: local('Open Sans Bold'), local('OpenSans-Bold'),
|
||||
url('open-sans-v17-all-charsets-700.woff2') format('woff2');
|
||||
url('{{ resource "fonts/open-sans-v17-all-charsets-700.woff2" }}') format('woff2');
|
||||
}
|
||||
|
||||
/* open-sans-700italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@@ -70,7 +70,7 @@
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
src: local('Open Sans Bold Italic'), local('OpenSans-BoldItalic'),
|
||||
url('open-sans-v17-all-charsets-700italic.woff2') format('woff2');
|
||||
url('{{ resource "fonts/open-sans-v17-all-charsets-700italic.woff2" }}') format('woff2');
|
||||
}
|
||||
|
||||
/* open-sans-800 - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@@ -79,7 +79,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 800;
|
||||
src: local('Open Sans ExtraBold'), local('OpenSans-ExtraBold'),
|
||||
url('open-sans-v17-all-charsets-800.woff2') format('woff2');
|
||||
url('{{ resource "fonts/open-sans-v17-all-charsets-800.woff2" }}') format('woff2');
|
||||
}
|
||||
|
||||
/* open-sans-800italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@@ -88,7 +88,7 @@
|
||||
font-style: italic;
|
||||
font-weight: 800;
|
||||
src: local('Open Sans ExtraBold Italic'), local('OpenSans-ExtraBoldItalic'),
|
||||
url('open-sans-v17-all-charsets-800italic.woff2') format('woff2');
|
||||
url('{{ resource "fonts/open-sans-v17-all-charsets-800italic.woff2" }}') format('woff2');
|
||||
}
|
||||
|
||||
/* source-code-pro-500 - latin_vietnamese_latin-ext_greek_cyrillic-ext_cyrillic */
|
||||
@@ -96,5 +96,5 @@
|
||||
font-family: 'Source Code Pro';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
src: url('source-code-pro-v11-all-charsets-500.woff2') format('woff2');
|
||||
src: url('{{ resource "fonts/source-code-pro-v11-all-charsets-500.woff2" }}') format('woff2');
|
||||
}
|
||||
|
||||
@@ -20,32 +20,32 @@
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
|
||||
{{#if favicon_svg}}
|
||||
<link rel="icon" href="{{ path_to_root }}favicon.svg">
|
||||
<link rel="icon" href="{{ resource "favicon.svg" }}">
|
||||
{{/if}}
|
||||
{{#if favicon_png}}
|
||||
<link rel="shortcut icon" href="{{ path_to_root }}favicon.png">
|
||||
<link rel="shortcut icon" href="{{ resource "favicon.png" }}">
|
||||
{{/if}}
|
||||
<link rel="stylesheet" href="{{ path_to_root }}css/variables.css">
|
||||
<link rel="stylesheet" href="{{ path_to_root }}css/general.css">
|
||||
<link rel="stylesheet" href="{{ path_to_root }}css/chrome.css">
|
||||
<link rel="stylesheet" href="{{ resource "css/variables.css" }}">
|
||||
<link rel="stylesheet" href="{{ resource "css/general.css" }}">
|
||||
<link rel="stylesheet" href="{{ resource "css/chrome.css" }}">
|
||||
{{#if print_enable}}
|
||||
<link rel="stylesheet" href="{{ path_to_root }}css/print.css" media="print">
|
||||
<link rel="stylesheet" href="{{ resource "css/print.css" }}" media="print">
|
||||
{{/if}}
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="stylesheet" href="{{ path_to_root }}FontAwesome/css/font-awesome.css">
|
||||
<link rel="stylesheet" href="{{ resource "FontAwesome/css/font-awesome.css" }}">
|
||||
{{#if copy_fonts}}
|
||||
<link rel="stylesheet" href="{{ path_to_root }}fonts/fonts.css">
|
||||
<link rel="stylesheet" href="{{ resource "fonts/fonts.css" }}">
|
||||
{{/if}}
|
||||
|
||||
<!-- Highlight.js Stylesheets -->
|
||||
<link rel="stylesheet" href="{{ path_to_root }}highlight.css">
|
||||
<link rel="stylesheet" href="{{ path_to_root }}tomorrow-night.css">
|
||||
<link rel="stylesheet" href="{{ path_to_root }}ayu-highlight.css">
|
||||
<link rel="stylesheet" id="highlight-css" href="{{ resource "highlight.css" }}">
|
||||
<link rel="stylesheet" id="tomorrow-night-css" href="{{ resource "tomorrow-night.css" }}">
|
||||
<link rel="stylesheet" id="ayu-highlight-css" href="{{ resource "ayu-highlight.css" }}">
|
||||
|
||||
<!-- Custom theme stylesheets -->
|
||||
{{#each additional_css}}
|
||||
<link rel="stylesheet" href="{{ ../path_to_root }}{{ this }}">
|
||||
<link rel="stylesheet" href="{{ resource this }}">
|
||||
{{/each}}
|
||||
|
||||
{{#if mathjax_support}}
|
||||
@@ -59,7 +59,7 @@
|
||||
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "{{ preferred_dark_theme }}" : "{{ default_theme }}";
|
||||
</script>
|
||||
<!-- Start loading toc.js asap -->
|
||||
<script src="{{ path_to_root }}toc.js"></script>
|
||||
<script src="{{ resource "toc.js" }}"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="body-container">
|
||||
@@ -280,26 +280,26 @@
|
||||
{{/if}}
|
||||
|
||||
{{#if playground_js}}
|
||||
<script src="{{ path_to_root }}ace.js"></script>
|
||||
<script src="{{ path_to_root }}editor.js"></script>
|
||||
<script src="{{ path_to_root }}mode-rust.js"></script>
|
||||
<script src="{{ path_to_root }}theme-dawn.js"></script>
|
||||
<script src="{{ path_to_root }}theme-tomorrow_night.js"></script>
|
||||
<script src="{{ resource "ace.js" }}"></script>
|
||||
<script src="{{ resource "editor.js" }}"></script>
|
||||
<script src="{{ resource "mode-rust.js" }}"></script>
|
||||
<script src="{{ resource "theme-dawn.js" }}"></script>
|
||||
<script src="{{ resource "theme-tomorrow_night.js" }}"></script>
|
||||
{{/if}}
|
||||
|
||||
{{#if search_js}}
|
||||
<script src="{{ path_to_root }}elasticlunr.min.js"></script>
|
||||
<script src="{{ path_to_root }}mark.min.js"></script>
|
||||
<script src="{{ path_to_root }}searcher.js"></script>
|
||||
<script src="{{ resource "elasticlunr.min.js" }}"></script>
|
||||
<script src="{{ resource "mark.min.js" }}"></script>
|
||||
<script src="{{ resource "searcher.js" }}"></script>
|
||||
{{/if}}
|
||||
|
||||
<script src="{{ path_to_root }}clipboard.min.js"></script>
|
||||
<script src="{{ path_to_root }}highlight.js"></script>
|
||||
<script src="{{ path_to_root }}book.js"></script>
|
||||
<script src="{{ resource "clipboard.min.js" }}"></script>
|
||||
<script src="{{ resource "highlight.js" }}"></script>
|
||||
<script src="{{ resource "book.js" }}"></script>
|
||||
|
||||
<!-- Custom JS scripts -->
|
||||
{{#each additional_js}}
|
||||
<script src="{{ ../path_to_root }}{{this}}"></script>
|
||||
<script src="{{ resource this}}"></script>
|
||||
{{/each}}
|
||||
|
||||
{{#if is_print}}
|
||||
|
||||
@@ -468,12 +468,12 @@ window.search = window.search || {};
|
||||
showResults(true);
|
||||
}
|
||||
|
||||
fetch(path_to_root + 'searchindex.json')
|
||||
fetch('{{ resource "searchindex.json" }}')
|
||||
.then(response => response.json())
|
||||
.then(json => init(json))
|
||||
.catch(error => { // Try to load searchindex.js if fetch failed
|
||||
var script = document.createElement('script');
|
||||
script.src = path_to_root + 'searchindex.js';
|
||||
script.src = '{{ resource "searchindex.js" }}';
|
||||
script.onload = () => init(window.search);
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
|
||||
@@ -21,20 +21,20 @@
|
||||
{{> head}}
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
<link rel="stylesheet" href="{{ path_to_root }}css/variables.css">
|
||||
<link rel="stylesheet" href="{{ path_to_root }}css/general.css">
|
||||
<link rel="stylesheet" href="{{ path_to_root }}css/chrome.css">
|
||||
<link rel="stylesheet" href="{{ resource "css/variables.css" }}">
|
||||
<link rel="stylesheet" href="{{ resource "css/general.css" }}">
|
||||
<link rel="stylesheet" href="{{ resource "css/chrome.css" }}">
|
||||
{{#if print_enable}}
|
||||
<link rel="stylesheet" href="{{ path_to_root }}css/print.css" media="print">
|
||||
<link rel="stylesheet" href="{{ resource "css/print.css" }}" media="print">
|
||||
{{/if}}
|
||||
<!-- Fonts -->
|
||||
<link rel="stylesheet" href="{{ path_to_root }}FontAwesome/css/font-awesome.css">
|
||||
<link rel="stylesheet" href="{{ resource "FontAwesome/css/font-awesome.css" }}">
|
||||
{{#if copy_fonts}}
|
||||
<link rel="stylesheet" href="{{ path_to_root }}fonts/fonts.css">
|
||||
<link rel="stylesheet" href="{{ resource "fonts/fonts.css" }}">
|
||||
{{/if}}
|
||||
<!-- Custom theme stylesheets -->
|
||||
{{#each additional_css}}
|
||||
<link rel="stylesheet" href="{{ ../path_to_root }}{{ this }}">
|
||||
<link rel="stylesheet" href="{{ resource this }}">
|
||||
{{/each}}
|
||||
</head>
|
||||
<body class="sidebar-iframe-inner">
|
||||
|
||||
@@ -9,6 +9,7 @@ edition = "2018"
|
||||
|
||||
[output.html]
|
||||
mathjax-support = true
|
||||
hash-files = true
|
||||
|
||||
[output.html.playground]
|
||||
editable = true
|
||||
|
||||
Reference in New Issue
Block a user