Remove 'default' property on languages, use book.language instead

This commit is contained in:
Ruin0x11 2020-08-28 12:26:08 -07:00
parent a042cfc72b
commit ee740aceff
9 changed files with 89 additions and 60 deletions

View File

@ -15,7 +15,7 @@ pub fn load_book<P: AsRef<Path>>(
cfg: &Config, cfg: &Config,
build_opts: &BuildOpts, build_opts: &BuildOpts,
) -> Result<LoadedBook> { ) -> Result<LoadedBook> {
if cfg.language.has_localized_dir_structure() { if cfg.has_localized_dir_structure() {
match build_opts.language_ident { match build_opts.language_ident {
// Build a single book's translation. // Build a single book's translation.
Some(_) => Ok(LoadedBook::Single(load_single_book_translation( Some(_) => Ok(LoadedBook::Single(load_single_book_translation(

View File

@ -14,18 +14,18 @@ pub struct BookBuilder {
create_gitignore: bool, create_gitignore: bool,
config: Config, config: Config,
copy_theme: bool, copy_theme: bool,
language_ident: String language_ident: String,
} }
fn add_default_language(cfg: &mut Config, language_ident: String) { fn add_default_language(cfg: &mut Config, language_ident: String) {
let language = Language { let language = Language {
name: String::from("English"), name: String::from("English"),
default: true,
title: None, title: None,
authors: None, authors: None,
description: None, description: None,
}; };
cfg.language.0.insert(language_ident, language); cfg.language.0.insert(language_ident.clone(), language);
cfg.book.language = Some(language_ident);
} }
impl BookBuilder { impl BookBuilder {
@ -41,13 +41,16 @@ impl BookBuilder {
create_gitignore: false, create_gitignore: false,
config: cfg, config: cfg,
copy_theme: false, copy_theme: false,
language_ident: language_ident language_ident: language_ident,
} }
} }
/// Get the output source directory of the builder. /// Get the output source directory of the builder.
pub fn source_dir(&self) -> PathBuf { pub fn source_dir(&self) -> PathBuf {
let src = self.config.get_localized_src_path(Some(&self.language_ident)).unwrap(); let src = self
.config
.get_localized_src_path(Some(&self.language_ident))
.unwrap();
self.root.join(src) self.root.join(src)
} }
@ -125,8 +128,8 @@ impl BookBuilder {
File::create(book_toml) File::create(book_toml)
.with_context(|| "Couldn't create book.toml")? .with_context(|| "Couldn't create book.toml")?
.write_all(&cfg) .write_all(&cfg)
.with_context(|| "Unable to write config to book.toml")?; .with_context(|| "Unable to write config to book.toml")?;
Ok(()) Ok(())
} }

View File

@ -19,7 +19,7 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
.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( .arg_from_usage(
"-l, --language=[language] 'Language to render the compiled book in.{n}\ "-l, --language=[language] 'Language to render the compiled book in.{n}\
Only valid if the [languages] table in the config is not empty.{n}\ Only valid if the [language] table in the config is not empty.{n}\
If omitted, builds all translations and provides a menu in the generated output for switching between them.'", If omitted, builds all translations and provides a menu in the generated output for switching between them.'",
) )
} }

View File

@ -20,7 +20,7 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
) )
.arg_from_usage( .arg_from_usage(
"-l, --language=[language] 'Language to render the compiled book in.{n}\ "-l, --language=[language] 'Language to render the compiled book in.{n}\
Only valid if the [languages] table in the config is not empty.{n}\ Only valid if the [language] table in the config is not empty.{n}\
If omitted, builds all translations and provides a menu in the generated output for switching between them.'", If omitted, builds all translations and provides a menu in the generated output for switching between them.'",
) )
} }

View File

@ -52,7 +52,7 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
.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( .arg_from_usage(
"-l, --language=[language] 'Language to render the compiled book in.{n}\ "-l, --language=[language] 'Language to render the compiled book in.{n}\
Only valid if the [languages] table in the config is not empty.{n}\ Only valid if the [language] table in the config is not empty.{n}\
If omitted, builds all translations and provides a menu in the generated output for switching between them.'", If omitted, builds all translations and provides a menu in the generated output for switching between them.'",
) )
} }
@ -86,7 +86,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
let language: Option<String> = match build_opts.language_ident { let language: Option<String> = match build_opts.language_ident {
// index.html will be at the root directory. // index.html will be at the root directory.
Some(_) => None, Some(_) => None,
None => match book.config.language.default_language() { None => match book.config.default_language() {
// If book has translations, index.html will be under src/en/ or // If book has translations, index.html will be under src/en/ or
// similar. // similar.
Some(lang_ident) => Some(lang_ident.clone()), Some(lang_ident) => Some(lang_ident.clone()),

View File

@ -26,7 +26,7 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
.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}\ .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}\ Only valid if the [language] table in the config is not empty.{n}\
If omitted, builds all translations and provides a menu in the generated output for switching between them.'") If omitted, builds all translations and provides a menu in the generated output for switching between them.'")
} }

View File

