Specify language for book in command line args

- Add a [language] table to book.toml. Each key in the table defines a
new language with `name` and `default` properties.
- Changes the directory structure of localized books. If the [language]
table exists, mdBook will now assume the src/ directory contains
subdirectories named after the keys in [language]. The behavior is
backwards-compatible if you don't specify [language].
- Specify which language of book to build using the -l/--language
argument to `mdbook build` and similar, or omit to use the default
language.
- Specify the default language by setting the `default` property to
`true` in an entry in [language]. Exactly one language must have `default`
set to `true` if the [language] table is defined.
- Each language has its own SUMMARY.md. It can include links to files
not in other translations. If a link in SUMMARY.md refers to a
nonexistent file that is specified in the default language, the renderer
will gracefully degrade the link to the default language's page. If it
still doesn't exist, the config's `create_missing` option will be
respected instead.
This commit is contained in:
Ruin0x11 2020-08-27 19:44:24 -07:00
parent 3049d9f103
commit 96d9271d64
27 changed files with 348 additions and 66 deletions

1
.gitignore vendored
View File

@ -8,6 +8,7 @@ guide/book
.vscode .vscode
tests/dummy_book/book/ tests/dummy_book/book/
tests/localized_book/book/
# Ignore Jetbrains specific files. # Ignore Jetbrains specific files.
.idea/ .idea/

View File

