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
|
.vscode
|
||||||
tests/dummy_book/book/
|
tests/dummy_book/book/
|
||||||
|
tests/localized_book/book/
|
||||||
|
|
||||||
# Ignore Jetbrains specific files.
|
# Ignore Jetbrains specific files.
|
||||||
.idea/
|
.idea/
|
||||||
|
155
src/book/book.rs
155
src/book/book.rs
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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(),
|
||||||
|
@ -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();
|
||||||
|
@ -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
|
||||||
|
@ -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") {
|
||||||
|
@ -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();
|
||||||
|
|
||||||
|
@ -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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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,
|
||||||
|
@ -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>,
|
||||||
|
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 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;
|
||||||
|
Loading…
Reference in New Issue
Block a user