@ -25,7 +25,7 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
.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( .arg_from_usage(
"-l, --language=[language] 'Language to render the compiled book in.{n}\ "-l, --language=[language] 'Language to render the compiled book in.{n}\
Only valid if the [languages] table in the config is not empty.{n}\ Only valid if the [language] table in the config is not empty.{n}\
If omitted, builds all translations and provides a menu in the generated output for switching between them.'", If omitted, builds all translations and provides a menu in the generated output for switching between them.'",
) )
} }

View File

@ -255,7 +255,7 @@ impl Config {
/// Gets the language configured for a book. /// Gets the language configured for a book.
pub fn get_language<I: AsRef<str>>(&self, index: Option<I>) -> Result<Option<String>> { pub fn get_language<I: AsRef<str>>(&self, index: Option<I>) -> Result<Option<String>> {
match self.language.default_language() { match self.default_language() {
// Languages have been specified, assume directory structure with // Languages have been specified, assume directory structure with
// language subfolders. // language subfolders.
Some(ref default) => match index { Some(ref default) => match index {
@ -342,7 +342,7 @@ impl Config {
/// missing in a localization, any links to them will gracefully degrade to /// missing in a localization, any links to them will gracefully degrade to
/// the files that exist in this directory. /// the files that exist in this directory.
pub fn get_fallback_src_path(&self) -> PathBuf { pub fn get_fallback_src_path(&self) -> PathBuf {
match self.language.default_language() { match self.default_language() {
// Languages have been specified, assume directory structure with // Languages have been specified, assume directory structure with
// language subfolders. // language subfolders.
Some(default) => { Some(default) => {
@ -358,6 +358,31 @@ impl Config {
} }
} }
/// If true, mdBook should assume there are subdirectories under src/
/// corresponding to the localizations in the config. If false, src/ is a
/// single directory containing the summary file and the rest.
pub fn has_localized_dir_structure(&self) -> bool {
!self.language.0.is_empty()
}
/// Obtains the default language for this config.
pub fn default_language(&self) -> Option<String> {
if self.has_localized_dir_structure() {
let language_ident = self
.book
.language
.clone()
.expect("Config has [language] table, but `book.language` not was declared");
self.language.0.get(&language_ident).expect(&format!(
"Expected [language.{}] to be declared in book.toml",
language_ident
));
Some(language_ident)
} else {
None
}
}
fn from_legacy(mut table: Value) -> Config { fn from_legacy(mut table: Value) -> Config {
let mut cfg = Config::default(); let mut cfg = Config::default();
@ -455,12 +480,28 @@ impl<'de> Deserialize<'de> for Config {
if !language.0.is_empty() { 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 {
if default_languages != 1 {
return Err(D::Error::custom( return Err(D::Error::custom(
"If [languages] table is specified, exactly one entry must be set as 'default'", "If the [language] table is specified, then `book.language` must be declared",
)); ));
} }
let language_ident = book.language.clone().unwrap();
if language.0.get(&language_ident).is_none() {
use serde::de::Error;
return Err(D::Error::custom(format!(
"Expected [language.{}] to be declared in book.toml",
language_ident
)));
}
for (ident, language) in language.0.iter() {
if language.name.is_empty() {
use serde::de::Error;
return Err(D::Error::custom(format!(
"`name` property for [language.{}] must be non-empty",
ident
)));
}
}
} }
Ok(Config { Ok(Config {
@ -492,7 +533,8 @@ impl Serialize for Config {
} }
if !self.language.0.is_empty() { if !self.language.0.is_empty() {
let language_config = Value::try_from(&self.language).expect("should always be serializable"); let language_config =
Value::try_from(&self.language).expect("should always be serializable");
table.insert("language", language_config); table.insert("language", language_config);
} }
@ -543,9 +585,7 @@ pub struct BookConfig {
pub description: Option<String>, pub description: Option<String>,
/// Location of the book source relative to the book's root directory. /// Location of the book source relative to the book's root directory.
pub src: PathBuf, pub src: PathBuf,
/// Does this book support more than one language? (Deprecated.) /// The main language of the book.
pub multilingual: bool,
/// The main language of the book. (Deprecated.)
pub language: Option<String>, pub language: Option<String>,
} }
@ -556,7 +596,6 @@ impl Default for BookConfig {
authors: Vec::new(), authors: Vec::new(),
description: None, description: None,
src: PathBuf::from("src"), src: PathBuf::from("src"),
multilingual: true,
language: Some(String::from("en")), language: Some(String::from("en")),
} }
} }
@ -830,9 +869,6 @@ pub struct LanguageConfig(pub HashMap<String, Language>);
pub struct Language { pub struct Language {
/// Human-readable name of the language. /// Human-readable name of the language.
pub name: String, pub name: String,
/// If true, this language is the default. There must be exactly one default
/// language in the config.
pub default: bool,
/// Localized title of the book. /// Localized title of the book.
pub title: Option<String>, pub title: Option<String>,
/// The authors of the translation. /// The authors of the translation.
@ -841,22 +877,7 @@ pub struct Language {
pub description: Option<String>, pub description: Option<String>,
} }
impl LanguageConfig { impl LanguageConfig {}
/// If true, mdBook should assume there are subdirectories under src/
/// corresponding to the localizations in the config. If false, src/ is a
/// single directory containing the summary file and the rest.
pub fn has_localized_dir_structure(&self) -> bool {
self.default_language().is_some()
}
/// Returns the default language specified in the config.
pub fn default_language(&self) -> Option<&String> {
self.0
.iter()
.find(|(_, lang)| lang.default)
.map(|(lang_ident, _)| lang_ident)
}
}
/// Allows you to "update" any arbitrary field in a struct by round-tripping via /// Allows you to "update" any arbitrary field in a struct by round-tripping via
/// a `toml::Value`. /// a `toml::Value`.
@ -923,7 +944,6 @@ mod tests {
[language.en] [language.en]
name = "English" name = "English"
default = true
[language.ja] [language.ja]
name = "日本語" name = "日本語"
@ -940,7 +960,6 @@ mod tests {
title: Some(String::from("Some Book")), title: Some(String::from("Some Book")),
authors: vec![String::from("Michael-F-Bryan <michaelfbryan@gmail.com>")], authors: vec![String::from("Michael-F-Bryan <michaelfbryan@gmail.com>")],
description: Some(String::from("A completely useless book")), description: Some(String::from("A completely useless book")),
multilingual: true,
src: PathBuf::from("source"), src: PathBuf::from("source"),
language: Some(String::from("ja")), language: Some(String::from("ja")),
}; };
@ -981,7 +1000,6 @@ mod tests {
String::from("en"), String::from("en"),
Language { Language {
name: String::from("English"), name: String::from("English"),
default: true,
title: None, title: None,
description: None, description: None,
authors: None, authors: None,
@ -991,7 +1009,6 @@ mod tests {
String::from("ja"), String::from("ja"),
Language { Language {
name: String::from("日本語"), name: String::from("日本語"),
default: false,
title: Some(String::from("なんかの本")), title: Some(String::from("なんかの本")),
description: Some(String::from("何の役にも立たない本")), description: Some(String::from("何の役にも立たない本")),
authors: Some(vec![String::from("Ruin0x11")]), authors: Some(vec![String::from("Ruin0x11")]),
@ -1354,10 +1371,10 @@ mod tests {
#[test] #[test]
#[should_panic(expected = "Invalid configuration file")] #[should_panic(expected = "Invalid configuration file")]
fn validate_default_language() { fn validate_config_default_language_must_exist_in_languages_table() {
let src = r#" let src = r#"
[language.en] [language.ja]
name = "English" name = "日本語"
"#; "#;
Config::from_str(src).unwrap(); Config::from_str(src).unwrap();
@ -1365,15 +1382,12 @@ mod tests {
#[test] #[test]
#[should_panic(expected = "Invalid configuration file")] #[should_panic(expected = "Invalid configuration file")]
fn validate_default_language_2() { fn validate_language_config_must_have_name() {
let src = r#" let src = r#"
[language.en] [book]
name = "English" language = "en"
default = true
[language.fr] [language.en]
name = "Français"
default = true
"#; "#;
Config::from_str(src).unwrap(); Config::from_str(src).unwrap();

View File

@ -10,7 +10,13 @@ use tempfile::Builder as TempFileBuilder;
/// are created. /// are created.
#[test] #[test]
fn base_mdbook_init_should_create_default_content() { fn base_mdbook_init_should_create_default_content() {
let created_files = vec!["book", "src", "src/en", "src/en/SUMMARY.md", "src/en/chapter_1.md"]; let created_files = vec![
"book",
"src",
"src/en",
"src/en/SUMMARY.md",
"src/en/chapter_1.md",
];
let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap(); let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap();
for file in &created_files { for file in &created_files {
@ -28,7 +34,7 @@ fn base_mdbook_init_should_create_default_content() {
let contents = fs::read_to_string(temp.path().join("book.toml")).unwrap(); let contents = fs::read_to_string(temp.path().join("book.toml")).unwrap();
assert_eq!( assert_eq!(
contents, contents,
"[book]\nauthors = []\nlanguage = \"en\"\nmultilingual = true\nsrc = \"src\"\n[language.en]\ndefault = true\nname = \"English\"\n" "[book]\nauthors = []\nlanguage = \"en\"\nsrc = \"src\"\n[language.en]\nname = \"English\"\n"
); );
} }
@ -66,7 +72,13 @@ fn run_mdbook_init_should_create_content_from_summary() {
/// files, then call `mdbook init`. /// files, then call `mdbook init`.
#[test] #[test]
fn run_mdbook_init_with_custom_book_and_src_locations() { fn run_mdbook_init_with_custom_book_and_src_locations() {
let created_files = vec!["out", "in", "in/en", "in/en/SUMMARY.md", "in/en/chapter_1.md"]; let created_files = vec![
"out",
"in",
"in/en",
"in/en/SUMMARY.md",
"in/en/chapter_1.md",
];
let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap(); let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap();
for file in &created_files { for file in &created_files {