@ -5,27 +5,37 @@ use std::io::{Read, Write};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use super::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem}; use super::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem};
use crate::config::BuildConfig; use crate::build_opts::BuildOpts;
use crate::config::Config;
use crate::errors::*; use crate::errors::*;
/// Load a book into memory from its `src/` directory. /// Load a book into memory from its `src/` directory.
pub fn load_book<P: AsRef<Path>>(src_dir: P, cfg: &BuildConfig) -> Result<Book> { pub fn load_book<P: AsRef<Path>>(
let src_dir = src_dir.as_ref(); root_dir: P,
let summary_md = src_dir.join("SUMMARY.md"); cfg: &Config,
build_opts: &BuildOpts,
) -> Result<Book> {
let localized_src_dir = root_dir.as_ref().join(
cfg.get_localized_src_path(build_opts.language_ident.as_ref())
.unwrap(),
);
let fallback_src_dir = root_dir.as_ref().join(cfg.get_fallback_src_path());
let summary_md = localized_src_dir.join("SUMMARY.md");
let mut summary_content = String::new(); let mut summary_content = String::new();
File::open(&summary_md) File::open(&summary_md)
.with_context(|| format!("Couldn't open SUMMARY.md in {:?} directory", src_dir))? .with_context(|| format!("Couldn't open SUMMARY.md in {:?} directory", localized_src_dir))?
.read_to_string(&mut summary_content)?; .read_to_string(&mut summary_content)?;
let summary = parse_summary(&summary_content) let summary = parse_summary(&summary_content)
.with_context(|| format!("Summary parsing failed for file={:?}", summary_md))?; .with_context(|| format!("Summary parsing failed for file={:?}", summary_md))?;
if cfg.create_missing { if cfg.create_missing {
create_missing(src_dir, &summary).with_context(|| "Unable to create missing chapters")?; create_missing(localized_src_dir, &summary).with_context(|| "Unable to create missing chapters")?;
} }
load_book_from_disk(&summary, src_dir) load_book_from_disk(&summary, localized_src_dir, fallback_src_dir, cfg)
} }
fn create_missing(src_dir: &Path, summary: &Summary) -> Result<()> { fn create_missing(src_dir: &Path, summary: &Summary) -> Result<()> {
@ -208,9 +218,13 @@ impl Chapter {
/// ///
/// You need to pass in the book's source directory because all the links in /// You need to pass in the book's source directory because all the links in
/// `SUMMARY.md` give the chapter locations relative to it. /// `SUMMARY.md` give the chapter locations relative to it.
pub(crate) fn load_book_from_disk<P: AsRef<Path>>(summary: &Summary, src_dir: P) -> Result<Book> { pub(crate) fn load_book_from_disk<P: AsRef<Path>>(
summary: &Summary,
localized_src_dir: P,
fallback_src_dir: P,
cfg: &Config,
) -> Result<Book> {
debug!("Loading the book from disk"); debug!("Loading the book from disk");
let src_dir = src_dir.as_ref();
let prefix = summary.prefix_chapters.iter(); let prefix = summary.prefix_chapters.iter();
let numbered = summary.numbered_chapters.iter(); let numbered = summary.numbered_chapters.iter();
@ -221,7 +235,13 @@ pub(crate) fn load_book_from_disk<P: AsRef<Path>>(summary: &Summary, src_dir: P)
let mut chapters = Vec::new(); let mut chapters = Vec::new();
for summary_item in summary_items { for summary_item in summary_items {
let chapter = load_summary_item(summary_item, src_dir, Vec::new())?; let chapter = load_summary_item(
summary_item,
localized_src_dir.as_ref(),
fallback_src_dir.as_ref(),
Vec::new(),
cfg,
)?;
chapters.push(chapter); chapters.push(chapter);
} }
@ -233,13 +253,16 @@ pub(crate) fn load_book_from_disk<P: AsRef<Path>>(summary: &Summary, src_dir: P)
fn load_summary_item<P: AsRef<Path> + Clone>( fn load_summary_item<P: AsRef<Path> + Clone>(
item: &SummaryItem, item: &SummaryItem,
src_dir: P, localized_src_dir: P,
fallback_src_dir: P,
parent_names: Vec<String>, parent_names: Vec<String>,
cfg: &Config,
) -> Result<BookItem> { ) -> Result<BookItem> {
match item { match item {
SummaryItem::Separator => Ok(BookItem::Separator), SummaryItem::Separator => Ok(BookItem::Separator),
SummaryItem::Link(ref link) => { SummaryItem::Link(ref link) => {
load_chapter(link, src_dir, parent_names).map(BookItem::Chapter) load_chapter(link, localized_src_dir, fallback_src_dir, parent_names, cfg)
.map(BookItem::Chapter)
} }
SummaryItem::PartTitle(title) => Ok(BookItem::PartTitle(title.clone())), SummaryItem::PartTitle(title) => Ok(BookItem::PartTitle(title.clone())),
} }
@ -247,20 +270,34 @@ fn load_summary_item<P: AsRef<Path> + Clone>(
fn load_chapter<P: AsRef<Path>>( fn load_chapter<P: AsRef<Path>>(
link: &Link, link: &Link,
src_dir: P, localized_src_dir: P,
fallback_src_dir: P,
parent_names: Vec<String>, parent_names: Vec<String>,
cfg: &Config,
) -> Result<Chapter> { ) -> Result<Chapter> {
let src_dir = src_dir.as_ref(); let src_dir_localized = localized_src_dir.as_ref();
let src_dir_fallback = fallback_src_dir.as_ref();
let mut ch = if let Some(ref link_location) = link.location { let mut ch = if let Some(ref link_location) = link.location {
debug!("Loading {} ({})", link.name, link_location.display()); debug!("Loading {} ({})", link.name, link_location.display());
let location = if link_location.is_absolute() { let mut src_dir = src_dir_localized;
let mut location = if link_location.is_absolute() {
link_location.clone() link_location.clone()
} else { } else {
src_dir.join(link_location) src_dir.join(link_location)
}; };
if !location.exists() && !link_location.is_absolute() {
src_dir = src_dir_fallback;
location = src_dir.join(link_location);
debug!("Falling back to {}", location.display());
}
if !location.exists() && cfg.build.create_missing {
create_missing(&location, &link)
.with_context(|| "Unable to create missing chapters")?;
}
let mut f = File::open(&location) let mut f = File::open(&location)
.with_context(|| format!("Chapter file not found, {}", link_location.display()))?; .with_context(|| format!("Chapter file not found, {}", link_location.display()))?;
@ -290,7 +327,15 @@ fn load_chapter<P: AsRef<Path>>(
let sub_items = link let sub_items = link
.nested_items .nested_items
.iter() .iter()
.map(|i| load_summary_item(i, src_dir, sub_item_parents.clone())) .map(|i| {
load_summary_item(
i,
src_dir_localized,
src_dir_fallback,
sub_item_parents.clone(),
cfg,
)
})
.collect::<Result<Vec<_>>>()?; .collect::<Result<Vec<_>>>()?;
ch.sub_items = sub_items; ch.sub_items = sub_items;
@ -347,7 +392,7 @@ mod tests {
this is some dummy text. this is some dummy text.
And here is some \ And here is some \
more text. more text.
"; ";
/// Create a dummy `Link` in a temporary directory. /// Create a dummy `Link` in a temporary directory.
@ -389,6 +434,7 @@ And here is some \
#[test] #[test]
fn load_a_single_chapter_from_disk() { fn load_a_single_chapter_from_disk() {
let (link, temp_dir) = dummy_link(); let (link, temp_dir) = dummy_link();
let cfg = Config::default();
let should_be = Chapter::new( let should_be = Chapter::new(
"Chapter 1", "Chapter 1",
DUMMY_SRC.to_string(), DUMMY_SRC.to_string(),
@ -396,7 +442,7 @@ And here is some \
Vec::new(), Vec::new(),
); );
let got = load_chapter(&link, temp_dir.path(), Vec::new()).unwrap(); let got = load_chapter(&link, temp_dir.path(), temp_dir.path(), Vec::new(), &cfg).unwrap();
assert_eq!(got, should_be); assert_eq!(got, should_be);
} }
@ -427,7 +473,7 @@ And here is some \
fn cant_load_a_nonexistent_chapter() { fn cant_load_a_nonexistent_chapter() {
let link = Link::new("Chapter 1", "/foo/bar/baz.md"); let link = Link::new("Chapter 1", "/foo/bar/baz.md");
let got = load_chapter(&link, "", Vec::new()); let got = load_chapter(&link, "", "", Vec::new(), &Config::default());
assert!(got.is_err()); assert!(got.is_err());
} }
@ -444,6 +490,7 @@ And here is some \
parent_names: vec![String::from("Chapter 1")], parent_names: vec![String::from("Chapter 1")],
sub_items: Vec::new(), sub_items: Vec::new(),
}; };
let cfg = Config::default();
let should_be = BookItem::Chapter(Chapter { let should_be = BookItem::Chapter(Chapter {
name: String::from("Chapter 1"), name: String::from("Chapter 1"),
content: String::from(DUMMY_SRC), content: String::from(DUMMY_SRC),
@ -458,7 +505,14 @@ And here is some \
], ],
}); });
let got = load_summary_item(&SummaryItem::Link(root), temp.path(), Vec::new()).unwrap(); let got = load_summary_item(
&SummaryItem::Link(root),
temp.path(),
temp.path(),
Vec::new(),
&cfg,
)
.unwrap();
assert_eq!(got, should_be); assert_eq!(got, should_be);
} }
@ -469,6 +523,7 @@ And here is some \
numbered_chapters: vec![SummaryItem::Link(link)], numbered_chapters: vec![SummaryItem::Link(link)],
..Default::default() ..Default::default()
}; };
let cfg = Config::default();
let should_be = Book { let should_be = Book {
sections: vec![BookItem::Chapter(Chapter { sections: vec![BookItem::Chapter(Chapter {
name: String::from("Chapter 1"), name: String::from("Chapter 1"),
@ -480,7 +535,7 @@ And here is some \
..Default::default() ..Default::default()
}; };
let got = load_book_from_disk(&summary, temp.path()).unwrap(); let got = load_book_from_disk(&summary, temp.path(), temp.path(), &cfg).unwrap();
assert_eq!(got, should_be); assert_eq!(got, should_be);
} }
@ -611,8 +666,9 @@ And here is some \
..Default::default() ..Default::default()
}; };
let cfg = Config::default();
let got = load_book_from_disk(&summary, temp.path()); let got = load_book_from_disk(&summary, temp.path(), temp.path(), &cfg);
assert!(got.is_err()); assert!(got.is_err());
} }
@ -630,8 +686,61 @@ And here is some \
})], })],
..Default::default() ..Default::default()
}; };
let cfg = Config::default();
let got = load_book_from_disk(&summary, temp.path(), temp.path(), &cfg);
assert!(got.is_err());
}
#[test]
fn can_load_a_nonexistent_chapter_with_fallback() {
let (_, temp_localized) = dummy_link();
let chapter_path = temp_localized.path().join("chapter_1.md");
fs::remove_file(&chapter_path).unwrap();
let (_, temp_fallback) = dummy_link();
let link_relative = Link::new("Chapter 1", "chapter_1.md");
let summary = Summary {
numbered_chapters: vec![SummaryItem::Link(link_relative)],
..Default::default()
};
let mut cfg = Config::default();
cfg.build.create_missing = false;
let should_be = Book {
sections: vec![BookItem::Chapter(Chapter {
name: String::from("Chapter 1"),
content: String::from(DUMMY_SRC),
path: Some(PathBuf::from("chapter_1.md")),
..Default::default()
})],
..Default::default()
};
let got = load_book_from_disk(&summary, temp_localized.path(), temp_fallback.path(), &cfg)
.unwrap();
assert_eq!(got, should_be);
}
#[test]
fn cannot_load_a_nonexistent_absolute_link_with_fallback() {
let (link_absolute, temp_localized) = dummy_link();
let chapter_path = temp_localized.path().join("chapter_1.md");
fs::remove_file(&chapter_path).unwrap();
let (_, temp_fallback) = dummy_link();
let summary = Summary {
numbered_chapters: vec![SummaryItem::Link(link_absolute)],
..Default::default()
};
let mut cfg = Config::default();
cfg.build.create_missing = false;
let got = load_book_from_disk(&summary, temp_localized.path(), temp_fallback.path(), &cfg);
let got = load_book_from_disk(&summary, temp.path());
assert!(got.is_err()); assert!(got.is_err());
} }
} }

