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:
parent
3049d9f103
commit
96d9271d64
1
.gitignore
vendored
1
.gitignore
vendored
@ -8,6 +8,7 @@ guide/book
|
||||
|
||||
.vscode
|
||||
tests/dummy_book/book/
|
||||
tests/localized_book/book/
|
||||
|
||||
# Ignore Jetbrains specific files.
|
||||
.idea/
|
||||
|
153
src/book/book.rs
153
src/book/book.rs
@ -5,27 +5,37 @@ use std::io::{Read, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
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::*;
|
||||
|
||||
/// Load a book into memory from its `src/` directory.
|
||||
pub fn load_book<P: AsRef<Path>>(src_dir: P, cfg: &BuildConfig) -> Result<Book> {
|
||||
let src_dir = src_dir.as_ref();
|
||||
let summary_md = src_dir.join("SUMMARY.md");
|
||||
pub fn load_book<P: AsRef<Path>>(
|
||||
root_dir: P,
|
||||
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();
|
||||
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)?;
|
||||
|
||||
let summary = parse_summary(&summary_content)
|
||||
.with_context(|| format!("Summary parsing failed for file={:?}", summary_md))?;
|
||||
|
||||
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<()> {
|
||||
@ -208,9 +218,13 @@ impl Chapter {
|
||||
///
|
||||
/// You need to pass in the book's source directory because all the links in
|
||||
/// `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");
|
||||
let src_dir = src_dir.as_ref();
|
||||
|
||||
let prefix = summary.prefix_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();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@ -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>(
|
||||
item: &SummaryItem,
|
||||
src_dir: P,
|
||||
localized_src_dir: P,
|
||||
fallback_src_dir: P,
|
||||
parent_names: Vec<String>,
|
||||
cfg: &Config,
|
||||
) -> Result<BookItem> {
|
||||
match item {
|
||||
SummaryItem::Separator => Ok(BookItem::Separator),
|
||||
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())),
|
||||
}
|
||||
@ -247,20 +270,34 @@ fn load_summary_item<P: AsRef<Path> + Clone>(
|
||||
|
||||
fn load_chapter<P: AsRef<Path>>(
|
||||
link: &Link,
|
||||
src_dir: P,
|
||||
localized_src_dir: P,
|
||||
fallback_src_dir: P,
|
||||
parent_names: Vec<String>,
|
||||
cfg: &Config,
|
||||
) -> 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 {
|
||||
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()
|
||||
} else {
|
||||
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)
|
||||
.with_context(|| format!("Chapter file not found, {}", link_location.display()))?;
|
||||
|
||||
@ -290,7 +327,15 @@ fn load_chapter<P: AsRef<Path>>(
|
||||
let sub_items = link
|
||||
.nested_items
|
||||
.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<_>>>()?;
|
||||
|
||||
ch.sub_items = sub_items;
|
||||
@ -389,6 +434,7 @@ And here is some \
|
||||
#[test]
|
||||
fn load_a_single_chapter_from_disk() {
|
||||
let (link, temp_dir) = dummy_link();
|
||||
let cfg = Config::default();
|
||||
let should_be = Chapter::new(
|
||||
"Chapter 1",
|
||||
DUMMY_SRC.to_string(),
|
||||
@ -396,7 +442,7 @@ And here is some \
|
||||
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);
|
||||
}
|
||||
|
||||
@ -427,7 +473,7 @@ And here is some \
|
||||
fn cant_load_a_nonexistent_chapter() {
|
||||
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());
|
||||
}
|
||||
|
||||
@ -444,6 +490,7 @@ And here is some \
|
||||
parent_names: vec![String::from("Chapter 1")],
|
||||
sub_items: Vec::new(),
|
||||
};
|
||||
let cfg = Config::default();
|
||||
let should_be = BookItem::Chapter(Chapter {
|
||||
name: String::from("Chapter 1"),
|
||||
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);
|
||||
}
|
||||
|
||||
@ -469,6 +523,7 @@ And here is some \
|
||||
numbered_chapters: vec![SummaryItem::Link(link)],
|
||||
..Default::default()
|
||||
};
|
||||
let cfg = Config::default();
|
||||
let should_be = Book {
|
||||
sections: vec![BookItem::Chapter(Chapter {
|
||||
name: String::from("Chapter 1"),
|
||||
@ -480,7 +535,7 @@ And here is some \
|
||||
..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);
|
||||
}
|
||||
@ -611,8 +666,9 @@ And here is some \
|
||||
|
||||
..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());
|
||||
}
|
||||
|
||||
@ -630,8 +686,61 @@ And here is some \
|
||||
})],
|
||||
..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());
|
||||
}
|
||||
}
|
||||
|
@ -28,8 +28,8 @@ use crate::preprocess::{
|
||||
use crate::renderer::{CmdRenderer, HtmlHandlebars, MarkdownRenderer, RenderContext, Renderer};
|
||||
use crate::utils;
|
||||
|
||||
use crate::config::{Config, RustEdition};
|
||||
use crate::build_opts::BuildOpts;
|
||||
use crate::config::{Config, RustEdition};
|
||||
|
||||
/// The object used to manage and build a book.
|
||||
pub struct MDBook {
|
||||
@ -57,7 +57,10 @@ impl MDBook {
|
||||
|
||||
/// 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> {
|
||||
pub fn load_with_build_opts<P: Into<PathBuf>>(
|
||||
book_root: P,
|
||||
build_opts: BuildOpts,
|
||||
) -> Result<MDBook> {
|
||||
let book_root = book_root.into();
|
||||
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.
|
||||
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 src_dir = root.join(&config.book.src);
|
||||
let book = book::load_book(&src_dir, &config.build)?;
|
||||
let book = book::load_book(&root, &config, &build_opts)?;
|
||||
|
||||
let renderers = determine_renderers(&config);
|
||||
let preprocessors = determine_preprocessors(&config)?;
|
||||
@ -118,8 +124,14 @@ impl MDBook {
|
||||
) -> Result<MDBook> {
|
||||
let root = book_root.into();
|
||||
|
||||
let src_dir = root.join(&config.book.src);
|
||||
let book = book::load_book_from_disk(&summary, &src_dir)?;
|
||||
let localized_src_dir = root.join(
|
||||
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 preprocessors = determine_preprocessors(&config)?;
|
||||
@ -256,8 +268,12 @@ impl MDBook {
|
||||
let temp_dir = TempFileBuilder::new().prefix("mdbook-").tempdir()?;
|
||||
|
||||
// FIXME: Is "test" the proper renderer name to use here?
|
||||
let preprocess_context =
|
||||
PreprocessorContext::new(self.root.clone(), self.build_opts.clone(), self.config.clone(), "test".to_string());
|
||||
let preprocess_context = 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
|
||||
|
@ -17,6 +17,11 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
(Defaults to the Current Directory when omitted)'",
|
||||
)
|
||||
.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
|
||||
|
@ -1,4 +1,4 @@
|
||||
use crate::get_book_dir;
|
||||
use crate::{get_book_dir, get_build_opts};
|
||||
use anyhow::Context;
|
||||
use clap::{App, ArgMatches, SubCommand};
|
||||
use mdbook::MDBook;
|
||||
@ -18,12 +18,18 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
"[dir] 'Root directory for the book{n}\
|
||||
(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
|
||||
pub fn execute(args: &ArgMatches) -> mdbook::errors::Result<()> {
|
||||
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") {
|
||||
Some(dest_dir) => dest_dir.into(),
|
||||
|
@ -1,6 +1,6 @@
|
||||
#[cfg(feature = "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 futures_util::sink::SinkExt;
|
||||
use futures_util::StreamExt;
|
||||
@ -49,12 +49,18 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
.help("Port to use for HTTP connections"),
|
||||
)
|
||||
.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
|
||||
pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
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 hostname = args.value_of("hostname").unwrap();
|
||||
|
@ -25,6 +25,9 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
.multiple(true)
|
||||
.empty_values(false)
|
||||
.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
|
||||
|
@ -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::utils;
|
||||
@ -23,12 +23,18 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
(Defaults to the Current Directory when omitted)'",
|
||||
)
|
||||
.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
|
||||
pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
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| {
|
||||
if let Some(dest_dir) = args.value_of("dest-dir") {
|
||||
|
@ -49,6 +49,7 @@
|
||||
|
||||
#![deny(missing_docs)]
|
||||
|
||||
use anyhow::anyhow;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
@ -253,28 +254,67 @@ impl Config {
|
||||
}
|
||||
|
||||
/// 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() {
|
||||
// Languages have been specified, assume directory structure with
|
||||
// language subfolders.
|
||||
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(|_| {
|
||||
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());
|
||||
buf
|
||||
}),
|
||||
// Use the default specified in book.toml.
|
||||
None => Some(PathBuf::from(default))
|
||||
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();
|
||||
buf.push(self.book.src.clone());
|
||||
buf.push(default);
|
||||
Ok(buf)
|
||||
}
|
||||
},
|
||||
|
||||
// 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()),
|
||||
// We passed in a language from the frontend, but the config
|
||||
// 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(),
|
||||
}
|
||||
}
|
||||
|
||||
fn from_legacy(mut table: Value) -> Config {
|
||||
@ -373,14 +413,11 @@ impl<'de> Deserialize<'de> for Config {
|
||||
.unwrap_or_default();
|
||||
|
||||
if !language.0.is_empty() {
|
||||
let default_languages = language.0
|
||||
.iter()
|
||||
.filter(|(_, lang)| lang.default)
|
||||
.count();
|
||||
let default_languages = language.0.iter().filter(|(_, lang)| lang.default).count();
|
||||
|
||||
if default_languages != 1 {
|
||||
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,7 +789,8 @@ pub struct Language {
|
||||
impl LanguageConfig {
|
||||
/// Returns the default language specified in the config.
|
||||
pub fn default_language(&self) -> Option<&String> {
|
||||
self.0.iter()
|
||||
self.0
|
||||
.iter()
|
||||
.find(|(_, lang)| lang.default)
|
||||
.map(|(lang_ident, _)| lang_ident)
|
||||
}
|
||||
@ -874,8 +912,20 @@ mod tests {
|
||||
..Default::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(String::from("fr"), Language { name: String::from("Français"), default: false });
|
||||
language_should_be.0.insert(
|
||||
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();
|
||||
|
||||
|
@ -8,8 +8,8 @@ use chrono::Local;
|
||||
use clap::{App, AppSettings, Arg, ArgMatches, Shell, SubCommand};
|
||||
use env_logger::Builder;
|
||||
use log::LevelFilter;
|
||||
use mdbook::utils;
|
||||
use mdbook::build_opts::BuildOpts;
|
||||
use mdbook::utils;
|
||||
use std::env;
|
||||
use std::ffi::OsStr;
|
||||
use std::io::Write;
|
||||
@ -136,7 +136,7 @@ fn get_build_opts(args: &ArgMatches) -> BuildOpts {
|
||||
let language = args.value_of("language");
|
||||
|
||||
BuildOpts {
|
||||
language_ident: language.map(String::from)
|
||||
language_ident: language.map(String::from),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -178,8 +178,8 @@ impl Preprocessor for CmdPreprocessor {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::MDBook;
|
||||
use crate::build_opts::BuildOpts;
|
||||
use crate::MDBook;
|
||||
use std::path::Path;
|
||||
|
||||
fn guide() -> MDBook {
|
||||
|
@ -9,8 +9,8 @@ mod index;
|
||||
mod links;
|
||||
|
||||
use crate::book::Book;
|
||||
use crate::config::Config;
|
||||
use crate::build_opts::BuildOpts;
|
||||
use crate::config::Config;
|
||||
use crate::errors::*;
|
||||
|
||||
use std::cell::RefCell;
|
||||
@ -39,7 +39,12 @@ pub struct PreprocessorContext {
|
||||
|
||||
impl 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 {
|
||||
root,
|
||||
build_opts,
|
||||
|
@ -25,8 +25,8 @@ use std::path::{Path, PathBuf};
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
use crate::book::Book;
|
||||
use crate::config::Config;
|
||||
use crate::build_opts::BuildOpts;
|
||||
use crate::config::Config;
|
||||
use crate::errors::*;
|
||||
use toml::Value;
|
||||
|
||||
@ -75,7 +75,13 @@ pub struct RenderContext {
|
||||
|
||||
impl 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
|
||||
P: Into<PathBuf>,
|
||||
Q: Into<PathBuf>,
|
||||
|
32
tests/localized_book/book.toml
Normal file
32
tests/localized_book/book.toml
Normal 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 = "日本語"
|
3
tests/localized_book/src/en/README.md
Normal file
3
tests/localized_book/src/en/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Localized Book
|
||||
|
||||
This is a test of the book localization features.
|
7
tests/localized_book/src/en/SUMMARY.md
Normal file
7
tests/localized_book/src/en/SUMMARY.md
Normal 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)
|
2
tests/localized_book/src/en/chapter/1.md
Normal file
2
tests/localized_book/src/en/chapter/1.md
Normal file
@ -0,0 +1,2 @@
|
||||
# First section.
|
||||
|
2
tests/localized_book/src/en/chapter/2.md
Normal file
2
tests/localized_book/src/en/chapter/2.md
Normal file
@ -0,0 +1,2 @@
|
||||
# Second section.
|
||||
|
1
tests/localized_book/src/en/chapter/README.md
Normal file
1
tests/localized_book/src/en/chapter/README.md
Normal file
@ -0,0 +1 @@
|
||||
# First chapter page.
|
3
tests/localized_book/src/en/untranslated.md
Normal file
3
tests/localized_book/src/en/untranslated.md
Normal 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.
|
3
tests/localized_book/src/ja/README.md
Normal file
3
tests/localized_book/src/ja/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# 本の翻訳
|
||||
|
||||
これは本の翻訳のテストです。
|
8
tests/localized_book/src/ja/SUMMARY.md
Normal file
8
tests/localized_book/src/ja/SUMMARY.md
Normal file
@ -0,0 +1,8 @@
|
||||
# 目次
|
||||
|
||||
- [README](README.md)
|
||||
- [第一章](chapter/README.md)
|
||||
- [第一節](chapter/1.md)
|
||||
- [第二節](chapter/2.md)
|
||||
- [第三節](chapter/3.md)
|
||||
- [Untranslated Chapter](untranslated.md)
|
1
tests/localized_book/src/ja/chapter/1.md
Normal file
1
tests/localized_book/src/ja/chapter/1.md
Normal file
@ -0,0 +1 @@
|
||||
# 第一節。
|
1
tests/localized_book/src/ja/chapter/2.md
Normal file
1
tests/localized_book/src/ja/chapter/2.md
Normal file
@ -0,0 +1 @@
|
||||
# 第二節。
|
5
tests/localized_book/src/ja/chapter/3.md
Normal file
5
tests/localized_book/src/ja/chapter/3.md
Normal file
@ -0,0 +1,5 @@
|
||||
# 第三節。
|
||||
|
||||
実は、このページは英語バージョンに存在しません。
|
||||
|
||||
This page doesn't exist in the English translation. It is unique to this translation only.
|
1
tests/localized_book/src/ja/chapter/README.md
Normal file
1
tests/localized_book/src/ja/chapter/README.md
Normal file
@ -0,0 +1 @@
|
||||
# 第一章のページ。
|
@ -6,8 +6,8 @@ mod dummy_book;
|
||||
use crate::dummy_book::{assert_contains_strings, assert_doesnt_contain_strings, DummyBook};
|
||||
|
||||
use anyhow::Context;
|
||||
use mdbook::config::Config;
|
||||
use mdbook::build_opts::BuildOpts;
|
||||
use mdbook::config::Config;
|
||||
use mdbook::errors::*;
|
||||
use mdbook::utils::fs::write_file;
|
||||
use mdbook::MDBook;
|
||||
|
Loading…
Reference in New Issue
Block a user