From 2903f0711fde7c404076bbb39f5a906507eeb3b8 Mon Sep 17 00:00:00 2001 From: Coil Date: Sun, 17 Mar 2024 09:13:38 +0100 Subject: [PATCH] Add 'shelf' command Initial processing of the shelf.toml file. Build the books in the current_dir. --- Cargo.lock | 148 ++++++++++ Cargo.toml | 2 + src/cmd/mod.rs | 1 + src/cmd/shelf.rs | 281 +++++++++++++++++++ src/config.rs | 42 +++ src/main.rs | 4 +- src/renderer/html_handlebars/hbs_renderer.rs | 4 + src/theme/index.hbs | 5 + test_shelf/.gitignore | 1 + test_shelf/shelf.toml | 10 + 10 files changed, 497 insertions(+), 1 deletion(-) create mode 100644 src/cmd/shelf.rs create mode 100644 test_shelf/.gitignore create mode 100644 test_shelf/shelf.toml diff --git a/Cargo.lock b/Cargo.lock index 1290c89fcf..22acd88dc0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -221,6 +221,7 @@ version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" dependencies = [ + "jobserver", "libc", ] @@ -372,6 +373,26 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + [[package]] name = "doc-comment" version = "0.3.3" @@ -571,6 +592,21 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +[[package]] +name = "git2" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "232e6a7bfe35766bf715e55a88b39a700596c0ccfd88cd3680b4cdb40d66ef70" +dependencies = [ + "bitflags 2.4.2", + "libc", + "libgit2-sys", + "log", + "openssl-probe", + "openssl-sys", + "url", +] + [[package]] name = "globset" version = "0.4.14" @@ -816,6 +852,15 @@ version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +[[package]] +name = "jobserver" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab46a6e9526ddef3ae7f787c06f0f2600639ba80ea3eade3d8e670a2230f51d6" +dependencies = [ + "libc", +] + [[package]] name = "js-sys" version = "0.3.67" @@ -851,6 +896,57 @@ version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +[[package]] +name = "libgit2-sys" +version = "0.16.2+1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4126d8b4ee5c9d9ea891dd875cfdc1e9d0950437179104b183d7d8a74d24e8" +dependencies = [ + "cc", + "libc", + "libssh2-sys", + "libz-sys", + "openssl-sys", + "pkg-config", +] + +[[package]] +name = "libredox" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" +dependencies = [ + "bitflags 2.4.2", + "libc", + "redox_syscall", +] + +[[package]] +name = "libssh2-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dc8a030b787e2119a731f1951d6a773e2280c660f8ec4b0f5e1505a386e71ee" +dependencies = [ + "cc", + "libc", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "libz-sys" +version = "1.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e143b5e666b2695d28f6bca6497720813f699c9602dd7f5cac91008b8ada7f9" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "linux-raw-sys" version = "0.4.13" @@ -924,6 +1020,7 @@ dependencies = [ "elasticlunr-rs", "env_logger", "futures-util", + "git2", "handlebars", "ignore", "log", @@ -937,6 +1034,7 @@ dependencies = [ "pretty_assertions", "pulldown-cmark", "regex", + "resolve-path", "select", "semver", "serde", @@ -1089,6 +1187,24 @@ dependencies = [ "winapi", ] +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dda2b0f344e78efc2facf7d195d098df0dd72151b26ab98da807afc26c198dff" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "parking_lot" version = "0.12.1" @@ -1239,6 +1355,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -1366,6 +1488,17 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_users" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + [[package]] name = "regex" version = "1.10.3" @@ -1395,6 +1528,15 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +[[package]] +name = "resolve-path" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "321e5e41b3b192dab6f1e75b9deacb6688b4b8c5e68906a78e8f43e7c2887bb5" +dependencies = [ + "dirs", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -1893,6 +2035,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.4" diff --git a/Cargo.toml b/Cargo.toml index 2beec26ac9..c0046203ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,8 @@ shlex = "1.3.0" tempfile = "3.4.0" toml = "0.5.11" # Do not update, see https://github.com/rust-lang/mdBook/issues/2037 topological-sort = "0.2.2" +resolve-path = "0.1.0" +git2 = "0.18.3" # Watch feature notify = { version = "6.1.1", optional = true } diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs index b21979b276..6b2af55594 100644 --- a/src/cmd/mod.rs +++ b/src/cmd/mod.rs @@ -6,6 +6,7 @@ pub mod command_prelude; pub mod init; #[cfg(feature = "serve")] pub mod serve; +pub mod shelf; pub mod test; #[cfg(feature = "watch")] pub mod watch; diff --git a/src/cmd/shelf.rs b/src/cmd/shelf.rs new file mode 100644 index 0000000000..f6fa845810 --- /dev/null +++ b/src/cmd/shelf.rs @@ -0,0 +1,281 @@ +use std::fs::File; +use std::io::Read; +use std::io::Write; +use std::path::PathBuf; + +use resolve_path::PathResolveExt; + +use super::command_prelude::*; +use mdbook::config::ShelfConfig; +use mdbook::errors::Result; +use mdbook::MDBook; + +const SHELF_DIR: &str = "shelf"; +const REPOS_DIR: &str = "repositories"; +const INDEX_MD_FILE: &str = "index.md"; +const INDEX_HTML_FILE: &str = "index.html"; +const BOOKS_DIR: &str = "books"; +const BOOKSHELF_DIR: &str = "bookshelf"; +const SUMMARY_MD_FILE: &str = "SUMMARY.md"; + +pub fn make_subcommand() -> Command { + Command::new("shelf").about("Build a bookshelf from shelf.toml file") +} + +struct BookContext { + title: String, + desc: String, + authors: String, +} + +struct ShelfContext { + book_dir: PathBuf, + source_dir: PathBuf, + url_prefix: String, + url: String, + index_file_name: PathBuf, + summary_file_name: PathBuf, +} + +fn update_index( + index_file: &mut File, + summary_file: &mut File, + shelf_source: &PathBuf, + root_prefix: &str, + context: BookContext, +) -> Result<()> { + // Create post in index file + let book_link = format!( + "## [{title}](<{prefix}/{BOOKSHELF_DIR}/{BOOKS_DIR}/{title}/{INDEX_HTML_FILE}>)", + title = context.title, + prefix = root_prefix + ); + writeln!(index_file, "{book_link}")?; + writeln!(index_file)?; + writeln!(index_file, "{desc}", desc = context.desc)?; + + // Create a separate chapter file for the book + let fixed_title = context.title.replace(' ', "_"); + let file_name = format!("{fixed_title}.md"); + let mut file_path = shelf_source.clone(); + file_path.push(&file_name); + let mut bf = File::create(file_path)?; + writeln!(bf, "{book_link}")?; + writeln!(bf)?; + writeln!(bf, "{desc}", desc = context.desc)?; + writeln!(bf)?; + writeln!(bf)?; + writeln!(bf, "*{authors}*", authors = context.authors)?; + + // Add the chapter to the summary + writeln!( + summary_file, + "- [{title}](./{file_name})", + title = context.title + )?; + writeln!(summary_file)?; + + Ok(()) +} + +fn process_book(path: &str, books_dir: &PathBuf, shelf_url: &str) -> Result { + let book_dir = path.try_resolve()?; + let book_dir = std::fs::canonicalize(book_dir)?; + let mut book = MDBook::load(book_dir)?; + + // Build book + let title = book.config.book.title.clone().unwrap(); + let mut build_path = books_dir.to_owned(); + build_path.push(title); + book.config.build.build_dir = build_path; + // Create back reference to bookshelf + book.config.book.shelf_url = Some(shelf_url.to_owned()); + book.build()?; + + let book_context = BookContext { + title: book.config.book.title.unwrap_or_default(), + desc: book.config.book.description.unwrap_or_default(), + authors: book.config.book.authors.join(", "), + }; + + Ok(book_context) +} + +fn setup_shelf_book(config: &ShelfConfig) -> Result { + let book_dir = format!("{BOOKSHELF_DIR}/{SHELF_DIR}"); + let book = MDBook::init(&book_dir).build()?; + let build_dir = book.config.build.build_dir.to_str().unwrap_or_default(); + let url_prefix = if !config.root_url_prefix.is_empty() { + let mut full_prefix = "/".to_owned(); + full_prefix.push_str(&config.root_url_prefix); + full_prefix + } else { + config.root_url_prefix.to_owned() + }; + let url = format!("{url_prefix}/{book_dir}/{build_dir}/{INDEX_HTML_FILE}"); + + let mut index_file_name = book.source_dir(); + index_file_name.push(INDEX_MD_FILE); + + let mut summary_file_name = book.source_dir(); + summary_file_name.push(SUMMARY_MD_FILE); + + Ok(ShelfContext { + book_dir: book_dir.into(), + source_dir: book.source_dir(), + url_prefix, + url, + index_file_name, + summary_file_name, + }) +} + +pub fn execute(_args: &ArgMatches) -> Result<()> { + let mut file = File::open("shelf.toml")?; + let mut contents = String::new(); + file.read_to_string(&mut contents)?; + let shelf_config: ShelfConfig = toml::from_str(&contents)?; + + let _ = std::fs::remove_dir_all(BOOKSHELF_DIR); + let _ = std::fs::remove_dir_all(REPOS_DIR); + + let shelf_context = setup_shelf_book(&shelf_config)?; + + let mut index_file = File::create(shelf_context.index_file_name).unwrap(); + writeln!(index_file, "# {title}", title = shelf_config.title)?; + writeln!(index_file)?; + + let mut summary_file = File::create(shelf_context.summary_file_name).unwrap(); + writeln!(summary_file, "# Summary")?; + writeln!( + summary_file, + "- [{title}](./{INDEX_MD_FILE})", + title = shelf_config.title + )?; + + let mut books_build_dir = std::env::current_dir()?; + books_build_dir.push(BOOKSHELF_DIR); + books_build_dir.push(BOOKS_DIR); + + for sb in &shelf_config.books { + let book_path = if let Some(url) = &sb.git_url { + prepare_git(sb, url) + } else if let Some(path) = &sb.path { + Some(path.to_owned()) + } else { + warn!("Neither path or git specified. Invalid book"); + None + }; + + if let Some(path) = book_path { + let update_context = process_book(&path, &books_build_dir, &shelf_context.url)?; + let _ = update_index( + &mut index_file, + &mut summary_file, + &shelf_context.source_dir, + &shelf_context.url_prefix, + update_context, + )?; + } + } + + let shelf = MDBook::load(&shelf_context.book_dir)?; + shelf.build()?; + + Ok(()) +} + +fn prepare_git(sb: &mdbook::config::ShelfBook, url: &String) -> Option { + println!("{:?}", sb); + + // Prepare checkout directory name + let path = sb.path.clone().unwrap_or("root".to_owned()); + let repo_raw_name = url.split('/').last().unwrap_or(&path); + let repo_name = format!("{repo_raw_name}-{path}"); + let mut checkout_path = PathBuf::from(REPOS_DIR); + checkout_path.push(repo_name); + + let book_path = if let Some(path) = &sb.path { + let mut bp = checkout_path.clone(); + bp.push(path); + bp + } else { + checkout_path.clone() + }; + + let repo = match git2::Repository::open(&checkout_path) { + Ok(repo) => repo, + Err(_) => match git2::Repository::clone(&url, &checkout_path) { + Ok(repo) => repo, + Err(e) => panic!("failed to clone: {}", e), + }, + }; + + if let Some(refname) = &sb.git_ref { + // branch or a tag (v0.1.1) or a commit (8e8128) + let (object, reference) = if let Ok((object, reference)) = repo.revparse_ext(refname) { + (object, reference) + } else if let Ok((object, reference)) = repo.revparse_ext(&format!("origin/{refname}")) { + (object, reference) + } else { + panic!("Could not checkout {refname}"); + }; + + repo.checkout_tree(&object, None) + .expect("Failed to checkout"); + + match reference { + // gref is an actual reference like branches or tags + Some(gref) => repo.set_head(gref.name().unwrap()), + // this is a commit, not a reference + None => repo.set_head_detached(object.id()), + } + .expect("Failed to set HEAD"); + } + + Some(book_path.to_str().unwrap().to_owned()) +} + +#[test] +fn test_parse_toml() { + let toml = r#" +root_url_prefix = "myprefix" + +[[book]] +git_url = "firsturl" +git_ref = "shelf" +path = "guide" + +[[book]] +git_url = "secondurl" + +[[book]] +path = "../test_book" +"#; + let cfg: ShelfConfig = toml::from_str(&toml).unwrap(); + assert_eq!(cfg.root_url_prefix, "myprefix"); + + let book = &cfg.books[0]; + assert_eq!(book.git_url.clone().unwrap(), "firsturl"); + assert_eq!(book.git_ref.clone().unwrap(), "shelf"); + assert_eq!(book.path.clone().unwrap(), "guide"); + + let book = &cfg.books[1]; + assert_eq!(book.git_url.clone().unwrap(), "secondurl"); + assert!(book.git_ref.is_none()); + assert!(book.path.is_none()); + + let book = &cfg.books[2]; + assert_eq!(book.path.clone().unwrap(), "../test_book"); +} + +#[test] +fn test_config_defaults() { + let toml = r#" +[[book]] +path = "../test_book" + "#; + let cfg: ShelfConfig = toml::from_str(&toml).unwrap(); + assert_eq!(cfg.root_url_prefix, "".to_owned()); + assert_eq!(cfg.title, "Bookshelf".to_owned()); +} diff --git a/src/config.rs b/src/config.rs index b7c01599f6..d15435d73c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -414,6 +414,9 @@ pub struct BookConfig { /// The direction of text in the book: Left-to-right (LTR) or Right-to-left (RTL). /// When not specified, the text direction is derived from [`BookConfig::language`]. pub text_direction: Option, + /// Indicates if the book is part of a bookshelf + /// and how to return to the index of the shelf if so + pub shelf_url: Option, } impl Default for BookConfig { @@ -426,6 +429,7 @@ impl Default for BookConfig { multilingual: false, language: Some(String::from("en")), text_direction: None, + shelf_url: None, } } } @@ -633,6 +637,43 @@ impl HtmlConfig { } } +#[derive(Deserialize, Debug)] +/// Represents a book in a shelf +pub struct ShelfBook { + /// Path to filesystem local book + /// or if git_url is specified, the path inside the git + /// where the book is located + pub path: Option, + /// git url + pub git_url: Option, + /// reference to checkout in git + /// This can be a branch, commit or tag + pub git_ref: Option, +} + +#[derive(Deserialize, Debug)] +/// Represents a shelf that contains a lot of books +pub struct ShelfConfig { + /// The books in the shelf + #[serde(alias = "book")] + pub books: Vec, + /// this will be prepeneded to the backreference url + /// Say you want to publish to www.example.com/mydocs + /// you would set this to "mydocs" and then find your bookshelf at + /// www.example.com/mydocs/bookshelf/shelf/book/index.html + #[serde(default = "default_shelf_root_url")] + pub root_url_prefix: String, + /// Name of the shelf + #[serde(default = "default_shelf_title")] + pub title: String, +} +fn default_shelf_root_url() -> String { + "".to_owned() +} +fn default_shelf_title() -> String { + "Bookshelf".to_owned() +} + /// Configuration for how to render the print icon, print.html, and print.css. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(default, rename_all = "kebab-case")] @@ -837,6 +878,7 @@ mod tests { src: PathBuf::from("source"), language: Some(String::from("ja")), text_direction: None, + shelf_url: None, }; let build_should_be = BuildConfig { build_dir: PathBuf::from("outputs"), diff --git a/src/main.rs b/src/main.rs index 3e576c5b53..e1fc5b1dcf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -34,6 +34,7 @@ fn main() { #[cfg(feature = "serve")] Some(("serve", sub_matches)) => cmd::serve::execute(sub_matches), Some(("test", sub_matches)) => cmd::test::execute(sub_matches), + Some(("shelf", sub_matches)) => cmd::shelf::execute(sub_matches), Some(("completions", sub_matches)) => (|| { let shell = sub_matches .get_one::("shell") @@ -84,7 +85,8 @@ fn create_clap_command() -> Command { .value_name("SHELL") .required(true), ), - ); + ) + .subcommand(cmd::shelf::make_subcommand()); #[cfg(feature = "watch")] let app = app.subcommand(cmd::watch::make_subcommand()); diff --git a/src/renderer/html_handlebars/hbs_renderer.rs b/src/renderer/html_handlebars/hbs_renderer.rs index 30d4c84b8c..001278ac3a 100644 --- a/src/renderer/html_handlebars/hbs_renderer.rs +++ b/src/renderer/html_handlebars/hbs_renderer.rs @@ -715,6 +715,10 @@ fn make_data( data.insert("fold_enable".to_owned(), json!(html_config.fold.enable)); data.insert("fold_level".to_owned(), json!(html_config.fold.level)); + if let Some(shelf_url) = &config.book.shelf_url { + data.insert("book_shelf_url".to_owned(), json!(shelf_url)); + } + let search = html_config.search.clone(); if cfg!(feature = "search") { let search = search.unwrap_or_default(); diff --git a/src/theme/index.hbs b/src/theme/index.hbs index 080b78516a..515f63007b 100644 --- a/src/theme/index.hbs +++ b/src/theme/index.hbs @@ -184,6 +184,11 @@ {{/if}} + {{#if book_shelf_url}} + + + + {{/if}} diff --git a/test_shelf/.gitignore b/test_shelf/.gitignore new file mode 100644 index 0000000000..f59ec20aab --- /dev/null +++ b/test_shelf/.gitignore @@ -0,0 +1 @@ +* \ No newline at end of file diff --git a/test_shelf/shelf.toml b/test_shelf/shelf.toml new file mode 100644 index 0000000000..b62bfd84cb --- /dev/null +++ b/test_shelf/shelf.toml @@ -0,0 +1,10 @@ +[[book]] +git_url = "https://github.com/Coi-l/mdBook.git" +git_ref = "shelf" +path = "guide" + +[[book]] +git_url = "https://github.com/rust-lang/book.git" + +[[book]] +path = "../test_book"