Build multiple books from localizations at once
Changes how the `book` module loads books. Now it is possible to load all of the translations of a book and put them into a single output folder. If a book is generated this way, a menu will be created in the handlebars renderer for switching between languages.
This commit is contained in:
parent
96d9271d64
commit
8869c2cf06
115
src/book/book.rs
115
src/book/book.rs
@ -1,4 +1,4 @@
|
|||||||
use std::collections::VecDeque;
|
use std::collections::{HashMap, VecDeque};
|
||||||
use std::fmt::{self, Display, Formatter};
|
use std::fmt::{self, Display, Formatter};
|
||||||
use std::fs::{self, File};
|
use std::fs::{self, File};
|
||||||
use std::io::{Read, Write};
|
use std::io::{Read, Write};
|
||||||
@ -14,10 +14,30 @@ pub fn load_book<P: AsRef<Path>>(
|
|||||||
root_dir: P,
|
root_dir: P,
|
||||||
cfg: &Config,
|
cfg: &Config,
|
||||||
build_opts: &BuildOpts,
|
build_opts: &BuildOpts,
|
||||||
) -> Result<Book> {
|
) -> Result<LoadedBook> {
|
||||||
|
if cfg.language.has_localized_dir_structure() {
|
||||||
|
match build_opts.language_ident {
|
||||||
|
// Build a single book's translation.
|
||||||
|
Some(_) => Ok(LoadedBook::Single(load_single_book_translation(&root_dir, cfg, &build_opts.language_ident)?)),
|
||||||
|
// Build all available translations at once.
|
||||||
|
None => {
|
||||||
|
let mut translations = HashMap::new();
|
||||||
|
for (lang_ident, _) in cfg.language.0.iter() {
|
||||||
|
let book = load_single_book_translation(&root_dir, cfg, &Some(lang_ident.clone()))?;
|
||||||
|
translations.insert(lang_ident.clone(), book);
|
||||||
|
}
|
||||||
|
Ok(LoadedBook::Localized(LocalizedBooks(translations)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(LoadedBook::Single(load_single_book_translation(&root_dir, cfg, &None)?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_single_book_translation<P: AsRef<Path>>(root_dir: P, cfg: &Config, language_ident: &Option<String>) -> Result<Book> {
|
||||||
let localized_src_dir = root_dir.as_ref().join(
|
let localized_src_dir = root_dir.as_ref().join(
|
||||||
cfg.get_localized_src_path(build_opts.language_ident.as_ref())
|
cfg.get_localized_src_path(language_ident.as_ref())
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
);
|
);
|
||||||
let fallback_src_dir = root_dir.as_ref().join(cfg.get_fallback_src_path());
|
let fallback_src_dir = root_dir.as_ref().join(cfg.get_fallback_src_path());
|
||||||
|
|
||||||
@ -139,6 +159,91 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A collection of `Books`, each one a single localization.
|
||||||
|
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct LocalizedBooks(pub HashMap<String, Book>);
|
||||||
|
|
||||||
|
impl LocalizedBooks {
|
||||||
|
/// Get a depth-first iterator over the items in the book.
|
||||||
|
pub fn iter(&self) -> BookItems<'_> {
|
||||||
|
let mut items = VecDeque::new();
|
||||||
|
|
||||||
|
for (_, book) in self.0.iter() {
|
||||||
|
items.extend(book.iter().items);
|
||||||
|
}
|
||||||
|
|
||||||
|
BookItems {
|
||||||
|
items: items
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recursively apply a closure to each item in the book, allowing you to
|
||||||
|
/// mutate them.
|
||||||
|
///
|
||||||
|
/// # Note
|
||||||
|
///
|
||||||
|
/// Unlike the `iter()` method, this requires a closure instead of returning
|
||||||
|
/// an iterator. This is because using iterators can possibly allow you
|
||||||
|
/// to have iterator invalidation errors.
|
||||||
|
pub fn for_each_mut<F>(&mut self, mut func: F)
|
||||||
|
where
|
||||||
|
F: FnMut(&mut BookItem),
|
||||||
|
{
|
||||||
|
for (_, book) in self.0.iter_mut() {
|
||||||
|
book.for_each_mut(&mut func);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A book which has been loaded and is ready for rendering.
|
||||||
|
///
|
||||||
|
/// This exists because the result of loading a book directory can be multiple
|
||||||
|
/// books, each one representing a separate translation, or a single book with
|
||||||
|
/// no translations.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub enum LoadedBook {
|
||||||
|
/// The book was loaded with all translations.
|
||||||
|
Localized(LocalizedBooks),
|
||||||
|
/// The book was loaded without any additional translations.
|
||||||
|
Single(Book),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LoadedBook {
|
||||||
|
/// Get a depth-first iterator over the items in the book.
|
||||||
|
pub fn iter(&self) -> BookItems<'_> {
|
||||||
|
match self {
|
||||||
|
LoadedBook::Localized(books) => books.iter(),
|
||||||
|
LoadedBook::Single(book) => book.iter(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recursively apply a closure to each item in the book, allowing you to
|
||||||
|
/// mutate them.
|
||||||
|
///
|
||||||
|
/// # Note
|
||||||
|
///
|
||||||
|
/// Unlike the `iter()` method, this requires a closure instead of returning
|
||||||
|
/// an iterator. This is because using iterators can possibly allow you
|
||||||
|
/// to have iterator invalidation errors.
|
||||||
|
pub fn for_each_mut<F>(&mut self, mut func: F)
|
||||||
|
where
|
||||||
|
F: FnMut(&mut BookItem),
|
||||||
|
{
|
||||||
|
match self {
|
||||||
|
LoadedBook::Localized(books) => books.for_each_mut(&mut func),
|
||||||
|
LoadedBook::Single(book) => book.for_each_mut(&mut func),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns one of the books loaded. Used for compatibility.
|
||||||
|
pub fn first(&self) -> &Book {
|
||||||
|
match self {
|
||||||
|
LoadedBook::Localized(books) => books.0.iter().next().unwrap().1,
|
||||||
|
LoadedBook::Single(book) => &book
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Enum representing any type of item which can be added to a book.
|
/// Enum representing any type of item which can be added to a book.
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub enum BookItem {
|
pub enum BookItem {
|
||||||
@ -512,7 +617,7 @@ more text.
|
|||||||
Vec::new(),
|
Vec::new(),
|
||||||
&cfg,
|
&cfg,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(got, should_be);
|
assert_eq!(got, should_be);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ mod book;
|
|||||||
mod init;
|
mod init;
|
||||||
mod summary;
|
mod summary;
|
||||||
|
|
||||||
pub use self::book::{load_book, Book, BookItem, BookItems, Chapter};
|
pub use self::book::{load_book, BookItem, BookItems, Chapter, Book, LocalizedBooks, LoadedBook};
|
||||||
pub use self::init::BookBuilder;
|
pub use self::init::BookBuilder;
|
||||||
pub use self::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem};
|
pub use self::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem};
|
||||||
|
|
||||||
@ -18,7 +18,9 @@ use std::io::Write;
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use std::string::ToString;
|
use std::string::ToString;
|
||||||
|
use std::collections::HashMap;
|
||||||
use tempfile::Builder as TempFileBuilder;
|
use tempfile::Builder as TempFileBuilder;
|
||||||
|
use tempfile::TempDir;
|
||||||
use toml::Value;
|
use toml::Value;
|
||||||
|
|
||||||
use crate::errors::*;
|
use crate::errors::*;
|
||||||
@ -37,8 +39,9 @@ pub struct MDBook {
|
|||||||
pub root: PathBuf,
|
pub root: PathBuf,
|
||||||
/// The configuration used to tweak now a book is built.
|
/// The configuration used to tweak now a book is built.
|
||||||
pub config: Config,
|
pub config: Config,
|
||||||
/// A representation of the book's contents in memory.
|
/// A representation of the book's contents in memory. Can be a single book,
|
||||||
pub book: Book,
|
/// or multiple books in different languages.
|
||||||
|
pub book: LoadedBook,
|
||||||
/// Build options passed from frontend.
|
/// Build options passed from frontend.
|
||||||
pub build_opts: BuildOpts,
|
pub build_opts: BuildOpts,
|
||||||
|
|
||||||
@ -131,7 +134,7 @@ impl MDBook {
|
|||||||
);
|
);
|
||||||
let fallback_src_dir = root.join(config.get_fallback_src_path());
|
let fallback_src_dir = root.join(config.get_fallback_src_path());
|
||||||
let book =
|
let book =
|
||||||
book::load_book_from_disk(&summary, localized_src_dir, fallback_src_dir, &config)?;
|
LoadedBook::Single(book::load_book_from_disk(&summary, localized_src_dir, fallback_src_dir, &config)?);
|
||||||
|
|
||||||
let renderers = determine_renderers(&config);
|
let renderers = determine_renderers(&config);
|
||||||
let preprocessors = determine_preprocessors(&config)?;
|
let preprocessors = determine_preprocessors(&config)?;
|
||||||
@ -208,7 +211,6 @@ impl MDBook {
|
|||||||
|
|
||||||
/// Run the entire build process for a particular [`Renderer`].
|
/// Run the entire build process for a particular [`Renderer`].
|
||||||
pub fn execute_build_process(&self, renderer: &dyn Renderer) -> Result<()> {
|
pub fn execute_build_process(&self, renderer: &dyn Renderer) -> Result<()> {
|
||||||
let mut preprocessed_book = self.book.clone();
|
|
||||||
let preprocess_ctx = PreprocessorContext::new(
|
let preprocess_ctx = PreprocessorContext::new(
|
||||||
self.root.clone(),
|
self.root.clone(),
|
||||||
self.build_opts.clone(),
|
self.build_opts.clone(),
|
||||||
@ -216,19 +218,26 @@ impl MDBook {
|
|||||||
renderer.name().to_string(),
|
renderer.name().to_string(),
|
||||||
);
|
);
|
||||||
|
|
||||||
for preprocessor in &self.preprocessors {
|
let preprocessed_books = match &self.book {
|
||||||
if preprocessor_should_run(&**preprocessor, renderer, &self.config) {
|
LoadedBook::Localized(ref books) => {
|
||||||
debug!("Running the {} preprocessor.", preprocessor.name());
|
let mut new_books = HashMap::new();
|
||||||
preprocessed_book = preprocessor.run(&preprocess_ctx, preprocessed_book)?;
|
|
||||||
}
|
for (ident, book) in books.0.iter() {
|
||||||
}
|
let preprocessed_book = self.preprocess(&preprocess_ctx, renderer, book.clone())?;
|
||||||
|
new_books.insert(ident.clone(), preprocessed_book);
|
||||||
|
}
|
||||||
|
|
||||||
|
LoadedBook::Localized(LocalizedBooks(new_books))
|
||||||
|
},
|
||||||
|
LoadedBook::Single(ref book) => LoadedBook::Single(self.preprocess(&preprocess_ctx, renderer, book.clone())?),
|
||||||
|
};
|
||||||
|
|
||||||
let name = renderer.name();
|
let name = renderer.name();
|
||||||
let build_dir = self.build_dir_for(name);
|
let build_dir = self.build_dir_for(name);
|
||||||
|
|
||||||
let mut render_context = RenderContext::new(
|
let mut render_context = RenderContext::new(
|
||||||
self.root.clone(),
|
self.root.clone(),
|
||||||
preprocessed_book.clone(),
|
preprocessed_books.clone(),
|
||||||
self.build_opts.clone(),
|
self.build_opts.clone(),
|
||||||
self.config.clone(),
|
self.config.clone(),
|
||||||
build_dir,
|
build_dir,
|
||||||
@ -257,16 +266,7 @@ impl MDBook {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run `rustdoc` tests on the book, linking against the provided libraries.
|
fn test_book(&self, book: &Book, temp_dir: &TempDir, library_args: &Vec<&str>) -> Result<()> {
|
||||||
pub fn test(&mut self, library_paths: Vec<&str>) -> Result<()> {
|
|
||||||
let library_args: Vec<&str> = (0..library_paths.len())
|
|
||||||
.map(|_| "-L")
|
|
||||||
.zip(library_paths.into_iter())
|
|
||||||
.flat_map(|x| vec![x.0, x.1])
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let temp_dir = TempFileBuilder::new().prefix("mdbook-").tempdir()?;
|
|
||||||
|
|
||||||
// FIXME: Is "test" the proper renderer name to use here?
|
// FIXME: Is "test" the proper renderer name to use here?
|
||||||
let preprocess_context = PreprocessorContext::new(
|
let preprocess_context = PreprocessorContext::new(
|
||||||
self.root.clone(),
|
self.root.clone(),
|
||||||
@ -274,8 +274,7 @@ impl MDBook {
|
|||||||
self.config.clone(),
|
self.config.clone(),
|
||||||
"test".to_string(),
|
"test".to_string(),
|
||||||
);
|
);
|
||||||
|
let book = LinkPreprocessor::new().run(&preprocess_context, book.clone())?;
|
||||||
let book = LinkPreprocessor::new().run(&preprocess_context, self.book.clone())?;
|
|
||||||
// Index Preprocessor is disabled so that chapter paths continue to point to the
|
// Index Preprocessor is disabled so that chapter paths continue to point to the
|
||||||
// actual markdown files.
|
// actual markdown files.
|
||||||
|
|
||||||
@ -296,7 +295,7 @@ impl MDBook {
|
|||||||
tmpf.write_all(ch.content.as_bytes())?;
|
tmpf.write_all(ch.content.as_bytes())?;
|
||||||
|
|
||||||
let mut cmd = Command::new("rustdoc");
|
let mut cmd = Command::new("rustdoc");
|
||||||
cmd.arg(&path).arg("--test").args(&library_args);
|
cmd.arg(&path).arg("--test").args(library_args);
|
||||||
|
|
||||||
if let Some(edition) = self.config.rust.edition {
|
if let Some(edition) = self.config.rust.edition {
|
||||||
match edition {
|
match edition {
|
||||||
|
@ -20,7 +20,7 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
|||||||
.arg_from_usage(
|
.arg_from_usage(
|
||||||
"-l, --language=[language] 'Language to render the compiled book in.{n}\
|
"-l, --language=[language] 'Language to render the compiled book in.{n}\
|
||||||
Only valid if the [languages] table in the config is not empty.{n}\
|
Only valid if the [languages] table in the config is not empty.{n}\
|
||||||
If omitted, defaults to the language with `default` set to true.'",
|
If omitted, builds all translations and provides a menu in the generated output for switching between them.'",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
|||||||
.arg_from_usage(
|
.arg_from_usage(
|
||||||
"-l, --language=[language] 'Language to render the compiled book in.{n}\
|
"-l, --language=[language] 'Language to render the compiled book in.{n}\
|
||||||
Only valid if the [languages] table in the config is not empty.{n}\
|
Only valid if the [languages] table in the config is not empty.{n}\
|
||||||
If omitted, defaults to the language with `default` set to true.'",
|
If omitted, builds all translations and provides a menu in the generated output for switching between them.'",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,7 +52,7 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
|||||||
.arg_from_usage(
|
.arg_from_usage(
|
||||||
"-l, --language=[language] 'Language to render the compiled book in.{n}\
|
"-l, --language=[language] 'Language to render the compiled book in.{n}\
|
||||||
Only valid if the [languages] table in the config is not empty.{n}\
|
Only valid if the [languages] table in the config is not empty.{n}\
|
||||||
If omitted, defaults to the language with `default` set to true.'",
|
If omitted, builds all translations and provides a menu in the generated output for switching between them.'",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
|||||||
.help("A comma-separated list of directories to add to {n}the crate search path when building tests"))
|
.help("A comma-separated list of directories to add to {n}the crate search path when building tests"))
|
||||||
.arg_from_usage("-l, --language=[language] 'Language to render the compiled book in.{n}\
|
.arg_from_usage("-l, --language=[language] 'Language to render the compiled book in.{n}\
|
||||||
Only valid if the [languages] table in the config is not empty.{n}\
|
Only valid if the [languages] table in the config is not empty.{n}\
|
||||||
If omitted, defaults to the language with `default` set to true.'")
|
If omitted, builds all translations and provides a menu in the generated output for switching between them.'")
|
||||||
}
|
}
|
||||||
|
|
||||||
// test command implementation
|
// test command implementation
|
||||||
|
@ -26,7 +26,7 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
|||||||
.arg_from_usage(
|
.arg_from_usage(
|
||||||
"-l, --language=[language] 'Language to render the compiled book in.{n}\
|
"-l, --language=[language] 'Language to render the compiled book in.{n}\
|
||||||
Only valid if the [languages] table in the config is not empty.{n}\
|
Only valid if the [languages] table in the config is not empty.{n}\
|
||||||
If omitted, defaults to the language with `default` set to true.'",
|
If omitted, builds all translations and provides a menu in the generated output for switching between them.'",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -776,17 +776,27 @@ impl Default for Search {
|
|||||||
/// Configuration for localizations of this book
|
/// Configuration for localizations of this book
|
||||||
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
#[serde(transparent)]
|
#[serde(transparent)]
|
||||||
pub struct LanguageConfig(HashMap<String, Language>);
|
pub struct LanguageConfig(pub HashMap<String, Language>);
|
||||||
|
|
||||||
/// Configuration for a single localization
|
/// Configuration for a single localization
|
||||||
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
#[serde(default, rename_all = "kebab-case")]
|
#[serde(default, rename_all = "kebab-case")]
|
||||||
pub struct Language {
|
pub struct Language {
|
||||||
name: String,
|
/// Human-readable name of the language.
|
||||||
default: bool,
|
pub name: String,
|
||||||
|
/// If true, this language is the default. There can only be one default
|
||||||
|
/// language in the config.
|
||||||
|
pub default: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LanguageConfig {
|
impl LanguageConfig {
|
||||||
|
/// If true, mdBook should assume there are subdirectories under src/
|
||||||
|
/// corresponding to the localizations in the config. If false, src/ is a
|
||||||
|
/// single directory containing the summary file and the rest.
|
||||||
|
pub fn has_localized_dir_structure(&self) -> bool {
|
||||||
|
self.default_language().is_some()
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the default language specified in the config.
|
/// Returns the default language specified in the config.
|
||||||
pub fn default_language(&self) -> Option<&String> {
|
pub fn default_language(&self) -> Option<&String> {
|
||||||
self.0
|
self.0
|
||||||
|
@ -199,11 +199,11 @@ mod tests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let mut buffer = Vec::new();
|
let mut buffer = Vec::new();
|
||||||
cmd.write_input(&mut buffer, &md.book, &ctx).unwrap();
|
cmd.write_input(&mut buffer, &md.book.first(), &ctx).unwrap();
|
||||||
|
|
||||||
let (got_ctx, got_book) = CmdPreprocessor::parse_input(buffer.as_slice()).unwrap();
|
let (got_ctx, got_book) = CmdPreprocessor::parse_input(buffer.as_slice()).unwrap();
|
||||||
|
|
||||||
assert_eq!(got_book, md.book);
|
assert_eq!(got_book, *md.book.first());
|
||||||
assert_eq!(got_ctx, ctx);
|
assert_eq!(got_ctx, ctx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use crate::book::{Book, BookItem};
|
use crate::book::{Book, BookItem, LoadedBook};
|
||||||
use crate::config::{BookConfig, Config, HtmlConfig, Playground, RustEdition};
|
use crate::config::{BookConfig, Config, HtmlConfig, Playground, RustEdition};
|
||||||
use crate::errors::*;
|
use crate::errors::*;
|
||||||
use crate::renderer::html_handlebars::helpers;
|
use crate::renderer::html_handlebars::helpers;
|
||||||
@ -24,6 +24,135 @@ impl HtmlHandlebars {
|
|||||||
HtmlHandlebars
|
HtmlHandlebars
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_books<'a>(
|
||||||
|
&self,
|
||||||
|
ctx: &RenderContext,
|
||||||
|
src_dir: &PathBuf,
|
||||||
|
html_config: &HtmlConfig,
|
||||||
|
handlebars: &mut Handlebars<'a>,
|
||||||
|
theme: &Theme,
|
||||||
|
) -> Result<()> {
|
||||||
|
match ctx.book {
|
||||||
|
LoadedBook::Localized(ref books) => {
|
||||||
|
for (lang_ident, book) in books.0.iter() {
|
||||||
|
let localized_src_dir = src_dir.join(lang_ident);
|
||||||
|
let localized_destination = ctx.destination.join(lang_ident);
|
||||||
|
let localized_build_dir = ctx.config.build.build_dir.join(lang_ident);
|
||||||
|
self.render_book(
|
||||||
|
ctx,
|
||||||
|
&book,
|
||||||
|
&localized_src_dir,
|
||||||
|
&localized_src_dir,
|
||||||
|
&localized_destination,
|
||||||
|
&localized_build_dir,
|
||||||
|
html_config,
|
||||||
|
handlebars,
|
||||||
|
theme,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LoadedBook::Single(ref book) => {
|
||||||
|
let extra_file_dir = match &ctx.build_opts.language_ident {
|
||||||
|
// `src_dir` points to the root source directory, not the
|
||||||
|
// subdirectory with the translation's index/summary files.
|
||||||
|
// We have to append the language identifier to prevent the
|
||||||
|
// files from the other translations from being copied in
|
||||||
|
// the final step.
|
||||||
|
Some(lang_ident) => {
|
||||||
|
let mut path = src_dir.clone();
|
||||||
|
path.push(lang_ident);
|
||||||
|
path
|
||||||
|
},
|
||||||
|
// `src_dir` is where index.html and the other extra files
|
||||||
|
// are, so use that.
|
||||||
|
None => src_dir.clone()
|
||||||
|
};
|
||||||
|
self.render_book(ctx, &book, src_dir, &extra_file_dir, &ctx.destination, &ctx.config.build.build_dir, html_config, handlebars, theme)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_book<'a>(&self,
|
||||||
|
ctx: &RenderContext,
|
||||||
|
book: &Book,
|
||||||
|
src_dir: &PathBuf,
|
||||||
|
extra_file_dir: &PathBuf,
|
||||||
|
destination: &PathBuf,
|
||||||
|
build_dir: &PathBuf,
|
||||||
|
html_config: &HtmlConfig,
|
||||||
|
handlebars: &mut Handlebars<'a>,
|
||||||
|
theme: &Theme,
|
||||||
|
) -> Result<()> {
|
||||||
|
let build_dir = ctx.root.join(build_dir);
|
||||||
|
let mut data = make_data(&ctx.root, &book, &ctx.book, &ctx.config, &html_config, &theme)?;
|
||||||
|
|
||||||
|
// Print version
|
||||||
|
let mut print_content = String::new();
|
||||||
|
|
||||||
|
fs::create_dir_all(&destination)
|
||||||
|
.with_context(|| "Unexpected error when constructing destination path")?;
|
||||||
|
|
||||||
|
let mut is_index = true;
|
||||||
|
for item in book.iter() {
|
||||||
|
let ctx = RenderItemContext {
|
||||||
|
handlebars: &handlebars,
|
||||||
|
destination: destination.to_path_buf(),
|
||||||
|
data: data.clone(),
|
||||||
|
is_index,
|
||||||
|
html_config: html_config.clone(),
|
||||||
|
edition: ctx.config.rust.edition,
|
||||||
|
};
|
||||||
|
self.render_item(item, ctx, &mut print_content)?;
|
||||||
|
is_index = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render 404 page
|
||||||
|
if html_config.input_404 != Some("".to_string()) {
|
||||||
|
self.render_404(ctx, &html_config, src_dir, destination, handlebars, &mut data)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print version
|
||||||
|
self.configure_print_version(&mut data, &print_content);
|
||||||
|
if let Some(ref title) = ctx.config.book.title {
|
||||||
|
data.insert("title".to_owned(), json!(title));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render the handlebars template with the data
|
||||||
|
debug!("Render template");
|
||||||
|
let rendered = handlebars.render("index", &data)?;
|
||||||
|
|
||||||
|
let rendered = self.post_process(rendered, &html_config.playground, ctx.config.rust.edition);
|
||||||
|
|
||||||
|
utils::fs::write_file(&destination, "print.html", rendered.as_bytes())?;
|
||||||
|
debug!("Creating print.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.clone().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")?;
|
||||||
|
|
||||||
|
// Copy all remaining files, avoid a recursive copy from/to the book build dir
|
||||||
|
utils::fs::copy_files_except_ext(&extra_file_dir, &destination, true, Some(&build_dir), &["md"])?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
fn render_item(
|
fn render_item(
|
||||||
&self,
|
&self,
|
||||||
item: &BookItem,
|
item: &BookItem,
|
||||||
@ -100,7 +229,7 @@ impl HtmlHandlebars {
|
|||||||
);
|
);
|
||||||
if let Some(ref section) = ch.number {
|
if let Some(ref section) = ch.number {
|
||||||
ctx.data
|
ctx.data
|
||||||
.insert("section".to_owned(), json!(section.to_string()));
|
.insert("section".to_owned(), json!(section.to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render the handlebars template with the data
|
// Render the handlebars template with the data
|
||||||
@ -131,11 +260,11 @@ impl HtmlHandlebars {
|
|||||||
&self,
|
&self,
|
||||||
ctx: &RenderContext,
|
ctx: &RenderContext,
|
||||||
html_config: &HtmlConfig,
|
html_config: &HtmlConfig,
|
||||||
src_dir: &Path,
|
src_dir: &PathBuf,
|
||||||
|
destination: &PathBuf,
|
||||||
handlebars: &mut Handlebars<'_>,
|
handlebars: &mut Handlebars<'_>,
|
||||||
data: &mut serde_json::Map<String, serde_json::Value>,
|
data: &mut serde_json::Map<String, serde_json::Value>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let destination = &ctx.destination;
|
|
||||||
let content_404 = if let Some(ref filename) = html_config.input_404 {
|
let content_404 = if let Some(ref filename) = html_config.input_404 {
|
||||||
let path = src_dir.join(filename);
|
let path = src_dir.join(filename);
|
||||||
std::fs::read_to_string(&path)
|
std::fs::read_to_string(&path)
|
||||||
@ -149,7 +278,7 @@ impl HtmlHandlebars {
|
|||||||
})?
|
})?
|
||||||
} else {
|
} else {
|
||||||
"# Document not found (404)\n\nThis URL is invalid, sorry. Please use the \
|
"# Document not found (404)\n\nThis URL is invalid, sorry. Please use the \
|
||||||
navigation bar or search to continue."
|
navigation bar or search to continue."
|
||||||
.to_string()
|
.to_string()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -161,14 +290,15 @@ impl HtmlHandlebars {
|
|||||||
} else {
|
} else {
|
||||||
debug!(
|
debug!(
|
||||||
"HTML 'site-url' parameter not set, defaulting to '/'. Please configure \
|
"HTML 'site-url' parameter not set, defaulting to '/'. Please configure \
|
||||||
this to ensure the 404 page work correctly, especially if your site is hosted in a \
|
this to ensure the 404 page work correctly, especially if your site is hosted in a \
|
||||||
subdirectory on the HTTP server."
|
subdirectory on the HTTP server."
|
||||||
);
|
);
|
||||||
"/"
|
"/"
|
||||||
};
|
};
|
||||||
data_404.insert("base_url".to_owned(), json!(base_url));
|
data_404.insert("base_url".to_owned(), json!(base_url));
|
||||||
// Set a dummy path to ensure other paths (e.g. in the TOC) are generated correctly
|
// Set a dummy path to ensure other paths (e.g. in the TOC) are generated correctly
|
||||||
data_404.insert("path".to_owned(), json!("404.md"));
|
data_404.insert("path".to_owned(), json!("404.md"));
|
||||||
|
data_404.insert("path_to_root".to_owned(), json!(""));
|
||||||
data_404.insert("content".to_owned(), json!(html_content_404));
|
data_404.insert("content".to_owned(), json!(html_content_404));
|
||||||
let rendered = handlebars.render("index", &data_404)?;
|
let rendered = handlebars.render("index", &data_404)?;
|
||||||
|
|
||||||
@ -331,6 +461,7 @@ impl HtmlHandlebars {
|
|||||||
handlebars.register_helper("previous", Box::new(helpers::navigation::previous));
|
handlebars.register_helper("previous", Box::new(helpers::navigation::previous));
|
||||||
handlebars.register_helper("next", Box::new(helpers::navigation::next));
|
handlebars.register_helper("next", Box::new(helpers::navigation::next));
|
||||||
handlebars.register_helper("theme_option", Box::new(helpers::theme::theme_option));
|
handlebars.register_helper("theme_option", Box::new(helpers::theme::theme_option));
|
||||||
|
handlebars.register_helper("language_option", Box::new(helpers::language::language_option));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Copy across any additional CSS and JavaScript files which the book
|
/// Copy across any additional CSS and JavaScript files which the book
|
||||||
@ -437,7 +568,7 @@ impl HtmlHandlebars {
|
|||||||
fn maybe_wrong_theme_dir(dir: &Path) -> Result<bool> {
|
fn maybe_wrong_theme_dir(dir: &Path) -> Result<bool> {
|
||||||
fn entry_is_maybe_book_file(entry: fs::DirEntry) -> Result<bool> {
|
fn entry_is_maybe_book_file(entry: fs::DirEntry) -> Result<bool> {
|
||||||
Ok(entry.file_type()?.is_file()
|
Ok(entry.file_type()?.is_file()
|
||||||
&& entry.path().extension().map_or(false, |ext| ext == "md"))
|
&& entry.path().extension().map_or(false, |ext| ext == "md"))
|
||||||
}
|
}
|
||||||
|
|
||||||
if dir.is_dir() {
|
if dir.is_dir() {
|
||||||
@ -462,8 +593,6 @@ impl Renderer for HtmlHandlebars {
|
|||||||
let html_config = ctx.config.html_config().unwrap_or_default();
|
let html_config = ctx.config.html_config().unwrap_or_default();
|
||||||
let src_dir = ctx.source_dir();
|
let src_dir = ctx.source_dir();
|
||||||
let destination = &ctx.destination;
|
let destination = &ctx.destination;
|
||||||
let book = &ctx.book;
|
|
||||||
let build_dir = ctx.root.join(&ctx.config.build.build_dir);
|
|
||||||
|
|
||||||
if destination.exists() {
|
if destination.exists() {
|
||||||
utils::fs::remove_dir_content(destination)
|
utils::fs::remove_dir_content(destination)
|
||||||
@ -581,6 +710,7 @@ impl Renderer for HtmlHandlebars {
|
|||||||
fn make_data(
|
fn make_data(
|
||||||
root: &Path,
|
root: &Path,
|
||||||
book: &Book,
|
book: &Book,
|
||||||
|
loaded_book: &LoadedBook,
|
||||||
config: &Config,
|
config: &Config,
|
||||||
html_config: &HtmlConfig,
|
html_config: &HtmlConfig,
|
||||||
theme: &Theme,
|
theme: &Theme,
|
||||||
@ -702,6 +832,22 @@ fn make_data(
|
|||||||
};
|
};
|
||||||
data.insert("git_repository_icon".to_owned(), json!(git_repository_icon));
|
data.insert("git_repository_icon".to_owned(), json!(git_repository_icon));
|
||||||
|
|
||||||
|
match loaded_book {
|
||||||
|
LoadedBook::Localized(books) => {
|
||||||
|
data.insert("languages_enabled".to_owned(), json!(true));
|
||||||
|
let mut languages = Vec::new();
|
||||||
|
for (lang_ident, _) in books.0.iter() {
|
||||||
|
languages.push(lang_ident.clone());
|
||||||
|
}
|
||||||
|
languages.sort();
|
||||||
|
data.insert("languages".to_owned(), json!(languages));
|
||||||
|
data.insert("language_config".to_owned(), json!(config.language.clone()));
|
||||||
|
},
|
||||||
|
LoadedBook::Single(_) => {
|
||||||
|
data.insert("languages_enabled".to_owned(), json!(false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mut chapters = vec![];
|
let mut chapters = vec![];
|
||||||
|
|
||||||
for item in book.iter() {
|
for item in book.iter() {
|
||||||
@ -866,7 +1012,7 @@ fn add_playground_pre(
|
|||||||
"\n# #![allow(unused)]\n{}#fn main() {{\n{}#}}",
|
"\n# #![allow(unused)]\n{}#fn main() {{\n{}#}}",
|
||||||
attrs, code
|
attrs, code
|
||||||
)
|
)
|
||||||
.into()
|
.into()
|
||||||
};
|
};
|
||||||
hide_lines(&content)
|
hide_lines(&content)
|
||||||
}
|
}
|
||||||
@ -988,20 +1134,20 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn add_playground() {
|
fn add_playground() {
|
||||||
let inputs = [
|
let inputs = [
|
||||||
("<code class=\"language-rust\">x()</code>",
|
("<code class=\"language-rust\">x()</code>",
|
||||||
"<pre class=\"playground\"><code class=\"language-rust\">\n<span class=\"boring\">#![allow(unused)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}\n</span></code></pre>"),
|
"<pre class=\"playground\"><code class=\"language-rust\">\n<span class=\"boring\">#![allow(unused)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}\n</span></code></pre>"),
|
||||||
("<code class=\"language-rust\">fn main() {}</code>",
|
("<code class=\"language-rust\">fn main() {}</code>",
|
||||||
"<pre class=\"playground\"><code class=\"language-rust\">fn main() {}\n</code></pre>"),
|
"<pre class=\"playground\"><code class=\"language-rust\">fn main() {}\n</code></pre>"),
|
||||||
("<code class=\"language-rust editable\">let s = \"foo\n # bar\n\";</code>",
|
("<code class=\"language-rust editable\">let s = \"foo\n # bar\n\";</code>",
|
||||||
"<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n<span class=\"boring\"> bar\n</span>\";\n</code></pre>"),
|
"<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n<span class=\"boring\"> bar\n</span>\";\n</code></pre>"),
|
||||||
("<code class=\"language-rust editable\">let s = \"foo\n ## bar\n\";</code>",
|
("<code class=\"language-rust editable\">let s = \"foo\n ## bar\n\";</code>",
|
||||||
"<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n # bar\n\";\n</code></pre>"),
|
"<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n # bar\n\";\n</code></pre>"),
|
||||||
("<code class=\"language-rust editable\">let s = \"foo\n # bar\n#\n\";</code>",
|
("<code class=\"language-rust editable\">let s = \"foo\n # bar\n#\n\";</code>",
|
||||||
"<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n<span class=\"boring\"> bar\n</span><span class=\"boring\">\n</span>\";\n</code></pre>"),
|
"<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n<span class=\"boring\"> bar\n</span><span class=\"boring\">\n</span>\";\n</code></pre>"),
|
||||||
("<code class=\"language-rust ignore\">let s = \"foo\n # bar\n\";</code>",
|
("<code class=\"language-rust ignore\">let s = \"foo\n # bar\n\";</code>",
|
||||||
"<code class=\"language-rust ignore\">let s = \"foo\n<span class=\"boring\"> bar\n</span>\";\n</code>"),
|
"<code class=\"language-rust ignore\">let s = \"foo\n<span class=\"boring\"> bar\n</span>\";\n</code>"),
|
||||||
("<code class=\"language-rust editable\">#![no_std]\nlet s = \"foo\";\n #[some_attr]</code>",
|
("<code class=\"language-rust editable\">#![no_std]\nlet s = \"foo\";\n #[some_attr]</code>",
|
||||||
"<pre class=\"playground\"><code class=\"language-rust editable\">#![no_std]\nlet s = \"foo\";\n #[some_attr]\n</code></pre>"),
|
"<pre class=\"playground\"><code class=\"language-rust editable\">#![no_std]\nlet s = \"foo\";\n #[some_attr]\n</code></pre>"),
|
||||||
];
|
];
|
||||||
for (src, should_be) in &inputs {
|
for (src, should_be) in &inputs {
|
||||||
let got = add_playground_pre(
|
let got = add_playground_pre(
|
||||||
@ -1018,14 +1164,14 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn add_playground_edition2015() {
|
fn add_playground_edition2015() {
|
||||||
let inputs = [
|
let inputs = [
|
||||||
("<code class=\"language-rust\">x()</code>",
|
("<code class=\"language-rust\">x()</code>",
|
||||||
"<pre class=\"playground\"><code class=\"language-rust edition2015\">\n<span class=\"boring\">#![allow(unused)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}\n</span></code></pre>"),
|
"<pre class=\"playground\"><code class=\"language-rust edition2015\">\n<span class=\"boring\">#![allow(unused)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}\n</span></code></pre>"),
|
||||||
("<code class=\"language-rust\">fn main() {}</code>",
|
("<code class=\"language-rust\">fn main() {}</code>",
|
||||||
"<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}\n</code></pre>"),
|
"<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}\n</code></pre>"),
|
||||||
("<code class=\"language-rust edition2015\">fn main() {}</code>",
|
("<code class=\"language-rust edition2015\">fn main() {}</code>",
|
||||||
"<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}\n</code></pre>"),
|
"<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}\n</code></pre>"),
|
||||||
("<code class=\"language-rust edition2018\">fn main() {}</code>",
|
("<code class=\"language-rust edition2018\">fn main() {}</code>",
|
||||||
"<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}\n</code></pre>"),
|
"<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}\n</code></pre>"),
|
||||||
];
|
];
|
||||||
for (src, should_be) in &inputs {
|
for (src, should_be) in &inputs {
|
||||||
let got = add_playground_pre(
|
let got = add_playground_pre(
|
||||||
@ -1042,14 +1188,14 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn add_playground_edition2018() {
|
fn add_playground_edition2018() {
|
||||||
let inputs = [
|
let inputs = [
|
||||||
("<code class=\"language-rust\">x()</code>",
|
("<code class=\"language-rust\">x()</code>",
|
||||||
"<pre class=\"playground\"><code class=\"language-rust edition2018\">\n<span class=\"boring\">#![allow(unused)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}\n</span></code></pre>"),
|
"<pre class=\"playground\"><code class=\"language-rust edition2018\">\n<span class=\"boring\">#![allow(unused)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}\n</span></code></pre>"),
|
||||||
("<code class=\"language-rust\">fn main() {}</code>",
|
("<code class=\"language-rust\">fn main() {}</code>",
|
||||||
"<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}\n</code></pre>"),
|
"<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}\n</code></pre>"),
|
||||||
("<code class=\"language-rust edition2015\">fn main() {}</code>",
|
("<code class=\"language-rust edition2015\">fn main() {}</code>",
|
||||||
"<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}\n</code></pre>"),
|
"<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}\n</code></pre>"),
|
||||||
("<code class=\"language-rust edition2018\">fn main() {}</code>",
|
("<code class=\"language-rust edition2018\">fn main() {}</code>",
|
||||||
"<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}\n</code></pre>"),
|
"<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}\n</code></pre>"),
|
||||||
];
|
];
|
||||||
for (src, should_be) in &inputs {
|
for (src, should_be) in &inputs {
|
||||||
let got = add_playground_pre(
|
let got = add_playground_pre(
|
||||||
|
59
src/renderer/html_handlebars/helpers/language.rs
Normal file
59
src/renderer/html_handlebars/helpers/language.rs
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
use handlebars::{Context, Handlebars, Helper, Output, RenderContext, RenderError};
|
||||||
|
use std::path::Path;
|
||||||
|
use crate::config::LanguageConfig;
|
||||||
|
|
||||||
|
pub fn language_option(
|
||||||
|
h: &Helper<'_, '_>,
|
||||||
|
_r: &Handlebars<'_>,
|
||||||
|
ctx: &Context,
|
||||||
|
rc: &mut RenderContext<'_, '_>,
|
||||||
|
out: &mut dyn Output,
|
||||||
|
) -> Result<(), RenderError> {
|
||||||
|
trace!("language_option (handlebars helper)");
|
||||||
|
|
||||||
|
let param = h.param(0).and_then(|v| v.value().as_str()).ok_or_else(|| {
|
||||||
|
RenderError::new("Param 0 with String type is required for language_option helper.")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let languages = rc.evaluate(ctx, "@root/language_config").and_then(|c| {
|
||||||
|
serde_json::value::from_value::<LanguageConfig>(c.as_json().clone())
|
||||||
|
.map_err(|_| RenderError::new("Could not decode the JSON data"))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let current_path = rc
|
||||||
|
.evaluate(ctx, "@root/path")?
|
||||||
|
.as_json()
|
||||||
|
.as_str()
|
||||||
|
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
|
||||||
|
.replace("\"", "");
|
||||||
|
|
||||||
|
let rendered_path = Path::new(¤t_path)
|
||||||
|
.with_extension("html")
|
||||||
|
.to_str()
|
||||||
|
.ok_or_else(|| RenderError::new("Path could not be converted to str"))?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let path_to_root = rc
|
||||||
|
.evaluate(ctx, "@root/path_to_root")?
|
||||||
|
.as_json()
|
||||||
|
.as_str()
|
||||||
|
.ok_or_else(|| RenderError::new("Type error for `path_to_root`, string expected"))?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let language = languages.0.get(param).ok_or_else(|| {
|
||||||
|
RenderError::new(format!("Unknown language identifier '{}'", param))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut href = String::new();
|
||||||
|
href.push_str(&path_to_root);
|
||||||
|
href.push_str("../");
|
||||||
|
href.push_str(param);
|
||||||
|
href.push_str("/");
|
||||||
|
href.push_str(&rendered_path);
|
||||||
|
|
||||||
|
out.write(&format!("<a href=\"{}\"><button role=\"menuitem\" class=\"language\" id=\"light\">", href))?;
|
||||||
|
out.write(&language.name)?;
|
||||||
|
out.write("</button></a>")?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
pub mod navigation;
|
pub mod navigation;
|
||||||
pub mod theme;
|
pub mod theme;
|
||||||
pub mod toc;
|
pub mod toc;
|
||||||
|
pub mod language;
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
use crate::book::BookItem;
|
use crate::book::{BookItem, LoadedBook, Book};
|
||||||
use crate::errors::*;
|
use crate::errors::*;
|
||||||
use crate::renderer::{RenderContext, Renderer};
|
use crate::renderer::{RenderContext, Renderer};
|
||||||
use crate::utils;
|
use crate::utils;
|
||||||
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
/// A renderer to output the Markdown after the preprocessors have run. Mostly useful
|
/// A renderer to output the Markdown after the preprocessors have run. Mostly useful
|
||||||
@ -31,22 +32,36 @@ impl Renderer for MarkdownRenderer {
|
|||||||
.with_context(|| "Unable to remove stale Markdown output")?;
|
.with_context(|| "Unable to remove stale Markdown output")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
trace!("markdown render");
|
match book {
|
||||||
for item in book.iter() {
|
LoadedBook::Localized(books) => {
|
||||||
if let BookItem::Chapter(ref ch) = *item {
|
for (lang_ident, book) in books.0.iter() {
|
||||||
if !ch.is_draft_chapter() {
|
let localized_destination = destination.join(lang_ident);
|
||||||
utils::fs::write_file(
|
render_book(&localized_destination, book)?;
|
||||||
&ctx.destination,
|
|
||||||
&ch.path.as_ref().expect("Checked path exists before"),
|
|
||||||
ch.content.as_bytes(),
|
|
||||||
)?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
LoadedBook::Single(book) => render_book(destination, &book)?,
|
||||||
}
|
}
|
||||||
|
|
||||||
fs::create_dir_all(&destination)
|
|
||||||
.with_context(|| "Unexpected error when constructing destination path")?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_book(destination: &Path, book: &Book) -> Result<()> {
|
||||||
|
fs::create_dir_all(destination)
|
||||||
|
.with_context(|| "Unexpected error when constructing destination path")?;
|
||||||
|
|
||||||
|
trace!("markdown render");
|
||||||
|
for item in book.iter() {
|
||||||
|
if let BookItem::Chapter(ref ch) = *item {
|
||||||
|
if !ch.is_draft_chapter() {
|
||||||
|
utils::fs::write_file(
|
||||||
|
destination,
|
||||||
|
&ch.path.as_ref().expect("Checked path exists before"),
|
||||||
|
ch.content.as_bytes(),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
@ -24,7 +24,7 @@ use std::io::{self, ErrorKind, Read};
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::{Command, Stdio};
|
use std::process::{Command, Stdio};
|
||||||
|
|
||||||
use crate::book::Book;
|
use crate::book::LoadedBook;
|
||||||
use crate::build_opts::BuildOpts;
|
use crate::build_opts::BuildOpts;
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::errors::*;
|
use crate::errors::*;
|
||||||
@ -57,8 +57,10 @@ pub struct RenderContext {
|
|||||||
pub version: String,
|
pub version: String,
|
||||||
/// The book's root directory.
|
/// The book's root directory.
|
||||||
pub root: PathBuf,
|
pub root: PathBuf,
|
||||||
/// A loaded representation of the book itself.
|
/// A loaded representation of the book itself. This can either be a single
|
||||||
pub book: Book,
|
/// book or a set of localized books, to allow for the renderer to insert
|
||||||
|
/// its own logic for handling switching between the localizations.
|
||||||
|
pub book: LoadedBook,
|
||||||
/// The build options passed from the frontend.
|
/// The build options passed from the frontend.
|
||||||
pub build_opts: BuildOpts,
|
pub build_opts: BuildOpts,
|
||||||
/// The loaded configuration file.
|
/// The loaded configuration file.
|
||||||
@ -77,7 +79,7 @@ impl RenderContext {
|
|||||||
/// Create a new `RenderContext`.
|
/// Create a new `RenderContext`.
|
||||||
pub fn new<P, Q>(
|
pub fn new<P, Q>(
|
||||||
root: P,
|
root: P,
|
||||||
book: Book,
|
book: LoadedBook,
|
||||||
build_opts: BuildOpts,
|
build_opts: BuildOpts,
|
||||||
config: Config,
|
config: Config,
|
||||||
destination: Q,
|
destination: Q,
|
||||||
|
@ -424,6 +424,87 @@ function playground_text(playground) {
|
|||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
(function languages() {
|
||||||
|
var languageToggleButton = document.getElementById('language-toggle');
|
||||||
|
var languagePopup = document.getElementById('language-list');
|
||||||
|
|
||||||
|
function showLanguages() {
|
||||||
|
languagePopup.style.display = 'block';
|
||||||
|
languageToggleButton.setAttribute('aria-expanded', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideLanguages() {
|
||||||
|
languagePopup.style.display = 'none';
|
||||||
|
languageToggleButton.setAttribute('aria-expanded', false);
|
||||||
|
languageToggleButton.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function set_language(language) {
|
||||||
|
console.log("Set language " + language)
|
||||||
|
}
|
||||||
|
|
||||||
|
languageToggleButton.addEventListener('click', function () {
|
||||||
|
if (languagePopup.style.display === 'block') {
|
||||||
|
hideLanguages();
|
||||||
|
} else {
|
||||||
|
showLanguages();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
languagePopup.addEventListener('click', function (e) {
|
||||||
|
var language = e.target.id || e.target.parentElement.id;
|
||||||
|
set_language(language);
|
||||||
|
});
|
||||||
|
|
||||||
|
languagePopup.addEventListener('focusout', function(e) {
|
||||||
|
// e.relatedTarget is null in Safari and Firefox on macOS (see workaround below)
|
||||||
|
if (!!e.relatedTarget && !languageToggleButton.contains(e.relatedTarget) && !languagePopup.contains(e.relatedTarget)) {
|
||||||
|
hideLanguages();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should not be needed, but it works around an issue on macOS & iOS: https://github.com/rust-lang/mdBook/issues/628
|
||||||
|
document.addEventListener('click', function(e) {
|
||||||
|
if (languagePopup.style.display === 'block' && !languageToggleButton.contains(e.target) && !languagePopup.contains(e.target)) {
|
||||||
|
hideLanguages();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('keydown', function (e) {
|
||||||
|
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { return; }
|
||||||
|
if (!languagePopup.contains(e.target)) { return; }
|
||||||
|
|
||||||
|
switch (e.key) {
|
||||||
|
case 'Escape':
|
||||||
|
e.preventDefault();
|
||||||
|
hideLanguages();
|
||||||
|
break;
|
||||||
|
case 'ArrowUp':
|
||||||
|
e.preventDefault();
|
||||||
|
var li = document.activeElement.parentElement;
|
||||||
|
if (li && li.previousElementSibling) {
|
||||||
|
li.previousElementSibling.querySelector('button').focus();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'ArrowDown':
|
||||||
|
e.preventDefault();
|
||||||
|
var li = document.activeElement.parentElement;
|
||||||
|
if (li && li.nextElementSibling) {
|
||||||
|
li.nextElementSibling.querySelector('button').focus();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'Home':
|
||||||
|
e.preventDefault();
|
||||||
|
languagePopup.querySelector('li:first-child button').focus();
|
||||||
|
break;
|
||||||
|
case 'End':
|
||||||
|
e.preventDefault();
|
||||||
|
languagePopup.querySelector('li:last-child button').focus();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
(function sidebar() {
|
(function sidebar() {
|
||||||
var html = document.querySelector("html");
|
var html = document.querySelector("html");
|
||||||
var sidebar = document.getElementById("sidebar");
|
var sidebar = document.getElementById("sidebar");
|
||||||
|
@ -493,3 +493,50 @@ ul#searchresults span.teaser em {
|
|||||||
border-top-left-radius: inherit;
|
border-top-left-radius: inherit;
|
||||||
border-top-right-radius: inherit;
|
border-top-right-radius: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Language Menu Popup */
|
||||||
|
|
||||||
|
.language-popup {
|
||||||
|
position: absolute;
|
||||||
|
left: 150px;
|
||||||
|
top: var(--menu-bar-height);
|
||||||
|
z-index: 1000;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.7em;
|
||||||
|
color: var(--fg);
|
||||||
|
background: var(--theme-popup-bg);
|
||||||
|
border: 1px solid var(--theme-popup-border);
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.language-popup .default {
|
||||||
|
color: var(--icons);
|
||||||
|
}
|
||||||
|
.language-popup .language {
|
||||||
|
width: 100%;
|
||||||
|
border: 0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 2px 10px;
|
||||||
|
line-height: 25px;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
color: inherit;
|
||||||
|
background: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
.language-popup .language:hover {
|
||||||
|
background-color: var(--theme-hover);
|
||||||
|
}
|
||||||
|
.language-popup .language:hover:first-child,
|
||||||
|
.language-popup .language:hover:last-child {
|
||||||
|
border-top-left-radius: inherit;
|
||||||
|
border-top-right-radius: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-popup a {
|
||||||
|
color: var(--fg);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
@ -133,6 +133,16 @@
|
|||||||
<i class="fa fa-search"></i>
|
<i class="fa fa-search"></i>
|
||||||
</button>
|
</button>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
{{#if languages_enabled}}
|
||||||
|
<button id="language-toggle" class="icon-button" type="button" title="Select language" aria-label="Select language" aria-haspopup="true" aria-expanded="false" aria-controls="language-list">
|
||||||
|
<i class="fa fa-globe"></i>
|
||||||
|
</button>
|
||||||
|
<ul id="language-list" class="language-popup" aria-label="Languages" role="menu">
|
||||||
|
{{#each languages}}
|
||||||
|
<li role="none">{{ language_option this }}</li>
|
||||||
|
{{/each}}
|
||||||
|
</ul>
|
||||||
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1 class="menu-title">{{ book_title }}</h1>
|
<h1 class="menu-title">{{ book_title }}</h1>
|
||||||
|
Loading…
Reference in New Issue
Block a user