View File

@ -28,8 +28,8 @@ use crate::preprocess::{
use crate::renderer::{CmdRenderer, HtmlHandlebars, MarkdownRenderer, RenderContext, Renderer}; use crate::renderer::{CmdRenderer, HtmlHandlebars, MarkdownRenderer, RenderContext, Renderer};
use crate::utils; use crate::utils;
use crate::config::{Config, RustEdition};
use crate::build_opts::BuildOpts; use crate::build_opts::BuildOpts;
use crate::config::{Config, RustEdition};
/// The object used to manage and build a book. /// The object used to manage and build a book.
pub struct MDBook { pub struct MDBook {
@ -57,7 +57,10 @@ impl MDBook {
/// Load a book from its root directory on disk, passing in options from the /// Load a book from its root directory on disk, passing in options from the
/// frontend. /// frontend.
pub fn load_with_build_opts<P: Into<PathBuf>>(book_root: P, build_opts: BuildOpts) -> Result<MDBook> { 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");
@ -90,11 +93,14 @@ impl MDBook {
} }
/// 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, build_opts: BuildOpts) -> 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 book = book::load_book(&root, &config, &build_opts)?;
let book = book::load_book(&src_dir, &config.build)?;
let renderers = determine_renderers(&config); let renderers = determine_renderers(&config);
let preprocessors = determine_preprocessors(&config)?; let preprocessors = determine_preprocessors(&config)?;
@ -118,8 +124,14 @@ impl MDBook {
) -> Result<MDBook> { ) -> Result<MDBook> {
let root = book_root.into(); let root = book_root.into();
let src_dir = root.join(&config.book.src); let localized_src_dir = root.join(
let book = book::load_book_from_disk(&summary, &src_dir)?; config
.get_localized_src_path(build_opts.language_ident.as_ref())
.unwrap(),
);
let fallback_src_dir = root.join(config.get_fallback_src_path());
let book =
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)?;
@ -256,8 +268,12 @@ impl MDBook {
let temp_dir = TempFileBuilder::new().prefix("mdbook-").tempdir()?; 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 = let preprocess_context = PreprocessorContext::new(
PreprocessorContext::new(self.root.clone(), self.build_opts.clone(), self.config.clone(), "test".to_string()); 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

View File

@ -17,6 +17,11 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
(Defaults to the Current Directory when omitted)'", (Defaults to the Current Directory when omitted)'",
) )
.arg_from_usage("-o, --open 'Opens the compiled book in a web browser'") .arg_from_usage("-o, --open 'Opens the compiled book in a web browser'")
.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}\
If omitted, defaults to the language with `default` set to true.'",
)
} }
// Build command implementation // Build command implementation

