Change book source root depending on language

This commit is contained in:
Ruin0x11 2020-08-27 16:26:07 -07:00
parent e4b443cd4a
commit 24e6d6b605
15 changed files with 111 additions and 24 deletions

View File

@ -29,6 +29,7 @@ use crate::renderer::{CmdRenderer, HtmlHandlebars, MarkdownRenderer, RenderConte
use crate::utils; use crate::utils;
use crate::config::{Config, RustEdition}; use crate::config::{Config, RustEdition};
use crate::build_opts::BuildOpts;
/// The object used to manage and build a book. /// The object used to manage and build a book.
pub struct MDBook { pub struct MDBook {
@ -38,6 +39,10 @@ pub struct MDBook {
pub config: Config, pub config: Config,
/// A representation of the book's contents in memory. /// A representation of the book's contents in memory.
pub book: Book, pub book: Book,
/// Build options passed from frontend.
pub build_opts: BuildOpts,
/// List of renderers to be run on the book.
renderers: Vec<Box<dyn Renderer>>, renderers: Vec<Box<dyn Renderer>>,
/// List of pre-processors to be run on the book. /// List of pre-processors to be run on the book.
@ -47,6 +52,12 @@ pub struct MDBook {
impl MDBook { impl MDBook {
/// Load a book from its root directory on disk. /// Load a book from its root directory on disk.
pub fn load<P: Into<PathBuf>>(book_root: P) -> Result<MDBook> { pub fn load<P: Into<PathBuf>>(book_root: P) -> Result<MDBook> {
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<P: Into<PathBuf>>(book_root: P, build_opts: BuildOpts) -> Result<MDBook> {
let book_root = book_root.into(); let book_root = book_root.into();
let config_location = book_root.join("book.toml"); 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`. /// Load a book from its root directory using a custom config.
pub fn load_with_config<P: Into<PathBuf>>(book_root: P, config: Config) -> Result<MDBook> { pub fn load_with_config<P: Into<PathBuf>>(book_root: P, config: Config, build_opts: BuildOpts) -> Result<MDBook> {
let root = book_root.into(); let root = book_root.into();
let src_dir = root.join(&config.book.src); let src_dir = root.join(&config.book.src);
@ -92,6 +103,7 @@ impl MDBook {
root, root,
config, config,
book, book,
build_opts,
renderers, renderers,
preprocessors, preprocessors,
}) })
@ -102,6 +114,7 @@ impl MDBook {
book_root: P, book_root: P,
config: Config, config: Config,
summary: Summary, summary: Summary,
build_opts: BuildOpts,
) -> Result<MDBook> { ) -> Result<MDBook> {
let root = book_root.into(); let root = book_root.into();
@ -115,6 +128,7 @@ impl MDBook {
root, root,
config, config,
book, book,
build_opts,
renderers, renderers,
preprocessors, preprocessors,
}) })
@ -185,6 +199,7 @@ impl MDBook {
let mut preprocessed_book = self.book.clone(); 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.config.clone(), self.config.clone(),
renderer.name().to_string(), renderer.name().to_string(),
); );
@ -201,7 +216,8 @@ impl MDBook {
let mut render_context = RenderContext::new( let mut render_context = RenderContext::new(
self.root.clone(), self.root.clone(),
preprocessed_book, preprocessed_book.clone(),
self.build_opts.clone(),
self.config.clone(), self.config.clone(),
build_dir, build_dir,
); );
@ -241,7 +257,7 @@ impl MDBook {
// FIXME: Is "test" the proper renderer name to use here? // FIXME: Is "test" the proper renderer name to use here?
let preprocess_context = 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())?; 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
@ -336,7 +352,8 @@ impl MDBook {
/// Get the directory containing this book's source files. /// Get the directory containing this book's source files.
pub fn source_dir(&self) -> PathBuf { 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. /// Get the directory containing the theme resources for the book.

10
src/build_opts.rs Normal file
View File

@ -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<String>,
}

View File

@ -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 clap::{App, ArgMatches, SubCommand};
use mdbook::errors::Result; use mdbook::errors::Result;
use mdbook::MDBook; use mdbook::MDBook;
@ -22,7 +22,8 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
// Build command implementation // Build command implementation
pub fn execute(args: &ArgMatches) -> Result<()> { pub fn execute(args: &ArgMatches) -> Result<()> {
let book_dir = get_book_dir(args); 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") { if let Some(dest_dir) = args.value_of("dest-dir") {
book.config.build.build_dir = dest_dir.into(); book.config.build.build_dir = dest_dir.into();

View File

@ -1,4 +1,4 @@
use crate::get_book_dir; use crate::{get_book_dir, get_build_opts};
use clap::{App, Arg, ArgMatches, SubCommand}; use clap::{App, Arg, ArgMatches, SubCommand};
use mdbook::errors::Result; use mdbook::errors::Result;
use mdbook::MDBook; use mdbook::MDBook;
@ -34,7 +34,8 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
.map(std::iter::Iterator::collect) .map(std::iter::Iterator::collect)
.unwrap_or_default(); .unwrap_or_default();
let book_dir = get_book_dir(args); 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") { if let Some(dest_dir) = args.value_of("dest-dir") {
book.config.build.build_dir = dest_dir.into(); book.config.build.build_dir = dest_dir.into();

View File

@ -252,6 +252,31 @@ impl Config {
self.get(&key).and_then(Value::as_table) 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<I: AsRef<str>>(&self, index: Option<I>) -> Option<PathBuf> {
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 { fn from_legacy(mut table: Value) -> Config {
let mut cfg = Config::default(); let mut cfg = Config::default();
@ -354,7 +379,6 @@ impl<'de> Deserialize<'de> for Config {
.count(); .count();
if default_languages != 1 { if default_languages != 1 {
use serde::de::Error;
return Err(D::Error::custom( return Err(D::Error::custom(
"If languages are specified, exactly one must be set as 'default'" "If languages are specified, exactly one must be set as 'default'"
)); ));
@ -727,10 +751,10 @@ pub struct Language {
impl LanguageConfig { impl LanguageConfig {
/// Returns the default language specified in the config. /// 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() self.0.iter()
.find(|(_, lang)| lang.default) .find(|(_, lang)| lang.default)
.map(|(_, lang)| lang) .map(|(lang_ident, _)| lang_ident)
} }
} }

View File

@ -98,6 +98,7 @@ extern crate serde_json;
extern crate pretty_assertions; extern crate pretty_assertions;
pub mod book; pub mod book;
pub mod build_opts;
pub mod config; pub mod config;
pub mod preprocess; pub mod preprocess;
pub mod renderer; pub mod renderer;

View File

@ -9,6 +9,7 @@ use clap::{App, AppSettings, Arg, ArgMatches, Shell, SubCommand};
use env_logger::Builder; use env_logger::Builder;
use log::LevelFilter; use log::LevelFilter;
use mdbook::utils; use mdbook::utils;
use mdbook::build_opts::BuildOpts;
use std::env; use std::env;
use std::ffi::OsStr; use std::ffi::OsStr;
use std::io::Write; 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<P: AsRef<OsStr>>(path: P) { fn open<P: AsRef<OsStr>>(path: P) {
info!("Opening web browser"); info!("Opening web browser");
if let Err(e) = open::that(path) { if let Err(e) = open::that(path) {

View File

@ -179,6 +179,7 @@ impl Preprocessor for CmdPreprocessor {
mod tests { mod tests {
use super::*; use super::*;
use crate::MDBook; use crate::MDBook;
use crate::build_opts::BuildOpts;
use std::path::Path; use std::path::Path;
fn guide() -> MDBook { fn guide() -> MDBook {
@ -192,6 +193,7 @@ mod tests {
let md = guide(); let md = guide();
let ctx = PreprocessorContext::new( let ctx = PreprocessorContext::new(
md.root.clone(), md.root.clone(),
BuildOpts::default(),
md.config.clone(), md.config.clone(),
"some-renderer".to_string(), "some-renderer".to_string(),
); );

View File

@ -26,7 +26,7 @@ impl Preprocessor for IndexPreprocessor {
} }
fn run(&self, ctx: &PreprocessorContext, mut book: Book) -> Result<Book> { fn run(&self, ctx: &PreprocessorContext, mut book: Book) -> Result<Book> {
let source_dir = ctx.root.join(&ctx.config.book.src); let source_dir = ctx.source_dir();
book.for_each_mut(|section: &mut BookItem| { book.for_each_mut(|section: &mut BookItem| {
if let BookItem::Chapter(ref mut ch) = *section { if let BookItem::Chapter(ref mut ch) = *section {
if let Some(ref mut path) = ch.path { if let Some(ref mut path) = ch.path {

View File

@ -42,7 +42,7 @@ impl Preprocessor for LinkPreprocessor {
} }
fn run(&self, ctx: &PreprocessorContext, mut book: Book) -> Result<Book> { fn run(&self, ctx: &PreprocessorContext, mut book: Book) -> Result<Book> {
let src_dir = ctx.root.join(&ctx.config.book.src); let src_dir = ctx.source_dir();
book.for_each_mut(|section: &mut BookItem| { book.for_each_mut(|section: &mut BookItem| {
if let BookItem::Chapter(ref mut ch) = *section { if let BookItem::Chapter(ref mut ch) = *section {

View File

@ -10,6 +10,7 @@ mod links;
use crate::book::Book; use crate::book::Book;
use crate::config::Config; use crate::config::Config;
use crate::build_opts::BuildOpts;
use crate::errors::*; use crate::errors::*;
use std::cell::RefCell; use std::cell::RefCell;
@ -22,6 +23,8 @@ use std::path::PathBuf;
pub struct PreprocessorContext { pub struct PreprocessorContext {
/// The location of the book directory on disk. /// The location of the book directory on disk.
pub root: PathBuf, pub root: PathBuf,
/// The build options passed from the frontend.
pub build_opts: BuildOpts,
/// The book configuration (`book.toml`). /// The book configuration (`book.toml`).
pub config: Config, pub config: Config,
/// The `Renderer` this preprocessor is being used with. /// The `Renderer` this preprocessor is being used with.
@ -36,9 +39,10 @@ pub struct PreprocessorContext {
impl PreprocessorContext { impl PreprocessorContext {
/// Create a new `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 { PreprocessorContext {
root, root,
build_opts,
config, config,
renderer, renderer,
mdbook_version: crate::MDBOOK_VERSION.to_string(), mdbook_version: crate::MDBOOK_VERSION.to_string(),
@ -46,6 +50,12 @@ impl PreprocessorContext {
__non_exhaustive: (), __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 /// An operation which is run immediately after loading a book into memory and

View File

@ -460,7 +460,7 @@ impl Renderer for HtmlHandlebars {
fn render(&self, ctx: &RenderContext) -> Result<()> { fn render(&self, ctx: &RenderContext) -> Result<()> {
let book_config = &ctx.config.book; let book_config = &ctx.config.book;
let html_config = ctx.config.html_config().unwrap_or_default(); 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 destination = &ctx.destination;
let book = &ctx.book; let book = &ctx.book;
let build_dir = ctx.root.join(&ctx.config.build.build_dir); let build_dir = ctx.root.join(&ctx.config.build.build_dir);

View File

@ -26,6 +26,7 @@ use std::process::{Command, Stdio};
use crate::book::Book; use crate::book::Book;
use crate::config::Config; use crate::config::Config;
use crate::build_opts::BuildOpts;
use crate::errors::*; use crate::errors::*;
use toml::Value; use toml::Value;
@ -58,6 +59,8 @@ pub struct RenderContext {
pub root: PathBuf, pub root: PathBuf,
/// A loaded representation of the book itself. /// A loaded representation of the book itself.
pub book: Book, pub book: Book,
/// The build options passed from the frontend.
pub build_opts: BuildOpts,
/// The loaded configuration file. /// The loaded configuration file.
pub config: Config, pub config: Config,
/// Where the renderer *must* put any build artefacts generated. To allow /// Where the renderer *must* put any build artefacts generated. To allow
@ -72,13 +75,14 @@ pub struct RenderContext {
impl RenderContext { impl RenderContext {
/// Create a new `RenderContext`. /// Create a new `RenderContext`.
pub fn new<P, Q>(root: P, book: Book, config: Config, destination: Q) -> RenderContext pub fn new<P, Q>(root: P, book: Book, build_opts: BuildOpts, config: Config, destination: Q) -> RenderContext
where where
P: Into<PathBuf>, P: Into<PathBuf>,
Q: Into<PathBuf>, Q: Into<PathBuf>,
{ {
RenderContext { RenderContext {
book, book,
build_opts,
config, config,
version: crate::MDBOOK_VERSION.to_string(), version: crate::MDBOOK_VERSION.to_string(),
root: root.into(), root: root.into(),
@ -90,7 +94,8 @@ impl RenderContext {
/// Get the source directory's (absolute) path on disk. /// Get the source directory's (absolute) path on disk.
pub fn source_dir(&self) -> PathBuf { 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. /// Load a `RenderContext` from its JSON representation.

View File

@ -2,6 +2,7 @@ mod dummy_book;
use crate::dummy_book::DummyBook; use crate::dummy_book::DummyBook;
use mdbook::book::Book; use mdbook::book::Book;
use mdbook::build_opts::BuildOpts;
use mdbook::config::Config; use mdbook::config::Config;
use mdbook::errors::*; use mdbook::errors::*;
use mdbook::preprocess::{Preprocessor, PreprocessorContext}; use mdbook::preprocess::{Preprocessor, PreprocessorContext};
@ -48,8 +49,9 @@ fn mdbook_runs_preprocessors() {
let temp = DummyBook::new().build().unwrap(); let temp = DummyBook::new().build().unwrap();
let cfg = Config::default(); 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.with_preprocessor(Spy(Arc::clone(&spy)));
book.build().unwrap(); book.build().unwrap();
@ -68,8 +70,9 @@ fn mdbook_runs_renderers() {
let temp = DummyBook::new().build().unwrap(); let temp = DummyBook::new().build().unwrap();
let cfg = Config::default(); 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.with_renderer(Spy(Arc::clone(&spy)));
book.build().unwrap(); book.build().unwrap();

View File

@ -7,6 +7,7 @@ use crate::dummy_book::{assert_contains_strings, assert_doesnt_contain_strings,
use anyhow::Context; use anyhow::Context;
use mdbook::config::Config; use mdbook::config::Config;
use mdbook::build_opts::BuildOpts;
use mdbook::errors::*; use mdbook::errors::*;
use mdbook::utils::fs::write_file; use mdbook::utils::fs::write_file;
use mdbook::MDBook; use mdbook::MDBook;
@ -326,8 +327,9 @@ fn failure_on_missing_file() {
let mut cfg = Config::default(); let mut cfg = Config::default();
cfg.build.create_missing = false; 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()); assert!(got.is_err());
} }
@ -339,9 +341,10 @@ fn create_missing_file_with_config() {
let mut cfg = Config::default(); let mut cfg = Config::default();
cfg.build.create_missing = true; cfg.build.create_missing = true;
let build_opts = BuildOpts::default();
assert!(!temp.path().join("src").join("intro.md").exists()); 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()); 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(); let mut cfg = Config::default();
cfg.set("book.src", "src2") cfg.set("book.src", "src2")
.expect("Couldn't set config.book.src to \"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(); md.build().unwrap();
let first_index = temp.path().join("book").join("first").join("index.html"); let first_index = temp.path().join("book").join("first").join("index.html");