From 24e6d6b6055fcf31c4ba65746979e304e2408b8b Mon Sep 17 00:00:00 2001 From: Ruin0x11 Date: Thu, 27 Aug 2020 16:26:07 -0700 Subject: [PATCH] Change book source root depending on language --- src/book/mod.rs | 29 +++++++++++++++---- src/build_opts.rs | 10 +++++++ src/cmd/build.rs | 5 ++-- src/cmd/test.rs | 5 ++-- src/config.rs | 30 ++++++++++++++++++-- src/lib.rs | 1 + src/main.rs | 9 ++++++ src/preprocess/cmd.rs | 2 ++ src/preprocess/index.rs | 2 +- src/preprocess/links.rs | 2 +- src/preprocess/mod.rs | 12 +++++++- src/renderer/html_handlebars/hbs_renderer.rs | 2 +- src/renderer/mod.rs | 9 ++++-- tests/build_process.rs | 7 +++-- tests/rendered_output.rs | 10 +++++-- 15 files changed, 111 insertions(+), 24 deletions(-) create mode 100644 src/build_opts.rs diff --git a/src/book/mod.rs b/src/book/mod.rs index 7f1159b4fb..636290523c 100644 --- a/src/book/mod.rs +++ b/src/book/mod.rs @@ -29,6 +29,7 @@ use crate::renderer::{CmdRenderer, HtmlHandlebars, MarkdownRenderer, RenderConte use crate::utils; use crate::config::{Config, RustEdition}; +use crate::build_opts::BuildOpts; /// The object used to manage and build a book. pub struct MDBook { @@ -38,6 +39,10 @@ pub struct MDBook { pub config: Config, /// A representation of the book's contents in memory. pub book: Book, + /// Build options passed from frontend. + pub build_opts: BuildOpts, + + /// List of renderers to be run on the book. renderers: Vec>, /// List of pre-processors to be run on the book. @@ -47,6 +52,12 @@ pub struct MDBook { impl MDBook { /// Load a book from its root directory on disk. pub fn load>(book_root: P) -> Result { + MDBook::load_with_build_opts(book_root, BuildOpts::default()) + } + + /// Load a book from its root directory on disk, passing in options from the + /// frontend. + pub fn load_with_build_opts>(book_root: P, build_opts: BuildOpts) -> Result { let book_root = book_root.into(); let config_location = book_root.join("book.toml"); @@ -75,11 +86,11 @@ impl MDBook { } } - MDBook::load_with_config(book_root, config) + MDBook::load_with_config(book_root, config, build_opts) } - /// Load a book from its root directory using a custom `Config`. - pub fn load_with_config>(book_root: P, config: Config) -> Result { + /// Load a book from its root directory using a custom config. + pub fn load_with_config>(book_root: P, config: Config, build_opts: BuildOpts) -> Result { let root = book_root.into(); let src_dir = root.join(&config.book.src); @@ -92,6 +103,7 @@ impl MDBook { root, config, book, + build_opts, renderers, preprocessors, }) @@ -102,6 +114,7 @@ impl MDBook { book_root: P, config: Config, summary: Summary, + build_opts: BuildOpts, ) -> Result { let root = book_root.into(); @@ -115,6 +128,7 @@ impl MDBook { root, config, book, + build_opts, renderers, preprocessors, }) @@ -185,6 +199,7 @@ impl MDBook { let mut preprocessed_book = self.book.clone(); let preprocess_ctx = PreprocessorContext::new( self.root.clone(), + self.build_opts.clone(), self.config.clone(), renderer.name().to_string(), ); @@ -201,7 +216,8 @@ impl MDBook { let mut render_context = RenderContext::new( self.root.clone(), - preprocessed_book, + preprocessed_book.clone(), + self.build_opts.clone(), self.config.clone(), build_dir, ); @@ -241,7 +257,7 @@ impl MDBook { // FIXME: Is "test" the proper renderer name to use here? let preprocess_context = - PreprocessorContext::new(self.root.clone(), self.config.clone(), "test".to_string()); + PreprocessorContext::new(self.root.clone(), self.build_opts.clone(), self.config.clone(), "test".to_string()); let book = LinkPreprocessor::new().run(&preprocess_context, self.book.clone())?; // Index Preprocessor is disabled so that chapter paths continue to point to the @@ -336,7 +352,8 @@ impl MDBook { /// Get the directory containing this book's source files. pub fn source_dir(&self) -> PathBuf { - self.root.join(&self.config.book.src) + let src = self.config.get_localized_src_path(self.build_opts.language_ident.as_ref()).unwrap(); + self.root.join(src) } /// Get the directory containing the theme resources for the book. diff --git a/src/build_opts.rs b/src/build_opts.rs new file mode 100644 index 0000000000..ac7513f49b --- /dev/null +++ b/src/build_opts.rs @@ -0,0 +1,10 @@ +//! Build options. + +/// Build options passed from the frontend to control how the book is built. +/// Separate from `Config`, which is global to all book languages. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] +#[serde(default, rename_all = "kebab-case")] +pub struct BuildOpts { + /// Language of the book to render. + pub language_ident: Option, +} diff --git a/src/cmd/build.rs b/src/cmd/build.rs index d1c663026d..edc3be44ac 100644 --- a/src/cmd/build.rs +++ b/src/cmd/build.rs @@ -1,4 +1,4 @@ -use crate::{get_book_dir, open}; +use crate::{get_book_dir, get_build_opts, open}; use clap::{App, ArgMatches, SubCommand}; use mdbook::errors::Result; use mdbook::MDBook; @@ -22,7 +22,8 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> { // Build command implementation pub fn execute(args: &ArgMatches) -> Result<()> { let book_dir = get_book_dir(args); - let mut book = MDBook::load(&book_dir)?; + let opts = get_build_opts(args); + let mut book = MDBook::load_with_build_opts(&book_dir, opts)?; if let Some(dest_dir) = args.value_of("dest-dir") { book.config.build.build_dir = dest_dir.into(); diff --git a/src/cmd/test.rs b/src/cmd/test.rs index f6d97aa656..c52ebfa0cc 100644 --- a/src/cmd/test.rs +++ b/src/cmd/test.rs @@ -1,4 +1,4 @@ -use crate::get_book_dir; +use crate::{get_book_dir, get_build_opts}; use clap::{App, Arg, ArgMatches, SubCommand}; use mdbook::errors::Result; use mdbook::MDBook; @@ -34,7 +34,8 @@ pub fn execute(args: &ArgMatches) -> Result<()> { .map(std::iter::Iterator::collect) .unwrap_or_default(); let book_dir = get_book_dir(args); - let mut book = MDBook::load(&book_dir)?; + let build_opts = get_build_opts(args); + let mut book = MDBook::load_with_build_opts(&book_dir, build_opts)?; if let Some(dest_dir) = args.value_of("dest-dir") { book.config.build.build_dir = dest_dir.into(); diff --git a/src/config.rs b/src/config.rs index 950d1afae0..b2c88eb59a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -252,6 +252,31 @@ impl Config { self.get(&key).and_then(Value::as_table) } + /// Get the source directory of a localized book corresponding to language ident `index`. + pub fn get_localized_src_path>(&self, index: Option) -> Option { + match self.language.default_language() { + Some(default) => match index { + // Make sure that the language we passed was actually + // declared in the config, and return `None` if not. + Some(lang_ident) => self.language.0.get(lang_ident.as_ref()).map(|_| { + let mut buf = PathBuf::new(); + buf.push(self.book.src.clone()); + buf.push(lang_ident.as_ref()); + buf + }), + // Use the default specified in book.toml. + None => Some(PathBuf::from(default)) + } + + // No default language was configured in book.toml. Preserve + // backwards compatibility by just returning `src`. + None => match index { + Some(_) => None, + None => Some(self.book.src.clone()), + } + } + } + fn from_legacy(mut table: Value) -> Config { let mut cfg = Config::default(); @@ -354,7 +379,6 @@ impl<'de> Deserialize<'de> for Config { .count(); if default_languages != 1 { - use serde::de::Error; return Err(D::Error::custom( "If languages are specified, exactly one must be set as 'default'" )); @@ -727,10 +751,10 @@ pub struct Language { impl LanguageConfig { /// Returns the default language specified in the config. - pub fn default_language(&self) -> Option<&Language> { + pub fn default_language(&self) -> Option<&String> { self.0.iter() .find(|(_, lang)| lang.default) - .map(|(_, lang)| lang) + .map(|(lang_ident, _)| lang_ident) } } diff --git a/src/lib.rs b/src/lib.rs index 82d9b6f7d8..732389f563 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -98,6 +98,7 @@ extern crate serde_json; extern crate pretty_assertions; pub mod book; +pub mod build_opts; pub mod config; pub mod preprocess; pub mod renderer; diff --git a/src/main.rs b/src/main.rs index 4843c95d09..f0c9bd3c6f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,6 +9,7 @@ use clap::{App, AppSettings, Arg, ArgMatches, Shell, SubCommand}; use env_logger::Builder; use log::LevelFilter; use mdbook::utils; +use mdbook::build_opts::BuildOpts; use std::env; use std::ffi::OsStr; use std::io::Write; @@ -131,6 +132,14 @@ fn get_book_dir(args: &ArgMatches) -> PathBuf { } } +fn get_build_opts(args: &ArgMatches) -> BuildOpts { + let language = args.value_of("language"); + + BuildOpts { + language_ident: language.map(String::from) + } +} + fn open>(path: P) { info!("Opening web browser"); if let Err(e) = open::that(path) { diff --git a/src/preprocess/cmd.rs b/src/preprocess/cmd.rs index c47fd5d22e..dbcda528fb 100644 --- a/src/preprocess/cmd.rs +++ b/src/preprocess/cmd.rs @@ -179,6 +179,7 @@ impl Preprocessor for CmdPreprocessor { mod tests { use super::*; use crate::MDBook; + use crate::build_opts::BuildOpts; use std::path::Path; fn guide() -> MDBook { @@ -192,6 +193,7 @@ mod tests { let md = guide(); let ctx = PreprocessorContext::new( md.root.clone(), + BuildOpts::default(), md.config.clone(), "some-renderer".to_string(), ); diff --git a/src/preprocess/index.rs b/src/preprocess/index.rs index fd60ad4dae..37cd665876 100644 --- a/src/preprocess/index.rs +++ b/src/preprocess/index.rs @@ -26,7 +26,7 @@ impl Preprocessor for IndexPreprocessor { } fn run(&self, ctx: &PreprocessorContext, mut book: Book) -> Result { - let source_dir = ctx.root.join(&ctx.config.book.src); + let source_dir = ctx.source_dir(); book.for_each_mut(|section: &mut BookItem| { if let BookItem::Chapter(ref mut ch) = *section { if let Some(ref mut path) = ch.path { diff --git a/src/preprocess/links.rs b/src/preprocess/links.rs index edd97ba924..8d96a011ed 100644 --- a/src/preprocess/links.rs +++ b/src/preprocess/links.rs @@ -42,7 +42,7 @@ impl Preprocessor for LinkPreprocessor { } fn run(&self, ctx: &PreprocessorContext, mut book: Book) -> Result { - let src_dir = ctx.root.join(&ctx.config.book.src); + let src_dir = ctx.source_dir(); book.for_each_mut(|section: &mut BookItem| { if let BookItem::Chapter(ref mut ch) = *section { diff --git a/src/preprocess/mod.rs b/src/preprocess/mod.rs index ee660636a1..e78397b094 100644 --- a/src/preprocess/mod.rs +++ b/src/preprocess/mod.rs @@ -10,6 +10,7 @@ mod links; use crate::book::Book; use crate::config::Config; +use crate::build_opts::BuildOpts; use crate::errors::*; use std::cell::RefCell; @@ -22,6 +23,8 @@ use std::path::PathBuf; pub struct PreprocessorContext { /// The location of the book directory on disk. pub root: PathBuf, + /// The build options passed from the frontend. + pub build_opts: BuildOpts, /// The book configuration (`book.toml`). pub config: Config, /// The `Renderer` this preprocessor is being used with. @@ -36,9 +39,10 @@ pub struct PreprocessorContext { impl PreprocessorContext { /// Create a new `PreprocessorContext`. - pub(crate) fn new(root: PathBuf, config: Config, renderer: String) -> Self { + pub(crate) fn new(root: PathBuf, build_opts: BuildOpts, config: Config, renderer: String) -> Self { PreprocessorContext { root, + build_opts, config, renderer, mdbook_version: crate::MDBOOK_VERSION.to_string(), @@ -46,6 +50,12 @@ impl PreprocessorContext { __non_exhaustive: (), } } + + /// Get the directory containing this book's source files. + pub fn source_dir(&self) -> PathBuf { + let src = self.config.get_localized_src_path(self.build_opts.language_ident.as_ref()).unwrap(); + self.root.join(src) + } } /// An operation which is run immediately after loading a book into memory and diff --git a/src/renderer/html_handlebars/hbs_renderer.rs b/src/renderer/html_handlebars/hbs_renderer.rs index e8da5b4a86..5263b25c44 100644 --- a/src/renderer/html_handlebars/hbs_renderer.rs +++ b/src/renderer/html_handlebars/hbs_renderer.rs @@ -460,7 +460,7 @@ impl Renderer for HtmlHandlebars { fn render(&self, ctx: &RenderContext) -> Result<()> { let book_config = &ctx.config.book; let html_config = ctx.config.html_config().unwrap_or_default(); - let src_dir = ctx.root.join(&ctx.config.book.src); + let src_dir = ctx.source_dir(); let destination = &ctx.destination; let book = &ctx.book; let build_dir = ctx.root.join(&ctx.config.build.build_dir); diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs index 9d2952c1c6..78ef5faaad 100644 --- a/src/renderer/mod.rs +++ b/src/renderer/mod.rs @@ -26,6 +26,7 @@ use std::process::{Command, Stdio}; use crate::book::Book; use crate::config::Config; +use crate::build_opts::BuildOpts; use crate::errors::*; use toml::Value; @@ -58,6 +59,8 @@ pub struct RenderContext { pub root: PathBuf, /// A loaded representation of the book itself. pub book: Book, + /// The build options passed from the frontend. + pub build_opts: BuildOpts, /// The loaded configuration file. pub config: Config, /// Where the renderer *must* put any build artefacts generated. To allow @@ -72,13 +75,14 @@ pub struct RenderContext { impl RenderContext { /// Create a new `RenderContext`. - pub fn new(root: P, book: Book, config: Config, destination: Q) -> RenderContext + pub fn new(root: P, book: Book, build_opts: BuildOpts, config: Config, destination: Q) -> RenderContext where P: Into, Q: Into, { RenderContext { book, + build_opts, config, version: crate::MDBOOK_VERSION.to_string(), root: root.into(), @@ -90,7 +94,8 @@ impl RenderContext { /// Get the source directory's (absolute) path on disk. pub fn source_dir(&self) -> PathBuf { - self.root.join(&self.config.book.src) + let src = self.config.get_localized_src_path(self.build_opts.language_ident.as_ref()).unwrap(); + self.root.join(src) } /// Load a `RenderContext` from its JSON representation. diff --git a/tests/build_process.rs b/tests/build_process.rs index 10d0b4a9a8..88018d8609 100644 --- a/tests/build_process.rs +++ b/tests/build_process.rs @@ -2,6 +2,7 @@ mod dummy_book; use crate::dummy_book::DummyBook; use mdbook::book::Book; +use mdbook::build_opts::BuildOpts; use mdbook::config::Config; use mdbook::errors::*; use mdbook::preprocess::{Preprocessor, PreprocessorContext}; @@ -48,8 +49,9 @@ fn mdbook_runs_preprocessors() { let temp = DummyBook::new().build().unwrap(); let cfg = Config::default(); + let build_opts = BuildOpts::default(); - let mut book = MDBook::load_with_config(temp.path(), cfg).unwrap(); + let mut book = MDBook::load_with_config(temp.path(), cfg, build_opts).unwrap(); book.with_preprocessor(Spy(Arc::clone(&spy))); book.build().unwrap(); @@ -68,8 +70,9 @@ fn mdbook_runs_renderers() { let temp = DummyBook::new().build().unwrap(); let cfg = Config::default(); + let build_opts = BuildOpts::default(); - let mut book = MDBook::load_with_config(temp.path(), cfg).unwrap(); + let mut book = MDBook::load_with_config(temp.path(), cfg, build_opts).unwrap(); book.with_renderer(Spy(Arc::clone(&spy))); book.build().unwrap(); diff --git a/tests/rendered_output.rs b/tests/rendered_output.rs index 5ec6e64b1d..ebae94e7be 100644 --- a/tests/rendered_output.rs +++ b/tests/rendered_output.rs @@ -7,6 +7,7 @@ use crate::dummy_book::{assert_contains_strings, assert_doesnt_contain_strings, use anyhow::Context; use mdbook::config::Config; +use mdbook::build_opts::BuildOpts; use mdbook::errors::*; use mdbook::utils::fs::write_file; use mdbook::MDBook; @@ -326,8 +327,9 @@ fn failure_on_missing_file() { let mut cfg = Config::default(); cfg.build.create_missing = false; + let build_opts = BuildOpts::default(); - let got = MDBook::load_with_config(temp.path(), cfg); + let got = MDBook::load_with_config(temp.path(), cfg, build_opts); assert!(got.is_err()); } @@ -339,9 +341,10 @@ fn create_missing_file_with_config() { let mut cfg = Config::default(); cfg.build.create_missing = true; + let build_opts = BuildOpts::default(); assert!(!temp.path().join("src").join("intro.md").exists()); - let _md = MDBook::load_with_config(temp.path(), cfg).unwrap(); + let _md = MDBook::load_with_config(temp.path(), cfg, build_opts).unwrap(); assert!(temp.path().join("src").join("intro.md").exists()); } @@ -429,7 +432,8 @@ fn by_default_mdbook_use_index_preprocessor_to_convert_readme_to_index() { let mut cfg = Config::default(); cfg.set("book.src", "src2") .expect("Couldn't set config.book.src to \"src2\"."); - let md = MDBook::load_with_config(temp.path(), cfg).unwrap(); + let build_opts = BuildOpts::default(); + let md = MDBook::load_with_config(temp.path(), cfg, build_opts).unwrap(); md.build().unwrap(); let first_index = temp.path().join("book").join("first").join("index.html");