View File

@ -1,4 +1,4 @@
use crate::get_book_dir; use crate::{get_book_dir, get_build_opts};
use anyhow::Context; use anyhow::Context;
use clap::{App, ArgMatches, SubCommand}; use clap::{App, ArgMatches, SubCommand};
use mdbook::MDBook; use mdbook::MDBook;
@ -18,12 +18,18 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
"[dir] 'Root directory for the book{n}\ "[dir] 'Root directory for the book{n}\
(Defaults to the Current Directory when omitted)'", (Defaults to the Current Directory when omitted)'",
) )
.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}\
If omitted, defaults to the language with `default` set to true.'",
)
} }
// Clean command implementation // Clean command implementation
pub fn execute(args: &ArgMatches) -> mdbook::errors::Result<()> { pub fn execute(args: &ArgMatches) -> mdbook::errors::Result<()> {
let book_dir = get_book_dir(args); let book_dir = get_book_dir(args);
let book = MDBook::load(&book_dir)?; let build_opts = get_build_opts(args);
let book = MDBook::load_with_build_opts(&book_dir, build_opts)?;
let dir_to_remove = match args.value_of("dest-dir") { let dir_to_remove = match args.value_of("dest-dir") {
Some(dest_dir) => dest_dir.into(), Some(dest_dir) => dest_dir.into(),

View File

@ -1,6 +1,6 @@
#[cfg(feature = "watch")] #[cfg(feature = "watch")]
use super::watch; use super::watch;
use crate::{get_book_dir, open}; use crate::{get_book_dir, get_build_opts, open};
use clap::{App, Arg, ArgMatches, SubCommand}; use clap::{App, Arg, ArgMatches, SubCommand};
use futures_util::sink::SinkExt; use futures_util::sink::SinkExt;
use futures_util::StreamExt; use futures_util::StreamExt;
@ -49,12 +49,18 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
.help("Port to use for HTTP connections"), .help("Port to use for HTTP connections"),
) )
.arg_from_usage("-o, --open 'Opens the book server in a web browser'") .arg_from_usage("-o, --open 'Opens the book server in a web browser'")
.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}\
If omitted, defaults to the language with `default` set to true.'",
)
} }
// Serve command implementation // Serve 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 build_opts = get_build_opts(args);
let mut book = MDBook::load_with_build_opts(&book_dir, build_opts)?;
let port = args.value_of("port").unwrap(); let port = args.value_of("port").unwrap();
let hostname = args.value_of("hostname").unwrap(); let hostname = args.value_of("hostname").unwrap();

