Skip to content

Commit

Permalink
Specify language for book in command line args
Browse files Browse the repository at this point in the history
- 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.
  • Loading branch information
Ruin0x11 committed Sep 15, 2021
1 parent 3049d9f commit 96d9271
Show file tree
Hide file tree
Showing 27 changed files with 348 additions and 66 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ guide/book

.vscode
tests/dummy_book/book/
tests/localized_book/book/

# Ignore Jetbrains specific files.
.idea/
Expand Down
155 changes: 132 additions & 23 deletions src/book/book.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<()> {
Expand Down Expand Up @@ -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();
Expand All @@ -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);
}

Expand All @@ -233,34 +253,51 @@ 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())),
}
}

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()))?;

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -347,7 +392,7 @@ mod tests {
this is some dummy text.
And here is some \
more text.
more text.
";

/// Create a dummy `Link` in a temporary directory.
Expand Down Expand Up @@ -389,14 +434,15 @@ 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(),
"chapter_1.md",
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);
}

Expand Down Expand Up @@ -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());
}

Expand All @@ -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),
Expand All @@ -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);
}

Expand All @@ -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"),
Expand All @@ -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);
}
Expand Down Expand Up @@ -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());
}

Expand All @@ -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());
}
}
34 changes: 25 additions & 9 deletions src/book/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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");

Expand Down Expand Up @@ -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)?;
Expand All @@ -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)?;
Expand Down Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions src/cmd/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit 96d9271

Please sign in to comment.