View File

@ -25,6 +25,9 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
.multiple(true) .multiple(true)
.empty_values(false) .empty_values(false)
.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}\
Only valid if the [languages] table in the config is not empty.{n}\
If omitted, defaults to the language with `default` set to true.'")
} }
// test command implementation // test command implementation

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::utils; use mdbook::utils;
@ -23,12 +23,18 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
(Defaults to the Current Directory when omitted)'", (Defaults to the Current Directory when omitted)'",
) )
.arg_from_usage("-o, --open 'Open the compiled book in a web browser'") .arg_from_usage("-o, --open 'Open the compiled book in a web browser'")
.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}\
If omitted, defaults to the language with `default` set to true.'",
)
} }
// Watch command implementation // Watch 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 build_opts = get_build_opts(args);
let mut book = MDBook::load_with_build_opts(&book_dir, build_opts)?;
let update_config = |book: &mut MDBook| { let update_config = |book: &mut MDBook| {
if let Some(dest_dir) = args.value_of("dest-dir") { if let Some(dest_dir) = args.value_of("dest-dir") {

View File

@ -49,6 +49,7 @@
#![deny(missing_docs)] #![deny(missing_docs)]
use anyhow::anyhow;
use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::collections::HashMap; use std::collections::HashMap;
use std::env; use std::env;
@ -253,27 +254,66 @@ impl Config {
} }
/// Get the source directory of a localized book corresponding to language ident `index`. /// 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> { pub fn get_localized_src_path<I: AsRef<str>>(&self, index: Option<I>) -> Result<PathBuf> {
match self.language.default_language() { match self.language.default_language() {
// Languages have been specified, assume directory structure with
// language subfolders.
Some(default) => match index { Some(default) => match index {
// Make sure that the language we passed was actually // Make sure that the language we passed was actually
// declared in the config, and return `None` if not. // declared in the config, and return `None` if not.
Some(lang_ident) => self.language.0.get(lang_ident.as_ref()).map(|_| { Some(lang_ident) => match self.language.0.get(lang_ident.as_ref()) {
Some(_) => {
let mut buf = PathBuf::new();
buf.push(self.book.src.clone());
buf.push(lang_ident.as_ref());
Ok(buf)
}
None => Err(anyhow!(
"Expected [language.{}] to be declared in book.toml",
lang_ident.as_ref()
)),
},
// Use the default specified in book.toml.
None => {
let mut buf = PathBuf::new(); let mut buf = PathBuf::new();
buf.push(self.book.src.clone()); buf.push(self.book.src.clone());
buf.push(lang_ident.as_ref()); buf.push(default);
buf Ok(buf)
}), }
// Use the default specified in book.toml. },
None => Some(PathBuf::from(default))
}
// No default language was configured in book.toml. Preserve // No default language was configured in book.toml. Preserve
// backwards compatibility by just returning `src`. // backwards compatibility by just returning `src`.
None => match index { None => match index {
Some(_) => None, // We passed in a language from the frontend, but the config
None => Some(self.book.src.clone()), // offers no languages.
Some(lang_ident) => Err(anyhow!(
"No [language] table in book.toml, expected [language.{}] to be declared",
lang_ident.as_ref()
)),
// Default to previous non-localized behavior.
None => Ok(self.book.src.clone()),
},
}
}
/// Get the fallback source directory of a book. For example, if chapters
/// are missing in a localization, the links will gracefully degrade to the
/// files that exist in this directory.
pub fn get_fallback_src_path(&self) -> PathBuf {
match self.language.default_language() {
// Languages have been specified, assume directory structure with
// language subfolders.
Some(default) => {
let mut buf = PathBuf::new();
buf.push(self.book.src.clone());
buf.push(default);
buf
} }
// No default language was configured in book.toml. Preserve
// backwards compatibility by just returning `src`.
None => self.book.src.clone(),
} }
} }
@ -373,14 +413,11 @@ impl<'de> Deserialize<'de> for Config {
.unwrap_or_default(); .unwrap_or_default();
if !language.0.is_empty() { if !language.0.is_empty() {
let default_languages = language.0 let default_languages = language.0.iter().filter(|(_, lang)| lang.default).count();
.iter()
.filter(|(_, lang)| lang.default)
.count();
if default_languages != 1 { if default_languages != 1 {
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'",
)); ));
} }
} }
@ -752,9 +789,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<&String> { pub fn default_language(&self) -> Option<&String> {
self.0.iter() self.0
.find(|(_, lang)| lang.default) .iter()
.map(|(lang_ident, _)| lang_ident) .find(|(_, lang)| lang.default)
.map(|(lang_ident, _)| lang_ident)
} }
} }
@ -874,8 +912,20 @@ mod tests {
..Default::default() ..Default::default()
}; };
let mut language_should_be = LanguageConfig::default(); let mut language_should_be = LanguageConfig::default();
language_should_be.0.insert(String::from("en"), Language { name: String::from("English"), default: true }); language_should_be.0.insert(
language_should_be.0.insert(String::from("fr"), Language { name: String::from("Français"), default: false }); String::from("en"),
Language {
name: String::from("English"),
default: true,
},
);
language_should_be.0.insert(
String::from("fr"),
Language {
name: String::from("Français"),
default: false,
},
);
let got = Config::from_str(src).unwrap(); let got = Config::from_str(src).unwrap();

View File

@ -8,8 +8,8 @@ use chrono::Local;
use clap::{App, AppSettings, Arg, ArgMatches, Shell, SubCommand}; 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::build_opts::BuildOpts; use mdbook::build_opts::BuildOpts;
use mdbook::utils;
use std::env; use std::env;
use std::ffi::OsStr; use std::ffi::OsStr;
use std::io::Write; use std::io::Write;
@ -136,7 +136,7 @@ fn get_build_opts(args: &ArgMatches) -> BuildOpts {
let language = args.value_of("language"); let language = args.value_of("language");
BuildOpts { BuildOpts {
language_ident: language.map(String::from) language_ident: language.map(String::from),
} }
} }

View File

@ -178,8 +178,8 @@ impl Preprocessor for CmdPreprocessor {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::MDBook;
use crate::build_opts::BuildOpts; use crate::build_opts::BuildOpts;
use crate::MDBook;
use std::path::Path; use std::path::Path;
fn guide() -> MDBook { fn guide() -> MDBook {

View File

@ -9,8 +9,8 @@ mod index;
mod links; mod links;
use crate::book::Book; use crate::book::Book;
use crate::config::Config;
use crate::build_opts::BuildOpts; use crate::build_opts::BuildOpts;
use crate::config::Config;
use crate::errors::*; use crate::errors::*;
use std::cell::RefCell; use std::cell::RefCell;
@ -39,7 +39,12 @@ pub struct PreprocessorContext {
impl PreprocessorContext { impl PreprocessorContext {
/// Create a new `PreprocessorContext`. /// Create a new `PreprocessorContext`.
pub(crate) fn new(root: PathBuf, build_opts: BuildOpts, 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, build_opts,

View File

@ -25,8 +25,8 @@ use std::path::{Path, PathBuf};
use std::process::{Command, Stdio}; use std::process::{Command, Stdio};
use crate::book::Book; use crate::book::Book;
use crate::config::Config;
use crate::build_opts::BuildOpts; use crate::build_opts::BuildOpts;
use crate::config::Config;
use crate::errors::*; use crate::errors::*;
use toml::Value; use toml::Value;
@ -75,7 +75,13 @@ pub struct RenderContext {
impl RenderContext { impl RenderContext {
/// Create a new `RenderContext`. /// Create a new `RenderContext`.
pub fn new<P, Q>(root: P, book: Book, build_opts: BuildOpts, 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>,

View File

@ -0,0 +1,32 @@
[book]
title = "Localized Book"
description = "Testing mdBook localization features"
authors = ["Ruin0x11"]
language = "en"
[rust]
edition = "2018"
[output.html]
mathjax-support = true
site-url = "/mdBook/"
[output.html.playground]
editable = true
line-numbers = true
[output.html.search]
limit-results = 20
use-boolean-and = true
boost-title = 2
boost-hierarchy = 2
boost-paragraph = 1
expand = true
heading-split-level = 2
[language.en]
name = "English"
default = true
[language.ja]
name = "日本語"

View File

@ -0,0 +1,3 @@
# Localized Book
This is a test of the book localization features.

View File

@ -0,0 +1,7 @@
# Summary
- [README](README.md)
- [Chapter 1](chapter/README.md)
- [Section 1](chapter/1.md)
- [Section 2](chapter/2.md)
- [Untranslated Chapter](untranslated.md)

View File

@ -0,0 +1,2 @@
# First section.

View File

@ -0,0 +1,2 @@
# Second section.

View File

@ -0,0 +1 @@
# First chapter page.

View File

@ -0,0 +1,3 @@
# Untranslated chapter.
This chapter is not available in any translation. If things work correctly, you should see this page written in the fallback language (English) if the other translations list it on their summary page.

View File

@ -0,0 +1,3 @@
# 本の翻訳
これは本の翻訳のテストです。

View File

@ -0,0 +1,8 @@
# 目次
- [README](README.md)
- [第一章](chapter/README.md)
- [第一節](chapter/1.md)
- [第二節](chapter/2.md)
- [第三節](chapter/3.md)
- [Untranslated Chapter](untranslated.md)

View File

@ -0,0 +1 @@
# 第一節。

View File

@ -0,0 +1 @@
# 第二節。

View File

@ -0,0 +1,5 @@
# 第三節。
実は、このページは英語バージョンに存在しません。
This page doesn't exist in the English translation. It is unique to this translation only.

View File

@ -0,0 +1 @@
# 第一章のページ。

View File

@ -6,8 +6,8 @@ mod dummy_book;
use crate::dummy_book::{assert_contains_strings, assert_doesnt_contain_strings, DummyBook}; use crate::dummy_book::{assert_contains_strings, assert_doesnt_contain_strings, DummyBook};
use anyhow::Context; use anyhow::Context;
use mdbook::config::Config;
use mdbook::build_opts::BuildOpts; use mdbook::build_opts::BuildOpts;
use mdbook::config::Config;
use mdbook::errors::*; use mdbook::errors::*;
use mdbook::utils::fs::write_file; use mdbook::utils::fs::write_file;
use mdbook::MDBook; use mdbook::MDBook;